z230
This commit is contained in:
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
Kombinovaný viewer: náhled originálu + náhled duplicit + listbox duplicit.
|
||||
Nahrazuje preview_viewer.py pro hlavní dokument.
|
||||
|
||||
Spouští se jako subprocess z extract_patient_info_novy_test.py.
|
||||
Argumenty: duplicity_viewer.py <json_soubor> [--write-geometry=<cesta>]
|
||||
|
||||
JSON vstup: {
|
||||
"original": "cesta/k/originalu.pdf", # povinné
|
||||
"duplicity": ["cesta1.pdf", ...], # volitelné, může být prázdné
|
||||
"labels": ["název1.pdf", ...] # zobrazované názvy v listboxu
|
||||
}
|
||||
|
||||
Výstup geometrie (spodní hrana) do --write-geometry souboru pro rename_dialog.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import tkinter as tk
|
||||
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
from ctypes import windll
|
||||
windll.shcore.SetProcessDpiAwareness(1)
|
||||
except Exception:
|
||||
pass
|
||||
import io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
def _open_doc(path_str: str):
|
||||
"""Otevře PDF nebo obrázek, vrátí (fitz.Document nebo None, PIL.Image nebo None)."""
|
||||
import fitz
|
||||
from PIL import Image
|
||||
p = Path(path_str)
|
||||
if not p.exists():
|
||||
return None, None
|
||||
suffix = p.suffix.lower()
|
||||
if suffix in (".jpg", ".jpeg", ".png"):
|
||||
return None, Image.open(p)
|
||||
return fitz.open(str(p)), None
|
||||
|
||||
|
||||
def _render_page(doc, pil_img, page_n: int, max_w: int, max_h: int):
|
||||
"""Vykreslí stránku, vrátí PIL.Image."""
|
||||
import fitz
|
||||
from PIL import Image
|
||||
if doc is not None:
|
||||
page = doc[page_n]
|
||||
zoom = min(max_w / page.rect.width, max_h / page.rect.height)
|
||||
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
||||
return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
|
||||
elif pil_img is not None:
|
||||
img = pil_img.copy()
|
||||
img.thumbnail((max_w, max_h))
|
||||
return img
|
||||
return None
|
||||
|
||||
|
||||
class PdfPane:
|
||||
"""Jeden panel s náhledem PDF + navigací stránek."""
|
||||
|
||||
def __init__(self, parent, max_w: int, max_h: int, bg: str = "#222"):
|
||||
self.max_w = max_w
|
||||
self.max_h = max_h
|
||||
self.doc = None
|
||||
self.pil_img = None
|
||||
self.page_n = 0
|
||||
self.page_count = 1
|
||||
self.photo_ref = None
|
||||
|
||||
self.frame = tk.Frame(parent, bg=bg, width=max_w)
|
||||
self.frame.pack_propagate(False)
|
||||
|
||||
self.lbl_title = tk.Label(self.frame, text="", bg=bg, fg="#aaa",
|
||||
font=("Segoe UI", 9), wraplength=max_w - 10)
|
||||
self.lbl_title.pack(pady=(4, 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=4)
|
||||
|
||||
self.lbl_page = tk.Label(frame_nav, text="", bg=bg, fg="#ccc",
|
||||
font=("Segoe UI", 9))
|
||||
self.lbl_page.pack(side="left", padx=6)
|
||||
|
||||
self.btn_prev = tk.Button(frame_nav, text="◄", bg="#444", fg="#fff",
|
||||
relief="flat", padx=6,
|
||||
command=self._prev)
|
||||
self.btn_prev.pack(side="left", padx=2)
|
||||
|
||||
self.btn_next = tk.Button(frame_nav, text="►", bg="#444", fg="#fff",
|
||||
relief="flat", padx=6,
|
||||
command=self._next)
|
||||
self.btn_next.pack(side="left", padx=2)
|
||||
|
||||
def load(self, path_str: str, title: str = ""):
|
||||
from PIL import ImageTk
|
||||
if self.doc:
|
||||
try:
|
||||
self.doc.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.doc, self.pil_img = _open_doc(path_str)
|
||||
self.page_count = len(self.doc) if self.doc else 1
|
||||
self.lbl_title.config(text=title or Path(path_str).name, fg="#ddd")
|
||||
self._show(0)
|
||||
|
||||
def clear(self, msg: str = ""):
|
||||
if self.doc:
|
||||
try:
|
||||
self.doc.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.doc = None
|
||||
self.pil_img = None
|
||||
self.lbl_img.config(image="")
|
||||
self.lbl_title.config(text=msg, fg="#666")
|
||||
self.lbl_page.config(text="")
|
||||
self.btn_prev.config(state="disabled")
|
||||
self.btn_next.config(state="disabled")
|
||||
self.photo_ref = None
|
||||
|
||||
def _show(self, n: int):
|
||||
from PIL import ImageTk
|
||||
self.page_n = n
|
||||
img = _render_page(self.doc, self.pil_img, n, self.max_w - 10, self.max_h - 60)
|
||||
if img:
|
||||
self.photo_ref = ImageTk.PhotoImage(img)
|
||||
self.lbl_img.config(image=self.photo_ref)
|
||||
self.lbl_page.config(text=f"{n + 1} / {self.page_count}" if self.page_count > 1 else "")
|
||||
self.btn_prev.config(state="normal" if n > 0 else "disabled")
|
||||
self.btn_next.config(state="normal" if n < self.page_count - 1 else "disabled")
|
||||
|
||||
def _prev(self):
|
||||
if self.page_n > 0:
|
||||
self._show(self.page_n - 1)
|
||||
|
||||
def _next(self):
|
||||
if self.page_n < self.page_count - 1:
|
||||
self._show(self.page_n + 1)
|
||||
|
||||
def close(self):
|
||||
if self.doc:
|
||||
try:
|
||||
self.doc.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(1)
|
||||
|
||||
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
original_path = data.get("original") or ""
|
||||
duplicity_paths = data.get("duplicity") or []
|
||||
labels = data.get("labels") or [Path(p).name for p in duplicity_paths]
|
||||
|
||||
write_geom = None
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("--write-geometry="):
|
||||
write_geom = Path(arg.split("=", 1)[1])
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
import fitz
|
||||
except ImportError as e:
|
||||
print(f"[duplicity_viewer] Chybí knihovna: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# ── Layout z JSON ─────────────────────────────────────────────────────────
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
try:
|
||||
from window_layout import get_layout
|
||||
layout = get_layout()
|
||||
lw = layout.get("duplicity_viewer") or {}
|
||||
except Exception:
|
||||
lw = {}
|
||||
|
||||
WIN_X = lw.get("x", 0)
|
||||
WIN_Y = lw.get("y", 0)
|
||||
WIN_W = lw.get("w", 2200)
|
||||
WIN_H = lw.get("h", 1080)
|
||||
|
||||
LISTBOX_W = 380
|
||||
PANE_W = (WIN_W - LISTBOX_W - 20) // 2
|
||||
PANE_H = WIN_H - 40
|
||||
|
||||
# ── Okno ──────────────────────────────────────────────────────────────────
|
||||
root = tk.Tk()
|
||||
root.tk.call("encoding", "system", "utf-8")
|
||||
root.title("Náhled dokumentů")
|
||||
root.configure(bg="#1a1a1a")
|
||||
root.geometry(f"{WIN_W}x{WIN_H}+{WIN_X}+{WIN_Y}")
|
||||
|
||||
# ── Tři sloupce ───────────────────────────────────────────────────────────
|
||||
# Vlevo: originál
|
||||
pane_orig = PdfPane(root, max_w=PANE_W, max_h=PANE_H, bg="#1e1e2e")
|
||||
pane_orig.frame.pack(side="left", fill="both", expand=False, padx=(6, 3), pady=6)
|
||||
|
||||
# Uprostřed: duplicita
|
||||
pane_dup = PdfPane(root, max_w=PANE_W, max_h=PANE_H, bg="#2e1e1e")
|
||||
pane_dup.frame.pack(side="left", fill="both", expand=False, padx=(3, 3), pady=6)
|
||||
|
||||
# Vpravo: listbox
|
||||
frame_right = tk.Frame(root, bg="#1a1a1a", width=LISTBOX_W)
|
||||
frame_right.pack(side="left", fill="y", padx=(3, 6), pady=6)
|
||||
frame_right.pack_propagate(False)
|
||||
|
||||
tk.Label(frame_right, text="Existující dokumenty:", anchor="w",
|
||||
bg="#1a1a1a", fg="#cc4444", font=("Segoe UI", 9, "bold")).pack(
|
||||
fill="x", padx=4, pady=(8, 2))
|
||||
|
||||
sb = tk.Scrollbar(frame_right, orient="vertical")
|
||||
lb = tk.Listbox(
|
||||
frame_right,
|
||||
yscrollcommand=sb.set,
|
||||
font=("Segoe UI", 8),
|
||||
selectmode="single",
|
||||
activestyle="dotbox",
|
||||
bg="#2a1a1a",
|
||||
fg="#ddd",
|
||||
selectbackground="#cc4444",
|
||||
selectforeground="#fff",
|
||||
cursor="hand2",
|
||||
)
|
||||
sb.config(command=lb.yview)
|
||||
sb.pack(side="right", fill="y")
|
||||
lb.pack(side="left", fill="both", expand=True, padx=(4, 0))
|
||||
|
||||
if not duplicity_paths:
|
||||
lb.insert(tk.END, "(žádné duplicity)")
|
||||
lb.config(state="disabled")
|
||||
else:
|
||||
for label in labels:
|
||||
lb.insert(tk.END, Path(label).name if Path(label).exists() else label)
|
||||
|
||||
# ── Načti originál ────────────────────────────────────────────────────────
|
||||
if original_path and Path(original_path).exists():
|
||||
pane_orig.load(original_path, title=Path(original_path).name)
|
||||
else:
|
||||
pane_orig.clear(msg="(originál nedostupný)")
|
||||
|
||||
pane_dup.clear(msg="← vyberte duplicitu vlevo" if duplicity_paths else "(žádné duplicity)")
|
||||
|
||||
# ── Výběr duplicity ───────────────────────────────────────────────────────
|
||||
def on_select(event):
|
||||
sel = lb.curselection()
|
||||
if not sel:
|
||||
return
|
||||
idx = sel[0]
|
||||
p = duplicity_paths[idx]
|
||||
pane_dup.load(p, title=labels[idx] if idx < len(labels) else Path(p).name)
|
||||
|
||||
lb.bind("<<ListboxSelect>>", on_select)
|
||||
|
||||
# Automaticky vyber první duplicitu
|
||||
if duplicity_paths:
|
||||
lb.selection_set(0)
|
||||
lb.event_generate("<<ListboxSelect>>")
|
||||
|
||||
# ── Zavření ───────────────────────────────────────────────────────────────
|
||||
def on_close():
|
||||
pane_orig.close()
|
||||
pane_dup.close()
|
||||
root.destroy()
|
||||
|
||||
root.protocol("WM_DELETE_WINDOW", on_close)
|
||||
|
||||
# ── Geometrie pro rename_dialog ───────────────────────────────────────────
|
||||
root.update_idletasks()
|
||||
if write_geom:
|
||||
_y = root.winfo_y()
|
||||
_h = root.winfo_height()
|
||||
write_geom.write_text(
|
||||
json.dumps({"x": WIN_X, "y": _y, "w": WIN_W, "h": _h}),
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
root.lift()
|
||||
root.attributes("-topmost", True)
|
||||
root.after(1500, lambda: root.attributes("-topmost", False))
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,763 @@
|
||||
"""
|
||||
Zpracování naskenovaných PDF — nová verze.
|
||||
1. Preview originálu + Claude Vision API
|
||||
2. Rename dialog
|
||||
3. 5 variant komprese → uživatel vybere
|
||||
4. Uložit do Processed, smazat originál
|
||||
"""
|
||||
import base64
|
||||
import gc
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from Knihovny.najdi_dropbox import get_dropbox_root
|
||||
|
||||
FB_CONFIG = {
|
||||
'dsn': r'reporter:c:\medicus\medicus.fdb',
|
||||
'user': 'SYSDBA',
|
||||
'password': 'masterkey',
|
||||
'charset': 'win1250',
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
def _find_poppler() -> str | None:
|
||||
candidates = [
|
||||
r"C:/Poppler/Library/bin",
|
||||
str(Path.home() / r"scoop\apps\poppler\current\Library\bin"),
|
||||
str(Path.home() / r"scoop\apps\poppler\current\bin"),
|
||||
]
|
||||
for p in candidates:
|
||||
if Path(p).exists():
|
||||
return p
|
||||
pdfinfo = shutil.which("pdfinfo")
|
||||
if pdfinfo:
|
||||
return str(Path(pdfinfo).parent)
|
||||
return None
|
||||
|
||||
POPPLER_PATH = _find_poppler()
|
||||
CORRECTIONS = True # True = corrections.json se načítá a ukládá; False = ignorovat
|
||||
|
||||
_DROPBOX = Path(get_dropbox_root())
|
||||
TO_PROCESS = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\KeZpracování"
|
||||
PROCESSED = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\Zpracováno"
|
||||
CORRECTIONS_FILE = Path(__file__).parent / "corrections.json"
|
||||
NAMING_RULES_FILE = Path(__file__).parent / "naming_rules.md"
|
||||
DOKUMENTACE = _DROPBOX / r"Ordinace\Dokumentace_zpracovaná"
|
||||
|
||||
import threading
|
||||
|
||||
_dokumentace_index: set[str] = set()
|
||||
_dokumentace_ready = threading.Event()
|
||||
|
||||
def _load_dokumentace_index_bg():
|
||||
if DOKUMENTACE.exists():
|
||||
names = {f.name for f in DOKUMENTACE.iterdir() if f.is_file()}
|
||||
else:
|
||||
names = set()
|
||||
global _dokumentace_index
|
||||
_dokumentace_index = names
|
||||
_dokumentace_ready.set()
|
||||
print(f" Index dokumentace: {len(names)} souborů načteno.")
|
||||
|
||||
def start_dokumentace_index():
|
||||
t = threading.Thread(target=_load_dokumentace_index_bg, daemon=True)
|
||||
t.start()
|
||||
|
||||
VIEWER = Path(__file__).parent / "preview_viewer.py"
|
||||
RENAME_DIALOG = Path(__file__).parent / "rename_dialog.py"
|
||||
VARIANT_PICKER = Path(__file__).parent / "variant_picker.py"
|
||||
|
||||
# 5 kompresních variant
|
||||
COMPRESS_VARIANTS = [
|
||||
("300 DPI / q90", 300, 90),
|
||||
("200 DPI / q85", 200, 85),
|
||||
("150 DPI / q80", 150, 80),
|
||||
("120 DPI / q75", 120, 75),
|
||||
( "96 DPI / q70", 96, 70),
|
||||
]
|
||||
|
||||
|
||||
# ─── Komprese jedné varianty ──────────────────────────────────────────────────
|
||||
|
||||
def set_single_page_view(pdf_path: Path):
|
||||
from pikepdf import Pdf, Name
|
||||
with Pdf.open(str(pdf_path), allow_overwriting_input=True) as pdf:
|
||||
pdf.Root.PageLayout = Name("/SinglePage")
|
||||
pdf.Root.PageMode = Name("/UseNone")
|
||||
pdf.save()
|
||||
|
||||
|
||||
def compress_to_temp(pdf_path: Path, dpi: int, quality: int) -> Path:
|
||||
import fitz
|
||||
src = fitz.open(str(pdf_path))
|
||||
mat = fitz.Matrix(dpi / 72.0, dpi / 72.0)
|
||||
out = fitz.open()
|
||||
for page in src:
|
||||
pix = page.get_pixmap(matrix=mat, colorspace=fitz.csRGB)
|
||||
img_bytes = pix.tobytes("jpeg", jpg_quality=quality)
|
||||
img_doc = fitz.open("pdf", fitz.open("jpeg", img_bytes).convert_to_pdf())
|
||||
rect = page.rect
|
||||
np = out.new_page(width=rect.width, height=rect.height)
|
||||
np.show_pdf_page(np.rect, img_doc, 0)
|
||||
src.close()
|
||||
tmp = Path(tempfile.mktemp(suffix=".pdf"))
|
||||
out.save(tmp, deflate=True, garbage=4)
|
||||
out.close()
|
||||
return tmp
|
||||
|
||||
|
||||
# ─── Medicus ověření ─────────────────────────────────────────────────────────
|
||||
|
||||
def _medicus_connect():
|
||||
try:
|
||||
import fdb
|
||||
return fdb.connect(**FB_CONFIG)
|
||||
except Exception as e:
|
||||
print(f" [Medicus] Nepřipojeno: {e}")
|
||||
return None
|
||||
|
||||
def _lookup_by_rc(cur, rc_digits: str) -> dict | None:
|
||||
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]:
|
||||
similar = {"0": "8", "8": "0", "1": "7", "7": "1", "5": "6", "6": "5", "3": "8"}
|
||||
candidates = set()
|
||||
for i in range(len(rc)):
|
||||
candidates.add(rc[:i] + rc[i+1:])
|
||||
for i in range(len(rc) + 1):
|
||||
candidates.add(rc[:i] + "0" + rc[i:])
|
||||
for i, ch in enumerate(rc):
|
||||
if ch in similar:
|
||||
candidates.add(rc[:i] + similar[ch] + rc[i+1:])
|
||||
candidates.discard(rc)
|
||||
return sorted(c for c in candidates if len(c) in (9, 10))
|
||||
|
||||
def _rc_checksum_ok(rc: str) -> bool:
|
||||
digits = re.sub(r"\D", "", rc)
|
||||
if len(digits) == 10:
|
||||
return int(digits) % 11 == 0
|
||||
return True
|
||||
|
||||
def _parse_jmeno_prijmeni(name_str: str) -> tuple[str, str] | None:
|
||||
"""Parsuje 'Příjmení, Jméno' nebo 'Příjmení Jméno' -> (prijmeni, jmeno)."""
|
||||
name_str = name_str.strip()
|
||||
if "," in name_str:
|
||||
parts = [p.strip() for p in name_str.split(",", 1)]
|
||||
if len(parts) == 2 and parts[0] and parts[1]:
|
||||
return parts[0], parts[1]
|
||||
parts = name_str.split()
|
||||
if len(parts) >= 2:
|
||||
return parts[0], " ".join(parts[1:])
|
||||
return None
|
||||
|
||||
def _lookup_by_name(cur, prijmeni: str, jmeno: str) -> list[dict]:
|
||||
"""Vyhledá pacienty v KAR podle příjmení a prvního slova jména."""
|
||||
jmeno_first = jmeno.split()[0] if jmeno.split() else jmeno
|
||||
cur.execute(
|
||||
"SELECT IDPAC, PRIJMENI, JMENO, RODCIS FROM KAR "
|
||||
"WHERE UPPER(PRIJMENI) = UPPER(?)",
|
||||
(prijmeni,)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
result = []
|
||||
for row in rows:
|
||||
db_jmeno = (row[2] or "").strip().upper()
|
||||
if db_jmeno.startswith(jmeno_first.upper()):
|
||||
result.append({
|
||||
"idpac": row[0],
|
||||
"prijmeni": row[1].strip(),
|
||||
"jmeno": row[2].strip(),
|
||||
"rodcis": row[3].strip(),
|
||||
})
|
||||
return result
|
||||
|
||||
def verify_patient_by_name(name_str: str) -> dict:
|
||||
"""Vyhledá pacienta v Medicus podle jména — fallback když RČ chybí."""
|
||||
parsed = _parse_jmeno_prijmeni(name_str)
|
||||
if not parsed:
|
||||
return {"status": "not_found", "patient": None, "rc_corrected": None}
|
||||
prijmeni, jmeno = parsed
|
||||
con = _medicus_connect()
|
||||
if con is None:
|
||||
return {"status": "offline", "patient": None, "rc_corrected": None}
|
||||
try:
|
||||
cur = con.cursor()
|
||||
matches = _lookup_by_name(cur, prijmeni, jmeno)
|
||||
if not matches:
|
||||
return {"status": "not_found", "patient": None, "rc_corrected": None}
|
||||
if len(matches) == 1:
|
||||
return {"status": "by_name", "patient": matches[0], "rc_corrected": None}
|
||||
return {"status": "by_name_multi", "patient": matches[0], "rc_corrected": None, "all_matches": matches}
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
def verify_patient(rc_raw: str) -> dict:
|
||||
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()
|
||||
patient = _lookup_by_rc(cur, rc)
|
||||
if patient:
|
||||
return {"status": "ok", "patient": patient, "rc_corrected": None}
|
||||
candidates = _rc_candidates(rc)
|
||||
matches = [(c, _lookup_by_rc(cur, c)) for c in candidates]
|
||||
matches = [(c, p) for c, p in matches if p]
|
||||
if not matches:
|
||||
return {"status": "not_found", "patient": None, "rc_corrected": None}
|
||||
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()
|
||||
|
||||
def check_duplicates(rc: str, datum: str) -> list[str]:
|
||||
if not rc or not datum:
|
||||
return []
|
||||
# Počkej max 15s na dokončení indexu (typicky hotovo za dobu volání Claude)
|
||||
_dokumentace_ready.wait(timeout=15)
|
||||
prefix = f"{rc} {datum}"
|
||||
return [name for name in _dokumentace_index if name.startswith(prefix)]
|
||||
|
||||
|
||||
# ─── EKG zpracování ──────────────────────────────────────────────────────────
|
||||
|
||||
_EKG_FLAG = "rotated-by-script"
|
||||
|
||||
|
||||
def _is_ekg(pdf_path: Path) -> bool:
|
||||
"""Detekuje EKG PDF podle metadat — PDFCreator 2.4.x je specifický pro EKG přístroj."""
|
||||
if pdf_path.suffix.lower() != ".pdf":
|
||||
return False
|
||||
try:
|
||||
import fitz
|
||||
doc = fitz.open(str(pdf_path))
|
||||
meta = doc.metadata
|
||||
doc.close()
|
||||
haystack = " ".join(filter(None, [
|
||||
meta.get("creator", ""), meta.get("producer", "")
|
||||
])).lower()
|
||||
return "pdfcreator" in haystack
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _ekg_rotate_if_needed(pdf_path: Path):
|
||||
"""Otočí první stránku o 90° CW a odstraní případnou druhou stránku."""
|
||||
import fitz
|
||||
doc = fitz.open(str(pdf_path))
|
||||
meta = doc.metadata
|
||||
keywords = meta.get("keywords", "") or ""
|
||||
if _EKG_FLAG in keywords:
|
||||
doc.close()
|
||||
return
|
||||
page = doc[0]
|
||||
page.set_rotation((page.rotation + 90) % 360)
|
||||
if doc.page_count > 1:
|
||||
doc.delete_page(1)
|
||||
meta["keywords"] = (keywords + " " + _EKG_FLAG).strip()
|
||||
doc.set_metadata(meta)
|
||||
tmp = pdf_path.with_suffix(".tmp.pdf")
|
||||
doc.save(tmp, deflate=True)
|
||||
doc.close()
|
||||
os.replace(tmp, pdf_path)
|
||||
print(" [EKG] Stránka otočena o 90°.")
|
||||
|
||||
|
||||
def _ekg_ocr(pdf_path: Path) -> str:
|
||||
import fitz
|
||||
import pytesseract
|
||||
from PIL import Image as _PILImage
|
||||
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
|
||||
doc = fitz.open(str(pdf_path))
|
||||
pix = doc[0].get_pixmap(dpi=300)
|
||||
img = _PILImage.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
||||
doc.close()
|
||||
return pytesseract.image_to_string(img, lang="ces", config="--psm 6")
|
||||
|
||||
|
||||
def _ekg_extract_rc(text: str) -> str | None:
|
||||
m = re.search(r"(\d{6})\s*/?\s*(\d{3,4})", text)
|
||||
if not m:
|
||||
return None
|
||||
return m.group(1) + m.group(2).zfill(4)
|
||||
|
||||
|
||||
def _ekg_extract_date(text: str) -> str | None:
|
||||
"""Vrátí datum ve formátu YYYY-MM-DD nebo None."""
|
||||
m = re.search(r"(\d{1,2})[\.,]\s*(\d{1,2})[\.,]\s*(\d{4})", text)
|
||||
if m:
|
||||
d, mo, y = m.groups()
|
||||
return f"{y}-{mo.zfill(2)}-{d.zfill(2)}"
|
||||
for pat in [r"\b(\d{2})(\d{2})(\d{4})\b", r"\b(\d{2})(\d{1})(\d{4})\b"]:
|
||||
for m in re.finditer(pat, text):
|
||||
d, mo, y = m.groups()
|
||||
if 1 <= int(d) <= 31 and 1 <= int(mo) <= 12 and 1900 <= int(y) <= 2100:
|
||||
return f"{y}-{mo.zfill(2)}-{d.zfill(2)}"
|
||||
return None
|
||||
|
||||
|
||||
def extract_info_ekg(pdf_path: Path) -> dict:
|
||||
"""EKG větev: rotace in-place, Tesseract OCR, Medicus ověření."""
|
||||
_ekg_rotate_if_needed(pdf_path)
|
||||
|
||||
print(" [EKG] OCR přes Tesseract...")
|
||||
raw_text = _ekg_ocr(pdf_path)
|
||||
print(f"\n--- EKG OCR TEXT ---\n{raw_text}\n--- KONEC ---\n")
|
||||
|
||||
rc_ocr = _ekg_extract_rc(raw_text)
|
||||
date_iso = _ekg_extract_date(raw_text)
|
||||
print(f" [EKG] RČ: {rc_ocr or 'NENALEZENO'} | Datum: {date_iso or 'NENALEZENO'}")
|
||||
|
||||
print(f" [EKG] Ověřuji v Medicus (RČ: {rc_ocr or '?'})...")
|
||||
verif = verify_patient(rc_ocr or "")
|
||||
rc_final = rc_ocr
|
||||
if verif["status"] == "fuzzy" and verif.get("rc_corrected"):
|
||||
rc_final = verif["rc_corrected"]
|
||||
print(f" [EKG] RČ opraveno: {rc_ocr} → {rc_final}")
|
||||
|
||||
patient = verif.get("patient")
|
||||
name_part = f"{patient['prijmeni']}, {patient['jmeno']}" if patient else ""
|
||||
|
||||
if rc_final and date_iso:
|
||||
nazev = f"{rc_final} {date_iso}{(' ' + name_part) if name_part else ''} [EKG] [bez hodnocení].pdf"
|
||||
else:
|
||||
nazev = None
|
||||
|
||||
return {
|
||||
"rodne_cislo": rc_final,
|
||||
"datum_zpravy": date_iso,
|
||||
"nazev_souboru": nazev,
|
||||
"_verif": verif,
|
||||
"_rc_ocr": rc_ocr or "",
|
||||
}
|
||||
|
||||
|
||||
# ─── Korekce (few-shot příklady) ─────────────────────────────────────────────
|
||||
|
||||
def load_corrections() -> list[dict]:
|
||||
if not CORRECTIONS_FILE.exists():
|
||||
return []
|
||||
content = CORRECTIONS_FILE.read_text(encoding="utf-8")
|
||||
idx = content.find('[')
|
||||
if idx < 0:
|
||||
return []
|
||||
try:
|
||||
result = json.loads(content[idx:])
|
||||
# Pokud byl soubor poškozený (garbage před [), přepiš ho čistě
|
||||
if idx > 0:
|
||||
CORRECTIONS_FILE.write_text(
|
||||
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
def save_correction(original: str, corrected: str):
|
||||
if not CORRECTIONS:
|
||||
return
|
||||
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 load_naming_rules() -> str:
|
||||
if NAMING_RULES_FILE.exists():
|
||||
content = NAMING_RULES_FILE.read_text(encoding="utf-8").strip()
|
||||
if content:
|
||||
return f"Pravidla pro pojmenování souborů (dodržuj vždy):\n{content}\n\n"
|
||||
return ""
|
||||
|
||||
def build_corrections_prompt() -> str:
|
||||
if not CORRECTIONS:
|
||||
return ""
|
||||
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"
|
||||
|
||||
|
||||
# ─── Claude Vision API ────────────────────────────────────────────────────────
|
||||
|
||||
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...")
|
||||
suffix = pdf_path.suffix.lower()
|
||||
if suffix in (".jpg", ".jpeg", ".png"):
|
||||
from PIL import Image
|
||||
img = Image.open(pdf_path)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=95)
|
||||
img.close()
|
||||
else:
|
||||
from PIL import Image as _PILImage
|
||||
_PILImage.MAX_IMAGE_PIXELS = None # vypni limit pro naše vlastní PDFs
|
||||
images = convert_from_path(str(pdf_path), poppler_path=POPPLER_PATH, dpi=300)
|
||||
buf = io.BytesIO()
|
||||
images[0].save(buf, format="JPEG", quality=95)
|
||||
del images
|
||||
gc.collect()
|
||||
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 = (
|
||||
load_naming_rules() +
|
||||
build_corrections_prompt() +
|
||||
patient_hint +
|
||||
"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 — "
|
||||
"\"LZ {oddělení}\" = ambulantní/lékařská zpráva (např. \"LZ chirurgie\", \"LZ kardiologie\", \"LZ plicní\", \"LZ ORL\"); "
|
||||
"\"PZ {oddělení}\" = propouštěcí zpráva z hospitalizace (např. \"PZ interna\", \"PZ neurologie\"). "
|
||||
"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ů. "
|
||||
"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ší. "
|
||||
"Teprve pokud závěr chybí, shrň obsah z celé zprávy.\n"
|
||||
f"- \"nazev_souboru\": název souboru ve formátu {nazev_format}\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...")
|
||||
try:
|
||||
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
|
||||
print(f" Tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${usage.input_tokens*3/1e6 + usage.output_tokens*15/1e6:.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}
|
||||
except Exception as e:
|
||||
print(f" VAROVÁNÍ: Claude API selhalo ({e}) — otevírám dialog pro ruční vyplnění.")
|
||||
return {"nazev_souboru": None}
|
||||
|
||||
|
||||
# ─── Subprocess helpers ───────────────────────────────────────────────────────
|
||||
|
||||
def open_preview(pdf_path: Path) -> tuple[subprocess.Popen, Path]:
|
||||
geom_file = Path(tempfile.mktemp(suffix=".json"))
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(VIEWER), str(pdf_path), f"--write-geometry={geom_file}"],
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
return proc, geom_file
|
||||
|
||||
|
||||
def read_preview_bottom(geom_file: Path, timeout: float = 5.0) -> int:
|
||||
import time
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if geom_file.exists():
|
||||
geom = json.loads(geom_file.read_text(encoding="utf-8"))
|
||||
geom_file.unlink(missing_ok=True)
|
||||
return geom["y"] + geom["h"] + 30 # +30 pro title bar
|
||||
time.sleep(0.1)
|
||||
geom_file.unlink(missing_ok=True)
|
||||
return None
|
||||
|
||||
|
||||
def run_rename_dialog(nazev: str, info_lines: list, below_y: int = None) -> str | None:
|
||||
tmp = Path(tempfile.mktemp(suffix=".json"))
|
||||
tmp.write_text(json.dumps({"nazev": nazev, "info_lines": info_lines}, ensure_ascii=False), encoding="utf-8")
|
||||
args = [sys.executable, str(RENAME_DIALOG), str(tmp)]
|
||||
if below_y is not None:
|
||||
args.append(f"--below-y={below_y}")
|
||||
env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
|
||||
proc = subprocess.run(args, capture_output=True, text=True, encoding="utf-8", env=env)
|
||||
tmp.unlink(missing_ok=True)
|
||||
if proc.stderr.strip():
|
||||
print(f" [rename_dialog] CHYBA:\n{proc.stderr.strip()}")
|
||||
out = proc.stdout.strip()
|
||||
return json.loads(out).get("value") if out else None
|
||||
|
||||
|
||||
def run_variant_picker(variants_data: list) -> str | None:
|
||||
tmp = Path(tempfile.mktemp(suffix=".json"))
|
||||
tmp.write_text(json.dumps(variants_data, ensure_ascii=False), encoding="utf-8")
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(VARIANT_PICKER), str(tmp)],
|
||||
capture_output=True, text=True, encoding="utf-8",
|
||||
)
|
||||
tmp.unlink(missing_ok=True)
|
||||
if proc.returncode != 0 or not proc.stdout.strip():
|
||||
print(f" [variant_picker] returncode={proc.returncode}")
|
||||
if proc.stderr.strip():
|
||||
print(f" [variant_picker] CHYBA:\n{proc.stderr.strip()}")
|
||||
out = proc.stdout.strip()
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
def process_file(pdf_path: Path):
|
||||
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"]
|
||||
|
||||
# 1. Otevři preview (pro EKG: soubor je již otočen)
|
||||
preview, geom_file = open_preview(pdf_path)
|
||||
below_y = read_preview_bottom(geom_file)
|
||||
|
||||
if not is_ekg:
|
||||
# 2. Zjisti RČ a jméno — buď z názvu (split soubor) nebo přes Claude Vision API
|
||||
split = _parse_split_filename(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}")
|
||||
info = extract_info(pdf_path, known_patient=name_from_filename, known_rc=rc_from_scan)
|
||||
nazev = info.get("nazev_souboru") or pdf_path.name
|
||||
nazev = re.sub(r"^\d{9,10}\s+", f"{rc_from_scan} ", nazev)
|
||||
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Č
|
||||
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}")
|
||||
verif_name = verify_patient_by_name(jmeno_z_claude)
|
||||
if verif_name["status"] in ("by_name", "by_name_multi"):
|
||||
verif = verif_name
|
||||
rc_z_medicus = re.sub(r"\D", "", verif["patient"]["rodcis"])
|
||||
p = verif["patient"]
|
||||
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}")
|
||||
if status == "ok":
|
||||
info_lines.append(f"✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||
elif status == "fuzzy":
|
||||
info_lines.append(f"⚠ RČ ze skenu '{rc_ocr}' → opraveno na {verif['rc_corrected']}")
|
||||
info_lines.append(f" Pacient: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||
elif status == "by_name":
|
||||
info_lines.append(f"✓ Nalezeno dle jména: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||
elif status == "by_name_multi":
|
||||
count = len(verif.get("all_matches", []))
|
||||
info_lines.append(f"⚠ Nalezeno dle jména ({count} shod, 1. výsledek): {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||
elif status == "not_found":
|
||||
info_lines.append(f"✗ RČ '{rc_ocr}' nenalezeno v Medicus")
|
||||
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 duplicity:
|
||||
info_lines.append(f"⚠ DUPLICITA: {', '.join(duplicity)}")
|
||||
|
||||
if not info_lines:
|
||||
info_lines = ["[uprav ručně]"]
|
||||
print(" Otevírám dialog pro schválení názvu...")
|
||||
final_name = run_rename_dialog(nazev, info_lines, below_y=below_y)
|
||||
|
||||
preview.terminate()
|
||||
stderr_out = preview.stderr.read().decode("utf-8", errors="replace").strip() if preview.stderr else ""
|
||||
if stderr_out:
|
||||
print(f" [preview] CHYBA: {stderr_out}")
|
||||
|
||||
if not final_name:
|
||||
print(" Přeskočeno.")
|
||||
return
|
||||
|
||||
if not final_name.endswith(".pdf"):
|
||||
final_name += ".pdf"
|
||||
final_name = re.sub(r'[<>:"/\\|?*]', '', final_name)
|
||||
|
||||
if nazev and final_name != nazev:
|
||||
save_correction(nazev, final_name)
|
||||
|
||||
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)
|
||||
variants_data = [{"path": str(pdf_path), "label": "Originál", "size_kb": orig_kb}]
|
||||
for label, dpi, quality in COMPRESS_VARIANTS:
|
||||
tmp = compress_to_temp(pdf_path, dpi, quality)
|
||||
size_kb = round(tmp.stat().st_size / 1024)
|
||||
temp_files.append(tmp)
|
||||
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)
|
||||
|
||||
if not chosen:
|
||||
print(" Žádná varianta nevybrána, přeskakuji.")
|
||||
for t in temp_files:
|
||||
t.unlink(missing_ok=True)
|
||||
return
|
||||
|
||||
# 6. Ulož do Processed
|
||||
PROCESSED.mkdir(exist_ok=True)
|
||||
dest = PROCESSED / final_name
|
||||
if dest.exists():
|
||||
print(f" Přepisuji existující: {dest.name}")
|
||||
shutil.copy2(chosen, dest)
|
||||
set_single_page_view(dest)
|
||||
pdf_path.unlink()
|
||||
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é
|
||||
|
||||
|
||||
def process_folder(folder: Path):
|
||||
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}")
|
||||
print("\nHotovo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PROCESSED.mkdir(exist_ok=True)
|
||||
TO_PROCESS.mkdir(exist_ok=True)
|
||||
|
||||
target = Path(sys.argv[1]) if len(sys.argv) > 1 else TO_PROCESS
|
||||
|
||||
if target.is_file():
|
||||
process_file(target)
|
||||
elif target.is_dir():
|
||||
process_folder(target)
|
||||
else:
|
||||
print("Použití: python extract_patient_info_novy.py [soubor.pdf nebo složka]")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,462 @@
|
||||
"""
|
||||
Hlavní okno pro zpracování naskenovaných dokumentů.
|
||||
Kombinuje náhled originálu, náhled duplicit, listbox duplicit a rename panel.
|
||||
|
||||
Spouští se jako subprocess z extract_patient_info_novy_test.py.
|
||||
Argumenty: main_viewer.py <json_soubor>
|
||||
|
||||
JSON vstup: {
|
||||
"original": "cesta/k/originalu.pdf",
|
||||
"duplicity": ["cesta1.pdf", ...], # plné cesty
|
||||
"labels": ["název1.pdf", ...], # zobrazované názvy
|
||||
"nazev": "navrzeny_nazev.pdf",
|
||||
"info_lines": ["✓ Medicus: ...", ...],
|
||||
"varianty": ["varianta1.pdf", ...]
|
||||
}
|
||||
JSON výstup (stdout): { "value": "schvaleny nazev" } nebo { "value": null }
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import tkinter as tk
|
||||
from tkinter import font as tkfont
|
||||
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
from ctypes import windll
|
||||
windll.shcore.SetProcessDpiAwareness(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── PDF rendering ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _open_doc(path_str: str):
|
||||
import fitz
|
||||
from PIL import Image
|
||||
p = Path(path_str)
|
||||
if not p.exists():
|
||||
return None, None
|
||||
suffix = p.suffix.lower()
|
||||
if suffix in (".jpg", ".jpeg", ".png"):
|
||||
return None, Image.open(p)
|
||||
return fitz.open(str(p)), None
|
||||
|
||||
|
||||
def _render(doc, pil_img, page_n: int, max_w: int, max_h: int):
|
||||
import fitz
|
||||
from PIL import Image
|
||||
if doc is not None:
|
||||
page = doc[page_n]
|
||||
zoom = min(max_w / page.rect.width, max_h / page.rect.height)
|
||||
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
||||
return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
|
||||
elif pil_img is not None:
|
||||
img = pil_img.copy()
|
||||
img.thumbnail((max_w, max_h))
|
||||
return img
|
||||
return None
|
||||
|
||||
|
||||
class PdfPane:
|
||||
def __init__(self, parent, max_w: int, max_h: int, bg: str = "#1e1e2e", label_text: str = ""):
|
||||
self.max_w = max_w
|
||||
self.max_h = max_h
|
||||
self.doc = None
|
||||
self.pil_img = None
|
||||
self.page_n = 0
|
||||
self.page_count = 1
|
||||
self.photo_ref = None
|
||||
|
||||
self.frame = tk.Frame(parent, bg=bg, width=max_w, height=max_h)
|
||||
self.frame.pack_propagate(False)
|
||||
|
||||
if label_text:
|
||||
tk.Label(self.frame, text=label_text, bg=bg, fg="#888",
|
||||
font=("Segoe UI", 8, "bold")).pack(pady=(2, 0))
|
||||
|
||||
self.lbl_title = tk.Label(self.frame, text="", bg=bg, fg="#aaa",
|
||||
font=("Segoe UI", 8), wraplength=max_w - 10)
|
||||
self.lbl_title.pack(pady=(2, 0))
|
||||
|
||||
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:
|
||||
try: self.doc.close()
|
||||
except Exception: pass
|
||||
self.doc, self.pil_img = _open_doc(path_str)
|
||||
self.page_count = len(self.doc) if self.doc else 1
|
||||
self.lbl_title.config(text=title or Path(path_str).name, fg="#ccc")
|
||||
self._show(0)
|
||||
|
||||
def clear(self, msg: str = ""):
|
||||
if self.doc:
|
||||
try: self.doc.close()
|
||||
except Exception: pass
|
||||
self.doc = None
|
||||
self.pil_img = None
|
||||
self.lbl_img.config(image="")
|
||||
self.lbl_title.config(text=msg, fg="#555")
|
||||
self.lbl_page.config(text="")
|
||||
self.btn_prev.config(state="disabled")
|
||||
self.btn_next.config(state="disabled")
|
||||
self.photo_ref = None
|
||||
|
||||
def _show(self, n: int):
|
||||
from PIL import ImageTk
|
||||
self.page_n = n
|
||||
img = _render(self.doc, self.pil_img, n, self.max_w - 10, self.max_h - 60)
|
||||
if img:
|
||||
self.photo_ref = ImageTk.PhotoImage(img)
|
||||
self.lbl_img.config(image=self.photo_ref)
|
||||
self.lbl_page.config(text=f"{n+1} / {self.page_count}" if self.page_count > 1 else "")
|
||||
self.btn_prev.config(state="normal" if n > 0 else "disabled")
|
||||
self.btn_next.config(state="normal" if n < self.page_count - 1 else "disabled")
|
||||
|
||||
def _prev(self):
|
||||
if self.page_n > 0: self._show(self.page_n - 1)
|
||||
|
||||
def _next(self):
|
||||
if self.page_n < self.page_count - 1: self._show(self.page_n + 1)
|
||||
|
||||
def close(self):
|
||||
if self.doc:
|
||||
try: self.doc.close()
|
||||
except Exception: pass
|
||||
|
||||
|
||||
# ── Pomocné funkce pro layout ─────────────────────────────────────────────────
|
||||
|
||||
def _get_layout() -> dict:
|
||||
"""Načte layout oken pro aktuální hostname z layout_settings.json."""
|
||||
import json as _json, socket as _socket
|
||||
settings_file = Path(__file__).parent / "layout_settings.json"
|
||||
hostname = _socket.gethostname().upper()
|
||||
_default = {"duplicity_viewer": None}
|
||||
if not settings_file.exists():
|
||||
return _default
|
||||
try:
|
||||
settings = _json.loads(settings_file.read_text(encoding="utf-8"))
|
||||
return settings.get(hostname, _default)
|
||||
except Exception:
|
||||
return _default
|
||||
|
||||
|
||||
# ── Hlavní funkce ─────────────────────────────────────────────────────────────
|
||||
|
||||
def show(
|
||||
original_path: str = "",
|
||||
duplicity_paths: list = None,
|
||||
labels: list = None,
|
||||
nazev: str = "",
|
||||
info_lines: list = None,
|
||||
varianty: list = None,
|
||||
) -> str | None:
|
||||
"""
|
||||
Zobrazí hlavní viewer přímo (bez subprocesů).
|
||||
Vrátí schválený název souboru (bez .pdf) nebo None.
|
||||
"""
|
||||
duplicity_paths = duplicity_paths or []
|
||||
labels = labels or [Path(p).name for p in duplicity_paths]
|
||||
info_lines = info_lines or []
|
||||
varianty = varianty or []
|
||||
|
||||
result = {"value": None}
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
import fitz
|
||||
except ImportError as e:
|
||||
print(f"[main_viewer] Chybí knihovna: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# ── Layout ────────────────────────────────────────────────────────────────
|
||||
try:
|
||||
lw = _get_layout().get("duplicity_viewer") or {}
|
||||
except Exception:
|
||||
lw = {}
|
||||
|
||||
WIN_X = lw.get("x", 0)
|
||||
WIN_Y = lw.get("y", 0)
|
||||
WIN_W = lw.get("w", 3840)
|
||||
WIN_H = lw.get("h", 1700)
|
||||
|
||||
BOTTOM_H = 260 # výška spodního panelu
|
||||
TOP_H = WIN_H - BOTTOM_H - 8
|
||||
GAP = 4 # mezera mezi náhledy (zmenšená)
|
||||
LISTBOX_W = 560 # širší listbox duplicit
|
||||
PANE_W = (WIN_W - LISTBOX_W - GAP - 20) // 2
|
||||
|
||||
BG = "#1a1a1a" # jednotné tmavé pozadí pro celé okno
|
||||
COL_INFO = int(WIN_W * 0.15)
|
||||
COL_MID = int(WIN_W * 0.45)
|
||||
COL_VAR = WIN_W - COL_INFO - COL_MID
|
||||
|
||||
# ── Okno ──────────────────────────────────────────────────────────────────
|
||||
root = tk.Tk()
|
||||
root.tk.call("encoding", "system", "utf-8")
|
||||
root.title("Zpracování dokumentu")
|
||||
root.configure(bg=BG)
|
||||
root.geometry(f"{WIN_W}x{WIN_H}+{WIN_X}+{WIN_Y}")
|
||||
root.resizable(True, True)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# HORNÍ ČÁST — náhledy + listbox
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
frame_top = tk.Frame(root, bg=BG, height=TOP_H)
|
||||
frame_top.pack(side="top", fill="x", expand=False)
|
||||
frame_top.pack_propagate(False)
|
||||
|
||||
# Náhled originálu
|
||||
pane_orig = PdfPane(frame_top, max_w=PANE_W, max_h=TOP_H,
|
||||
bg=BG, label_text="ORIGINÁL")
|
||||
pane_orig.frame.pack(side="left", fill="y", padx=(6, GAP), pady=4)
|
||||
|
||||
# Náhled duplicity
|
||||
pane_dup = PdfPane(frame_top, max_w=PANE_W, max_h=TOP_H,
|
||||
bg=BG, label_text="DUPLICITA")
|
||||
pane_dup.frame.pack(side="left", fill="y", padx=(GAP, GAP), pady=4)
|
||||
|
||||
# Listbox duplicit vpravo
|
||||
frame_lb = tk.Frame(frame_top, bg=BG, width=LISTBOX_W)
|
||||
frame_lb.pack(side="left", fill="y", padx=(3, 6), pady=4)
|
||||
frame_lb.pack_propagate(False)
|
||||
|
||||
tk.Label(frame_lb, text="Existující dokumenty:", anchor="w",
|
||||
bg=BG, fg="#cc4444", font=("Segoe UI", 9, "bold")).pack(
|
||||
fill="x", padx=6, pady=(8, 2))
|
||||
|
||||
sb_dup = tk.Scrollbar(frame_lb, orient="vertical")
|
||||
txt_dup = tk.Text(
|
||||
frame_lb, yscrollcommand=sb_dup.set,
|
||||
font=("Segoe UI", 9), bg=BG, fg="#ddd",
|
||||
bd=0, highlightthickness=0, relief="flat",
|
||||
wrap="word", cursor="hand2", state="normal",
|
||||
selectbackground="#ffffff", selectforeground="#000000",
|
||||
)
|
||||
sb_dup.config(command=txt_dup.yview)
|
||||
sb_dup.pack(side="right", fill="y")
|
||||
txt_dup.pack(side="left", fill="both", expand=True, padx=(6, 0))
|
||||
|
||||
# Tag pro výběr (zvýraznění vybraného řádku)
|
||||
txt_dup.tag_config("selected", background="#ffffff", foreground="#000000")
|
||||
txt_dup.tag_config("normal", background=BG, foreground="#ddd")
|
||||
|
||||
def _shorten_dup_label(name: str) -> str:
|
||||
import re as _re
|
||||
m = _re.match(r"\d{9,10}\s+(\d{4}-\d{2}-\d{2})\s+[^[]+(\[.+)", name)
|
||||
if m:
|
||||
return f"{m.group(1)} {m.group(2)}"
|
||||
return name
|
||||
|
||||
dup_line_indices = [] # (line_start, line_end) pro každou duplicitu
|
||||
|
||||
if not duplicity_paths:
|
||||
txt_dup.insert("end", "(žádné duplicity)")
|
||||
txt_dup.config(state="disabled")
|
||||
else:
|
||||
for i, label in enumerate(labels):
|
||||
short = _shorten_dup_label(Path(label).name if not Path(label).exists() else label)
|
||||
line_start = txt_dup.index("end")
|
||||
txt_dup.insert("end", short + "\n\n")
|
||||
line_end = txt_dup.index("end")
|
||||
dup_line_indices.append((line_start, line_end))
|
||||
txt_dup.tag_add("normal", line_start, line_end)
|
||||
txt_dup.config(state="disabled")
|
||||
|
||||
selected_dup = [None]
|
||||
|
||||
def _dup_click(event):
|
||||
txt_dup.config(state="normal")
|
||||
idx = txt_dup.index(f"@{event.x},{event.y}")
|
||||
for i, (ls, le) in enumerate(dup_line_indices):
|
||||
if txt_dup.compare(ls, "<=", idx) and txt_dup.compare(idx, "<", le):
|
||||
# Odznač předchozí
|
||||
if selected_dup[0] is not None:
|
||||
prev_ls, prev_le = dup_line_indices[selected_dup[0]]
|
||||
txt_dup.tag_remove("selected", prev_ls, prev_le)
|
||||
txt_dup.tag_add("normal", prev_ls, prev_le)
|
||||
# Označ nový
|
||||
txt_dup.tag_remove("normal", ls, le)
|
||||
txt_dup.tag_add("selected", ls, le)
|
||||
selected_dup[0] = i
|
||||
txt_dup.config(state="disabled")
|
||||
# Načti PDF
|
||||
if i < len(duplicity_paths):
|
||||
pane_dup.load(duplicity_paths[i],
|
||||
title=labels[i] if i < len(labels) else "")
|
||||
return
|
||||
txt_dup.config(state="disabled")
|
||||
|
||||
txt_dup.bind("<Button-1>", _dup_click)
|
||||
|
||||
# Oddělovač
|
||||
tk.Frame(root, bg="#333", height=2).pack(fill="x")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SPODNÍ ČÁST — info | entry+tlačítka | návrhy
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
frame_bot = tk.Frame(root, bg=BG, height=BOTTOM_H)
|
||||
frame_bot.pack(side="top", fill="both", expand=True)
|
||||
frame_bot.pack_propagate(False)
|
||||
|
||||
# ── Vlevo: info o pacientovi (15%) ────────────────────────────────────────
|
||||
frame_info = tk.Frame(frame_bot, bg=BG, width=COL_INFO)
|
||||
frame_info.pack(side="left", fill="y", padx=(10, 4), pady=8)
|
||||
frame_info.pack_propagate(False)
|
||||
|
||||
tk.Label(frame_info, text="Informace o pacientovi", anchor="w",
|
||||
bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(fill="x")
|
||||
|
||||
for line in info_lines:
|
||||
color = "#cc4444" if line.startswith("⚠") or line.startswith("✗") else \
|
||||
"#44cc44" if line.startswith("✓") else "#aaa"
|
||||
tk.Label(frame_info, text=line, anchor="w", bg=BG,
|
||||
fg=color, font=("Segoe UI", 9), wraplength=COL_INFO - 20,
|
||||
justify="left").pack(fill="x", pady=1)
|
||||
|
||||
# ── Uprostřed: název + tlačítka (45%) ────────────────────────────────────
|
||||
frame_mid = tk.Frame(frame_bot, bg=BG, width=COL_MID)
|
||||
frame_mid.pack(side="left", fill="y", padx=(4, 20), pady=8)
|
||||
frame_mid.pack_propagate(False)
|
||||
|
||||
tk.Label(frame_mid, text="Název souboru (bez .pdf):", anchor="w",
|
||||
bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(fill="x")
|
||||
|
||||
nazev_bez = nazev[:-4] if nazev.endswith(".pdf") else nazev
|
||||
|
||||
# Multiline Text widget — zalamuje dlouhé názvy
|
||||
txt = tk.Text(frame_mid, font=("Segoe UI", 10), bg="#ffffff", fg="#000000",
|
||||
insertbackground="#000000", relief="flat", height=3,
|
||||
wrap="word", padx=6, pady=4, bd=0, highlightthickness=0)
|
||||
txt.pack(fill="x", pady=(4, 0))
|
||||
txt.insert("1.0", nazev_bez)
|
||||
txt.mark_set("insert", "end")
|
||||
txt.focus_set()
|
||||
|
||||
def get_txt_value() -> str:
|
||||
return txt.get("1.0", "end").strip()
|
||||
|
||||
frame_btn = tk.Frame(frame_mid, bg=BG)
|
||||
frame_btn.pack(pady=(6, 0))
|
||||
|
||||
def schvalit(event=None):
|
||||
result["value"] = get_txt_value()
|
||||
pane_orig.close()
|
||||
pane_dup.close()
|
||||
root.destroy()
|
||||
|
||||
def preskocit(event=None):
|
||||
result["value"] = None
|
||||
pane_orig.close()
|
||||
pane_dup.close()
|
||||
root.destroy()
|
||||
|
||||
tk.Button(frame_btn, text="✓ Schválit (Enter)", command=schvalit,
|
||||
bg="#2a7a2a", fg="white", font=("Segoe UI", 10, "bold"),
|
||||
padx=16, pady=6, relief="flat").pack(side="left", padx=8)
|
||||
tk.Button(frame_btn, text="✗ Přeskočit (Esc)", command=preskocit,
|
||||
bg="#7a2a2a", fg="white", font=("Segoe UI", 10),
|
||||
padx=16, pady=6, relief="flat").pack(side="left", padx=8)
|
||||
|
||||
# Enter schvalí jen pokud není focus v Text widgetu (tam Enter = nový řádek)
|
||||
root.bind("<Escape>", preskocit)
|
||||
# Ctrl+Enter vždy schválí
|
||||
root.bind("<Control-Return>", schvalit)
|
||||
|
||||
# ── Vpravo: návrhy od Claudea (40%) ──────────────────────────────────────
|
||||
frame_right = tk.Frame(frame_bot, bg=BG, width=COL_VAR)
|
||||
frame_right.pack(side="left", fill="y", padx=(4, 10), pady=8)
|
||||
frame_right.pack_propagate(False)
|
||||
|
||||
tk.Label(frame_right, text="Návrhy pojmenování (kliknutím vyberte):", anchor="w",
|
||||
bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(fill="x")
|
||||
|
||||
sb_var = tk.Scrollbar(frame_right, orient="vertical")
|
||||
lb_var = tk.Listbox(
|
||||
frame_right, yscrollcommand=sb_var.set,
|
||||
font=("Segoe UI", 10), selectmode="single", activestyle="none",
|
||||
bg=BG, fg="#ddd", bd=0, highlightthickness=0,
|
||||
selectbackground="#ffffff", selectforeground="#000000",
|
||||
cursor="hand2",
|
||||
)
|
||||
sb_var.config(command=lb_var.yview)
|
||||
sb_var.pack(side="right", fill="y")
|
||||
lb_var.pack(side="left", fill="both", expand=True)
|
||||
|
||||
for v in varianty:
|
||||
v_bez = v[:-4] if v.endswith(".pdf") else v
|
||||
lb_var.insert(tk.END, v_bez)
|
||||
|
||||
def on_varianta(event):
|
||||
sel = lb_var.curselection()
|
||||
if sel:
|
||||
txt.delete("1.0", "end")
|
||||
txt.insert("1.0", lb_var.get(sel[0]))
|
||||
|
||||
lb_var.bind("<<ListboxSelect>>", on_varianta)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Načtení dokumentů
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
if original_path and Path(original_path).exists():
|
||||
pane_orig.load(original_path, title=Path(original_path).name)
|
||||
else:
|
||||
pane_orig.clear(msg="(originál nedostupný)")
|
||||
|
||||
pane_dup.clear(msg="← vyberte duplicitu" if duplicity_paths else "(žádné duplicity)")
|
||||
|
||||
# Automaticky zobraz první duplicitu
|
||||
if duplicity_paths:
|
||||
pane_dup.load(duplicity_paths[0], title=labels[0] if labels else "")
|
||||
if dup_line_indices:
|
||||
txt_dup.config(state="normal")
|
||||
ls, le = dup_line_indices[0]
|
||||
txt_dup.tag_remove("normal", ls, le)
|
||||
txt_dup.tag_add("selected", ls, le)
|
||||
selected_dup[0] = 0
|
||||
txt_dup.config(state="disabled")
|
||||
|
||||
# ── Zobrazení ─────────────────────────────────────────────────────────────
|
||||
root.protocol("WM_DELETE_WINDOW", preskocit)
|
||||
root.lift()
|
||||
root.attributes("-topmost", True)
|
||||
root.after(1500, lambda: root.attributes("-topmost", False))
|
||||
root.mainloop()
|
||||
|
||||
return result["value"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Subprocess mód — čte data z JSON souboru předaného jako argument
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({"value": None}))
|
||||
sys.exit(0)
|
||||
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
value = show(
|
||||
original_path = data.get("original") or "",
|
||||
duplicity_paths = data.get("duplicity") or [],
|
||||
labels = data.get("labels") or [],
|
||||
nazev = data.get("nazev") or "",
|
||||
info_lines = data.get("info_lines") or [],
|
||||
varianty = data.get("varianty") or [],
|
||||
)
|
||||
print(json.dumps({"value": value}, ensure_ascii=False))
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Test skript — zobrazí počet monitorů, jejich rozlišení a který je primární.
|
||||
"""
|
||||
import sys
|
||||
|
||||
try:
|
||||
from screeninfo import get_monitors
|
||||
monitors = get_monitors()
|
||||
print(f"Počet monitorů: {len(monitors)}\n")
|
||||
for i, m in enumerate(monitors):
|
||||
primary = " ← PRIMÁRNÍ" if getattr(m, "is_primary", False) else ""
|
||||
print(f" Monitor {i+1}: {m.width}x{m.height} | pozice x={m.x}, y={m.y}{primary} | název: {getattr(m, 'name', '?')}")
|
||||
except ImportError:
|
||||
print("Knihovna 'screeninfo' není nainstalována — instaluji...")
|
||||
import subprocess
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "screeninfo", "--break-system-packages", "-q"])
|
||||
from screeninfo import get_monitors
|
||||
monitors = get_monitors()
|
||||
print(f"Počet monitorů: {len(monitors)}\n")
|
||||
for i, m in enumerate(monitors):
|
||||
primary = " ← PRIMÁRNÍ" if getattr(m, "is_primary", False) else ""
|
||||
print(f" Monitor {i+1}: {m.width}x{m.height} | pozice x={m.x}, y={m.y}{primary} | název: {getattr(m, 'name', '?')}")
|
||||
+41
-12
@@ -33,6 +33,19 @@ def main():
|
||||
root.tk.call("encoding", "system", "utf-8")
|
||||
|
||||
sh = root.winfo_screenheight()
|
||||
|
||||
# Zjisti cílovou šířku z layoutu (pokud existuje), jinak výchozí 700px
|
||||
try:
|
||||
import sys as _sys_tmp
|
||||
_sys_tmp.path.insert(0, str(Path(__file__).parent))
|
||||
from window_layout import get_layout as _get_layout
|
||||
_lw = _get_layout().get("preview_viewer")
|
||||
RENDER_W = (_lw.get("w", 700) - 20) if _lw else 700
|
||||
RENDER_H = (_lw.get("h", sh) - 80) if _lw else (sh - 150)
|
||||
except Exception:
|
||||
RENDER_W = 700
|
||||
RENDER_H = sh - 150
|
||||
|
||||
page_count = len(doc) if doc else 1
|
||||
current = [0]
|
||||
photo_ref = [None]
|
||||
@@ -40,12 +53,12 @@ def main():
|
||||
def render(n) -> Image.Image:
|
||||
if doc is not None:
|
||||
page = doc[n]
|
||||
zoom = min(700 / page.rect.width, (sh - 150) / page.rect.height)
|
||||
zoom = min(RENDER_W / page.rect.width, RENDER_H / page.rect.height)
|
||||
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
||||
return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
|
||||
else:
|
||||
img = pil_img.copy()
|
||||
img.thumbnail((700, sh - 150), Image.LANCZOS)
|
||||
img.thumbnail((RENDER_W, RENDER_H), Image.LANCZOS)
|
||||
return img
|
||||
|
||||
def on_close():
|
||||
@@ -62,8 +75,6 @@ def main():
|
||||
root.destroy()
|
||||
|
||||
root.title(pdf_path.stem)
|
||||
root.attributes("-topmost", True)
|
||||
root.resizable(False, False)
|
||||
root.protocol("WM_DELETE_WINDOW", on_close)
|
||||
|
||||
lbl_img = tk.Label(root)
|
||||
@@ -91,23 +102,41 @@ def main():
|
||||
|
||||
show(0)
|
||||
root.update_idletasks()
|
||||
sw = root.winfo_screenwidth()
|
||||
w = root.winfo_width()
|
||||
h = root.winfo_height()
|
||||
x = (sw - w) // 2
|
||||
root.geometry(f"+{x}+0")
|
||||
try:
|
||||
import sys as _sys
|
||||
_sys.path.insert(0, str(Path(__file__).parent))
|
||||
from window_layout import get_layout, apply_geometry
|
||||
_layout = get_layout()
|
||||
|
||||
def _fallback_prev():
|
||||
sw = root.winfo_screenwidth()
|
||||
w = root.winfo_width()
|
||||
x = (sw - w) // 2
|
||||
root.geometry(f"+{x}+0")
|
||||
|
||||
apply_geometry(root, _layout, "preview_viewer", fallback_fn=_fallback_prev)
|
||||
except Exception:
|
||||
sw = root.winfo_screenwidth()
|
||||
w = root.winfo_width()
|
||||
x = (sw - w) // 2
|
||||
root.geometry(f"+{x}+0")
|
||||
|
||||
# Zapiš geometrii do souboru pokud byl předán argument --write-geometry=<cesta>
|
||||
import json as _json
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("--write-geometry="):
|
||||
geom_path = Path(arg.split("=", 1)[1])
|
||||
geom_path.write_text(_json.dumps({"x": x, "y": 0, "w": w, "h": h}), encoding="utf-8")
|
||||
root.update_idletasks()
|
||||
_x = root.winfo_x()
|
||||
_y = root.winfo_y()
|
||||
_w = root.winfo_width()
|
||||
_h = root.winfo_height()
|
||||
geom_path.write_text(_json.dumps({"x": _x, "y": _y, "w": _w, "h": _h}), encoding="utf-8")
|
||||
break
|
||||
|
||||
root.lift()
|
||||
root.focus_force()
|
||||
root.after(100, lambda: root.focus_force())
|
||||
root.attributes("-topmost", True)
|
||||
root.after(1500, lambda: root.attributes("-topmost", False))
|
||||
root.mainloop()
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Standalone dialog pro schválení / opravu názvu souboru.
|
||||
Spouští se jako subprocess z extract_patient_info_novy_test.py.
|
||||
Argumenty: rename_dialog_test.py <json_soubor>
|
||||
JSON vstup: {
|
||||
"nazev": "...",
|
||||
"info_lines": [...],
|
||||
"duplicity": [...], # seznam názvů existujících souborů pro stejné RC+datum
|
||||
"varianty": [...] # seznam návrhů názvu od Claude (unikátní, seřazené od nejlepší)
|
||||
}
|
||||
JSON výstup: { "value": "..." } nebo { "value": null }
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import tkinter as tk
|
||||
from tkinter import font as tkfont
|
||||
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
from ctypes import windll
|
||||
windll.shcore.SetProcessDpiAwareness(1)
|
||||
except Exception:
|
||||
pass
|
||||
import io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({"value": None}))
|
||||
sys.exit(0)
|
||||
|
||||
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
nazev = data.get("nazev") or ""
|
||||
info_lines = data.get("info_lines") or []
|
||||
duplicity = data.get("duplicity") or []
|
||||
varianty = data.get("varianty") or []
|
||||
|
||||
result = {"value": None}
|
||||
|
||||
root = tk.Tk()
|
||||
root.title("Schválení názvu souboru")
|
||||
root.resizable(True, False)
|
||||
root.attributes("-topmost", True)
|
||||
|
||||
pad = {"padx": 12, "pady": 4}
|
||||
font_ui = ("Segoe UI", 10)
|
||||
font_bold = ("Segoe UI", 9, "bold")
|
||||
font_small = ("Segoe UI", 9)
|
||||
|
||||
# ── 1. Info panel (Medicus status) ────────────────────────────────────────
|
||||
frame_info = tk.Frame(root, bg="#f0f0f0", bd=1, relief="sunken")
|
||||
frame_info.pack(fill="x", **pad)
|
||||
for line in info_lines:
|
||||
color = "#b00000" if line.startswith("⚠") or line.startswith("✗") else \
|
||||
"#2a7a2a" if line.startswith("✓") else "#333"
|
||||
tk.Label(frame_info, text=line, anchor="w", bg="#f0f0f0",
|
||||
fg=color, font=font_ui).pack(fill="x", padx=8, pady=1)
|
||||
|
||||
# ── 2. Listbox 1 — duplicity ──────────────────────────────────────────────
|
||||
if duplicity:
|
||||
tk.Label(root, text="⚠ Již existující dokumenty pro toto datum:",
|
||||
anchor="w", fg="#b00000", font=font_bold).pack(fill="x", padx=12, pady=(8, 2))
|
||||
|
||||
frame_dup = tk.Frame(root)
|
||||
frame_dup.pack(fill="x", padx=12, pady=(0, 4))
|
||||
|
||||
sb_dup = tk.Scrollbar(frame_dup, orient="vertical")
|
||||
lb_duplicity = tk.Listbox(
|
||||
frame_dup,
|
||||
yscrollcommand=sb_dup.set,
|
||||
font=font_small,
|
||||
height=min(len(duplicity), 4),
|
||||
selectmode="single",
|
||||
activestyle="dotbox",
|
||||
bg="#fff8f0",
|
||||
selectbackground="#e0b000",
|
||||
selectforeground="#000",
|
||||
)
|
||||
sb_dup.config(command=lb_duplicity.yview)
|
||||
sb_dup.pack(side="right", fill="y")
|
||||
lb_duplicity.pack(side="left", fill="x", expand=True)
|
||||
|
||||
for d in duplicity:
|
||||
lb_duplicity.insert(tk.END, d)
|
||||
|
||||
# OnClick — připraveno pro budoucí funkcionalitu
|
||||
def on_duplicita_click(event):
|
||||
pass # TODO: budoucí akce při výběru duplicity
|
||||
|
||||
lb_duplicity.bind("<<ListboxSelect>>", on_duplicita_click)
|
||||
|
||||
# ── 3. Listbox 2 — návrhy Claudea ─────────────────────────────────────────
|
||||
nazev_bez = nazev[:-4] if nazev.endswith(".pdf") else nazev
|
||||
var = tk.StringVar(value=nazev_bez)
|
||||
|
||||
if varianty:
|
||||
tk.Label(root, text="Návrhy pojmenování (kliknutím vyberte):",
|
||||
anchor="w", font=font_bold).pack(fill="x", padx=12, pady=(8, 2))
|
||||
|
||||
frame_var = tk.Frame(root)
|
||||
frame_var.pack(fill="x", padx=12, pady=(0, 4))
|
||||
|
||||
sb_var = tk.Scrollbar(frame_var, orient="vertical")
|
||||
lb_varianty = tk.Listbox(
|
||||
frame_var,
|
||||
yscrollcommand=sb_var.set,
|
||||
font=font_small,
|
||||
height=min(len(varianty), 6),
|
||||
selectmode="single",
|
||||
activestyle="dotbox",
|
||||
bg="#f0f8ff",
|
||||
selectbackground="#2a7a2a",
|
||||
selectforeground="#fff",
|
||||
)
|
||||
sb_var.config(command=lb_varianty.yview)
|
||||
sb_var.pack(side="right", fill="y")
|
||||
lb_varianty.pack(side="left", fill="x", expand=True)
|
||||
|
||||
for v in varianty:
|
||||
v_bez = v[:-4] if v.endswith(".pdf") else v
|
||||
lb_varianty.insert(tk.END, v_bez)
|
||||
|
||||
# Klik → přepsat Entry
|
||||
def on_varianta_click(event):
|
||||
sel = lb_varianty.curselection()
|
||||
if sel:
|
||||
var.set(lb_varianty.get(sel[0]))
|
||||
|
||||
lb_varianty.bind("<<ListboxSelect>>", on_varianta_click)
|
||||
|
||||
# ── 4. Entry — definitivní název ──────────────────────────────────────────
|
||||
tk.Label(root, text="Název souboru (bez .pdf):", anchor="w",
|
||||
font=font_bold).pack(fill="x", padx=12, pady=(10, 2))
|
||||
|
||||
entry = tk.Entry(root, textvariable=var, font=font_ui, width=135)
|
||||
entry.pack(fill="x", padx=12, pady=(0, 10))
|
||||
entry.icursor(tk.END)
|
||||
entry.focus_set()
|
||||
|
||||
# ── 5. Tlačítka ───────────────────────────────────────────────────────────
|
||||
frame_btn = tk.Frame(root)
|
||||
frame_btn.pack(pady=(0, 12))
|
||||
|
||||
def schvalit(event=None):
|
||||
result["value"] = var.get().strip()
|
||||
root.destroy()
|
||||
|
||||
def preskocit(event=None):
|
||||
result["value"] = None
|
||||
root.destroy()
|
||||
|
||||
tk.Button(frame_btn, text="✓ Schválit (Enter)", command=schvalit,
|
||||
bg="#2a7a2a", fg="white", font=("Segoe UI", 10, "bold"),
|
||||
padx=16, pady=6).pack(side="left", padx=8)
|
||||
tk.Button(frame_btn, text="✗ Přeskočit (Esc)", command=preskocit,
|
||||
bg="#7a2a2a", fg="white", font=font_ui,
|
||||
padx=16, pady=6).pack(side="left", padx=8)
|
||||
|
||||
root.bind("<Return>", schvalit)
|
||||
root.bind("<Escape>", preskocit)
|
||||
|
||||
# ── Pozicování okna ───────────────────────────────────────────────────────
|
||||
root.update_idletasks()
|
||||
sw = root.winfo_screenwidth()
|
||||
w = root.winfo_width()
|
||||
h = root.winfo_height()
|
||||
x = (sw - w) // 2
|
||||
|
||||
try:
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from window_layout import get_layout, apply_geometry
|
||||
_layout = get_layout()
|
||||
|
||||
def _fallback_dlg():
|
||||
import ctypes, ctypes.wintypes
|
||||
rect = ctypes.wintypes.RECT()
|
||||
ctypes.windll.user32.SystemParametersInfoW(48, 0, ctypes.byref(rect), 0)
|
||||
work_bottom = rect.bottom
|
||||
below_y = None
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("--below-y="):
|
||||
below_y = int(arg.split("=", 1)[1])
|
||||
break
|
||||
if below_y is not None and below_y + h + 10 <= work_bottom:
|
||||
y = below_y
|
||||
else:
|
||||
y = max(0, work_bottom - h - 10)
|
||||
root.geometry(f"+{x}+{y}")
|
||||
|
||||
apply_geometry(root, _layout, "rename_dialog", fallback_fn=_fallback_dlg)
|
||||
except Exception:
|
||||
import ctypes, ctypes.wintypes
|
||||
rect = ctypes.wintypes.RECT()
|
||||
ctypes.windll.user32.SystemParametersInfoW(48, 0, ctypes.byref(rect), 0)
|
||||
work_bottom = rect.bottom
|
||||
below_y = None
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("--below-y="):
|
||||
below_y = int(arg.split("=", 1)[1])
|
||||
break
|
||||
if below_y is not None and below_y + h + 10 <= work_bottom:
|
||||
y = below_y
|
||||
else:
|
||||
y = max(0, work_bottom - h - 10)
|
||||
root.geometry(f"+{x}+{y}")
|
||||
|
||||
root.lift()
|
||||
root.focus_force()
|
||||
root.after(100, lambda: root.focus_force())
|
||||
root.after(200, lambda: root.attributes("-topmost", True))
|
||||
root.mainloop()
|
||||
|
||||
print(json.dumps({"value": result["value"]}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+11
-6
@@ -13,11 +13,8 @@ from PIL import Image, ImageTk
|
||||
import fitz
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(1)
|
||||
|
||||
variants = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
def show(variants: list) -> str | None:
|
||||
"""Zobrazí picker přímo (bez subprocesů). Vrátí cestu k vybrané variantě nebo None."""
|
||||
chosen = {"path": None}
|
||||
docs = [fitz.open(v["path"]) for v in variants]
|
||||
current = [0]
|
||||
@@ -145,7 +142,15 @@ def main():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(json.dumps({"chosen": chosen["path"]}, ensure_ascii=False))
|
||||
return chosen["path"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(1)
|
||||
variants = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
result = show(variants)
|
||||
print(json.dumps({"chosen": result}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Pomocný modul pro pozicování oken podle hostname počítače.
|
||||
Nastavení se načítá z layout_settings.json ve stejném adresáři.
|
||||
|
||||
Použití:
|
||||
from window_layout import get_layout
|
||||
layout = get_layout()
|
||||
geom = layout["preview_viewer"] # {"x": 1100, "y": 0, "w": 1400, "h": 2100}
|
||||
root.geometry(f"{geom['w']}x{geom['h']}+{geom['x']}+{geom['y']}")
|
||||
|
||||
Pro okna s "anchor": "bottom" (rename_dialog) použij get_bottom_y():
|
||||
y = layout["rename_dialog"]["x"] # jen x, y se počítá dynamicky
|
||||
"""
|
||||
import json
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
_SETTINGS_FILE = Path(__file__).parent / "layout_settings.json"
|
||||
|
||||
# Výchozí fallback layout (jeden monitor, původní chování)
|
||||
_DEFAULT = {
|
||||
"duplicity_viewer": None, # None = nepoužívat pevnou geometrii
|
||||
"preview_viewer": None,
|
||||
"rename_dialog": None,
|
||||
}
|
||||
|
||||
|
||||
def get_hostname() -> str:
|
||||
return socket.gethostname().upper()
|
||||
|
||||
|
||||
def get_layout() -> dict:
|
||||
"""
|
||||
Vrátí slovník s geometrií oken pro aktuální počítač.
|
||||
Pokud hostname není v settings, vrátí fallback (None = původní chování).
|
||||
"""
|
||||
hostname = get_hostname()
|
||||
if not _SETTINGS_FILE.exists():
|
||||
return dict(_DEFAULT)
|
||||
try:
|
||||
settings = json.loads(_SETTINGS_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return dict(_DEFAULT)
|
||||
return settings.get(hostname, dict(_DEFAULT))
|
||||
|
||||
|
||||
def apply_geometry(root, layout: dict, window: str, fallback_fn=None):
|
||||
"""
|
||||
Aplikuje geometrii okna z layoutu.
|
||||
- root: tkinter Tk nebo Toplevel
|
||||
- layout: výsledek get_layout()
|
||||
- window: klíč ("duplicity_viewer", "preview_viewer", "rename_dialog")
|
||||
- fallback_fn: callable() volaný pokud pro toto okno není layout definován
|
||||
"""
|
||||
geom = layout.get(window)
|
||||
if not geom:
|
||||
if fallback_fn:
|
||||
fallback_fn()
|
||||
return
|
||||
|
||||
w = geom.get("w")
|
||||
h = geom.get("h")
|
||||
x = geom.get("x", 0)
|
||||
y = geom.get("y", 0)
|
||||
|
||||
if geom.get("anchor") == "bottom":
|
||||
# Výška se ignoruje, okno se přilepí ke spodnímu okraji work area
|
||||
import ctypes, ctypes.wintypes
|
||||
rect = ctypes.wintypes.RECT()
|
||||
ctypes.windll.user32.SystemParametersInfoW(48, 0, ctypes.byref(rect), 0)
|
||||
root.update_idletasks()
|
||||
h_actual = root.winfo_height()
|
||||
y = rect.bottom - h_actual - 10
|
||||
if w:
|
||||
root.geometry(f"+{x}+{y}")
|
||||
else:
|
||||
root.geometry(f"+{x}+{y}")
|
||||
else:
|
||||
if w and h:
|
||||
root.geometry(f"{w}x{h}+{x}+{y}")
|
||||
elif w:
|
||||
root.update_idletasks()
|
||||
root.geometry(f"{w}x{root.winfo_height()}+{x}+{y}")
|
||||
else:
|
||||
root.update_idletasks()
|
||||
root.geometry(f"+{x}+{y}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
hostname = get_hostname()
|
||||
layout = get_layout()
|
||||
print(f"Hostname: {hostname}")
|
||||
if all(v is None for v in layout.values()):
|
||||
print("Žádné nastavení pro tento počítač — používá se výchozí chování.")
|
||||
print(f"Přidej klíč '{hostname}' do {_SETTINGS_FILE}")
|
||||
else:
|
||||
print(f"Layout pro '{hostname}':")
|
||||
for k, v in layout.items():
|
||||
print(f" {k}: {v}")
|
||||
@@ -1674,5 +1674,73 @@
|
||||
{
|
||||
"original": "535510353 2026-05-28 Rejfířová, Marcela [IADL test] [40b – nesoběstačnost; jízda s pomocí, nákup 0, vaření ohřev, domácí práce 0, prádlo 0, tel. 10, léky 10, finance s pomocí].pdf",
|
||||
"corrected": "535510353 2026-05-28 Rejfířová, Marcela [IADL test] [35b – nesoběstačnost; jízda s pomocí, nákup 0, vaření ohřev, domácí práce 0, prádlo 0, tel. 10, léky 10, finance s pomocí].pdf"
|
||||
},
|
||||
{
|
||||
"original": "435720013 2026-05-25 Lišková, Jaroslava [LZ kardiologie] [EKG: zrychlená Tf, bez čerstvých lož. změn, repolarizace bez poruch, bez isch.].pdf",
|
||||
"corrected": "435720013 2026-05-25 Lišková, Jaroslava [LZ kardiologie] [EKG zrychlená Tf, bez čerstvých lož. změn, repolarizace bez poruch, bez isch.].pdf"
|
||||
},
|
||||
{
|
||||
"original": "455925093 2026-04-08 Fialová, Růžena [LZ kardiologie] [AH, non-dipper, LBBB, aort. reg. 2+, EF LK 59%, diastol. dysfunkce, TK 176/99].pdf",
|
||||
"corrected": "455925093 2026-04-08 Fialová, Růžena [LZ kardiologie] [AH, non-dipper, LBBB, aort. reg. 2+, EF LK 59%, diastol. dysfunkce, TK 17699].pdf"
|
||||
},
|
||||
{
|
||||
"original": "475424136 2026-05-26 Müllerová, Miluše [LZ endokrinologie] [Izoechogenní uzly v PL ŠŽ, bez růst.progrese, supresní terapie, TSH <0.010].pdf",
|
||||
"corrected": "475424136 2026-05-26 Müllerová, Miluše [LZ endokrinologie] [Izoechogenní uzly v PL ŠŽ, bez růst.progrese, supresní terapie, TSH 0.010].pdf"
|
||||
},
|
||||
{
|
||||
"original": "485507406 2026-05-25 Jourová, Eva [Laboratoř] [dg. I839 - D-dimery 0.52 mg/l FEU (nad std. cut-off 0.50, pod věk-adj. 0.78)].pdf",
|
||||
"corrected": "485507406 2026-05-25 Jourová, Eva [Laboratoř] [dg. I839 - D-dimery 0.52 mgl FEU (nad std. cut-off 0.50, pod věk-adj. 0.78)].pdf"
|
||||
},
|
||||
{
|
||||
"original": "486111054 2026-05-22 Pelcová, Ludmila [LZ neurologie] [myasthenia gravis, AntiACHR >20 mmol/l, po plazmaferézách, Prednison 45mg, Imuran].pdf",
|
||||
"corrected": "486111054 2026-05-22 Pelcová, Ludmila [LZ neurologie] [myasthenia gravis, AntiACHR 20 mmoll, po plazmaferézách, Prednison 45mg, Imuran].pdf"
|
||||
},
|
||||
{
|
||||
"original": "495227264 2026-05-27 Hajšmanová, Marcela [TK deník] [květen 2026, Telmisartan ratiopharm 1/2 tbl, TK ráno 120-135, večer 139-148].pdf",
|
||||
"corrected": "495227264 2026-05-27 Hajšmanová, Marcela [domácí měření TK] [květen 2026, Telmisartan ratiopharm 12 tbl, TK ráno 120-135, večer 139-148].pdf"
|
||||
},
|
||||
{
|
||||
"original": "505516240 2022-05-04 Michková, Miroslava [LZ interna] [EKG norma, veget. stigmata, bez čerstvých ischem. změn a dysrytmií].pdf",
|
||||
"corrected": "505516240 2022-05-04 Michková, Miroslava [EKG] [EKG norma, veget. stigmata, bez čerstvých ischem. změn a dysrytmií].pdf"
|
||||
},
|
||||
{
|
||||
"original": "5455142143 2026-05-27 Setničková, Jitka [řidičský posudek] [zdravotně způsobilá s podmínkou - brýle sk. B, platnost do 27.05.2028].pdf",
|
||||
"corrected": "5455142143 2026-05-27 Setničková, Jitka [Posudek ŘP] [zdravotně způsobilá s podmínkou - brýle sk. B, platnost do 27.05.2028].pdf"
|
||||
},
|
||||
{
|
||||
"original": "5455142143 2026-05-27 Setničková, Jitka [Prohlášení zdravotní způsobilosti ŘP] [zdravotně způsobilá, sk. B brýle, medikace cukrovka a ŠŽ].pdf",
|
||||
"corrected": "5455142143 2026-05-27 Setničková, Jitka [Prohlášení ŘP] [zdravotně způsobilá, sk. B brýle, medikace cukrovka a ŠŽ].pdf"
|
||||
},
|
||||
{
|
||||
"original": "6559102187 2017-03-15 Nikitina, Svitlana [EKG] [sinusový rytmus 75/min, vertikální poloha, bez sign. změn ST, fyziol. záznam].pdf",
|
||||
"corrected": "6559102187 2017-03-15 Nikitina, Svitlana [EKG] [sinusový rytmus 75min, vertikální poloha, bez sign. změn ST, fyziol. záznam].pdf"
|
||||
},
|
||||
{
|
||||
"original": "6857112064 2026-05-26 Dundrová, Markéta [deník pacienta ABPM] [8:30 pěšky do práce, 16:10 pěšky z práce, 17:05 jízda autem, 19:25 večeře, 20:15 práce na zahradě].pdf",
|
||||
"corrected": "6857112064 2026-05-26 Dundrová, Markéta [Holter TK] [].pdf"
|
||||
},
|
||||
{
|
||||
"original": "6907220320 2026-04-20 Výprachtický, Ondřej [LZ diabetologie] [Recentní DM 2.typu, intenzif. inzulin. režim, vstupní polyurie/polydipsie, DLP na dietě].pdf",
|
||||
"corrected": "6907220320 2026-04-20 Výprachtický, Ondřej [LZ diabetologie] [Recentní DM 2.typu, intenzif. inzulin. režim, vstupní polyuriepolydipsie, DLP na dietě].pdf"
|
||||
},
|
||||
{
|
||||
"original": "7755230450 2025-05-26 Moučková, Eva [domácí měření TK] [deník TK a tepu, březen–květen 2025, TK 94-137/58-89, tep 52-73].pdf",
|
||||
"corrected": "7755230450 2025-05-26 Moučková, Eva [domácí měření TK] [deník TK a tepu, březen–květen 2025, TK 94-13758-89, tep 52-73].pdf"
|
||||
},
|
||||
{
|
||||
"original": "7856230448 2024-09-30 Kulhánková, Eliška [LZ kardiologie] [EF 64%, diastol. dysfunkce pseudonorm., stopová MR/TR/PR, perikard výpotek 1mm (hypothyreosa?)].pdf",
|
||||
"corrected": "7856230448 2024-09-30 Kulhánková, Eliška [LZ kardiologie] [EF 64%, diastol. dysfunkce pseudonorm., stopová MRTRPR, perikard výpotek 1mm (hypothyreosa)].pdf"
|
||||
},
|
||||
{
|
||||
"original": "5455142143 2026-01-26 Setničková, Jitka [LZ diabetologie] [DM2 dg 5/2019, MTF+DPP4i+gliflozin, kompenzace uspokojivá, MAU-ACR 4g/mol].pdf",
|
||||
"corrected": "5455142143 2026-01-26 Setničková, Jitka [LZ diabetologie] [DM2 dg 52019, MTF+DPP4i+gliflozin, kompenzace uspokojivá, MAU-ACR 4gmol].pdf"
|
||||
},
|
||||
{
|
||||
"original": "5610101959 null Olach, Milan [EHIC] [OZP 20701, platnost do 25052035].pdf",
|
||||
"corrected": "5610101959 2026-02-06 Olach, Milan [Evropský průkaz zdravotního pojištění] [OZP 207, platnost do 25MAY2035].pdf"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@@ -27,6 +27,8 @@ from pdf2image import convert_from_path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from Knihovny.najdi_dropbox import get_dropbox_root
|
||||
|
||||
import tkinter as tk
|
||||
|
||||
FB_CONFIG = {
|
||||
'dsn': r'reporter:c:\medicus\medicus.fdb',
|
||||
'user': 'SYSDBA',
|
||||
@@ -88,9 +90,6 @@ def start_dokumentace_index():
|
||||
t = threading.Thread(target=_load_dokumentace_index_bg, daemon=True)
|
||||
t.start()
|
||||
|
||||
VIEWER = Path(__file__).parent / "preview_viewer.py"
|
||||
RENAME_DIALOG = Path(__file__).parent / "rename_dialog.py"
|
||||
VARIANT_PICKER = Path(__file__).parent / "variant_picker.py"
|
||||
|
||||
# 5 kompresních variant
|
||||
COMPRESS_VARIANTS = [
|
||||
@@ -102,6 +101,469 @@ COMPRESS_VARIANTS = [
|
||||
]
|
||||
|
||||
|
||||
# ─── GUI — viewer dokumentů ──────────────────────────────────────────────────
|
||||
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
from ctypes import windll as _windll
|
||||
_windll.shcore.SetProcessDpiAwareness(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _open_doc(path_str: str):
|
||||
import fitz
|
||||
from PIL import Image
|
||||
p = Path(path_str)
|
||||
if not p.exists():
|
||||
return None, None
|
||||
suffix = p.suffix.lower()
|
||||
if suffix in (".jpg", ".jpeg", ".png"):
|
||||
return None, Image.open(p)
|
||||
return fitz.open(str(p)), None
|
||||
|
||||
|
||||
def _render(doc, pil_img, page_n: int, max_w: int, max_h: int):
|
||||
import fitz
|
||||
from PIL import Image
|
||||
if doc is not None:
|
||||
page = doc[page_n]
|
||||
zoom = min(max_w / page.rect.width, max_h / page.rect.height)
|
||||
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
||||
return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
|
||||
elif pil_img is not None:
|
||||
img = pil_img.copy()
|
||||
img.thumbnail((max_w, max_h))
|
||||
return img
|
||||
return None
|
||||
|
||||
|
||||
class PdfPane:
|
||||
def __init__(self, parent, max_w: int, max_h: int, bg: str = "#1e1e2e", label_text: str = ""):
|
||||
self.max_w = max_w
|
||||
self.max_h = max_h
|
||||
self.doc = None
|
||||
self.pil_img = None
|
||||
self.page_n = 0
|
||||
self.page_count = 1
|
||||
self.photo_ref = None
|
||||
|
||||
self.frame = tk.Frame(parent, bg=bg, width=max_w, height=max_h)
|
||||
self.frame.pack_propagate(False)
|
||||
|
||||
if label_text:
|
||||
tk.Label(self.frame, text=label_text, bg=bg, fg="#888",
|
||||
font=("Segoe UI", 8, "bold")).pack(pady=(2, 0))
|
||||
|
||||
self.lbl_title = tk.Label(self.frame, text="", bg=bg, fg="#aaa",
|
||||
font=("Segoe UI", 8), wraplength=max_w - 10)
|
||||
self.lbl_title.pack(pady=(2, 0))
|
||||
|
||||
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:
|
||||
try: self.doc.close()
|
||||
except Exception: pass
|
||||
self.doc, self.pil_img = _open_doc(path_str)
|
||||
self.page_count = len(self.doc) if self.doc else 1
|
||||
self.lbl_title.config(text=title or Path(path_str).name, fg="#ccc")
|
||||
self._show(0)
|
||||
|
||||
def clear(self, msg: str = ""):
|
||||
if self.doc:
|
||||
try: self.doc.close()
|
||||
except Exception: pass
|
||||
self.doc = None
|
||||
self.pil_img = None
|
||||
self.lbl_img.config(image="")
|
||||
self.lbl_title.config(text=msg, fg="#555")
|
||||
self.lbl_page.config(text="")
|
||||
self.btn_prev.config(state="disabled")
|
||||
self.btn_next.config(state="disabled")
|
||||
self.photo_ref = None
|
||||
|
||||
def _show(self, n: int):
|
||||
from PIL import ImageTk
|
||||
self.page_n = n
|
||||
img = _render(self.doc, self.pil_img, n, self.max_w - 10, self.max_h - 60)
|
||||
if img:
|
||||
self.photo_ref = ImageTk.PhotoImage(img)
|
||||
self.lbl_img.config(image=self.photo_ref)
|
||||
self.lbl_page.config(text=f"{n+1} / {self.page_count}" if self.page_count > 1 else "")
|
||||
self.btn_prev.config(state="normal" if n > 0 else "disabled")
|
||||
self.btn_next.config(state="normal" if n < self.page_count - 1 else "disabled")
|
||||
|
||||
def _prev(self):
|
||||
if self.page_n > 0: self._show(self.page_n - 1)
|
||||
|
||||
def _next(self):
|
||||
if self.page_n < self.page_count - 1: self._show(self.page_n + 1)
|
||||
|
||||
def close(self):
|
||||
if self.doc:
|
||||
try: self.doc.close()
|
||||
except Exception: pass
|
||||
|
||||
|
||||
def _get_viewer_layout() -> dict:
|
||||
"""Načte layout oken pro aktuální hostname z layout_settings.json."""
|
||||
import socket as _socket
|
||||
settings_file = Path(__file__).parent / "layout_settings.json"
|
||||
hostname = _socket.gethostname().upper()
|
||||
_default = {"duplicity_viewer": None}
|
||||
if not settings_file.exists():
|
||||
return _default
|
||||
try:
|
||||
settings = json.loads(settings_file.read_text(encoding="utf-8"))
|
||||
return settings.get(hostname, _default)
|
||||
except Exception:
|
||||
return _default
|
||||
|
||||
|
||||
def show_main_viewer(
|
||||
original_path: str = "",
|
||||
duplicity_paths: list = None,
|
||||
labels: list = None,
|
||||
nazev: str = "",
|
||||
info_lines: list = None,
|
||||
varianty: list = None,
|
||||
) -> str | None:
|
||||
"""Zobrazí hlavní viewer. Vrátí schválený název souboru (bez .pdf) nebo None."""
|
||||
duplicity_paths = duplicity_paths or []
|
||||
labels = labels or [Path(p).name for p in duplicity_paths]
|
||||
info_lines = info_lines or []
|
||||
varianty = varianty or []
|
||||
result = {"value": None}
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
import fitz
|
||||
except ImportError as e:
|
||||
print(f"[viewer] Chybí knihovna: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
try:
|
||||
lw = _get_viewer_layout().get("duplicity_viewer") or {}
|
||||
except Exception:
|
||||
lw = {}
|
||||
|
||||
WIN_X = lw.get("x", 0)
|
||||
WIN_Y = lw.get("y", 0)
|
||||
WIN_W = lw.get("w", 3840)
|
||||
WIN_H = lw.get("h", 1700)
|
||||
BOTTOM_H = 260
|
||||
TOP_H = WIN_H - BOTTOM_H - 8
|
||||
GAP = 4
|
||||
LISTBOX_W = 560
|
||||
PANE_W = (WIN_W - LISTBOX_W - GAP - 20) // 2
|
||||
BG = "#1a1a1a"
|
||||
COL_INFO = int(WIN_W * 0.15)
|
||||
COL_MID = int(WIN_W * 0.45)
|
||||
COL_VAR = WIN_W - COL_INFO - COL_MID
|
||||
|
||||
root = tk.Tk()
|
||||
root.tk.call("encoding", "system", "utf-8")
|
||||
root.title("Zpracování dokumentu")
|
||||
root.configure(bg=BG)
|
||||
root.geometry(f"{WIN_W}x{WIN_H}+{WIN_X}+{WIN_Y}")
|
||||
root.resizable(True, True)
|
||||
|
||||
frame_top = tk.Frame(root, bg=BG, height=TOP_H)
|
||||
frame_top.pack(side="top", fill="x", expand=False)
|
||||
frame_top.pack_propagate(False)
|
||||
|
||||
pane_orig = PdfPane(frame_top, max_w=PANE_W, max_h=TOP_H, bg=BG, label_text="ORIGINÁL")
|
||||
pane_orig.frame.pack(side="left", fill="y", padx=(6, GAP), pady=4)
|
||||
|
||||
pane_dup = PdfPane(frame_top, max_w=PANE_W, max_h=TOP_H, bg=BG, label_text="DUPLICITA")
|
||||
pane_dup.frame.pack(side="left", fill="y", padx=(GAP, GAP), pady=4)
|
||||
|
||||
frame_lb = tk.Frame(frame_top, bg=BG, width=LISTBOX_W)
|
||||
frame_lb.pack(side="left", fill="y", padx=(3, 6), pady=4)
|
||||
frame_lb.pack_propagate(False)
|
||||
|
||||
tk.Label(frame_lb, text="Existující dokumenty:", anchor="w",
|
||||
bg=BG, fg="#cc4444", font=("Segoe UI", 9, "bold")).pack(fill="x", padx=6, pady=(8, 2))
|
||||
|
||||
sb_dup = tk.Scrollbar(frame_lb, orient="vertical")
|
||||
txt_dup = tk.Text(frame_lb, yscrollcommand=sb_dup.set,
|
||||
font=("Segoe UI", 9), bg=BG, fg="#ddd",
|
||||
bd=0, highlightthickness=0, relief="flat",
|
||||
wrap="word", cursor="hand2", state="normal",
|
||||
selectbackground="#ffffff", selectforeground="#000000")
|
||||
sb_dup.config(command=txt_dup.yview)
|
||||
sb_dup.pack(side="right", fill="y")
|
||||
txt_dup.pack(side="left", fill="both", expand=True, padx=(6, 0))
|
||||
txt_dup.tag_config("selected", background="#ffffff", foreground="#000000")
|
||||
txt_dup.tag_config("normal", background=BG, foreground="#ddd")
|
||||
|
||||
def _shorten_dup_label(name: str) -> str:
|
||||
m = re.match(r"\d{9,10}\s+(\d{4}-\d{2}-\d{2})\s+[^[]+(\[.+)", name)
|
||||
return f"{m.group(1)} {m.group(2)}" if m else name
|
||||
|
||||
dup_line_indices = []
|
||||
if not duplicity_paths:
|
||||
txt_dup.insert("end", "(žádné duplicity)")
|
||||
txt_dup.config(state="disabled")
|
||||
else:
|
||||
for i, label in enumerate(labels):
|
||||
short = _shorten_dup_label(Path(label).name if not Path(label).exists() else label)
|
||||
ls = txt_dup.index("end")
|
||||
txt_dup.insert("end", short + "\n\n")
|
||||
le = txt_dup.index("end")
|
||||
dup_line_indices.append((ls, le))
|
||||
txt_dup.tag_add("normal", ls, le)
|
||||
txt_dup.config(state="disabled")
|
||||
|
||||
selected_dup = [None]
|
||||
|
||||
def _dup_click(event):
|
||||
txt_dup.config(state="normal")
|
||||
idx = txt_dup.index(f"@{event.x},{event.y}")
|
||||
for i, (ls, le) in enumerate(dup_line_indices):
|
||||
if txt_dup.compare(ls, "<=", idx) and txt_dup.compare(idx, "<", le):
|
||||
if selected_dup[0] is not None:
|
||||
pls, ple = dup_line_indices[selected_dup[0]]
|
||||
txt_dup.tag_remove("selected", pls, ple)
|
||||
txt_dup.tag_add("normal", pls, ple)
|
||||
txt_dup.tag_remove("normal", ls, le)
|
||||
txt_dup.tag_add("selected", ls, le)
|
||||
selected_dup[0] = i
|
||||
txt_dup.config(state="disabled")
|
||||
if i < len(duplicity_paths):
|
||||
pane_dup.load(duplicity_paths[i], title=labels[i] if i < len(labels) else "")
|
||||
return
|
||||
txt_dup.config(state="disabled")
|
||||
|
||||
txt_dup.bind("<Button-1>", _dup_click)
|
||||
tk.Frame(root, bg="#333", height=2).pack(fill="x")
|
||||
|
||||
frame_bot = tk.Frame(root, bg=BG, height=BOTTOM_H)
|
||||
frame_bot.pack(side="top", fill="both", expand=True)
|
||||
frame_bot.pack_propagate(False)
|
||||
|
||||
frame_info = tk.Frame(frame_bot, bg=BG, width=COL_INFO)
|
||||
frame_info.pack(side="left", fill="y", padx=(10, 4), pady=8)
|
||||
frame_info.pack_propagate(False)
|
||||
tk.Label(frame_info, text="Informace o pacientovi", anchor="w",
|
||||
bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(fill="x")
|
||||
for line in info_lines:
|
||||
color = "#cc4444" if line.startswith("⚠") or line.startswith("✗") else \
|
||||
"#44cc44" if line.startswith("✓") else "#aaa"
|
||||
tk.Label(frame_info, text=line, anchor="w", bg=BG, fg=color,
|
||||
font=("Segoe UI", 9), wraplength=COL_INFO - 20, justify="left").pack(fill="x", pady=1)
|
||||
|
||||
frame_mid = tk.Frame(frame_bot, bg=BG, width=COL_MID)
|
||||
frame_mid.pack(side="left", fill="y", padx=(4, 20), pady=8)
|
||||
frame_mid.pack_propagate(False)
|
||||
tk.Label(frame_mid, text="Název souboru (bez .pdf):", anchor="w",
|
||||
bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(fill="x")
|
||||
|
||||
nazev_bez = nazev[:-4] if nazev.endswith(".pdf") else nazev
|
||||
txt = tk.Text(frame_mid, font=("Segoe UI", 10), bg="#ffffff", fg="#000000",
|
||||
insertbackground="#000000", relief="flat", height=3,
|
||||
wrap="word", padx=6, pady=4, bd=0, highlightthickness=0)
|
||||
txt.pack(fill="x", pady=(4, 0))
|
||||
txt.insert("1.0", nazev_bez)
|
||||
txt.mark_set("insert", "end")
|
||||
txt.focus_set()
|
||||
|
||||
frame_btn = tk.Frame(frame_mid, bg=BG)
|
||||
frame_btn.pack(pady=(6, 0))
|
||||
|
||||
def schvalit(event=None):
|
||||
result["value"] = txt.get("1.0", "end").strip()
|
||||
pane_orig.close()
|
||||
pane_dup.close()
|
||||
root.destroy()
|
||||
|
||||
def preskocit(event=None):
|
||||
result["value"] = None
|
||||
pane_orig.close()
|
||||
pane_dup.close()
|
||||
root.destroy()
|
||||
|
||||
tk.Button(frame_btn, text="✓ Schválit (Enter)", command=schvalit,
|
||||
bg="#2a7a2a", fg="white", font=("Segoe UI", 10, "bold"),
|
||||
padx=16, pady=6, relief="flat").pack(side="left", padx=8)
|
||||
tk.Button(frame_btn, text="✗ Přeskočit (Esc)", command=preskocit,
|
||||
bg="#7a2a2a", fg="white", font=("Segoe UI", 10),
|
||||
padx=16, pady=6, relief="flat").pack(side="left", padx=8)
|
||||
root.bind("<Escape>", preskocit)
|
||||
root.bind("<Control-Return>", schvalit)
|
||||
|
||||
frame_right = tk.Frame(frame_bot, bg=BG, width=COL_VAR)
|
||||
frame_right.pack(side="left", fill="y", padx=(4, 10), pady=8)
|
||||
frame_right.pack_propagate(False)
|
||||
tk.Label(frame_right, text="Návrhy pojmenování (kliknutím vyberte):", anchor="w",
|
||||
bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(fill="x")
|
||||
sb_var = tk.Scrollbar(frame_right, orient="vertical")
|
||||
lb_var = tk.Listbox(frame_right, yscrollcommand=sb_var.set,
|
||||
font=("Segoe UI", 10), selectmode="single", activestyle="none",
|
||||
bg=BG, fg="#ddd", bd=0, highlightthickness=0,
|
||||
selectbackground="#ffffff", selectforeground="#000000", cursor="hand2")
|
||||
sb_var.config(command=lb_var.yview)
|
||||
sb_var.pack(side="right", fill="y")
|
||||
lb_var.pack(side="left", fill="both", expand=True)
|
||||
for v in varianty:
|
||||
lb_var.insert(tk.END, v[:-4] if v.endswith(".pdf") else v)
|
||||
|
||||
def on_varianta(event):
|
||||
sel = lb_var.curselection()
|
||||
if sel:
|
||||
txt.delete("1.0", "end")
|
||||
txt.insert("1.0", lb_var.get(sel[0]))
|
||||
|
||||
lb_var.bind("<<ListboxSelect>>", on_varianta)
|
||||
|
||||
if original_path and Path(original_path).exists():
|
||||
pane_orig.load(original_path, title=Path(original_path).name)
|
||||
else:
|
||||
pane_orig.clear(msg="(originál nedostupný)")
|
||||
|
||||
pane_dup.clear(msg="← vyberte duplicitu" if duplicity_paths else "(žádné duplicity)")
|
||||
|
||||
if duplicity_paths:
|
||||
pane_dup.load(duplicity_paths[0], title=labels[0] if labels else "")
|
||||
if dup_line_indices:
|
||||
txt_dup.config(state="normal")
|
||||
ls, le = dup_line_indices[0]
|
||||
txt_dup.tag_remove("normal", ls, le)
|
||||
txt_dup.tag_add("selected", ls, le)
|
||||
selected_dup[0] = 0
|
||||
txt_dup.config(state="disabled")
|
||||
|
||||
root.protocol("WM_DELETE_WINDOW", preskocit)
|
||||
root.lift()
|
||||
root.attributes("-topmost", True)
|
||||
root.after(1500, lambda: root.attributes("-topmost", False))
|
||||
root.mainloop()
|
||||
|
||||
return result["value"]
|
||||
|
||||
|
||||
# ─── GUI — variant picker ─────────────────────────────────────────────────────
|
||||
|
||||
def show_variant_picker(variants: list) -> str | None:
|
||||
"""Zobrazí picker kompresních variant. Vrátí cestu k vybrané variantě nebo None."""
|
||||
import fitz
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
chosen = {"path": None}
|
||||
docs = [fitz.open(v["path"]) for v in variants]
|
||||
current = [0]
|
||||
photo_ref = [None]
|
||||
current_page = [0]
|
||||
|
||||
root = tk.Tk()
|
||||
root.tk.call("encoding", "system", "utf-8")
|
||||
root.attributes("-topmost", True)
|
||||
|
||||
sh = root.winfo_screenheight()
|
||||
sw = root.winfo_screenwidth()
|
||||
win_h = sh - 80
|
||||
img_h = win_h - 160
|
||||
img_w = sw // 2
|
||||
x = (sw - img_w) // 2
|
||||
root.geometry(f"{img_w}x{win_h}+{x}+0")
|
||||
root.resizable(False, False)
|
||||
|
||||
frame_top = tk.Frame(root, bg="#222")
|
||||
frame_top.pack(fill="x")
|
||||
btn_variants = []
|
||||
|
||||
def show(n, page_n=0):
|
||||
current[0] = n
|
||||
current_page[0] = page_n
|
||||
doc = docs[n]
|
||||
page = doc[page_n]
|
||||
zoom = min(img_w / page.rect.width, img_h / page.rect.height)
|
||||
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
||||
img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
|
||||
photo_ref[0] = ImageTk.PhotoImage(img)
|
||||
lbl_img.config(image=photo_ref[0])
|
||||
page_count = len(doc)
|
||||
root.title(f"Varianta {n+1}: {variants[n]['label']} ({variants[n]['size_kb']} kB) — strana {page_n+1}/{page_count}")
|
||||
for i, b in enumerate(btn_variants):
|
||||
b.config(bg="#2a5a9a" if i == n else "#444")
|
||||
btn_prev_page.config(state="normal" if page_n > 0 else "disabled")
|
||||
btn_next_page.config(state="normal" if page_n < page_count - 1 else "disabled")
|
||||
|
||||
for i, v in enumerate(variants):
|
||||
b = tk.Button(frame_top, text=f"{i+1}. {v['label']}\n{v['size_kb']} kB",
|
||||
font=("Segoe UI", 9, "bold"), bg="#444", fg="white",
|
||||
relief="flat", padx=8, pady=6, command=lambda n=i: show(n))
|
||||
b.pack(side="left", padx=2, pady=4)
|
||||
btn_variants.append(b)
|
||||
|
||||
def beru():
|
||||
chosen["path"] = variants[current[0]]["path"]
|
||||
root.destroy()
|
||||
|
||||
def preskocit():
|
||||
root.destroy()
|
||||
|
||||
tk.Button(frame_top, text="✓ Tohle beru\n", command=beru,
|
||||
bg="#2a7a2a", fg="white", font=("Segoe UI", 9, "bold"),
|
||||
relief="flat", padx=8, pady=6).pack(side="left", padx=2, pady=4)
|
||||
tk.Button(frame_top, text="✗ Přeskočit\n", command=preskocit,
|
||||
bg="#7a2a2a", fg="white", font=("Segoe UI", 9, "bold"),
|
||||
relief="flat", padx=8, pady=6).pack(side="left", padx=2, pady=4)
|
||||
|
||||
btn_next_page = tk.Button(frame_top, text="Další ►\n",
|
||||
command=lambda: show(current[0], current_page[0] + 1),
|
||||
bg="#555", fg="white", font=("Segoe UI", 9, "bold"),
|
||||
relief="flat", padx=8, pady=6)
|
||||
btn_next_page.pack(side="right", padx=2, pady=4)
|
||||
btn_prev_page = tk.Button(frame_top, text="◄ Před.\n",
|
||||
command=lambda: show(current[0], current_page[0] - 1),
|
||||
bg="#555", fg="white", font=("Segoe UI", 9, "bold"),
|
||||
relief="flat", padx=8, pady=6)
|
||||
btn_prev_page.pack(side="right", padx=2, pady=4)
|
||||
|
||||
lbl_img = tk.Label(root, bg="black")
|
||||
lbl_img.pack(fill="both", expand=True)
|
||||
|
||||
root.bind("<Key-1>", lambda e: show(0))
|
||||
root.bind("<Key-2>", lambda e: show(1))
|
||||
root.bind("<Key-3>", lambda e: show(2))
|
||||
root.bind("<Key-4>", lambda e: show(3))
|
||||
root.bind("<Key-5>", lambda e: show(4))
|
||||
root.bind("<Return>", lambda e: beru())
|
||||
root.bind("<Escape>", lambda e: preskocit())
|
||||
|
||||
show(0)
|
||||
root.lift()
|
||||
root.focus_force()
|
||||
root.after(100, lambda: root.focus_force())
|
||||
root.after(200, lambda: root.attributes("-topmost", True))
|
||||
root.mainloop()
|
||||
|
||||
for d in docs:
|
||||
try: d.close()
|
||||
except Exception: pass
|
||||
|
||||
return chosen["path"]
|
||||
|
||||
|
||||
# ─── Komprese jedné varianty ──────────────────────────────────────────────────
|
||||
|
||||
def set_single_page_view(pdf_path: Path):
|
||||
@@ -512,6 +974,87 @@ def extract_info(pdf_path: Path, known_patient: str | None = None, known_rc: str
|
||||
return {"nazev_souboru": None}
|
||||
|
||||
|
||||
# ─── 2. volání Claude — generování variant názvů ────────────────────────────
|
||||
|
||||
def generate_name_variants(info: dict, nazev_prvni: str) -> list[str]:
|
||||
"""
|
||||
Druhé volání Claude (jen text, bez obrázku) — vygeneruje 5 variant názvu souboru.
|
||||
Vrátí seznam unikátních variant seřazených od nejlepší, deduplikovaných vůči nazev_prvni.
|
||||
"""
|
||||
naming_rules = load_naming_rules()
|
||||
corrections = build_corrections_prompt()
|
||||
|
||||
# Sestavíme kontext z dat extrahovaných v 1. volání
|
||||
ctx_parts = []
|
||||
if info.get("jmeno"):
|
||||
ctx_parts.append(f"Jméno pacienta: {info['jmeno']}")
|
||||
if info.get("rodne_cislo"):
|
||||
ctx_parts.append(f"Rodné číslo: {info['rodne_cislo']}")
|
||||
if info.get("datum_zpravy"):
|
||||
ctx_parts.append(f"Datum zprávy: {info['datum_zpravy']}")
|
||||
if info.get("typ_dokumentu"):
|
||||
ctx_parts.append(f"Typ dokumentu: {info['typ_dokumentu']}")
|
||||
if info.get("poznamka"):
|
||||
ctx_parts.append(f"Klinická poznámka (z 1. analýzy): {info['poznamka']}")
|
||||
if nazev_prvni:
|
||||
ctx_parts.append(f"Návrh z 1. analýzy: {nazev_prvni}")
|
||||
|
||||
context = "\n".join(ctx_parts)
|
||||
|
||||
prompt = (
|
||||
naming_rules +
|
||||
corrections +
|
||||
"Na základě níže uvedených dat o lékařské zprávě vygeneruj 5 různých variant názvu souboru. "
|
||||
"Varianty se mají lišit především formulací druhé závorky (různá míra detail, různé zkratky, různý důraz). "
|
||||
"Seřaď je od nejlepší (nejvýstižnější a nejpřesnější dle pravidel) po nejhorší. "
|
||||
"Vrať POUZE JSON pole s řetězci — názvy souborů včetně .pdf. "
|
||||
"Žádný jiný text ani vysvětlení.\n\n"
|
||||
f"Data:\n{context}\n\n"
|
||||
"Příklad výstupu: [\"RC datum Příjmení, Jméno [LZ kardiologie] [kontrola, ICHS, EKG za 3 měsíce].pdf\", ...]"
|
||||
)
|
||||
|
||||
print(" Volám Claude API pro varianty názvů...")
|
||||
try:
|
||||
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
|
||||
response = client.messages.create(
|
||||
model="claude-sonnet-4-6",
|
||||
max_tokens=600,
|
||||
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}")
|
||||
|
||||
raw = response.content[0].text.strip()
|
||||
if raw.startswith("```"):
|
||||
raw = raw.split("```")[1]
|
||||
if raw.startswith("json"):
|
||||
raw = raw[4:]
|
||||
varianty_raw: list[str] = json.loads(raw.strip())
|
||||
|
||||
# Deduplikace — odstraň varianty identické s nazev_prvni (porovnáváme bez .pdf)
|
||||
def _norm(s: str) -> str:
|
||||
return s.strip().removesuffix(".pdf").strip()
|
||||
|
||||
seen = {_norm(nazev_prvni)} if nazev_prvni else set()
|
||||
varianty_unique = []
|
||||
for v in varianty_raw:
|
||||
n = _norm(v)
|
||||
if n not in seen:
|
||||
seen.add(n)
|
||||
varianty_unique.append(v if v.endswith(".pdf") else v + ".pdf")
|
||||
|
||||
# Výsledný seznam: první návrh z 1. volání + unikátní varianty z 2. volání
|
||||
vysledek = []
|
||||
if nazev_prvni:
|
||||
vysledek.append(nazev_prvni if nazev_prvni.endswith(".pdf") else nazev_prvni + ".pdf")
|
||||
vysledek.extend(varianty_unique)
|
||||
return vysledek
|
||||
|
||||
except Exception as e:
|
||||
print(f" VAROVÁNÍ: generování variant selhalo ({e})")
|
||||
return [nazev_prvni] if nazev_prvni else []
|
||||
|
||||
|
||||
# ─── Subprocess helpers ───────────────────────────────────────────────────────
|
||||
|
||||
def open_preview(pdf_path: Path) -> tuple[subprocess.Popen, Path]:
|
||||
@@ -536,9 +1079,21 @@ def read_preview_bottom(geom_file: Path, timeout: float = 5.0) -> int:
|
||||
return None
|
||||
|
||||
|
||||
def run_rename_dialog(nazev: str, info_lines: list, below_y: int = None) -> str | None:
|
||||
def run_rename_dialog(
|
||||
nazev: str,
|
||||
info_lines: list,
|
||||
below_y: int = None,
|
||||
duplicity: list = None,
|
||||
varianty: list = None,
|
||||
) -> str | None:
|
||||
payload = {
|
||||
"nazev": nazev,
|
||||
"info_lines": info_lines,
|
||||
"duplicity": duplicity or [],
|
||||
"varianty": varianty or [],
|
||||
}
|
||||
tmp = Path(tempfile.mktemp(suffix=".json"))
|
||||
tmp.write_text(json.dumps({"nazev": nazev, "info_lines": info_lines}, ensure_ascii=False), encoding="utf-8")
|
||||
tmp.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||
args = [sys.executable, str(RENAME_DIALOG), str(tmp)]
|
||||
if below_y is not None:
|
||||
args.append(f"--below-y={below_y}")
|
||||
@@ -551,20 +1106,27 @@ def run_rename_dialog(nazev: str, info_lines: list, below_y: int = None) -> str
|
||||
return json.loads(out).get("value") if out else None
|
||||
|
||||
|
||||
def run_variant_picker(variants_data: list) -> str | None:
|
||||
tmp = Path(tempfile.mktemp(suffix=".json"))
|
||||
tmp.write_text(json.dumps(variants_data, ensure_ascii=False), encoding="utf-8")
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(VARIANT_PICKER), str(tmp)],
|
||||
capture_output=True, text=True, encoding="utf-8",
|
||||
def run_main_viewer(
|
||||
original_path: Path,
|
||||
nazev: str,
|
||||
info_lines: list,
|
||||
duplicity_names: list[str],
|
||||
varianty: list[str],
|
||||
) -> str | None:
|
||||
dup_paths = [str(DOKUMENTACE / name) for name in duplicity_names
|
||||
if (DOKUMENTACE / name).exists()]
|
||||
return show_main_viewer(
|
||||
original_path = str(original_path),
|
||||
duplicity_paths = dup_paths,
|
||||
labels = duplicity_names,
|
||||
nazev = nazev,
|
||||
info_lines = info_lines,
|
||||
varianty = varianty,
|
||||
)
|
||||
tmp.unlink(missing_ok=True)
|
||||
if proc.returncode != 0 or not proc.stdout.strip():
|
||||
print(f" [variant_picker] returncode={proc.returncode}")
|
||||
if proc.stderr.strip():
|
||||
print(f" [variant_picker] CHYBA:\n{proc.stderr.strip()}")
|
||||
out = proc.stdout.strip()
|
||||
return json.loads(out).get("chosen") if out else None
|
||||
|
||||
|
||||
def run_variant_picker(variants_data: list) -> str | None:
|
||||
return show_variant_picker(variants_data)
|
||||
|
||||
|
||||
# ─── Detekce split názvu ──────────────────────────────────────────────────────
|
||||
@@ -600,10 +1162,6 @@ def process_file(pdf_path: Path):
|
||||
verif = info["_verif"]
|
||||
rc_ocr = info["_rc_ocr"]
|
||||
|
||||
# 1. Otevři preview (pro EKG: soubor je již otočen)
|
||||
preview, geom_file = open_preview(pdf_path)
|
||||
below_y = read_preview_bottom(geom_file)
|
||||
|
||||
if not is_ekg:
|
||||
# 2. Zjisti RČ a jméno — buď z názvu (split soubor) nebo přes Claude Vision API
|
||||
split = _parse_split_filename(pdf_path.name)
|
||||
@@ -672,18 +1230,23 @@ def process_file(pdf_path: Path):
|
||||
# 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 duplicity:
|
||||
info_lines.append(f"⚠ DUPLICITA: {', '.join(duplicity)}")
|
||||
|
||||
if not info_lines:
|
||||
info_lines = ["[uprav ručně]"]
|
||||
print(" Otevírám dialog pro schválení názvu...")
|
||||
final_name = run_rename_dialog(nazev, info_lines, below_y=below_y)
|
||||
|
||||
preview.terminate()
|
||||
stderr_out = preview.stderr.read().decode("utf-8", errors="replace").strip() if preview.stderr else ""
|
||||
if stderr_out:
|
||||
print(f" [preview] CHYBA: {stderr_out}")
|
||||
# 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)
|
||||
|
||||
print(" Otevírám hlavní viewer...")
|
||||
final_name = run_main_viewer(
|
||||
original_path=pdf_path,
|
||||
nazev=nazev,
|
||||
info_lines=info_lines,
|
||||
duplicity_names=duplicity,
|
||||
varianty=varianty,
|
||||
)
|
||||
|
||||
if not final_name:
|
||||
print(" Přeskočeno.")
|
||||
@@ -693,7 +1256,12 @@ def process_file(pdf_path: Path):
|
||||
final_name += ".pdf"
|
||||
final_name = re.sub(r'[<>:"/\\|?*]', '', final_name)
|
||||
|
||||
if nazev and final_name != nazev:
|
||||
# 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()
|
||||
|
||||
vsechny_varianty = {_norm_name(v) for v in varianty}
|
||||
if nazev and _norm_name(final_name) not in vsechny_varianty:
|
||||
save_correction(nazev, final_name)
|
||||
|
||||
print(f" Schválený název: {final_name}")
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"Z230": {
|
||||
"_comment": "3 monitory: primární 3840x2160 (x=0), vlevo 1920x1200 (x=-1920), vpravo 1920x1200 (x=3840)",
|
||||
"duplicity_viewer": { "x": 0, "y": 0, "w": 3840, "h": 1800 },
|
||||
"rename_dialog": { "x": 0, "anchor": "bottom" }
|
||||
}
|
||||
}
|
||||
@@ -32,3 +32,16 @@ Tato pravidla platí vždy při generování polí `poznamka` a `nazev_souboru`.
|
||||
- Příklad výsledného názvu: `[Laboratoř] [sideropenická anémie, Hb 98, MCV 71, Fe 5.2]`
|
||||
|
||||
9. Jaterní enzymy (ALT, AST, GGT, ALP, LD/LDH) a bilirubin — hodnoty pod dolní hranicí normy (snížené) nezmiňuj v `poznamka` ani v `nazev_souboru`. Uváděj pouze hodnoty nad normu (zvýšené).
|
||||
|
||||
10. Druhá závorka pro LZ a PZ — obsah a pořadí: Pro dokumenty typu LZ (lékařská zpráva) a PZ (propouštěcí zpráva) tvoří druhou závorku tyto části v tomto pořadí (oddělené čárkou):
|
||||
a) **Typ návštěvy** — uveď pouze pokud je explicitně rozpoznatelný ze zprávy:
|
||||
- `kontrola` — plánovaná kontrola (např. „plánovaná kontrola", „přichází na kontrolu")
|
||||
- `neplánovaná kontrola` — pokud je výslovně uvedeno, že kontrola nebyla plánovaná
|
||||
- `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]`
|
||||
- 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]`
|
||||
- Pro PZ zůstává datum hospitalizace jako první (před typem návštěvy), viz pravidlo 2.
|
||||
|
||||
Reference in New Issue
Block a user