diff --git a/Medevio/60 ScansProcessing/Extract_pacient_info_v1.0.py b/Medevio/60 ScansProcessing/Extract_pacient_info_v1.0.py index 3b497df..172661d 100644 --- a/Medevio/60 ScansProcessing/Extract_pacient_info_v1.0.py +++ b/Medevio/60 ScansProcessing/Extract_pacient_info_v1.0.py @@ -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}") diff --git a/Medevio/60 ScansProcessing/corrections.json b/Medevio/60 ScansProcessing/corrections.json index f6bbb9e..5482e67 100644 --- a/Medevio/60 ScansProcessing/corrections.json +++ b/Medevio/60 ScansProcessing/corrections.json @@ -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í] [17–20APR2026 scabies dermatoskopicky verifikovaný, léčba sirnou kúrou].pdf", + "corrected": "460614110 2026-04-20 Galus, Karel [PZ kožní] [17–20APR2026 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] [01–02JUN2026 EFV/RFA pro susp. FAT, AVNRT po RFA pomalé dráhy 0925].pdf", + "corrected": "5751211807 2026-06-02 Hnízdová, Eva [PZ kardiologie] [01–02JUN2026 EFVRFA pro susp. FAT, AVNRT po RFA pomalé dráhy 0925].pdf" } ] \ No newline at end of file diff --git a/Medevio/60 ScansProcessing/naming_rules.md b/Medevio/60 ScansProcessing/naming_rules.md index 34651a7..7c39dd6 100644 --- a/Medevio/60 ScansProcessing/naming_rules.md +++ b/Medevio/60 ScansProcessing/naming_rules.md @@ -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]`