This commit is contained in:
2026-05-05 09:02:04 +02:00
parent 4112b5d3d4
commit 14accd3d78
3 changed files with 100 additions and 11 deletions
@@ -710,5 +710,53 @@
{ {
"original": "8908180402 2026-03-05 Tůma, Patrik [Žádanka IPZS] [žádanka o vyšetření zdrav. stavu, invalidita kontrolní, 517 Kč].pdf", "original": "8908180402 2026-03-05 Tůma, Patrik [Žádanka IPZS] [žádanka o vyšetření zdrav. stavu, invalidita kontrolní, 517 Kč].pdf",
"corrected": "8908180402 2026-03-05 Tůma, Patrik [Žádanka IPZS] [žádanka o vyšetření zdrav. stavu, invalidita, cílené, 517 Kč].pdf" "corrected": "8908180402 2026-03-05 Tůma, Patrik [Žádanka IPZS] [žádanka o vyšetření zdrav. stavu, invalidita, cílené, 517 Kč].pdf"
},
{
"original": "400424003 2026-04-26 Faměra, Jiří [LZ interna] [86l, celk. zhoršení, kašel, perimaleol. otoky, flutter síní v anamnéze, EKG sinus, RTG hilová hyperemie].pdf",
"corrected": "400424003 2026-04-26 Faměra, Jiří [LZ interna] [přivezen RZZ, 86l, celk. zhoršení, kašel, perimaleol. otoky, flutter síní v anamnéze, EKG sinus, RTG hilová hyperemie].pdf"
},
{
"original": "415716459 2026-04-29 Kekrtová, Jarmila [Laboratoř] [dg. N394, D-dimery 6.99 mg/l FEU (norma 0.00-0.50), hlášeno].pdf",
"corrected": "415716459 2026-04-29 Kekrtová, Jarmila [Laboratoř] [dg. N394, D-dimery 6.99 mgl FEU (norma 0.00-0.50), hlášeno].pdf"
},
{
"original": "435720013 2026-04-29 Lišková, Jaroslava [Laboratoř] [dg. N309, glukóza 3 (↑), leukocyty 1 (↑), leukocyty sediment 37/ul (norma 0-10)].pdf",
"corrected": "435720013 2026-04-29 Lišková, Jaroslava [Laboratoř] [dg. N309, U_glukóza 3 (↑), U_leukocyty 1 (↑), U_leukocyty sediment 37ul (norma 0-10)].pdf"
},
{
"original": "436114002 2026-04-22 Petrovská, Eliška [diagnostická mamografie] [BI-RADS kat.1, normální nález, typ žlázy BI-RADS B tukově žlázový].pdf",
"corrected": "436114002 2026-04-22 Petrovská, Eliška [mamografie] [BIRADS kat.1, normální nález, typ žlázy BIRADS B tukově žlázový].pdf"
},
{
"original": "475727064 2026-04-08 Povolná, Věra [IADL test] [70 bodů částečná nesoběstačnost v aktivitách denního života].pdf",
"corrected": "475727064 2026-04-08 Povolná, Věra [IADL test] [50 bodů částečná nesoběstačnost v aktivitách denního života].pdf"
},
{
"original": "475727064 2026-04-06 Povolná, Věra [Barthelův test ADL] [100 bodů nezávislý].pdf",
"corrected": "475727064 2026-04-30 Povolná, Věra [Barthelův test ADL] [100 bodů nezávislý].pdf"
},
{
"original": "511212227 2026-04-30 Klimeš, Radko [LZ oční] [dg. H400, V PO 5/7.5, V LO +0.5 5/7.5, Tn PO 18.7 LO 22.7 mmHg, schopen řídit bez brýlí].pdf",
"corrected": "511212227 2026-04-30 Klimeš, Radko [LZ oční] [dg. H400, V PO 57.5, V LO +0.5 57.5, Tn PO 18.7 LO 22.7 mmHg, schopen řídit bez brýlí].pdf"
},
{
"original": "5657111867 2026-04-29 Kocumová, Zdeňka [Laboratoř] [cholesterol 5.40 (↑), MCV 98.9 (↑), MCH 34.1 (↑), U_hustota 1008 (↓)].pdf",
"corrected": "5657111867 2026-04-29 Kocumová, Zdeňka [Laboratoř] [Z000, cholesterol 5.40 (↑), MCV 98.9 (↑), MCH 34.1 (↑), U_hustota 1008 (↓)].pdf"
},
{
"original": "5962050149 2026-04-30 Jelínková, Eva [měření TK] [měření TK ráno i večer, hodnoty 108-133/61-76, 1 tableta 1-0-0].pdf",
"corrected": "5962050149 2026-04-30 Jelínková, Eva [domácí měření TK] [měření TK ráno i večer, hodnoty 108-13361-76, pěkně korigovaný TK, 1 tableta 1-0-0].pdf"
},
{
"original": "7602044780 2026-04-30 Suchý, Vladimír [PZ nefrologie] [SLE se sek. APS, AKI, proliferativní GN, ATN, po 1.+2. cyklu CFA Eurolupus, renální biopsie].pdf",
"corrected": "7602044780 2026-04-30 Suchý, Vladimír [LZ nefrologie] [SLE se sek. APS, AKI, proliferativní GN, ATN, po 1.+2. cyklu CFA Eurolupus, renální biopsie].pdf"
},
{
"original": "9705081617 Krob, Milan split_005.pdf",
"corrected": "9705081617 Krob, Milan [EKG] [normální křivka].pdf"
},
{
"original": "9705081617 null Krob, Milan [PZ pediatrie] [prematuritas gravis, dystrofie, psychomotorická retardace, susp. autistické rysy, ptosa, strabismus, amblyopie, tarsektomie l.dx].pdf",
"corrected": "9705081617 2002-04-26 Krob, Milan [LZ pediatrie] [prematuritas gravis, dystrofie, psychomotorická retardace, susp. autistické rysy, ptosa, strabismus, amblyopie, tarsektomie l.dx].pdf"
} }
] ]
@@ -231,7 +231,7 @@ def build_corrections_prompt() -> str:
# ─── Claude Vision API ──────────────────────────────────────────────────────── # ─── Claude Vision API ────────────────────────────────────────────────────────
def extract_info(pdf_path: Path) -> dict: def extract_info(pdf_path: Path, known_patient: str | None = None, known_rc: str | None = None) -> dict:
print(" Převádím na obrázek...") print(" Převádím na obrázek...")
suffix = pdf_path.suffix.lower() suffix = pdf_path.suffix.lower()
if suffix in (".jpg", ".jpeg", ".png"): if suffix in (".jpg", ".jpeg", ".png"):
@@ -248,9 +248,27 @@ def extract_info(pdf_path: Path) -> dict:
gc.collect() gc.collect()
image_b64 = base64.standard_b64encode(buf.getvalue()).decode("utf-8") image_b64 = base64.standard_b64encode(buf.getvalue()).decode("utf-8")
if known_patient and known_rc:
# Identita pacienta je známa z názvu souboru — Claude se soustředí jen na obsah zprávy
patient_hint = (
f"Pacient je již znám: RČ={known_rc}, jméno={known_patient}. "
f"Pole \"jmeno\" nastav na \"{known_patient}\" a \"rodne_cislo\" na \"{known_rc}\". "
f"Soustřeď se hlavně na datum zprávy, typ dokumentu a klinickou poznámku.\n"
)
nazev_format = (
f"\"{known_rc} {{datum_zpravy}} {known_patient} [{{typ_dokumentu}}] [{{poznamka}}].pdf\""
)
else:
patient_hint = ""
nazev_format = (
"\"{rodne_cislo} {datum_zpravy} {Příjmení}, {Jméno} [{typ_dokumentu}] [{poznamka}].pdf\" "
"(jméno bez titulu, RČ bez lomítka)"
)
prompt = ( prompt = (
load_naming_rules() + load_naming_rules() +
build_corrections_prompt() + build_corrections_prompt() +
patient_hint +
"Toto je naskenovaná lékařská zpráva v češtině. " "Toto je naskenovaná lékařská zpráva v češtině. "
"Vrať JSON s těmito poli:\n" "Vrať JSON s těmito poli:\n"
"- \"jmeno\": celé jméno pacienta (příjmení + jméno + případný titul)\n" "- \"jmeno\": celé jméno pacienta (příjmení + jméno + případný titul)\n"
@@ -265,9 +283,7 @@ def extract_info(pdf_path: Path) -> dict:
"DŮLEŽITÉ: pokud zpráva obsahuje sekci \"Závěr:\" nebo \"Závěr vyšetření:\", " "DŮLEŽITÉ: pokud zpráva obsahuje sekci \"Závěr:\" nebo \"Závěr vyšetření:\", "
"použij VÝHRADNĚ obsah této sekce — je nejdůležitější. " "použij VÝHRADNĚ obsah této sekce — je nejdůležitější. "
"Teprve pokud závěr chybí, shrň obsah z celé zprávy.\n" "Teprve pokud závěr chybí, shrň obsah z celé zprávy.\n"
"- \"nazev_souboru\": název souboru ve formátu " f"- \"nazev_souboru\": název souboru ve formátu {nazev_format}\n"
"\"{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 " "- \"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" "(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." "Pokud pole nenajdeš, použij null. Nepiš nic jiného než JSON."
@@ -357,6 +373,19 @@ def run_variant_picker(variants_data: list) -> str | None:
return json.loads(out).get("chosen") if out else None return json.loads(out).get("chosen") if out else None
# ─── Detekce split názvu ──────────────────────────────────────────────────────
# Vzor: "7952090443 Kalousová, Eva split_001.pdf"
_SPLIT_RE = re.compile(r"^(\d{9,10})\s+(.+?)\s+split_\d+\.pdf$", re.IGNORECASE)
def _parse_split_filename(name: str) -> tuple[str, str] | None:
"""Vrátí (rc_digits, 'Příjmení, Jméno') nebo None."""
m = _SPLIT_RE.match(name)
if m:
return m.group(1), m.group(2)
return None
# ─── Hlavní flow ────────────────────────────────────────────────────────────── # ─── Hlavní flow ──────────────────────────────────────────────────────────────
def process_file(pdf_path: Path): def process_file(pdf_path: Path):
@@ -369,17 +398,27 @@ def process_file(pdf_path: Path):
preview, geom_file = open_preview(pdf_path) preview, geom_file = open_preview(pdf_path)
below_y = read_preview_bottom(geom_file) below_y = read_preview_bottom(geom_file)
# 2. Claude Vision API # 2. Zjisti RČ a jméno — buď z názvu (split soubor) nebo přes Claude Vision API
info = extract_info(pdf_path) split = _parse_split_filename(pdf_path.name)
nazev = info.get("nazev_souboru") or pdf_path.name if split:
rc_from_scan, name_from_filename = split
print(f" Split soubor — RČ z názvu: {rc_from_scan}, jméno: {name_from_filename}")
# Claude stále voláme, ale předáme mu identitu pacienta — ať se soustředí na obsah
info = extract_info(pdf_path, known_patient=name_from_filename, known_rc=rc_from_scan)
# RC a jméno bereme z názvu souboru, ne z Claudovy odpovědi
nazev = info.get("nazev_souboru") or pdf_path.name
nazev = re.sub(r"^\d{9,10}\s+", f"{rc_from_scan} ", nazev) # přepiš RC v názvu naším
else:
info = extract_info(pdf_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Č # 3. Medicus ověření + fuzzy matching RČ
rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...") print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...")
verif = verify_patient(rc_from_scan) verif = verify_patient(rc_from_scan)
# Oprava RČ při fuzzy matchi # Oprava RČ při fuzzy matchi (jen pro nesplit soubory — u split máme RC spolehlivé)
if verif["status"] == "fuzzy" and verif.get("rc_corrected") and nazev: if not split and verif["status"] == "fuzzy" and verif.get("rc_corrected") and nazev:
nazev = nazev.replace(rc_from_scan, verif["rc_corrected"], 1) nazev = nazev.replace(rc_from_scan, verif["rc_corrected"], 1)
print(f" → RČ opraveno: {rc_from_scan}{verif['rc_corrected']}") print(f" → RČ opraveno: {rc_from_scan}{verif['rc_corrected']}")
@@ -387,6 +426,8 @@ def process_file(pdf_path: Path):
status = verif["status"] status = verif["status"]
patient = verif.get("patient") patient = verif.get("patient")
info_lines = [] info_lines = []
if split:
info_lines.append(f"⚡ Split soubor — identita z názvu: {name_from_filename} | RČ {rc_from_scan}")
if status == "ok": if status == "ok":
info_lines.append(f"✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}") info_lines.append(f"✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
elif status == "fuzzy": elif status == "fuzzy":
+1 -1
View File
@@ -12,7 +12,7 @@ Tato pravidla platí vždy při generování polí `poznamka` a `nazev_souboru`.
- Pokud je datum přijetí a propuštění ve stejném měsíci, stačí: `[1215APR2026 ...]` - Pokud je datum přijetí a propuštění ve stejném měsíci, stačí: `[1215APR2026 ...]`
- Pokud datum hospitalizace nelze určit, druhou závorku napiš bez datumu. - Pokud datum hospitalizace nelze určit, druhou závorku napiš bez datumu.
3. Když je dokument typ "Laboratoř", do `poznamka` uváděj POUZE hodnoty mimo normu (patologické nálezy) — hodnoty v normě vynech. Osmolalitu séra nikdy nezmiňuj, ani když je mimo normu. 3. Když je dokument typ "Laboratoř", do `poznamka` uváděj POUZE hodnoty mimo normu (patologické nálezy) — hodnoty v normě vynech. **Osmolalitu séra (Osmolalita, Osm, osmolality) NIKDY nezmiňuj ani když je mimo normu, ani v jakékoli zkratce.** Toto je absolutní výjimka: osmolalita se do názvu souboru ani do poznámky nepíše nikdy za žádných okolností. Chybně: `C_Osmolalita 293 (↑)` — správně: tuto hodnotu zcela vynech.
4. Pokud laboratorní výsledky obsahují glomerulární filtraci — bývá označena jako eGFR, CKD-EPI nebo CK-EPI — do `poznamka` nikdy nepiš číselnou hodnotu eGFR. Místo toho uveď pouze klasifikaci: eGFR ≥ 90 → CHRIG1, 6089 → CHRIG2, 4559 → CHRIG3a, 3044 → CHRIG3b, 1529 → CHRIG4, < 15 → CHRIG5. Klasifikaci uváděj pouze pokud je CHRIG2 nebo horší (tj. eGFR < 90) — CHRIG1 je v normě, nezmiňuj ho. 4. Pokud laboratorní výsledky obsahují glomerulární filtraci — bývá označena jako eGFR, CKD-EPI nebo CK-EPI — do `poznamka` nikdy nepiš číselnou hodnotu eGFR. Místo toho uveď pouze klasifikaci: eGFR ≥ 90 → CHRIG1, 6089 → CHRIG2, 4559 → CHRIG3a, 3044 → CHRIG3b, 1529 → CHRIG4, < 15 → CHRIG5. Klasifikaci uváděj pouze pokud je CHRIG2 nebo horší (tj. eGFR < 90) — CHRIG1 je v normě, nezmiňuj ho.
5. Když je dokument typ "Laboratoř" a zpráva obsahuje diagnózu (dg., dg:, diagnóza), umísti ji do `nazev_souboru` jako první část druhé závorky, tedy: `[Laboratoř] [dg. XY00 - stručná poznamka]`. 5. Když je dokument typ "Laboratoř" a zpráva obsahuje diagnózu (dg., dg:, diagnóza), umísti ji do `nazev_souboru` jako první část druhé závorky, tedy: `[Laboratoř] [dg. XY00 - stručná poznamka]`.
6. Zkratky a pojmenování: slovo „sono" (sonografie/ultrazvuk) piš vždy malými písmeny — `sono břicha`, `sono ŠŽ`, nikoli `SONO`. Štítnou žlázu označuj vždy zkratkou `ŠŽ`. Sonografii prsu/prsů (sono mamm., sono mamografie, sono mamma apod.) piš vždy jako `sono prsů`. Denzitometrii (DEXA, DXA, denzitometrie) piš vždy pouze jako `[DXA]` — bez prefixu LZ. Algologii piš vždy jako `[LZ léčba bolesti]`. Dermatovenerologii (dermatologie, dermatovenerologie, kožní) piš vždy jako `[LZ kožní]`. Angiologii piš vždy jako `[LZ cévní]`. 6. Zkratky a pojmenování: slovo „sono" (sonografie/ultrazvuk) piš vždy malými písmeny — `sono břicha`, `sono ŠŽ`, nikoli `SONO`. Štítnou žlázu označuj vždy zkratkou `ŠŽ`. Sonografii prsu/prsů (sono mamm., sono mamografie, sono mamma apod.) piš vždy jako `sono prsů`. Denzitometrii (DEXA, DXA, denzitometrie) piš vždy pouze jako `[DXA]` — bez prefixu LZ. Algologii piš vždy jako `[LZ léčba bolesti]`. Dermatovenerologii (dermatologie, dermatovenerologie, kožní) piš vždy jako `[LZ kožní]`. Angiologii piš vždy jako `[LZ cévní]`.