This commit is contained in:
2026-06-03 09:32:25 +02:00
parent a29a6845a1
commit d16038d09c
3 changed files with 183 additions and 46 deletions
@@ -89,8 +89,11 @@ CORRECTIONS_FILE = Path(__file__).parent / "corrections.json"
NAMING_RULES_FILE = Path(__file__).parent / "naming_rules.md"
DOKUMENTACE = _DROPBOX / r"Ordinace\Dokumentace_zpracovaná"
import concurrent.futures
import threading
_total_cost: float = 0.0 # kumulativní náklady za celé spuštění (USD)
_dokumentace_index: set[str] = set()
_dokumentace_ready = threading.Event()
@@ -177,24 +180,26 @@ class PdfPane:
font=("Segoe UI", 8), wraplength=max_w - 10)
self.lbl_title.pack(pady=(2, 0))
# frame_nav musí být packed jako side="bottom" PŘED image labelem,
# jinak ho expand=True na lbl_img vytlačí mimo viditelnou oblast
frame_nav = tk.Frame(self.frame, bg=bg)
frame_nav.pack(side="bottom", pady=4)
self.lbl_page = tk.Label(frame_nav, text="", bg=bg, fg="#aaa", font=("Segoe UI", 9))
self.lbl_page.pack(side="left", padx=6)
self.btn_prev = tk.Button(frame_nav, text="◄ Předchozí", bg="#444", fg="#fff",
relief="flat", padx=8, pady=3, font=("Segoe UI", 9),
command=self._prev)
self.btn_prev.pack(side="left", padx=2)
self.btn_next = tk.Button(frame_nav, text="Další ►", bg="#444", fg="#fff",
relief="flat", padx=8, pady=3, font=("Segoe UI", 9),
command=self._next)
self.btn_next.pack(side="left", padx=2)
self.lbl_img = tk.Label(self.frame, bg=bg)
self.lbl_img.pack(fill="both", expand=True)
frame_nav = tk.Frame(self.frame, bg=bg)
frame_nav.pack(pady=2)
self.lbl_page = tk.Label(frame_nav, text="", bg=bg, fg="#aaa", font=("Segoe UI", 8))
self.lbl_page.pack(side="left", padx=4)
self.btn_prev = tk.Button(frame_nav, text="", bg="#333", fg="#fff",
relief="flat", padx=4, font=("Segoe UI", 8),
command=self._prev)
self.btn_prev.pack(side="left", padx=1)
self.btn_next = tk.Button(frame_nav, text="", bg="#333", fg="#fff",
relief="flat", padx=4, font=("Segoe UI", 8),
command=self._next)
self.btn_next.pack(side="left", padx=1)
def load(self, path_str: str, title: str = ""):
from PIL import ImageTk
if self.doc:
@@ -975,7 +980,10 @@ def extract_info(pdf_path: Path, known_patient: str | None = None, known_rc: str
]}],
)
usage = response.usage
print(f" Tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${usage.input_tokens*3/1e6 + usage.output_tokens*15/1e6:.4f}")
call_cost = usage.input_tokens * 3 / 1e6 + usage.output_tokens * 15 / 1e6
global _total_cost
_total_cost += call_cost
print(f" Tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${call_cost:.4f}")
raw = response.content[0].text.strip()
if raw.startswith("```"):
@@ -1040,7 +1048,10 @@ def generate_name_variants(info: dict, nazev_prvni: str) -> list[str]:
messages=[{"role": "user", "content": prompt}],
)
usage = response.usage
print(f" Varianty — tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${usage.input_tokens*3/1e6 + usage.output_tokens*15/1e6:.4f}")
call_cost = usage.input_tokens * 3 / 1e6 + usage.output_tokens * 15 / 1e6
global _total_cost
_total_cost += call_cost
print(f" Varianty — tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${call_cost:.4f}")
raw = response.content[0].text.strip()
if raw.startswith("```"):
@@ -1162,26 +1173,20 @@ def _parse_split_filename(name: str) -> tuple[str, str] | None:
# ─── Hlavní flow ──────────────────────────────────────────────────────────────
def process_file(pdf_path: Path):
def _analyze_file(pdf_path: Path) -> dict:
"""Fáze 1: Claude API + Medicus ověření. Vhodné pro spuštění na pozadí."""
print(f"\nSoubor: {pdf_path.name}")
# Spusť načítání indexu dokumentace na pozadí — hotovo za dobu volání Claude/OCR
start_dokumentace_index()
is_ekg = _is_ekg(pdf_path)
split = None
if is_ekg:
# EKG větev: rotace in-place PŘED preview, pak Tesseract OCR + Medicus
print(" [EKG] Detekován EKG soubor (PDFCreator).")
info = extract_info_ekg(pdf_path)
nazev = info.get("nazev_souboru") or pdf_path.name
rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
verif = info["_verif"]
rc_ocr = info["_rc_ocr"]
if not is_ekg:
# 2. Zjisti RČ a jméno — buď z názvu (split soubor) nebo přes Claude Vision API
else:
split = _parse_split_filename(pdf_path.name)
if split:
rc_from_scan, name_from_filename = split
@@ -1194,17 +1199,14 @@ def process_file(pdf_path: Path):
nazev = info.get("nazev_souboru") or pdf_path.name
rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
# 3. Medicus ověření + fuzzy matching RČ
print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...")
verif = verify_patient(rc_from_scan)
rc_ocr = rc_from_scan
# Oprava RČ při fuzzy matchi (jen pro nesplit soubory — u split máme RC spolehlivé)
if not split and verif["status"] == "fuzzy" and verif.get("rc_corrected") and nazev:
nazev = nazev.replace(rc_from_scan, verif["rc_corrected"], 1)
print(f" → RČ opraveno: {rc_from_scan}{verif['rc_corrected']}")
# Fallback: RČ nenalezeno → zkus vyhledat podle jména z Claude
if verif["status"] == "not_found" and info.get("jmeno"):
jmeno_z_claude = info["jmeno"]
print(f" RČ nenalezeno, zkouším vyhledat dle jména: {jmeno_z_claude}")
@@ -1216,20 +1218,18 @@ def process_file(pdf_path: Path):
print(f" → Nalezeno dle jména: {p['prijmeni']} {p['jmeno']} | RČ {p['rodcis']}")
if verif_name["status"] == "by_name_multi":
print(f" ⚠ Více shod ({len(verif_name['all_matches'])}) — přijat první výsledek")
# Aktualizuj RČ v navrženém názvu
if nazev and rc_z_medicus:
nazev = re.sub(r"^null\s*", rc_z_medicus + " ", nazev)
if not nazev.startswith(rc_z_medicus):
nazev = re.sub(r"^\S+\s*", rc_z_medicus + " ", nazev)
# Info řádky pro dialog
status = verif["status"]
patient = verif.get("patient")
info_lines = []
if is_ekg:
info_lines.append("⚡ EKG soubor — Tesseract OCR")
elif split:
info_lines.append(f"⚡ Split soubor — identita z názvu: {name_from_filename} | RČ {rc_from_scan}")
info_lines.append(f"⚡ Split soubor — identita z názvu: {split[1]} | RČ {rc_from_scan}")
if status == "ok":
info_lines.append(f"✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
elif status == "fuzzy":
@@ -1245,18 +1245,34 @@ def process_file(pdf_path: Path):
else:
info_lines.append("— Medicus nedostupný (offline)")
# Duplicity
rc_final = re.sub(r"\D", "", verif["patient"]["rodcis"] if patient else rc_from_scan)
duplicity = check_duplicates(rc_final, info.get("datum_zpravy") or "")
if not info_lines:
info_lines = ["[uprav ručně]"]
# 2. volání Claude — varianty názvů (jen pro non-EKG dokumenty s výsledkem z 1. volání)
varianty = []
if not is_ekg and nazev:
varianty = generate_name_variants(info, nazev)
return {
"path": pdf_path,
"is_ekg": is_ekg,
"nazev": nazev,
"info_lines": info_lines,
"duplicity": duplicity,
"varianty": varianty,
}
def _present_file(analyzed: dict):
"""Fáze 2: Zobrazí UI, komprese, uloží soubor."""
pdf_path: Path = analyzed["path"]
nazev: str = analyzed["nazev"]
info_lines: list = analyzed["info_lines"]
duplicity: list = analyzed["duplicity"]
varianty: list = analyzed["varianty"]
print(" Otevírám hlavní viewer...")
final_name = run_main_viewer(
original_path=pdf_path,
@@ -1274,7 +1290,6 @@ def process_file(pdf_path: Path):
final_name += ".pdf"
final_name = re.sub(r'[<>:"/\\|?*]', '', final_name)
# Ulož korekci jen pokud se finální název liší od VŠECH navržených variant
def _norm_name(s: str) -> str:
return (s or "").strip().removesuffix(".pdf").strip()
@@ -1284,7 +1299,6 @@ def process_file(pdf_path: Path):
print(f" Schválený název: {final_name}")
# 4. Generuj kompresní varianty (originál + 5 variant)
print(" Generuji kompresní varianty...")
temp_files = []
orig_kb = round(pdf_path.stat().st_size / 1024)
@@ -1296,7 +1310,6 @@ def process_file(pdf_path: Path):
variants_data.append({"path": str(tmp), "label": label, "size_kb": size_kb})
print(f" {label}: {size_kb} kB")
# 5. Vyber variantu
print(" Vyber variantu v okně...")
chosen = run_variant_picker(variants_data)
@@ -1306,7 +1319,6 @@ def process_file(pdf_path: Path):
t.unlink(missing_ok=True)
return
# 6. Ulož do Processed
PROCESSED.mkdir(exist_ok=True)
dest = PROCESSED / final_name
if dest.exists():
@@ -1317,28 +1329,61 @@ def process_file(pdf_path: Path):
print(f" ✓ Uloženo: {dest.name}")
for t in temp_files:
t.unlink(missing_ok=True) # originál mezi temp_files není, je bezpečné
t.unlink(missing_ok=True)
def process_file(pdf_path: Path):
"""Zpracuje jeden soubor (sekvenčně — pro přímé spuštění s argumentem)."""
start_dokumentace_index()
_present_file(_analyze_file(pdf_path))
def process_folder(folder: Path):
"""Zpracuje celou složku s prefetch pipelinou — analýza N+1 běží na pozadí
zatímco uživatel pracuje s viewerem/kompresí pro soubor N."""
files = sorted(f for f in folder.iterdir() if f.suffix.lower() in (".pdf", ".jpg", ".jpeg", ".png"))
if not files:
print(f"Žádné soubory v: {folder}")
return
print(f"Nalezeno {len(files)} soubor(ů).")
for f in files:
try:
process_file(f)
except Exception as e:
print(f" CHYBA: {e}")
# Index dokumentace načteme jednou pro celou dávku
start_dokumentace_index()
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# Spusť analýzu prvního souboru ihned
next_future: concurrent.futures.Future = executor.submit(_analyze_file, files[0])
for i, f in enumerate(files):
# Počkej na dokončení analýzy aktuálního souboru
try:
analyzed = next_future.result()
except Exception as e:
print(f" CHYBA při analýze {f.name}: {e}")
if i + 1 < len(files):
next_future = executor.submit(_analyze_file, files[i + 1])
continue
# Ihned spusť analýzu dalšího souboru na pozadí —
# proběhne během toho, co uživatel pracuje s viewerem a kompresí
if i + 1 < len(files):
next_future = executor.submit(_analyze_file, files[i + 1])
try:
_present_file(analyzed)
except Exception as e:
print(f" CHYBA při zpracování {f.name}: {e}")
print("\nHotovo.")
if __name__ == "__main__":
import time as _time
PROCESSED.mkdir(exist_ok=True)
TO_PROCESS.mkdir(exist_ok=True)
target = Path(sys.argv[1]) if len(sys.argv) > 1 else TO_PROCESS
_start = _time.time()
if target.is_file():
process_file(target)
@@ -1347,3 +1392,9 @@ if __name__ == "__main__":
else:
print("Použití: python extract_patient_info_novy.py [soubor.pdf nebo složka]")
sys.exit(1)
elapsed = _time.time() - _start
print(f"\n{''*50}")
print(f" Celková cena API: ${_total_cost:.4f} ({_total_cost*25:.2f} Kč)")
print(f" Celková doba: {int(elapsed//60)}m {int(elapsed%60)}s")
print(f"{''*50}")
@@ -1742,5 +1742,73 @@
{
"original": "320312460 2026-05-20 Vlachovský, Ladislav [LZ kardiologie] [kontrola, EF 65%, diastol. dysfunkce, lehká MR, RBBB, kontrola za 6 měs.].pdf",
"corrected": "320312460 2026-05-20 Vlachovský, Ladislav [LZ kardiologie] [kontrola, EF 65% diastol. dysfunkce RBBB lehká MR, konzervat. postup kontrola 6 měs.].pdf"
},
{
"original": "325309100 2026-08-26 Maturová, Jaroslava [LZ interna] [kontrola, HFmrEF při ICHS elevace BNP, perzist. FiS eufrekvenční, ICHS].pdf",
"corrected": "325309100 2026-04-21 Maturová, Jaroslava [LZ interna] [kontrola, HFmrEF při ICHS elevace BNP, perzist. FiS eufrekvenční, ICHS].pdf"
},
{
"original": "325505726 2026-05-27 Mimrová, Ružena [poukaz DP] [omezená mobilita nejistá chůze, aplikace injekcí a odběr krve doma].pdf",
"corrected": "325505726 2026-05-27 Mimrová, Ružena [Domácí péče] [7 do 31MAY2026, 06313 ad hoc, 06323 ad hoc, omezená mobilita nejistá chůze, aplikace injekcí a odběr krve doma].pdf"
},
{
"original": "435225133 2026-05-14 Tichá, Věra [PZ oddělení] [14MAY2026 generalizovaná ateroskleróza, Ca endometria, metastázy OS, fraktura femuru].pdf",
"corrected": "435225133 2026-05-25 Tichá, Věra [PZ následná péče] [14MAY-25MAY2026 generalizovaná ateroskleróza, Ca endometria, metastázy OS, fraktura femuru, exitus letalis 25MAY2026].pdf"
},
{
"original": "455530096 2026-05-27 Vlachovská, Miroslava [LZ ortopedie] [kontrola, St.p. TEP coxae bilat., RTG bez uvolnění, Depo/Meso P SI kl.].pdf",
"corrected": "455530096 2026-05-27 Vlachovská, Miroslava [LZ ortopedie] [kontrola, St.p. TEP coxae bilat., RTG bez uvolnění, DepoMeso P SI kl.].pdf"
},
{
"original": "5655300222 2026-05-27 Kreibichová, Magdalena [Laboratoř] [dg. E789, P_Glukóza 6.5 (↑), CKD-EPI 1.47 ml/s → CHRIG2].pdf",
"corrected": "5655300222 2026-05-27 Kreibichová, Magdalena [Laboratoř] [dg. E789, P_Glukóza 6.5 (↑), CKD-EPI 1.47 mls → CHRIG2].pdf"
},
{
"original": "8956039037 2026-05-12 Slavíková, Zuzana [LZ revmatologie] [kontrola, primární SjS a vaskulitida, remise MALT lymfomu parotidy po Rituximabu, ko jaro2027].pdf",
"corrected": "8956039037 2026-05-12 Slavíková, Zuzana [LZ revmatologie] [kontrola, primární SjS a vaskulitida, remise MALT lymfomu parotidy po Rituximabu, ko +6m].pdf"
},
{
"original": "496219079 2026-06-02 Jindrová, Jiskra [EKG] [bez hodnocení].pdf",
"corrected": "496219079 2026-06-02 Jindrová, Jiskra [EKG] [bez hodnocení].pdf"
},
{
"original": "0161270054 2026-06-02 Škopková, Denisa [EKG] [bez hodnocení].pdf",
"corrected": "0161270054 2026-06-02 Škopková, Denisa [EKG] [bez hodnocení].pdf"
},
{
"original": "1006055083 2010-10-12 Šmíd, Jiří [Očkovací průkaz] [Prevenar 13: I. 12-10-2010, II. 01-12-2010, III. 25-01-2011, IV. 06-09-2011].pdf",
"corrected": "1006055083 2026-06-03 Šmíd, Jiří [Očkovací průkaz] [Prevenar 13 I. 12-10-2010, II. 01-12-2010, III. 25-01-2011, IV. 06-09-2011].pdf"
},
{
"original": "460614110 2026-04-20 Galus, Karel [PZ kožní] [1720APR2026 scabies dermatoskopicky verifikovaný, léčba sirnou kúrou].pdf",
"corrected": "460614110 2026-04-20 Galus, Karel [PZ kožní] [1720APR2026 scabies SVRAB dermatoskopicky verifikovaný, léčba sirnou kúrou].pdf"
},
{
"original": "465917444 2026-05-25 Trojková, Jana [DXA] [BMD bed. páteř T-skore -2.5 osteoporóza, krček femuru bilat. T-skore -1.6/-1.9 osteopenie].pdf",
"corrected": "465917444 2026-05-25 Trojková, Jana [DXA] [BMD bed. páteř T-skore -2.5 osteoporóza, krček femuru bilat. T-skore -1.6-1.9 osteopenie].pdf"
},
{
"original": "465917444 2026-05-26 Trojková, Jana [LZ urologie] [kontrola, ca renis dx. pT1bN0M0, recid. IMC, CT 03/25 bez recidivy, ko za6m].pdf",
"corrected": "465917444 2026-05-26 Trojková, Jana [LZ urologie] [kontrola, ca renis dx. pT1bN0M0, recid. IMC, CT 0325 bez recidivy, ko za6m].pdf"
},
{
"original": "5811180100 2026-05-28 Dalecký, Milan [LZ nefrologie] [kontrola, IgA nefropatie, CKD, kreatinin 116, ACR stabilní, TK holter 133/70].pdf",
"corrected": "5811180100 2026-05-28 Dalecký, Milan [LZ nefrologie] [kontrola, IgA nefropatie, CKD, kreatinin 116, ACR stabilní, TK holter 13370].pdf"
},
{
"original": "6861010288 Štefanská, Renáta split_016.pdf",
"corrected": "6861010288 2026-06-02 Štefanská, Renáta [LZ plicní] [akutní exacerbace chronické bronchitidy, atb].pdf"
},
{
"original": "split_012.pdf",
"corrected": "460614110 2026-06-03 Galus, Karel [přehled užívané medikace] [od pacienta].pdf"
},
{
"original": "split_021.pdf",
"corrected": "7101062386 2026-06-03 Schod, Pavel [domácí měření TK] [zjevná hypertenze].pdf"
},
{
"original": "5751211807 2026-06-02 Hnízdová, Eva [PZ kardiologie] [0102JUN2026 EFV/RFA pro susp. FAT, AVNRT po RFA pomalé dráhy 0925].pdf",
"corrected": "5751211807 2026-06-02 Hnízdová, Eva [PZ kardiologie] [0102JUN2026 EFVRFA pro susp. FAT, AVNRT po RFA pomalé dráhy 0925].pdf"
}
]
+20 -2
View File
@@ -40,8 +40,26 @@ Tato pravidla platí vždy při generování polí `poznamka` a `nazev_souboru`.
- `akutní` — pacient přichází do akutní ambulance nebo cestou RZS/záchranné služby
- Pokud typ návštěvy není ve zprávě uveden, tuto část zcela vynech (nepsat žádný fallback).
b) **Hlavní diagnóza** — získej z části „Diagnózy", „Závěr" nebo „Dg." — uveď první (hlavní) diagnózu, která je obvykle důvodem návštěvy. Stručně, výstižně.
c) **Co je domluveno dále** — z části „Doporučení", „Plán", „Závěr" apod.: další kontrola, doporučené vyšetření, změna léčby apod. Stručně.
- Příklad (s typem návštěvy): `[LZ kardiologie] [kontrola, ICHS, EKG za 3 měsíce]`
c) **Termín příští plánované kontroly** — pokud je na konci dokumentu uveden konkrétní plánovaný termín příští kontroly (např. „jaro 2027", „za 3 měsíce", „ročně"), umísti ho jako **poslední část druhé závorky**.
- Uváděj pouze explicitně naplánované termíny — formát: `ko` + termín bez mezery, např. `ko jaro2027`, `ko za6m`, `ko ročně`.
- **Nezahrn** podmíněné návštěvy jako „dle obtíží", „při zhoršení", „při hematurii ihned" apod. — ty jsou samozřejmé a do názvu nepatří.
- Pokud dokument žádný plánovaný termín neobsahuje, tuto část vynech.
- Příklad (s typem návštěvy): `[LZ kardiologie] [kontrola, ICHS, ko za3m]`
- Příklad (bez typu návštěvy): `[LZ neurologie] [migréna, pokračovat v léčbě]`
- Příklad akutní: `[LZ interna] [akutní, dekompenzovaná hypertenze, hospitalizace]`
- Příklad s termínem kontroly: `[LZ urologie] [kontrola, hematurie microsc., angiomyolipoma renis, ko jaro2027]`
- Pro PZ zůstává datum hospitalizace jako první (před typem návštěvy), viz pravidlo 2.
11. Datum v názvu souboru nesmí být v budoucnosti: Pokud datum nalezené na zprávě a navrhované pro název souboru je pozdější než dnešní datum, je to chyba (např. špatně rozpoznané číslo). Hledej na zprávě jiné datum. Pokud žádné vhodné datum nenajdeš, použij dnešní datum.
12. Poukaz domácí péče (DP): Dokument nadepsaný „POUKAZ NA VYŠETŘENÍ / OŠETŘENÍ DP" nebo „poukaz domácí péče" se pojmenovává takto:
- První závorka: vždy `[domácí péče]` (bez prefixu LZ/PZ).
- Datum souboru: pole „Datum" na poukazu (datum vystavení), ve formátu YYYY-MM-DD.
- Druhá závorka obsahuje v tomto pořadí, odděleno čárkou:
a) **Číslo poukazu** — pole „Pořadové číslo poukazu" (celé číslo, např. `1`).
b) **Platnost** — „do DDMMMYYYY" kde datum je z pole „Platnost do" (měsíc třemi velkými písmeny anglicky, bez mezer), např. `do 30JUN2026`.
c) **Výkony** — každý výkon (kód ze sloupce „Požadováno") se uvede jako:
- `{kód} ad hoc` — pokud je u výkonu uvedeno **0x týdně** (bez ohledu na četnost denně); znamená to výkon pouze dle potřeby, ne na pravidelné bázi.
- `{kód} {N}xd{M}xt` — pokud je týdenní četnost M > 0; N = četnost denně, M = četnost týdně. Např. pro 1x denně 3x týdně: `06313 1xd3xt`.
- Příklad (oba výkony ad hoc): `[domácí péče] [1 do 30JUN2026, 06313 ad hoc, 06323 ad hoc]`
- Příklad (pravidelné výkony): `[domácí péče] [2 do 31AUG2026, 06313 1xd5xt, 06321 2xd7xt]`