Z230
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ANTHROPIC_API_KEY=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
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 60 ScansProcessing
|
||||||
|
|
||||||
|
Agent pro zpracování naskenovaných lékařských zpráv (PDF i JPG/PNG).
|
||||||
|
|
||||||
|
## Skripty
|
||||||
|
|
||||||
|
### `extract_patient_info.py` — hlavní agent
|
||||||
|
Spuštění: `python extract_patient_info.py` (bez argumentů = celá složka ToProcess)
|
||||||
|
|
||||||
|
**Workflow:**
|
||||||
|
1. Načte soubory z `ToProcess/`
|
||||||
|
2. Claude Vision API (sonnet-4-6) extrahuje: jméno, RČ, datum, typ dokumentu, poznámku, navržený název, rotaci
|
||||||
|
3. Ověří pacienta v Medicus Firebird (tabulka KAR, pole RODCIS/PRIJMENI/JMENO)
|
||||||
|
4. Fuzzy matching RČ při nenalezení: vynechání cifry + záměna podobných (0↔8, 1↔7, 5↔6, 3↔8) + checksum /11
|
||||||
|
5. Upozorní na duplicitu v `U:\Dropbox\Ordinace\Dokumentace_zpracovaná\`
|
||||||
|
6. Interaktivní schválení / oprava názvu
|
||||||
|
7. JPG/PNG → skutečné PDF (správná orientace, DPI=150, quality=80)
|
||||||
|
8. Přesun do `Processed/`, smazání z `ToProcess/`
|
||||||
|
9. Opravy názvů se ukládají do `corrections.json` jako few-shot příklady
|
||||||
|
|
||||||
|
**Formát názvu souboru:**
|
||||||
|
`{RČ} {YYYY-MM-DD} {Příjmení}, {Jméno} [{typ dokumentu}] [{poznámka}].pdf`
|
||||||
|
|
||||||
|
Příklady typů: `LZ chirurgie`, `LZ kardiologie`, `Laboratoř`, `CT břicha`, `kolonoskopie`, `poukaz FT`
|
||||||
|
|
||||||
|
### `jpg_to_pdf.py` — konverze obrázku na PDF
|
||||||
|
```
|
||||||
|
python jpg_to_pdf.py soubor.jpg [vystup.pdf] [rotace_ccw]
|
||||||
|
```
|
||||||
|
- Opravuje EXIF orientaci
|
||||||
|
- Rotace: 0 / 90 / 180 / 270 (CCW)
|
||||||
|
- A4, DPI=150, quality=80, bez okrajů
|
||||||
|
- Používá se i interně z `extract_patient_info.py`
|
||||||
|
|
||||||
|
## Složky
|
||||||
|
|
||||||
|
| Složka | Účel |
|
||||||
|
|---|---|
|
||||||
|
| `ToProcess/` | Sem se házejí nové skeny (PDF, JPG, PNG) |
|
||||||
|
| `Processed/` | Správně pojmenované PDF po schválení |
|
||||||
|
| `U:\Dropbox\Ordinace\Dokumentace_zpracovaná\` | Finální archiv |
|
||||||
|
|
||||||
|
## Konfigurace
|
||||||
|
- API klíč: `U:\Medevio\.env` → `ANTHROPIC_API_KEY`
|
||||||
|
- Medicus: `localhost:c:\medicus 3\data\medicus.fdb` (Firebird, SYSDBA)
|
||||||
|
- Few-shot korekce: `corrections.json`
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"original": "505228025 2026-05-14 Titlbachová, Božena [Žádanka předoperační vyšetření GYNA] [Předop. vyšetření, dg. N890, malý výkon A, anestezie CA].pdf",
|
||||||
|
"corrected": "505228025 2026-05-14 Titlbachová, Božena [žádanka předoperační vyšetření] [gynekologie, dg. N890, malý výkon A, anestezie CA].pdf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"original": "6860241553 2026-02-12 Šímová, Helena [LZ neurologie] [VAS L páteře, iritačně zánikový radik sy L5/S1 vpravo, dg. M511].pdf",
|
||||||
|
"corrected": "6860241553 2026-02-12 Šímová, Helena [LZ neurologie] [VAS L páteře, po PRT pod CT, krásné zlepšení, iritačně zánikový radik sy L5/S1 vpravo, dg. M511].pdf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"original": "6860241553 2026-02-10 Šímová, Helena [denzitometrie] [osteopenie, L1-4 T-score -1,4, krček fem. l T-1,8, r T-2,3].pdf",
|
||||||
|
"corrected": "6860241553 2026-02-10 Šímová, Helena [DXA] [osteopenie, L1-4 T-score -1.4, krček fem. l T-1.8, r T-2.3].pdf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"original": "470629074 2026-03-31 Šebesta, Jaroslav [LZ kardiologie] [ECHO: EF 50%, hypokineza IVS a sp. stěny, dilatace LS, MR 1-2/4].pdf",
|
||||||
|
"corrected": "470629074 2026-03-31 Šebesta, Jaroslav [LZ kardiologie] [ECHO: EF 50%, hypokineza IVS a sp. stěny, dilatace LS, MR 1-2/4, indikace lázně II_3].pdf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"original": "505809020 2026-01-14 Šebestová, Zdenka [LZ ortopedie] [TEP kyčle l.sin., kontrola 6 týdnů, chůze 2FH, doporučení lázně].pdf",
|
||||||
|
"corrected": "505809020 2026-01-14 Šebestová, Zdenka [LZ ortopedie] [TEP kyčle l.sin., kontrola 6 týdnů, chůze 2FH, indikace lázně VII_10].pdf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"original": "505809020 2025-12-10 Šebestová, Zdenka [LZ ortopedie] [Fct. colli femor. l.sin., TEP kyčle l.sin., propuštění na RHB].pdf",
|
||||||
|
"corrected": "505809020 2025-12-10 Šebestová, Zdenka [PZ ortopedie] [29NOV-10DEC2025 Fct. colli femor. l.sin., TEP kyčle l.sin., propuštění na RHB].pdf"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
"""
|
||||||
|
Agent pro extrakci a pojmenování naskenovaných PDF lékařských zpráv.
|
||||||
|
- Claude Vision API — bez OCR, správná čeština s diakritikou
|
||||||
|
- Ověření pacienta proti Medicus (KAR), fuzzy matching RČ
|
||||||
|
- Interaktivní schválení / oprava názvu
|
||||||
|
- Few-shot learning z uložených korekcí
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import gc
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Windows: nastav stdout/stderr na UTF-8
|
||||||
|
if sys.platform == "win32":
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
import anthropic
|
||||||
|
from pdf2image import convert_from_path
|
||||||
|
|
||||||
|
POPPLER_PATH = r"C:/Poppler/Library/bin"
|
||||||
|
CORRECTIONS_FILE = Path(__file__).parent / "corrections.json"
|
||||||
|
TO_PROCESS = Path(__file__).parent / "ToProcess"
|
||||||
|
PROCESSED = Path(__file__).parent / "Processed"
|
||||||
|
DOKUMENTACE = Path(r"U:\Dropbox\Ordinace\Dokumentace_zpracovaná")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Konfigurace ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _load_env():
|
||||||
|
env_path = Path(__file__).parent.parent / ".env"
|
||||||
|
if env_path.exists():
|
||||||
|
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if "=" in line and not line.startswith("#"):
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
os.environ[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
_load_env()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Korekce (few-shot příklady) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_corrections() -> list[dict]:
|
||||||
|
if CORRECTIONS_FILE.exists():
|
||||||
|
return json.loads(CORRECTIONS_FILE.read_text(encoding="utf-8"))
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_correction(original: str, corrected: str):
|
||||||
|
corrections = load_corrections()
|
||||||
|
for c in corrections:
|
||||||
|
if c["original"] == original and c["corrected"] == corrected:
|
||||||
|
return
|
||||||
|
corrections.append({"original": original, "corrected": corrected})
|
||||||
|
CORRECTIONS_FILE.write_text(
|
||||||
|
json.dumps(corrections, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||||
|
)
|
||||||
|
print(f" ✓ Korekce uložena ({len(corrections)} celkem)")
|
||||||
|
|
||||||
|
def build_corrections_prompt() -> str:
|
||||||
|
corrections = load_corrections()
|
||||||
|
if not corrections:
|
||||||
|
return ""
|
||||||
|
lines = ["Příklady korekcí z minulých běhů (uč se z nich):"]
|
||||||
|
for c in corrections[-10:]:
|
||||||
|
lines.append(f' - špatně: "{c["original"]}"')
|
||||||
|
lines.append(f' správně: "{c["corrected"]}"')
|
||||||
|
return "\n".join(lines) + "\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Kontrola duplicit ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def check_duplicates(rc: str, datum: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
Hledá v Dokumentace_zpracovaná soubory se stejným RČ a datem.
|
||||||
|
Vrátí seznam názvů nalezených souborů.
|
||||||
|
"""
|
||||||
|
if not DOKUMENTACE.exists():
|
||||||
|
return []
|
||||||
|
prefix = f"{rc} {datum}"
|
||||||
|
return [f.name for f in DOKUMENTACE.iterdir() if f.name.startswith(prefix)]
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Medicus ověření ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _medicus_connect():
|
||||||
|
try:
|
||||||
|
import fdb
|
||||||
|
return fdb.connect(
|
||||||
|
dsn=r"localhost:c:\medicus 3\data\medicus.fdb",
|
||||||
|
user="SYSDBA", password="masterkey", charset="win1250"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [Medicus] Nepřipojeno: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _lookup_by_rc(cur, rc_digits: str) -> dict | None:
|
||||||
|
"""Přesné vyhledání podle RČ (bez lomítka)."""
|
||||||
|
cur.execute(
|
||||||
|
"SELECT IDPAC, PRIJMENI, JMENO, RODCIS FROM KAR "
|
||||||
|
"WHERE REPLACE(RODCIS, '/', '') = ?",
|
||||||
|
(rc_digits,)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
return {"idpac": row[0], "prijmeni": row[1].strip(), "jmeno": row[2].strip(), "rodcis": row[3].strip()}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _rc_candidates(rc: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
Generuje kandidáty RČ pro fuzzy matching (edit distance 1):
|
||||||
|
- vynechání každé cifry (oprava extra znaku z OCR)
|
||||||
|
- záměna podobně vypadajících číslic na každé pozici
|
||||||
|
Vrátí unikátní seznam kandidátů bez původního RČ.
|
||||||
|
"""
|
||||||
|
similar = {"0": "8", "8": "0", "1": "7", "7": "1", "5": "6", "6": "5", "3": "8"}
|
||||||
|
candidates = set()
|
||||||
|
|
||||||
|
# Vynechání jedné cifry (nejčastější OCR chyba — přebývající nula)
|
||||||
|
for i in range(len(rc)):
|
||||||
|
candidates.add(rc[:i] + rc[i+1:])
|
||||||
|
|
||||||
|
# Záměna podobné cifry na každé pozici
|
||||||
|
for i, ch in enumerate(rc):
|
||||||
|
if ch in similar:
|
||||||
|
candidates.add(rc[:i] + similar[ch] + rc[i+1:])
|
||||||
|
|
||||||
|
candidates.discard(rc)
|
||||||
|
return sorted(candidates)
|
||||||
|
|
||||||
|
def _rc_checksum_ok(rc: str) -> bool:
|
||||||
|
"""Ověří dělitelnost 11 pro 10místná RČ (platí pro narozené po 1.1.1954)."""
|
||||||
|
digits = re.sub(r"\D", "", rc)
|
||||||
|
if len(digits) == 10:
|
||||||
|
return int(digits) % 11 == 0
|
||||||
|
return True # 9místná RČ nemají checksum
|
||||||
|
|
||||||
|
def verify_patient(rc_raw: str) -> dict:
|
||||||
|
"""
|
||||||
|
Ověří pacienta v Medicus.
|
||||||
|
Vrací:
|
||||||
|
status: "ok" | "fuzzy" | "not_found" | "offline"
|
||||||
|
patient: dict nebo None
|
||||||
|
rc_corrected: opravené RČ (pokud fuzzy) nebo None
|
||||||
|
"""
|
||||||
|
rc = re.sub(r"\D", "", rc_raw or "")
|
||||||
|
if not rc:
|
||||||
|
return {"status": "not_found", "patient": None, "rc_corrected": None}
|
||||||
|
|
||||||
|
con = _medicus_connect()
|
||||||
|
if con is None:
|
||||||
|
return {"status": "offline", "patient": None, "rc_corrected": None}
|
||||||
|
|
||||||
|
try:
|
||||||
|
cur = con.cursor()
|
||||||
|
|
||||||
|
# 1. Přesná shoda
|
||||||
|
patient = _lookup_by_rc(cur, rc)
|
||||||
|
if patient:
|
||||||
|
return {"status": "ok", "patient": patient, "rc_corrected": None}
|
||||||
|
|
||||||
|
# 2. Fuzzy matching — zkus kandidáty, preferuj ty s platným checksumem
|
||||||
|
candidates = _rc_candidates(rc)
|
||||||
|
matches = []
|
||||||
|
for cand in candidates:
|
||||||
|
p = _lookup_by_rc(cur, cand)
|
||||||
|
if p:
|
||||||
|
matches.append((cand, p))
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
return {"status": "not_found", "patient": None, "rc_corrected": None}
|
||||||
|
|
||||||
|
# Seřaď: platný checksum na prvním místě
|
||||||
|
matches.sort(key=lambda x: (0 if _rc_checksum_ok(x[0]) else 1))
|
||||||
|
best_rc, best_patient = matches[0]
|
||||||
|
return {"status": "fuzzy", "patient": best_patient, "rc_corrected": best_rc, "all_matches": matches}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── PDF → obrázek ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def pdf_to_images(pdf_path: str) -> list:
|
||||||
|
return convert_from_path(pdf_path, poppler_path=POPPLER_PATH, dpi=300)
|
||||||
|
|
||||||
|
def image_to_base64(image) -> str:
|
||||||
|
buf = io.BytesIO()
|
||||||
|
image.save(buf, format="JPEG", quality=95)
|
||||||
|
return base64.standard_b64encode(buf.getvalue()).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Extrakce Claude Vision ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def extract_patient_info(pdf_path: str) -> dict:
|
||||||
|
pdf_path = Path(pdf_path)
|
||||||
|
if not pdf_path.exists():
|
||||||
|
raise FileNotFoundError(f"Soubor nenalezen: {pdf_path}")
|
||||||
|
|
||||||
|
print(f"\nNačítám: {pdf_path.name}")
|
||||||
|
suffix = pdf_path.suffix.lower()
|
||||||
|
if suffix in (".jpg", ".jpeg", ".png"):
|
||||||
|
from PIL import Image
|
||||||
|
img = Image.open(pdf_path)
|
||||||
|
image_b64 = image_to_base64(img)
|
||||||
|
img.close()
|
||||||
|
else:
|
||||||
|
images = pdf_to_images(str(pdf_path))
|
||||||
|
image_b64 = image_to_base64(images[0])
|
||||||
|
del images
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
build_corrections_prompt() +
|
||||||
|
"Toto je naskenovaná lékařská zpráva v češtině. "
|
||||||
|
"Vrať JSON s těmito poli:\n"
|
||||||
|
"- \"jmeno\": celé jméno pacienta (příjmení + jméno + případný titul)\n"
|
||||||
|
"- \"rodne_cislo\": rodné číslo pacienta BEZ lomítka (pouze číslice)\n"
|
||||||
|
"- \"datum_zpravy\": datum zprávy ve formátu YYYY-MM-DD\n"
|
||||||
|
"- \"typ_dokumentu\": typ dokumentu — pokud je to lékařská/ambulantní/propouštěcí zpráva, "
|
||||||
|
"použij \"LZ {oddělení}\" (např. \"LZ chirurgie\", \"LZ kardiologie\", \"LZ plicní\", \"LZ ORL\"). "
|
||||||
|
"Jiné typy: \"Laboratoř\", \"CT břicha\", \"MRI páteře\", \"kolonoskopie\", "
|
||||||
|
"\"operační protokol oční\", \"poukaz FT\", \"diagnostická mamografie\" atd.\n"
|
||||||
|
"- \"poznamka\": krátká klinická poznámka česky, max 80 znaků\n"
|
||||||
|
"- \"nazev_souboru\": název souboru ve formátu "
|
||||||
|
"\"{rodne_cislo} {datum_zpravy} {Příjmení}, {Jméno} [{typ_dokumentu}] [{poznamka}].pdf\" "
|
||||||
|
"(jméno bez titulu, RČ bez lomítka)\n"
|
||||||
|
"- \"rotace\": o kolik stupňů CCW je třeba otočit obrázek aby byl text čitelně na výšku nebo šířku "
|
||||||
|
"(hodnoty: 0, 90, 180, 270). Pokud je text již správně orientovaný, vrať 0.\n\n"
|
||||||
|
"Pokud pole nenajdeš, použij null. Nepiš nic jiného než JSON."
|
||||||
|
)
|
||||||
|
|
||||||
|
print(" Volám Claude Vision API...")
|
||||||
|
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
|
||||||
|
response = client.messages.create(
|
||||||
|
model="claude-sonnet-4-6",
|
||||||
|
max_tokens=400,
|
||||||
|
messages=[{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}},
|
||||||
|
{"type": "text", "text": prompt},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
|
||||||
|
usage = response.usage
|
||||||
|
cost_input = usage.input_tokens * 3 / 1_000_000
|
||||||
|
cost_output = usage.output_tokens * 15 / 1_000_000
|
||||||
|
print(f" Tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${cost_input + cost_output:.4f}")
|
||||||
|
|
||||||
|
raw = response.content[0].text.strip()
|
||||||
|
if raw.startswith("```"):
|
||||||
|
raw = raw.split("```")[1]
|
||||||
|
if raw.startswith("json"):
|
||||||
|
raw = raw[4:]
|
||||||
|
try:
|
||||||
|
return json.loads(raw.strip())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f" VAROVÁNÍ: nelze parsovat JSON: {raw!r}")
|
||||||
|
return {"nazev_souboru": None, "raw": raw}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Interaktivní schválení ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def sanitize_filename(name: str) -> str:
|
||||||
|
return re.sub(r'[<>:"/\\|?*]', '', name)
|
||||||
|
|
||||||
|
def print_verification(verif: dict, rc_from_scan: str):
|
||||||
|
"""Vypíše výsledek ověření proti Medicus."""
|
||||||
|
status = verif["status"]
|
||||||
|
patient = verif.get("patient")
|
||||||
|
|
||||||
|
if status == "ok":
|
||||||
|
print(f" ✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||||
|
elif status == "fuzzy":
|
||||||
|
rc_corr = verif["rc_corrected"]
|
||||||
|
print(f" ⚠ Medicus: RČ ze skenu '{rc_from_scan}' nenalezeno")
|
||||||
|
print(f" → Nalezen podobný pacient: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||||
|
print(f" → Pravděpodobná oprava RČ: {rc_from_scan} → {rc_corr} (OCR chyba)")
|
||||||
|
if len(verif.get("all_matches", [])) > 1:
|
||||||
|
print(f" → Další shody: {[m[0] for m in verif['all_matches'][1:]]}")
|
||||||
|
elif status == "not_found":
|
||||||
|
print(f" ✗ Medicus: RČ '{rc_from_scan}' nenalezeno ani při fuzzy hledání")
|
||||||
|
elif status == "offline":
|
||||||
|
print(f" — Medicus: nedostupný (offline), ověření přeskočeno")
|
||||||
|
|
||||||
|
def interactive_rename(pdf_path: Path, info: dict, verif: dict) -> bool:
|
||||||
|
"""
|
||||||
|
Zobrazí výsledek ověření a navržený název, umožní schválení nebo opravu.
|
||||||
|
Schválený soubor přesune do Processed/ a smaže z ToProcess/.
|
||||||
|
"""
|
||||||
|
# Kontrola duplicit v Dokumentace_zpracovaná
|
||||||
|
rc = re.sub(r"\D", "", verif["patient"]["rodcis"] if verif.get("patient") else info.get("rodne_cislo") or "")
|
||||||
|
datum = info.get("datum_zpravy") or ""
|
||||||
|
duplicity = check_duplicates(rc, datum)
|
||||||
|
if duplicity:
|
||||||
|
print()
|
||||||
|
print(f" ⚠ DUPLICITA — v Dokumentace_zpracovaná již existuje stejný pacient + datum:")
|
||||||
|
for d in duplicity:
|
||||||
|
print(f" · {d}")
|
||||||
|
|
||||||
|
# Pokud fuzzy match opravil RČ, aktualizuj navržený název souboru
|
||||||
|
nazev = info.get("nazev_souboru")
|
||||||
|
if verif["status"] == "fuzzy" and verif.get("rc_corrected") and nazev:
|
||||||
|
rc_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
|
||||||
|
nazev = nazev.replace(rc_scan, verif["rc_corrected"], 1)
|
||||||
|
print(f" → Název aktualizován s opraveným RČ")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("─" * 70)
|
||||||
|
if nazev:
|
||||||
|
print(f" Navržený název: {nazev}")
|
||||||
|
else:
|
||||||
|
print(" Nepodařilo se vygenerovat název souboru.")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" [Enter] = schválit → uložit do Processed a smazat z ToProcess")
|
||||||
|
print(" [n] = přeskočit")
|
||||||
|
print(" [text] = zadat správný název (bez .pdf)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
odpoved = input(" > ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nPřerušeno.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if odpoved.lower() == "n":
|
||||||
|
print(" Přeskočeno.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if odpoved == "":
|
||||||
|
final_name = nazev
|
||||||
|
else:
|
||||||
|
if not odpoved.endswith(".pdf"):
|
||||||
|
odpoved += ".pdf"
|
||||||
|
final_name = odpoved
|
||||||
|
if nazev and nazev != final_name:
|
||||||
|
save_correction(nazev, final_name)
|
||||||
|
|
||||||
|
if not final_name:
|
||||||
|
print(" Název je prázdný, přeskakuji.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
final_name = sanitize_filename(final_name)
|
||||||
|
dest = PROCESSED / final_name
|
||||||
|
|
||||||
|
if dest.exists():
|
||||||
|
print(f" VAROVÁNÍ: '{final_name}' již existuje v Processed, přeskakuji.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
for attempt in range(5):
|
||||||
|
try:
|
||||||
|
if pdf_path.suffix.lower() in (".jpg", ".jpeg", ".png"):
|
||||||
|
from jpg_to_pdf import image_to_pdf
|
||||||
|
image_to_pdf(pdf_path, dest, rotate_ccw=info.get("rotace") or 0)
|
||||||
|
else:
|
||||||
|
shutil.copy2(pdf_path, dest)
|
||||||
|
break
|
||||||
|
except PermissionError:
|
||||||
|
if attempt < 4:
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
pdf_path.unlink()
|
||||||
|
print(f" ✓ Uloženo: Processed/{final_name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Hlavní logika ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def process_file(pdf_path: Path):
|
||||||
|
info = extract_patient_info(str(pdf_path))
|
||||||
|
|
||||||
|
rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
|
||||||
|
print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...")
|
||||||
|
verif = verify_patient(rc_from_scan)
|
||||||
|
print_verification(verif, rc_from_scan)
|
||||||
|
|
||||||
|
interactive_rename(pdf_path, info, verif)
|
||||||
|
|
||||||
|
def process_folder(folder: Path):
|
||||||
|
pdf_files = sorted(f for f in folder.iterdir()
|
||||||
|
if f.suffix.lower() in (".pdf", ".jpg", ".jpeg", ".png"))
|
||||||
|
if not pdf_files:
|
||||||
|
print(f"Žádná PDF nenalezena v: {folder}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Nalezeno {len(pdf_files)} PDF soubor(ů).\n")
|
||||||
|
for pdf_file in pdf_files:
|
||||||
|
try:
|
||||||
|
process_file(pdf_file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" CHYBA: {e}")
|
||||||
|
|
||||||
|
print("\nHotovo.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
target = Path(sys.argv[1])
|
||||||
|
else:
|
||||||
|
target = TO_PROCESS
|
||||||
|
|
||||||
|
PROCESSED.mkdir(exist_ok=True)
|
||||||
|
TO_PROCESS.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
if target.is_file() and target.suffix.lower() in (".pdf", ".jpg", ".jpeg", ".png"):
|
||||||
|
process_file(target)
|
||||||
|
elif target.is_dir():
|
||||||
|
process_folder(target)
|
||||||
|
else:
|
||||||
|
print("Použití: python extract_patient_info.py [soubor.pdf nebo složka]")
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
Konverze JPG/PNG → PDF se správnou orientací stránky (A4).
|
||||||
|
|
||||||
|
Řeší:
|
||||||
|
- EXIF orientaci (fotky z telefonu/skeneru bývají otočené)
|
||||||
|
- Správné umístění na A4 stránce (na výšku nebo na šířku dle obsahu)
|
||||||
|
- Zachování kvality
|
||||||
|
|
||||||
|
Použití:
|
||||||
|
python jpg_to_pdf.py soubor.jpg
|
||||||
|
python jpg_to_pdf.py soubor.jpg vystup.pdf
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
|
||||||
|
# A4 rozměry v mm
|
||||||
|
A4_W_MM = 210
|
||||||
|
A4_H_MM = 297
|
||||||
|
MARGIN_MM = 0 # bez okraje, tisk si řeší Acrobat (Fit to Print)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_orientation(img: Image.Image) -> Image.Image:
|
||||||
|
"""Opraví rotaci podle EXIF dat (tag 274)."""
|
||||||
|
return ImageOps.exif_transpose(img)
|
||||||
|
|
||||||
|
|
||||||
|
def image_to_pdf(src: Path, dst: Path, dpi: int = 150, quality: int = 80, rotate_ccw: int = 0):
|
||||||
|
img = Image.open(src)
|
||||||
|
print(f" Originál: {img.size[0]}×{img.size[1]} px, mode={img.mode}, format={img.format}")
|
||||||
|
|
||||||
|
# 1. Oprav EXIF orientaci
|
||||||
|
img = fix_orientation(img)
|
||||||
|
print(f" Po EXIF korekci: {img.size[0]}×{img.size[1]} px")
|
||||||
|
|
||||||
|
# 2. Rotace dle parametru (od Claude nebo ručně)
|
||||||
|
if rotate_ccw and rotate_ccw != 0:
|
||||||
|
img = img.rotate(rotate_ccw, expand=True)
|
||||||
|
print(f" Po rotaci {rotate_ccw}° CCW: {img.size[0]}×{img.size[1]} px")
|
||||||
|
|
||||||
|
# 2. Převeď na RGB (PDF nepodporuje RGBA/P)
|
||||||
|
if img.mode in ("RGBA", "P", "LA"):
|
||||||
|
img = img.convert("RGB")
|
||||||
|
|
||||||
|
# 3. Urči orientaci stránky podle poměru stran obrázku
|
||||||
|
img_w, img_h = img.size
|
||||||
|
if img_w > img_h:
|
||||||
|
# Obrázek na šířku → stránka na šířku (A4 landscape)
|
||||||
|
page_w_mm, page_h_mm = A4_H_MM, A4_W_MM
|
||||||
|
print(f" Orientace stránky: na šířku (landscape)")
|
||||||
|
else:
|
||||||
|
# Obrázek na výšku → stránka na výšku (A4 portrait)
|
||||||
|
page_w_mm, page_h_mm = A4_W_MM, A4_H_MM
|
||||||
|
print(f" Orientace stránky: na výšku (portrait)")
|
||||||
|
|
||||||
|
# 4. Vypočti cílovou velikost s okrajem (mm → px při daném DPI)
|
||||||
|
mm_to_px = dpi / 25.4
|
||||||
|
max_w_px = int((page_w_mm - 2 * MARGIN_MM) * mm_to_px)
|
||||||
|
max_h_px = int((page_h_mm - 2 * MARGIN_MM) * mm_to_px)
|
||||||
|
|
||||||
|
# 5. Škáluj obrázek na stránku (zachovej poměr stran)
|
||||||
|
img.thumbnail((max_w_px, max_h_px), Image.LANCZOS)
|
||||||
|
print(f" Výsledná velikost obrázku: {img.size[0]}×{img.size[1]} px")
|
||||||
|
|
||||||
|
# 6. Vlož obrázek na bílé A4 plátno
|
||||||
|
page_w_px = int(page_w_mm * mm_to_px)
|
||||||
|
page_h_px = int(page_h_mm * mm_to_px)
|
||||||
|
canvas = Image.new("RGB", (page_w_px, page_h_px), "white")
|
||||||
|
|
||||||
|
offset_x = (page_w_px - img.size[0]) // 2
|
||||||
|
offset_y = (page_h_px - img.size[1]) // 2
|
||||||
|
canvas.paste(img, (offset_x, offset_y))
|
||||||
|
|
||||||
|
# 7. Ulož jako PDF
|
||||||
|
canvas.save(dst, "PDF", resolution=dpi, quality=quality)
|
||||||
|
print(f" ✓ Uloženo: {dst.name} ({dst.stat().st_size // 1024} KB)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if sys.platform == "win32":
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Použití: python jpg_to_pdf.py soubor.jpg [vystup.pdf] [rotace_ccw]")
|
||||||
|
print(" rotace_ccw: 0 / 90 / 180 / 270 (výchozí: 0)")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
src = Path(sys.argv[1])
|
||||||
|
if not src.exists():
|
||||||
|
print(f"Soubor nenalezen: {src}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
dst = Path(sys.argv[2]) if len(sys.argv) > 2 else src.with_suffix(".pdf")
|
||||||
|
rotate_ccw = int(sys.argv[3]) if len(sys.argv) > 3 else 0
|
||||||
|
|
||||||
|
print(f"Konvertuji: {src.name} → {dst.name}")
|
||||||
|
image_to_pdf(src, dst, rotate_ccw=rotate_ccw)
|
||||||
Reference in New Issue
Block a user