From a1cde8b4717fa897bd6f5d618799e4100a186174 Mon Sep 17 00:00:00 2001 From: "michaela.buzalkova" Date: Wed, 22 Apr 2026 09:08:34 +0200 Subject: [PATCH] =?UTF-8?q?60=20ScansProcessing:=20PDF=20n=C3=A1hled,=20fu?= =?UTF-8?q?zzy=20R=C4=8C,=20Medicus=20podle=20po=C4=8D=C3=ADta=C4=8De?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nová knihovna Knihovny/najdi_medicus.py — připojení k Medicus podle hostname (LEKAR=localhost/M:, SESTRA+LENOVO=192.168.1.10/M:, ostatní=localhost/C:) - Fuzzy matching RČ rozšířen o vkládání nuly (oprava sekvencí 00→0) - PDF náhled jako samostatný subprocess (preview_viewer.py, pymupdf) otevře se před OCR, zůstane do přejmenování, pracuje s temp kopií - Prompt: poznámka čerpá přednostně ze sekce Závěr: zprávy Co-Authored-By: Claude Sonnet 4.6 --- 60 ScansProcessing/corrections.json | 60 +++++ 60 ScansProcessing/extract_patient_info.py | 244 ++++++++++++++------- 60 ScansProcessing/preview_viewer.py | 98 +++++++++ 3 files changed, 328 insertions(+), 74 deletions(-) create mode 100644 60 ScansProcessing/preview_viewer.py diff --git a/60 ScansProcessing/corrections.json b/60 ScansProcessing/corrections.json index 825a290..6071a9d 100644 --- a/60 ScansProcessing/corrections.json +++ b/60 ScansProcessing/corrections.json @@ -226,5 +226,65 @@ { "original": "470629074 2026-04-21 Šebesta, Jaroslav [oznámení ZP správní řízení] [zahájení správního řízení, LRPéče indikace II/3 hypertenzní choroba II-III.st].pdf", "corrected": "470629074 2026-04-21 Šebesta, Jaroslav [oznámení ZP správní řízení] [zahájení správního řízení, návrh lázně, indikace II3 hypertenzní choroba II-III.st].pdf" + }, + { + "original": "5503040026 2026-02-17 Koubek, Jiří [LZ kardiologie] [ECHO: EF 65%, konc.hypertrofie, diastol.dysfunkce I.st, Bevimlar 20mg].pdf", + "corrected": "5503040026 2026-02-17 Koubek, Jiří [LZ kardiologie] [ECHO EF 65%, konc.hypertrofie, diastol.dysfunkce I.st, Bevimlar 20mg].pdf" + }, + { + "original": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [osteomarkery, Ca, P, ALP, vit.D 67,1 snížen, PTH, Beta-CrossLaps].pdf", + "corrected": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [osteomarkery, Ca, P, ALP, vit.D 67.1 snížen, PTH, Beta-CrossLaps].pdf" + }, + { + "original": "435520110 2026-04-20 Nechodomová, Marie [sonografie břicha] [hypersekr.žaludku, lipomatoza pankreatu, steatoza jat., cholecystolithiaza, splenomegalie].pdf", + "corrected": "435520110 2026-04-20 Nechodomová, Marie [sonografie břicha] [zesílení stěny žaludku - dovyšetřit, hypersekr.žaludku, lipomatoza pankreatu, steatoza jat., cholecystolithiaza, splenomegalie].pdf" + }, + { + "original": "6903020080 2026-04-20 Novotný, Martin [Laboratoř] [cholesterol 5.54, LDL 3.25, TG 2.06, glukoza 6.1, HbA1c 38].pdf", + "corrected": "6903020080 2026-04-20 Novotný, Martin [Laboratoř] [smíšená hyperlipidémie, prediabetes, cholesterol 5.54, LDL 3.25, TG 2.06, glukoza 6.1, HbA1c 38].pdf" + }, + { + "original": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [ELFO bílkovin, bílkovina 69.0, albumin 0.581, gama-globuliny 0.125].pdf", + "corrected": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [ELFO bílkovin OK, bílkovina 69.0, albumin 0.581, gama-globuliny 0.125].pdf" + }, + { + "original": "5556046672 2026-04-07 Simionová, Lýdia [Laboratoř] [močový konkrement, whewellit 100%, 6x3mm, hnědý, bradavičnatý].pdf", + "corrected": "5556046672 2026-04-07 Simionová, Lýdia [Laboratoř] [močový konkrement analýza, whewellit 100%, 6x3mm, hnědý, bradavičnatý].pdf" + }, + { + "original": "510802325 2026-04-20 Simion, Vladimír [LZ chirurgie] [chronický vřed kůže, TMT amputace IV.+V.prstu PDK, defekt LDK 5x3.5cm].pdf", + "corrected": "510802325 2026-04-20 Simion, Vladimír [LZ chirurgie] [chronický vřed kůže, TMT amputace IV.+V.prstu PDK, defekt LDK 5x3.5cm, DP 3xt].pdf" + }, + { + "original": "436114002 2026-03-17 Petrovská, Eliška [LZ kardiologie] [fibrilace síní paroxysmální, sinus, st.p.kardioverzi, rivaroxaban].pdf", + "corrected": "436114002 2026-03-17 Petrovská, Eliška [LZ kardiologie] [fibrilace síní paroxysmální, sinus, st.p.kardioverzi, rivaroxaban, ad Holter EKG, bisoprolol vysadí].pdf" + }, + { + "original": "436114002 2026-03-14 Petrovská, Eliška [LZ interna] [fibrilace síní paroxysmální, kardioverze, sinusový rytmus, rivaroxaban].pdf", + "corrected": "436114002 2026-03-14 Petrovská, Eliška [LZ interna urgent] [fibrilace síní paroxysmální, kardioverze, sinusový rytmus, rivaroxaban].pdf" + }, + { + "original": "6008091738 2026-04-20 Nikitin, Petro [Laboratoř] [urea 9.47 zvýš, CKD-EPI G2, glukoza 6.6, osmolalita 296, MCV 81.5].pdf", + "corrected": "6008091738 2026-04-20 Nikitin, Petro [Laboratoř] [Z000 prediabetes, mikrocyty, urea 9.47 zvýš, CKD-EPI G2, glukoza 6.6, osmolalita 296, MCV 81.5].pdf" + }, + { + "original": "440802018 2026-04-20 Havelka, Miroslav [Laboratoř] [CKD-EPI G2, NT-proBNP 6128 zvýš, CRP 6.6, MCV 81.8, MCHC 314].pdf", + "corrected": "440802018 2026-04-20 Havelka, Miroslav [Laboratoř] [srdeční selhání, mikrocyty, CKD-EPI G2, NT-proBNP 6128 zvýš, CRP 6.6, MCV 81.8, MCHC 314].pdf" + }, + { + "original": "7857260422 2023-02-28 Jindrová, Kateřina [LZ ORL] [st.p. incizi inflam aterom P tváře - zhojeno, extirpace atheromu P tváře].pdf", + "corrected": "7857260422 2023-02-28 Jindrová, Kateřina [LZ ORL] [st.p. incizi inflam aterom P tváře - zhojeno, extirpace atheromu P tváře domluveno].pdf" + }, + { + "original": "7857260422 2021-05-06 Jindrová, Kateřina [LZ angiologie] [CVI II. st. dle CEAP C4, ortostáza, flebitida/flebotrombóza bilat. neprokázána].pdf", + "corrected": "7857260422 2021-05-06 Jindrová, Kateřina [LZ angiologie] [CVI II. st. dle CEAP C4, ortostáza, flebitidaflebotrombóza bilat. neprokázána].pdf" + }, + { + "original": "7857260422 2021-05-20 Jindrová, Kateřina [LZ neurologie] [VAS C-pá, porucha statodynamiky C úseku, tinitus auric. bilat., ad rehab].pdf", + "corrected": "7857260422 2021-05-20 Jindrová, Kateřina [LZ neurologie] [VAS Cp, porucha statodynamiky C úseku, tinitus auric. bilat., ad RHB].pdf" + }, + { + "original": "7857260422 2024-02-12 Jindrová, Kateřina [EKG] [sinusový rytmus 70/min, semivertik poloha, osa 55st, fyziol záznam].pdf", + "corrected": "7857260422 2024-02-12 Jindrová, Kateřina [EKG] [sinusový rytmus 70min, semivertik poloha, osa 55st, fyziol záznam].pdf" } ] \ No newline at end of file diff --git a/60 ScansProcessing/extract_patient_info.py b/60 ScansProcessing/extract_patient_info.py index b14dd72..19d6ecc 100644 --- a/60 ScansProcessing/extract_patient_info.py +++ b/60 ScansProcessing/extract_patient_info.py @@ -13,6 +13,7 @@ import json import os import re import shutil +import subprocess import sys import time from pathlib import Path @@ -121,18 +122,23 @@ def _lookup_by_rc(cur, rc_digits: str) -> dict | None: def _rc_candidates(rc: str) -> list[str]: """ - Generuje kandidáty RČ pro fuzzy matching (edit distance 1): - - vynechání každé cifry (oprava extra znaku z OCR) + Generuje kandidáty RČ pro fuzzy matching: + - vynechání každé cifry (OCR přečetlo znak navíc) + - vložení nuly na každou pozici (OCR přehlédlo nulu v sekvenci 00) - záměna podobně vypadajících číslic na každé pozici Vrátí unikátní seznam kandidátů bez původního RČ. """ similar = {"0": "8", "8": "0", "1": "7", "7": "1", "5": "6", "6": "5", "3": "8"} candidates = set() - # Vynechání jedné cifry (nejčastější OCR chyba — přebývající nula) + # Vynechání jedné cifry (OCR přečetlo znak navíc) for i in range(len(rc)): candidates.add(rc[:i] + rc[i+1:]) + # Vložení nuly na každou pozici (nejčastější chyba: sekvence 00 přečtena jako 0) + for i in range(len(rc) + 1): + candidates.add(rc[:i] + "0" + rc[i:]) + # Záměna podobné cifry na každé pozici for i, ch in enumerate(rc): if ch in similar: @@ -235,7 +241,10 @@ def extract_patient_info(pdf_path: str) -> dict: "\"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ů\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" "- \"nazev_souboru\": název souboru ve formátu " "\"{rodne_cislo} {datum_zpravy} {Příjmení}, {Jméno} [{typ_dokumentu}] [{poznamka}].pdf\" " "(jméno bez titulu, RČ bez lomítka)\n" @@ -281,6 +290,95 @@ def sanitize_filename(name: str) -> str: return re.sub(r'[<>:"/\\|?*]', '', name) +def _open_preview(root, pdf_path: Path): + """Otevře náhledové okno PDF/obrázku jako Toplevel. Pracuje s temp kopií — žádné zamykání originálu.""" + import tkinter as tk + import tempfile + import shutil as _shutil + try: + from PIL import Image, ImageTk + import fitz + except ImportError: + return + + # Temp kopie — prohlížeč nikdy nesahá na originál + tmp = Path(tempfile.mktemp(suffix=pdf_path.suffix)) + _shutil.copy2(pdf_path, tmp) + + suffix = pdf_path.suffix.lower() + if suffix in (".jpg", ".jpeg", ".png"): + pil_pages = [Image.open(tmp)] + doc = None + else: + try: + doc = fitz.open(str(tmp)) + except Exception: + tmp.unlink(missing_ok=True) + return + pil_pages = [] + + def render(n) -> Image.Image: + if doc is not None: + page = doc[n] + zoom = min(700 / page.rect.width, (sh - 150) / 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_pages[0].copy() + img.thumbnail((700, sh - 150), Image.LANCZOS) + return img + + def on_close(): + try: + if doc: + doc.close() + except Exception: + pass + tmp.unlink(missing_ok=True) + win.destroy() + + page_count = len(doc) if doc else 1 + sh = root.winfo_screenheight() + current = [0] + photo_ref = [None] + + win = tk.Toplevel(root) + win.title(pdf_path.name) + win.attributes("-topmost", True) + win.resizable(False, False) + win.protocol("WM_DELETE_WINDOW", on_close) + + lbl_img = tk.Label(win) + lbl_img.pack() + + frame_nav = tk.Frame(win) + frame_nav.pack(pady=4) + + lbl_page = tk.Label(frame_nav, font=("Segoe UI", 9)) + lbl_page.pack(side="left", padx=10) + + def show(n): + current[0] = n + img = render(n) + photo_ref[0] = ImageTk.PhotoImage(img) + lbl_img.config(image=photo_ref[0]) + lbl_page.config(text=f"Strana {n + 1} / {page_count}") + btn_prev.config(state="normal" if n > 0 else "disabled") + btn_next.config(state="normal" if n < page_count - 1 else "disabled") + + btn_prev = tk.Button(frame_nav, text="◄ Předchozí", + command=lambda: show(current[0] - 1)) + btn_prev.pack(side="left") + btn_next = tk.Button(frame_nav, text="Další ►", + command=lambda: show(current[0] + 1)) + btn_next.pack(side="left") + + show(0) + + win.update_idletasks() + win.geometry(f"+0+0") + + def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None: """ Tkinter dialog pro schválení / opravu názvu souboru. @@ -291,15 +389,18 @@ def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None: result = {"value": None} root = tk.Tk() + root.withdraw() root.tk.call("encoding", "system", "utf-8") - root.title("Schválení názvu souboru") - root.resizable(True, False) - root.attributes("-topmost", True) + + dlg = tk.Toplevel(root) + dlg.title("Schválení názvu souboru") + dlg.resizable(True, False) + dlg.attributes("-topmost", True) pad = {"padx": 12, "pady": 6} # Informační sekce - frame_info = tk.Frame(root, bg="#f0f0f0", bd=1, relief="sunken") + frame_info = tk.Frame(dlg, bg="#f0f0f0", bd=1, relief="sunken") frame_info.pack(fill="x", **pad) for line in info_lines: color = "#b00000" if line.startswith("⚠") else "#004080" if line.startswith("✓") else "#333" @@ -307,18 +408,18 @@ def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None: fg=color, font=("Segoe UI", 10)).pack(fill="x", padx=8, pady=1) # Pole pro název (bez .pdf) - tk.Label(root, text="Název souboru (bez .pdf):", anchor="w", + tk.Label(dlg, text="Název souboru (bez .pdf):", anchor="w", font=("Segoe UI", 9, "bold")).pack(fill="x", padx=12, pady=(10, 2)) nazev_bez = nazev[:-4] if nazev and nazev.endswith(".pdf") else (nazev or "") var = tk.StringVar(value=nazev_bez) - entry = tk.Entry(root, textvariable=var, font=("Segoe UI", 10), width=90) + entry = tk.Entry(dlg, textvariable=var, font=("Segoe UI", 10), width=90) entry.pack(fill="x", padx=12, pady=(0, 10)) entry.icursor(tk.END) entry.focus_set() # Tlačítka - frame_btn = tk.Frame(root) + frame_btn = tk.Frame(dlg) frame_btn.pack(pady=(0, 12)) def schvalit(event=None): @@ -336,14 +437,18 @@ def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None: bg="#7a2a2a", fg="white", font=("Segoe UI", 10), padx=16, pady=6).pack(side="left", padx=8) - root.bind("", schvalit) - root.bind("", preskocit) + dlg.bind("", schvalit) + dlg.bind("", preskocit) - # Vystředit okno na obrazovce - root.update_idletasks() - w, h = root.winfo_width(), root.winfo_height() - sw, sh = root.winfo_screenwidth(), root.winfo_screenheight() - root.geometry(f"+{(sw - w) // 2}+{(sh - h) // 2}") + # Umísti dialog vpravo od náhledu (nebo vystředit pokud náhled není) + dlg.update_idletasks() + sw = dlg.winfo_screenwidth() + sh = dlg.winfo_screenheight() + w = dlg.winfo_width() + h = dlg.winfo_height() + x = min(720, sw - w - 20) + y = (sh - h) // 2 + dlg.geometry(f"+{x}+{y}") root.mainloop() return result["value"] @@ -408,43 +513,8 @@ def interactive_rename(pdf_path: Path, info: dict, verif: dict) -> bool: print(f" Navržený název: {nazev}") print(" Otevírám dialog...") - # Otevři PDF v prohlížeči (konkrétní proces, abychom zavřeli jen toto okno) - viewer_proc = None - try: - import subprocess - import winreg - import shlex - - def _get_pdf_exe() -> list[str] | None: - try: - with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r".pdf") as k: - prog_id = winreg.QueryValue(k, "") - with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, rf"{prog_id}\shell\open\command") as k: - cmd = winreg.QueryValue(k, "") - parts = shlex.split(cmd.replace("\\", "/")) - exe = parts[0].replace("/", "\\") - return [exe, str(pdf_path)] - except Exception: - return None - - cmd = _get_pdf_exe() - if cmd: - viewer_proc = subprocess.Popen(cmd) - else: - subprocess.Popen(["start", "", str(pdf_path)], shell=True) - except Exception as e: - print(f" [Prohlížeč] Nepodařilo se otevřít: {e}") - odpoved = _rename_dialog(nazev or "", info_lines) - # Zavři jen tento konkrétní proces prohlížeče a počkej na uvolnění zámku - if viewer_proc is not None: - try: - viewer_proc.terminate() - viewer_proc.wait(timeout=10) - except Exception: - pass - if odpoved is None: print(" Přeskočeno.") return False @@ -465,19 +535,11 @@ def interactive_rename(pdf_path: Path, info: dict, verif: dict) -> bool: print(f" VAROVÁNÍ: '{final_name}' již existuje v Processed, přeskakuji.") return False - for attempt in range(5): - try: - if pdf_path.suffix.lower() in (".jpg", ".jpeg", ".png"): - from jpg_to_pdf import image_to_pdf - image_to_pdf(pdf_path, dest, rotate_ccw=info.get("rotace") or 0) - else: - shutil.copy2(pdf_path, dest) - break - except PermissionError: - if attempt < 4: - time.sleep(0.5) - else: - raise + if pdf_path.suffix.lower() in (".jpg", ".jpeg", ".png"): + from jpg_to_pdf import image_to_pdf + image_to_pdf(pdf_path, dest, rotate_ccw=info.get("rotace") or 0) + else: + shutil.copy2(pdf_path, dest) pdf_path.unlink() print(f" ✓ Uloženo: Processed/{final_name}") @@ -486,15 +548,49 @@ def interactive_rename(pdf_path: Path, info: dict, verif: dict) -> bool: # ─── Hlavní logika ──────────────────────────────────────────────────────────── +def _start_preview_process(pdf_path: Path): + """ + Otevře náhled PDF jako samostatný subprocess (žádné tkinter threading problémy). + Pracuje s temp kopií — originál zůstane volný. + Vrátí funkci close() pro ukončení procesu. + """ + import tempfile + import shutil as _shutil + + tmp = Path(tempfile.mktemp(suffix=pdf_path.suffix)) + _shutil.copy2(pdf_path, tmp) + + viewer = Path(__file__).parent / "preview_viewer.py" + proc = subprocess.Popen( + [sys.executable, str(viewer), str(tmp), "--delete-on-close"], + creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0, + ) + + def close(): + try: + proc.terminate() + proc.wait(timeout=3) + except Exception: + pass + try: + tmp.unlink(missing_ok=True) + except Exception: + pass + + return close + + def process_file(pdf_path: Path): - info = extract_patient_info(str(pdf_path)) - - rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "") - print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...") - verif = verify_patient(rc_from_scan) - print_verification(verif, rc_from_scan) - - interactive_rename(pdf_path, info, verif) + close_preview = _start_preview_process(pdf_path) + try: + info = extract_patient_info(str(pdf_path)) + rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "") + print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...") + verif = verify_patient(rc_from_scan) + print_verification(verif, rc_from_scan) + interactive_rename(pdf_path, info, verif) + finally: + close_preview() def process_folder(folder: Path): pdf_files = sorted(f for f in folder.iterdir() diff --git a/60 ScansProcessing/preview_viewer.py b/60 ScansProcessing/preview_viewer.py new file mode 100644 index 0000000..f4b9cc2 --- /dev/null +++ b/60 ScansProcessing/preview_viewer.py @@ -0,0 +1,98 @@ +""" +Standalone PDF/obrázek náhled — spouští se jako subprocess z extract_patient_info.py. +Argumenty: preview_viewer.py [--delete-on-close] +""" +import sys +from pathlib import Path +import tkinter as tk + + +def main(): + if len(sys.argv) < 2: + sys.exit(1) + + pdf_path = Path(sys.argv[1]) + delete_on_close = "--delete-on-close" in sys.argv + + try: + from PIL import Image, ImageTk + import fitz + except ImportError: + sys.exit(2) + + suffix = pdf_path.suffix.lower() + if suffix in (".jpg", ".jpeg", ".png"): + pil_img = Image.open(pdf_path) + doc = None + else: + doc = fitz.open(str(pdf_path)) + pil_img = None + + root = tk.Tk() + root.tk.call("encoding", "system", "utf-8") + + sh = root.winfo_screenheight() + page_count = len(doc) if doc else 1 + current = [0] + photo_ref = [None] + + def render(n) -> Image.Image: + if doc is not None: + page = doc[n] + zoom = min(700 / page.rect.width, (sh - 150) / 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) + return img + + def on_close(): + if doc: + try: + doc.close() + except Exception: + pass + if delete_on_close: + try: + pdf_path.unlink(missing_ok=True) + except Exception: + pass + 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) + lbl_img.pack() + + frame_nav = tk.Frame(root) + frame_nav.pack(pady=4) + + lbl_page = tk.Label(frame_nav, font=("Segoe UI", 9)) + lbl_page.pack(side="left", padx=10) + + def show(n): + current[0] = n + img = render(n) + photo_ref[0] = ImageTk.PhotoImage(img) + lbl_img.config(image=photo_ref[0]) + lbl_page.config(text=f"Strana {n + 1} / {page_count}") + btn_prev.config(state="normal" if n > 0 else "disabled") + btn_next.config(state="normal" if n < page_count - 1 else "disabled") + + btn_prev = tk.Button(frame_nav, text="◄ Předchozí", command=lambda: show(current[0] - 1)) + btn_prev.pack(side="left") + btn_next = tk.Button(frame_nav, text="Další ►", command=lambda: show(current[0] + 1)) + btn_next.pack(side="left") + + show(0) + root.update_idletasks() + root.geometry("+0+0") + root.mainloop() + + +if __name__ == "__main__": + main()