From c9f94de2861e22915d112e40ded256d7ee314c80 Mon Sep 17 00:00:00 2001 From: "vladimir.buzalka" Date: Fri, 5 Jun 2026 15:48:09 +0200 Subject: [PATCH] z230 --- Faktury/{ => trash}/FakturyRenameClaude.py | 85 ++-- Faktury/trash/FakturyRenameClaudeLocalOCR.py | 386 +++++++++++++++++++ Faktury/trash/FakturyRenameOllama.py | 309 +++++++++++++++ Faktury/trash/FakturyRenameOpenAILocalOCR.py | 364 +++++++++++++++++ 4 files changed, 1120 insertions(+), 24 deletions(-) rename Faktury/{ => trash}/FakturyRenameClaude.py (69%) create mode 100644 Faktury/trash/FakturyRenameClaudeLocalOCR.py create mode 100644 Faktury/trash/FakturyRenameOllama.py create mode 100644 Faktury/trash/FakturyRenameOpenAILocalOCR.py diff --git a/Faktury/FakturyRenameClaude.py b/Faktury/trash/FakturyRenameClaude.py similarity index 69% rename from Faktury/FakturyRenameClaude.py rename to Faktury/trash/FakturyRenameClaude.py index 6ae8869..7cb1cff 100644 --- a/Faktury/FakturyRenameClaude.py +++ b/Faktury/trash/FakturyRenameClaude.py @@ -69,12 +69,27 @@ Vrať pouze JSON s polem "filename". CÍLOVÝ FORMÁT: YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf -PŘÍKLADY: -2026-06-01 Faktura ASKER 261103225 [kontejner Yannick 1.5 l] [339.00 CZK].pdf -2026-06-01 Faktura MEDIPOS 10195703 [CRP, kapiláry, písty, rukavice, nádoba] [5578.97 CZK].pdf -2026-05-29 Faktura Ptáček 202604570 [vakcíny Adacel, Vaqta, Havrix] [9235.20 CZK].pdf -2026-05-29 Faktura Poliklinika Prosek 91260763 [lékárna] [16165.40 CZK].pdf -2026-06-01 Dodací list QuickSeal 200609058 [VivaDiag Hydroxyvitamin D3] [2620.00 CZK].pdf +PŘÍKLADY — různé typy dokladů: +2026-01-22 Faktura MEDIPOS 101827406 [materiál do ordinace] [9620.80 CZK].pdf +2026-01-16 Faktura MEDEVIO 2600616 JAN2026 [1999.00 CZK].pdf +2026-01-07 Faktura Ptáček 202600168 [vakcíny] [6070.00 CZK].pdf +2026-01-15 Faktura Poliklinika Prosek 91251957 [telefon a sterilizace] [827.28 CZK].pdf +2026-01-29 Faktura QuickSeal 120600292 [kontrola kvality QSK 1. cyklus 2026] [6970.00 CZK].pdf +2026-01-20 Faktura Microsoft G136228996 [licence] [942.95 CZK].pdf +2026-01-04 Faktura OpenAI 3OXD6KWG-0006 [ChatGPT plus subscription] [558.88 CZK].pdf +2026-01-31 Faktura Mediately 80fae [předplatné] [175.12 CZK].pdf +2026-01-16 Faktura MEDATRON 2261100086 [coagucheck 2x] [5677.40 CZK].pdf +2026-02-11 Opravný doklad Alza 3260384509 [vratka faktury 4009941955] [-12941.00 CZK].pdf +2026-04-28 Faktura CLIMPROFI 900260026 [servis a čištění klimatizace] [2178.00 CZK].pdf +2026-01-19 Paragon [parkování Poliklinika Prosek 2026] [4800.00 CZK].pdf +2026-03-18 Paragon [papír do tiskárny] [180.00 CZK].pdf +2026-01-13 Mzdy MUDr. Buzalkové 202512 [Jarmila Kusinová].pdf +2025-03-31 Platba Michaela Buzalkové ČLK 2025 [4000.00 CZK].pdf +2025-01-24 Zálohová faktura Stormware 2512805657 [program Pohoda mini].pdf +2025-11-11 Faktura Avenier 425160437 [vakcíny] [28180.00 CZK].pdf +2025-04-03 Faktura CLIMPROFI 900250020 [servis a čištění klimatizace] [2057.00 CZK].pdf +2026-01-22 Dodatek Poliklinika Prosek [nájemní smlouva č.3].pdf +2025-12-31 Smlouva Kooperativa 8604142932 [profesní pojištění odpovědnosti 2026].pdf DŮLEŽITÁ PRAVIDLA: 1. Prefix [POHODA] nikdy nepřidávej. @@ -82,35 +97,54 @@ DŮLEŽITÁ PRAVIDLA: 3. Typ dokladu vyber podle dokumentu: - Faktura - Dobropis + - Opravný doklad ← pro storno/vrátky (ne Dobropis, pokud dokument říká "Opravný daňový doklad") - Paragon - Dodací list - Zálohová faktura - Smlouva - - Platba + - Dodatek ← pro dodatky ke smlouvám + - Platba ← pro členské příspěvky a podobné platby bez faktury - Poplatek + - Mzdy ← pro výplatní / mzdové dokumenty - Výdajový pokladní doklad 4. Pokud je v dokumentu napsáno "Dodací list není daňový doklad - nehraďte", typ musí být "Dodací list", ne "Faktura". -5. Dodavatel zapisuj krátce a konzistentně: - - MEDIPOS - - MEDEVIO - - MEDATRON - - ASKER - - QuickSeal - - Poliklinika Prosek +5. Dodavatel zapisuj krátce a konzistentně podle tohoto seznamu — použij přesně tato jména: + - MEDIPOS (Medipos, MEDIPOS s.r.o.) + - MEDEVIO (Medevio) + - MEDATRON (MEDATRON s.r.o., Medatron) + - ASKER (Asker) + - QuickSeal (QuickSeal International s.r.o.) + - Poliklinika Prosek (i pro "Lékárna Poliklinika Prosek a.s." — viz pravidlo 14) - Alza - Microsoft - - Anthropic - - Ptáček + - OpenAI + - Ptáček (i pro "Distribuce CZ" — viz pravidlo 6) + - Avenier + - Stormware (STORMWARE s.r.o., Pohoda software) + - CompuGroup (CompuGroup Medical, Medicus software) + - CLIMPROFI (CLIMPROFI s.r.o.) + - SEIVA (SEIVA s.r.o.) + - DrMAX + - Mediately (číslo je krátký hash, např. 80fae, bcd33) + - Kooperativa + - ICA (ICA a.s., První certifikační autorita — certifikáty) + - Česká pošta + - SOLDIERBOY + - OMNIPRAX + - Medicross (MediCross s.r.o.) 6. SPECIÁLNÍ PRAVIDLO: pokud je dodavatel/firma "Distribuce CZ", v názvu souboru použij dodavatele "Ptáček". 7. SPECIÁLNÍ PRAVIDLO: u faktur MEDIPOS použij jako číslo dokladu variabilní symbol nebo hlavní číslo faktury bez mezer, například 10195703. Nepoužívej interní evidenční číslo typu FV-5703/2026. -8. Částku piš vždy s desetinnou tečkou a měnou, například [5578.97 CZK]. -9. Když je částka v Kč, měna je CZK. -10. Popis drž krátký, praktický a česky. -11. Popis dávej do hranatých závorek. -12. Nepoužívej dvojtečky, lomítka, uvozovky ani znaky nevhodné pro Windows názvy souborů. -13. Pokud jde jen o dodací list bez daňového dokladu, částku můžeš uvést, ale typ musí zůstat Dodací list. -14. Pokud si nejsi jistý popisem, použij obecný popis typu [materiál do ordinace], [lékárna], [vakcíny], [testy]. -15. Výstup musí být pouze validní JSON, nic jiného. +8. SPECIÁLNÍ PRAVIDLO: u faktur MEDEVIO přidej za číslo faktury i měsíční kód, například "2600616 JAN2026". +9. Částku piš vždy s desetinnou tečkou a měnou, například [5578.97 CZK]. Pokud je částka záporná (dobropis/storno), piš ji jako [-12941.00 CZK]. +10. Pokud je částka v EUR, měna je EUR, pokud v Kč/CZK, měna je CZK. +11. Popis drž krátký, praktický a česky. +12. Popis dávej do hranatých závorek [popis]. +13. Nepoužívej dvojtečky, lomítka, uvozovky ani znaky nevhodné pro Windows názvy souborů. +14. Pokud je dodavatel "Lékárna Poliklinika Prosek", "Lékárna Prosek" nebo podobně, použij jako dodavatele "Poliklinika Prosek" a jako popis [lékárna] nebo [léky do ordinace]. +15. Paragon: pokud dokument nemá číslo dokladu, vynech ho. Popis musí popisovat co bylo nakoupeno. +16. Pokud jde jen o dodací list bez daňového dokladu, částku můžeš uvést, ale typ musí zůstat Dodací list. +17. Pokud si nejsi jistý popisem, použij obecný popis: [materiál do ordinace], [lékárna], [vakcíny], [testy], [licence], [předplatné]. +18. Výstup musí být pouze validní JSON, nic jiného. JSON FORMÁT: { @@ -161,6 +195,9 @@ def unique_path(target: Path) -> Path: def extract_json_object(text: str) -> dict: text = text.strip() + # Odstraň markdown code block (```json ... ``` nebo ``` ... ```) + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text.strip()).strip() try: return json.loads(text) except json.JSONDecodeError: diff --git a/Faktury/trash/FakturyRenameClaudeLocalOCR.py b/Faktury/trash/FakturyRenameClaudeLocalOCR.py new file mode 100644 index 0000000..c0908e6 --- /dev/null +++ b/Faktury/trash/FakturyRenameClaudeLocalOCR.py @@ -0,0 +1,386 @@ +# FakturyRenameClaudeLocalOCR.py +# Verze: 1.0 +# Datum: 05JUN2026 +# Autor: Claude (Anthropic) +# +# Popis: +# Jako FakturyRenameClaude.py, ale PDF se nezasílá do API. +# Místo toho se každá stránka PDF převede lokálně na obrázek (PyMuPDF), +# provede se OCR pomocí Tesseract (pytesseract) a Claude dostane pouze +# vytěžený text. Levnější a rychlejší — odesíláme jen text, ne velký PDF. +# +# Závislosti: +# pip install anthropic pymupdf pytesseract +# + nainstalovaný Tesseract: https://github.com/UB-Mannheim/tesseract/wiki +# +# Výsledný formát názvu: +# YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf +# +# Při DRY_RUN = False skript soubor přejmenuje a přesune do podadresáře +# NamedInvoicesbyClaude, aby další běh zpracovával jen nové dokumenty. + +import os +import json +import re +import time +from pathlib import Path + +import anthropic +import fitz # PyMuPDF +import pytesseract +from PIL import Image + + +# ========================= +# CENA API (jen text tokeny — levnější než posílat PDF) +# ========================= + +USD_TO_CZK = 25.0 + +MODEL = "claude-haiku-4-5" + +PRICE_INPUT_USD_PER_1M = 1.00 +PRICE_OUTPUT_USD_PER_1M = 5.00 + + +# ========================= +# NASTAVENÍ +# ========================= + +FOLDER = Path( + r"u:\Dropbox\Ordinace\!!MUDr. Michaela Buzalková s.r.o\Prosek\#040 Faktury přijaté" +) + +PROCESSED_FOLDER = FOLDER / "NamedInvoicesByClaudeLocalOCR" + +# Pro test na 3 fakturách nech DRY_RUN = True. +DRY_RUN = False + +PDF_PATTERN = "*.pdf" + +LOG_FILE = FOLDER / "_rename_log_invoices_claude_ocr.txt" + +ENV_FILE = Path(__file__).resolve().parent.parent / "Medevio" / ".env" + +# Cesta k Tesseract.exe (Windows výchozí instalace) +TESSERACT_CMD = r"C:\Program Files\Tesseract-OCR\tesseract.exe" + +# Jazyk OCR — češtiny + angličtina (pro čísla, názvy) +TESSERACT_LANG = "ces+eng" + +# DPI pro renderování stránek PDF před OCR (150 = rychlé, 300 = přesnější) +OCR_DPI = 300 + + +# ========================= +# PRAVIDLA PRO POJMENOVÁNÍ +# ========================= + +NAMING_RULES = """ +Jsi pomocník pro pojmenování naskenovaných PDF dokladů MUDr. Michaely Buzalkové. + +ÚKOL: +Z OCR textu faktury/dokladu vytěž datum, typ dokladu, dodavatele, číslo dokladu, stručný popis, částku a měnu. +Vrať pouze JSON s polem "filename". + +CÍLOVÝ FORMÁT: +YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf + +PŘÍKLADY — různé typy dokladů: +2026-01-22 Faktura MEDIPOS 101827406 [materiál do ordinace] [9620.80 CZK].pdf +2026-01-16 Faktura MEDEVIO 2600616 JAN2026 [1999.00 CZK].pdf +2026-01-07 Faktura Ptáček 202600168 [vakcíny] [6070.00 CZK].pdf +2026-01-15 Faktura Poliklinika Prosek 91251957 [telefon a sterilizace] [827.28 CZK].pdf +2026-01-29 Faktura QuickSeal 120600292 [kontrola kvality QSK 1. cyklus 2026] [6970.00 CZK].pdf +2026-01-20 Faktura Microsoft G136228996 [licence] [942.95 CZK].pdf +2026-01-04 Faktura OpenAI 3OXD6KWG-0006 [ChatGPT plus subscription] [558.88 CZK].pdf +2026-01-31 Faktura Mediately 80fae [předplatné] [175.12 CZK].pdf +2026-01-16 Faktura MEDATRON 2261100086 [coagucheck 2x] [5677.40 CZK].pdf +2026-02-11 Opravný doklad Alza 3260384509 [vratka faktury 4009941955] [-12941.00 CZK].pdf +2026-04-28 Faktura CLIMPROFI 900260026 [servis a čištění klimatizace] [2178.00 CZK].pdf +2026-01-19 Paragon [parkování Poliklinika Prosek 2026] [4800.00 CZK].pdf +2026-03-18 Paragon [papír do tiskárny] [180.00 CZK].pdf +2026-01-13 Mzdy MUDr. Buzalkové 202512 [Jarmila Kusinová].pdf +2025-03-31 Platba Michaela Buzalkové ČLK 2025 [4000.00 CZK].pdf +2025-01-24 Zálohová faktura Stormware 2512805657 [program Pohoda mini].pdf +2025-11-11 Faktura Avenier 425160437 [vakcíny] [28180.00 CZK].pdf +2025-04-03 Faktura CLIMPROFI 900250020 [servis a čištění klimatizace] [2057.00 CZK].pdf +2026-01-22 Dodatek Poliklinika Prosek [nájemní smlouva č.3].pdf +2025-12-31 Smlouva Kooperativa 8604142932 [profesní pojištění odpovědnosti 2026].pdf + +DŮLEŽITÁ PRAVIDLA: +1. Prefix [POHODA] nikdy nepřidávej. +2. Používej datum vystavení dokladu, ne datum splatnosti. +3. Typ dokladu vyber podle dokumentu: + - Faktura + - Dobropis + - Opravný doklad ← pro storno/vrátky (ne Dobropis, pokud dokument říká "Opravný daňový doklad") + - Paragon + - Dodací list + - Zálohová faktura + - Smlouva + - Dodatek ← pro dodatky ke smlouvám + - Platba ← pro členské příspěvky a podobné platby bez faktury + - Poplatek + - Mzdy ← pro výplatní / mzdové dokumenty + - Výdajový pokladní doklad +4. Pokud je v dokumentu napsáno "Dodací list není daňový doklad - nehraďte", typ musí být "Dodací list", ne "Faktura". +5. Dodavatel zapisuj krátce a konzistentně podle tohoto seznamu — použij přesně tato jména: + - MEDIPOS (Medipos, MEDIPOS s.r.o.) + - MEDEVIO (Medevio) + - MEDATRON (MEDATRON s.r.o., Medatron) + - ASKER (Asker) + - QuickSeal (QuickSeal International s.r.o.) + - Poliklinika Prosek (i pro "Lékárna Poliklinika Prosek a.s." — viz pravidlo 14) + - Alza + - Microsoft + - OpenAI + - Ptáček (i pro "Distribuce CZ" — viz pravidlo 6) + - Avenier + - Stormware (STORMWARE s.r.o., Pohoda software) + - CompuGroup (CompuGroup Medical, Medicus software) + - CLIMPROFI (CLIMPROFI s.r.o.) + - SEIVA (SEIVA s.r.o.) + - DrMAX + - Mediately (číslo je krátký hash, např. 80fae, bcd33) + - Kooperativa + - ICA (ICA a.s., První certifikační autorita — certifikáty) + - Česká pošta + - SOLDIERBOY + - OMNIPRAX + - Medicross (MediCross s.r.o.) +6. SPECIÁLNÍ PRAVIDLO: pokud je dodavatel/firma "Distribuce CZ", v názvu souboru použij dodavatele "Ptáček". +7. SPECIÁLNÍ PRAVIDLO: u faktur MEDIPOS použij jako číslo dokladu variabilní symbol nebo hlavní číslo faktury bez mezer, například 10195703. Nepoužívej interní evidenční číslo typu FV-5703/2026. +8. SPECIÁLNÍ PRAVIDLO: u faktur MEDEVIO přidej za číslo faktury i měsíční kód, například "2600616 JAN2026". +9. Částku piš vždy s desetinnou tečkou a měnou, například [5578.97 CZK]. Pokud je částka záporná (dobropis/storno), piš ji jako [-12941.00 CZK]. +10. Pokud je částka v EUR, měna je EUR, pokud v Kč/CZK, měna je CZK. +11. Popis drž krátký, praktický a česky. +12. Popis dávej do hranatých závorek [popis]. +13. Nepoužívej dvojtečky, lomítka, uvozovky ani znaky nevhodné pro Windows názvy souborů. +14. Pokud je dodavatel "Lékárna Poliklinika Prosek", "Lékárna Prosek" nebo podobně, použij jako dodavatele "Poliklinika Prosek" a jako popis [lékárna] nebo [léky do ordinace]. +15. Paragon: pokud dokument nemá číslo dokladu, vynech ho. Popis musí popisovat co bylo nakoupeno. +16. Pokud jde jen o dodací list bez daňového dokladu, částku můžeš uvést, ale typ musí zůstat Dodací list. +17. Pokud si nejsi jistý popisem, použij obecný popis: [materiál do ordinace], [lékárna], [vakcíny], [testy], [licence], [předplatné]. +18. Výstup musí být pouze validní JSON, nic jiného. + +JSON FORMÁT: +{ + "filename": "YYYY-MM-DD Faktura Dodavatel 123456 [popis] [123.45 CZK].pdf" +} +""" + + +# ========================= +# POMOCNÉ FUNKCE +# ========================= + +def load_env() -> None: + if ENV_FILE.exists(): + for line in ENV_FILE.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() + + +def ocr_pdf(pdf_path: Path) -> str: + """Převede PDF na obrázky a provede OCR Tesseractem. Vrátí spojený text.""" + doc = fitz.open(str(pdf_path)) + texts = [] + matrix = fitz.Matrix(OCR_DPI / 72, OCR_DPI / 72) + + for page_num, page in enumerate(doc, start=1): + pix = page.get_pixmap(matrix=matrix, colorspace=fitz.csRGB) + img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) + page_text = pytesseract.image_to_string(img, lang=TESSERACT_LANG) + texts.append(f"--- Strana {page_num} ---\n{page_text}") + + doc.close() + return "\n\n".join(texts) + + +def sanitize_windows_filename(name: str) -> str: + name = re.sub(r'[<>:"/\\|?*]', " ", name) + name = re.sub(r"\s+", " ", name).strip() + name = name.rstrip(" .") + if not name.lower().endswith(".pdf"): + name += ".pdf" + return name + + +def unique_path(target: Path) -> Path: + if not target.exists(): + return target + stem = target.stem + suffix = target.suffix + parent = target.parent + i = 2 + while True: + candidate = parent / f"{stem} ({i}){suffix}" + if not candidate.exists(): + return candidate + i += 1 + + +def extract_json_object(text: str) -> dict: + text = text.strip() + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text.strip()).strip() + try: + return json.loads(text) + except json.JSONDecodeError: + pass + match = re.search(r"\{.*\}", text, flags=re.DOTALL) + if not match: + raise ValueError(f"Model nevrátil JSON:\n{text}") + return json.loads(match.group(0)) + + +def calculate_cost(input_tokens: int, output_tokens: int) -> dict: + input_cost_usd = input_tokens / 1_000_000 * PRICE_INPUT_USD_PER_1M + output_cost_usd = output_tokens / 1_000_000 * PRICE_OUTPUT_USD_PER_1M + total_cost_usd = input_cost_usd + output_cost_usd + return { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "total_tokens": input_tokens + output_tokens, + "input_cost_usd": input_cost_usd, + "output_cost_usd": output_cost_usd, + "total_cost_usd": total_cost_usd, + "total_cost_czk": total_cost_usd * USD_TO_CZK, + } + + +def ask_claude_for_filename(client: anthropic.Anthropic, ocr_text: str) -> tuple[str, dict]: + prompt = f"OCR text z faktury:\n\n{ocr_text}\n\n{NAMING_RULES}" + + response = client.messages.create( + model=MODEL, + max_tokens=256, + messages=[ + { + "role": "user", + "content": prompt, + } + ], + ) + + text = next( + (block.text for block in response.content if block.type == "text"), "" + ).strip() + + obj = extract_json_object(text) + filename = obj.get("filename", "").strip() + if not filename: + raise ValueError(f"JSON neobsahuje filename:\n{text}") + + cost = calculate_cost(response.usage.input_tokens, response.usage.output_tokens) + return sanitize_windows_filename(filename), cost + + +def log_line(text: str) -> None: + print(text) + with LOG_FILE.open("a", encoding="utf-8") as f: + f.write(text + "\n") + + +# ========================= +# HLAVNÍ BĚH +# ========================= + +def main() -> None: + if not FOLDER.exists(): + raise FileNotFoundError(f"Adresář neexistuje: {FOLDER}") + + pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD + + load_env() + + if not os.getenv("ANTHROPIC_API_KEY"): + raise RuntimeError(f"Chybí ANTHROPIC_API_KEY. Zkontroluj soubor {ENV_FILE}") + + client = anthropic.Anthropic() + + pdfs = sorted(FOLDER.glob(PDF_PATTERN)) + pdfs = [p for p in pdfs if p.is_file() and p.suffix.lower() == ".pdf"] + + if not pdfs: + print("Nenalezeno žádné PDF.") + return + + total_input_tokens = 0 + total_output_tokens = 0 + total_tokens = 0 + total_cost_usd = 0.0 + total_cost_czk = 0.0 + + log_line("") + log_line("=" * 80) + log_line(f"START: {time.strftime('%Y-%m-%d %H:%M:%S')}") + log_line(f"Adresář: {FOLDER}") + log_line(f"Hotové faktury: {PROCESSED_FOLDER}") + log_line(f"Počet PDF: {len(pdfs)}") + log_line(f"DRY_RUN: {DRY_RUN}") + log_line(f"MODEL: {MODEL} (lokální OCR, do API jde jen text)") + log_line(f"OCR DPI: {OCR_DPI}, jazyk: {TESSERACT_LANG}") + log_line(f"Kurz: 1 USD = {USD_TO_CZK:.2f} CZK") + log_line("=" * 80) + + for i, pdf in enumerate(pdfs, start=1): + log_line(f"\n[{i}/{len(pdfs)}] Původní název: {pdf.name}") + + try: + log_line(" OCR...") + ocr_text = ocr_pdf(pdf) + ocr_chars = len(ocr_text) + log_line(f" OCR hotovo: {ocr_chars} znaků") + + new_name, cost = ask_claude_for_filename(client, ocr_text) + + total_input_tokens += cost["input_tokens"] + total_output_tokens += cost["output_tokens"] + total_tokens += cost["total_tokens"] + total_cost_usd += cost["total_cost_usd"] + total_cost_czk += cost["total_cost_czk"] + + log_line(f" Návrh: {new_name}") + log_line( + f" Tokeny: input={cost['input_tokens']}, " + f"output={cost['output_tokens']}, " + f"total={cost['total_tokens']}" + ) + log_line( + f" Cena volání: ${cost['total_cost_usd']:.6f} " + f"≈ {cost['total_cost_czk']:.2f} Kč" + ) + + target = unique_path(PROCESSED_FOLDER / new_name) + if target.name != new_name: + log_line(f" Cíl po vyřešení konfliktu: {target.name}") + + if DRY_RUN: + log_line(f" Cíl: {target}") + log_line(" Stav: DRY-RUN, nepřejmenováno/nepřesunuto") + else: + PROCESSED_FOLDER.mkdir(exist_ok=True) + pdf.rename(target) + if pdf.name == new_name: + log_line(" Stav: PŘESUNUTO") + else: + log_line(" Stav: PŘEJMENOVÁNO A PŘESUNUTO") + + except Exception as e: + log_line(f" CHYBA: {type(e).__name__}: {e}") + + log_line("") + log_line("=" * 80) + log_line("SOUHRN CENY") + log_line(f"Tokeny celkem: input={total_input_tokens}, output={total_output_tokens}, total={total_tokens}") + log_line(f"Cena celkem: ${total_cost_usd:.6f} ≈ {total_cost_czk:.2f} Kč") + log_line("=" * 80) + + log_line("\nHOTOVO") + + +if __name__ == "__main__": + main() diff --git a/Faktury/trash/FakturyRenameOllama.py b/Faktury/trash/FakturyRenameOllama.py new file mode 100644 index 0000000..87fdd47 --- /dev/null +++ b/Faktury/trash/FakturyRenameOllama.py @@ -0,0 +1,309 @@ +# FakturyRenameOllama.py +# Verze: 1.0 +# Datum: 05JUN2026 +# Autor: Claude (Anthropic) +# +# Popis: +# Jako FakturyRenameClaudeLocalOCR.py, ale místo Anthropic API posílá +# OCR text na lokální Ollama server (Unraid). Žádné API tokeny, žádné náklady. +# Lokální OCR (Tesseract) + lokální LLM (Ollama) = 100% offline. +# +# Závislosti: +# pip install pymupdf pytesseract pillow requests +# + nainstalovaný Tesseract: https://github.com/UB-Mannheim/tesseract/wiki +# +# Výsledný formát názvu: +# YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf + +import json +import re +import time +import requests +from pathlib import Path + +import fitz # PyMuPDF +import pytesseract +from PIL import Image + + +# ========================= +# NASTAVENÍ OLLAMA +# ========================= + +OLLAMA_HOST = "http://192.168.1.76:11434" +MODEL = "mistral:7b" + + +# ========================= +# NASTAVENÍ +# ========================= + +FOLDER = Path( + r"u:\Dropbox\Ordinace\!!MUDr. Michaela Buzalková s.r.o\Prosek\#040 Faktury přijaté" +) + +PROCESSED_FOLDER = FOLDER / "NamedInvoicesByOllama" + +# Pro test nech DRY_RUN = True — jen vypíše návrhy, nepřejmenuje. +DRY_RUN = False + +PDF_PATTERN = "*.pdf" + +LOG_FILE = FOLDER / "_rename_log_invoices_ollama.txt" + +# Cesta k Tesseract.exe +TESSERACT_CMD = r"C:\Program Files\Tesseract-OCR\tesseract.exe" + +TESSERACT_LANG = "ces+eng" + +OCR_DPI = 300 + + +# ========================= +# PRAVIDLA PRO POJMENOVÁNÍ +# ========================= + +NAMING_RULES = """ +Jsi pomocník pro pojmenování naskenovaných PDF dokladů MUDr. Michaely Buzalkové. + +ÚKOL: +Z OCR textu faktury/dokladu vytěž datum, typ dokladu, dodavatele, číslo dokladu, stručný popis, částku a měnu. +Vrať pouze JSON s polem "filename". + +CÍLOVÝ FORMÁT: +YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf + +PŘÍKLADY — různé typy dokladů: +2026-01-22 Faktura MEDIPOS 101827406 [materiál do ordinace] [9620.80 CZK].pdf +2026-01-16 Faktura MEDEVIO 2600616 JAN2026 [1999.00 CZK].pdf +2026-01-07 Faktura Ptáček 202600168 [vakcíny] [6070.00 CZK].pdf +2026-01-15 Faktura Poliklinika Prosek 91251957 [telefon a sterilizace] [827.28 CZK].pdf +2026-01-29 Faktura QuickSeal 120600292 [kontrola kvality QSK 1. cyklus 2026] [6970.00 CZK].pdf +2026-01-20 Faktura Microsoft G136228996 [licence] [942.95 CZK].pdf +2026-01-04 Faktura OpenAI 3OXD6KWG-0006 [ChatGPT plus subscription] [558.88 CZK].pdf +2026-01-31 Faktura Mediately 80fae [předplatné] [175.12 CZK].pdf +2026-01-16 Faktura MEDATRON 2261100086 [coagucheck 2x] [5677.40 CZK].pdf +2026-02-11 Opravný doklad Alza 3260384509 [vratka faktury 4009941955] [-12941.00 CZK].pdf +2026-04-28 Faktura CLIMPROFI 900260026 [servis a čištění klimatizace] [2178.00 CZK].pdf +2026-01-19 Paragon [parkování Poliklinika Prosek 2026] [4800.00 CZK].pdf +2026-03-18 Paragon [papír do tiskárny] [180.00 CZK].pdf +2026-01-13 Mzdy MUDr. Buzalkové 202512 [Jarmila Kusinová].pdf +2025-03-31 Platba Michaela Buzalkové ČLK 2025 [4000.00 CZK].pdf +2025-01-24 Zálohová faktura Stormware 2512805657 [program Pohoda mini].pdf +2025-11-11 Faktura Avenier 425160437 [vakcíny] [28180.00 CZK].pdf +2025-04-03 Faktura CLIMPROFI 900250020 [servis a čištění klimatizace] [2057.00 CZK].pdf +2026-01-22 Dodatek Poliklinika Prosek [nájemní smlouva č.3].pdf +2025-12-31 Smlouva Kooperativa 8604142932 [profesní pojištění odpovědnosti 2026].pdf + +DŮLEŽITÁ PRAVIDLA: +1. Prefix [POHODA] nikdy nepřidávej. +2. Používej datum vystavení dokladu, ne datum splatnosti. +3. Typ dokladu vyber podle dokumentu: + - Faktura + - Dobropis + - Opravný doklad + - Paragon + - Dodací list + - Zálohová faktura + - Smlouva + - Dodatek + - Platba + - Poplatek + - Mzdy + - Výdajový pokladní doklad +4. Pokud je v dokumentu "Dodací list není daňový doklad - nehraďte", typ = "Dodací list". +5. Dodavatel krátce a konzistentně: MEDIPOS, MEDEVIO, MEDATRON, ASKER, QuickSeal, + Poliklinika Prosek, Alza, Microsoft, OpenAI, Ptáček, Avenier, Stormware, + CompuGroup, CLIMPROFI, SEIVA, DrMAX, Mediately, Kooperativa, ICA, Česká pošta, + SOLDIERBOY, OMNIPRAX, Medicross. +6. "Distribuce CZ" → použij "Ptáček". +7. MEDIPOS: číslo = variabilní symbol (např. 10195703), ne FV-5703/2026. +8. MEDEVIO: přidej měsíční kód za číslo (např. "2600616 JAN2026"). +9. Částka s desetinnou tečkou a měnou [5578.97 CZK]. Záporná = [-12941.00 CZK]. +10. EUR = EUR, Kč = CZK. +11. Popis krátký, česky, do hranatých závorek. +12. Žádné znaky nevhodné pro Windows: < > : " / \\ | ? * +13. "Lékárna Poliklinika Prosek" → dodavatel "Poliklinika Prosek", popis [lékárna]. +14. Paragon bez čísla dokladu: číslo vynech. +15. Výstup musí být POUZE validní JSON, nic jiného, žádný komentář. + +JSON FORMÁT: +{"filename": "YYYY-MM-DD Faktura Dodavatel 123456 [popis] [123.45 CZK].pdf"} +""" + + +# ========================= +# POMOCNÉ FUNKCE +# ========================= + +def ocr_pdf(pdf_path: Path) -> str: + doc = fitz.open(str(pdf_path)) + texts = [] + matrix = fitz.Matrix(OCR_DPI / 72, OCR_DPI / 72) + + for page_num, page in enumerate(doc, start=1): + pix = page.get_pixmap(matrix=matrix, colorspace=fitz.csRGB) + img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) + page_text = pytesseract.image_to_string(img, lang=TESSERACT_LANG) + texts.append(f"--- Strana {page_num} ---\n{page_text}") + + doc.close() + return "\n\n".join(texts) + + +def sanitize_windows_filename(name: str) -> str: + name = re.sub(r'[<>:"/\\|?*]', " ", name) + name = re.sub(r"\s+", " ", name).strip() + name = name.rstrip(" .") + if not name.lower().endswith(".pdf"): + name += ".pdf" + return name + + +def unique_path(target: Path) -> Path: + if not target.exists(): + return target + stem = target.stem + suffix = target.suffix + parent = target.parent + i = 2 + while True: + candidate = parent / f"{stem} ({i}){suffix}" + if not candidate.exists(): + return candidate + i += 1 + + +def extract_json_object(text: str) -> dict: + text = text.strip() + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text.strip()).strip() + try: + return json.loads(text) + except json.JSONDecodeError: + pass + match = re.search(r"\{.*\}", text, flags=re.DOTALL) + if not match: + raise ValueError(f"Model nevrátil JSON:\n{text}") + return json.loads(match.group(0)) + + +def ask_ollama_for_filename(ocr_text: str) -> tuple[str, float]: + prompt = f"OCR text z faktury:\n\n{ocr_text}\n\n{NAMING_RULES}" + + payload = { + "model": MODEL, + "messages": [ + {"role": "user", "content": prompt} + ], + "stream": False, + "options": { + "temperature": 0, + }, + } + + t0 = time.time() + resp = requests.post( + f"{OLLAMA_HOST}/api/chat", + json=payload, + timeout=120, + ) + resp.raise_for_status() + elapsed = time.time() - t0 + + data = resp.json() + text = data["message"]["content"].strip() + + obj = extract_json_object(text) + filename = obj.get("filename", "").strip() + if not filename: + raise ValueError(f"JSON neobsahuje filename:\n{text}") + + return sanitize_windows_filename(filename), elapsed + + +def log_line(text: str) -> None: + print(text) + with LOG_FILE.open("a", encoding="utf-8") as f: + f.write(text + "\n") + + +# ========================= +# HLAVNÍ BĚH +# ========================= + +def main() -> None: + if not FOLDER.exists(): + raise FileNotFoundError(f"Adresář neexistuje: {FOLDER}") + + pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD + + # Test spojení s Ollama + try: + r = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5) + r.raise_for_status() + except Exception as e: + raise RuntimeError(f"Ollama server není dostupný na {OLLAMA_HOST}: {e}") + + pdfs = sorted(FOLDER.glob(PDF_PATTERN)) + pdfs = [p for p in pdfs if p.is_file() and p.suffix.lower() == ".pdf"] + + if not pdfs: + print("Nenalezeno žádné PDF.") + return + + log_line("") + log_line("=" * 80) + log_line(f"START: {time.strftime('%Y-%m-%d %H:%M:%S')}") + log_line(f"Adresář: {FOLDER}") + log_line(f"Hotové faktury: {PROCESSED_FOLDER}") + log_line(f"Počet PDF: {len(pdfs)}") + log_line(f"DRY_RUN: {DRY_RUN}") + log_line(f"MODEL: {MODEL} @ {OLLAMA_HOST} (lokální, bez nákladů)") + log_line(f"OCR DPI: {OCR_DPI}, jazyk: {TESSERACT_LANG}") + log_line("=" * 80) + + total_elapsed = 0.0 + + for i, pdf in enumerate(pdfs, start=1): + log_line(f"\n[{i}/{len(pdfs)}] Původní název: {pdf.name}") + + try: + log_line(" OCR...") + ocr_text = ocr_pdf(pdf) + log_line(f" OCR hotovo: {len(ocr_text)} znaků") + + new_name, elapsed = ask_ollama_for_filename(ocr_text) + total_elapsed += elapsed + + log_line(f" Návrh: {new_name}") + log_line(f" Čas odpovědi modelu: {elapsed:.1f}s") + + target = unique_path(PROCESSED_FOLDER / new_name) + if target.name != new_name: + log_line(f" Cíl po vyřešení konfliktu: {target.name}") + + if DRY_RUN: + log_line(f" Cíl: {target}") + log_line(" Stav: DRY-RUN, nepřejmenováno/nepřesunuto") + else: + PROCESSED_FOLDER.mkdir(exist_ok=True) + pdf.rename(target) + if pdf.name == new_name: + log_line(" Stav: PŘESUNUTO") + else: + log_line(" Stav: PŘEJMENOVÁNO A PŘESUNUTO") + + except Exception as e: + log_line(f" CHYBA: {type(e).__name__}: {e}") + + log_line("") + log_line("=" * 80) + log_line("SOUHRN") + log_line(f"Celkový čas modelu: {total_elapsed:.1f}s | Cena: 0 Kč (lokální model)") + log_line("=" * 80) + log_line("\nHOTOVO") + + +if __name__ == "__main__": + main() diff --git a/Faktury/trash/FakturyRenameOpenAILocalOCR.py b/Faktury/trash/FakturyRenameOpenAILocalOCR.py new file mode 100644 index 0000000..973c9e7 --- /dev/null +++ b/Faktury/trash/FakturyRenameOpenAILocalOCR.py @@ -0,0 +1,364 @@ +# FakturyRenameOpenAILocalOCR.py +# Verze: 1.0 +# Datum: 05JUN2026 +# Autor: Claude (Anthropic) +# +# Popis: +# Jako FakturyRenameOpenAI.py, ale PDF se nezasílá do API. +# Místo toho se každá stránka PDF převede lokálně na obrázek (PyMuPDF), +# provede se OCR pomocí Tesseract (pytesseract) a OpenAI dostane pouze +# vytěžený text. Levnější — odesíláme jen text, ne velký PDF. +# +# Závislosti: +# pip install openai pymupdf pytesseract pillow +# + nainstalovaný Tesseract: https://github.com/UB-Mannheim/tesseract/wiki +# +# Výsledný formát názvu: +# YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf + +import os +import json +import re +import time +from pathlib import Path + +from openai import OpenAI +from dotenv import load_dotenv +import fitz # PyMuPDF +import pytesseract +from PIL import Image + + +# ========================= +# CENA API +# ========================= + +USD_TO_CZK = 25.0 + +MODEL = "gpt-5.4-mini" + +PRICE_INPUT_USD_PER_1M = 0.75 +PRICE_OUTPUT_USD_PER_1M = 4.50 + + +# ========================= +# NASTAVENÍ +# ========================= + +FOLDER = Path( + r"u:\Dropbox\Ordinace\!!MUDr. Michaela Buzalková s.r.o\Prosek\#040 Faktury přijaté" +) + +PROCESSED_FOLDER = FOLDER / "NamedInvoicesByOpenAILocalOCR" + +# Pro test nech DRY_RUN = True — jen vypíše návrhy, nepřejmenuje. +DRY_RUN = False + +PDF_PATTERN = "*.pdf" + +LOG_FILE = FOLDER / "_rename_log_invoices_openai_ocr.txt" + +ENV_FILE = Path(r"U:\ordinaceprojekt\.env") + +# Cesta k Tesseract.exe +TESSERACT_CMD = r"C:\Program Files\Tesseract-OCR\tesseract.exe" + +TESSERACT_LANG = "ces+eng" + +OCR_DPI = 300 + + +# ========================= +# PRAVIDLA PRO POJMENOVÁNÍ +# ========================= + +NAMING_RULES = """ +Jsi pomocník pro pojmenování naskenovaných PDF dokladů MUDr. Michaely Buzalkové. + +ÚKOL: +Z OCR textu faktury/dokladu vytěž datum, typ dokladu, dodavatele, číslo dokladu, stručný popis, částku a měnu. +Vrať pouze JSON s polem "filename". + +CÍLOVÝ FORMÁT: +YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf + +PŘÍKLADY — různé typy dokladů: +2026-01-22 Faktura MEDIPOS 101827406 [materiál do ordinace] [9620.80 CZK].pdf +2026-01-16 Faktura MEDEVIO 2600616 JAN2026 [1999.00 CZK].pdf +2026-01-07 Faktura Ptáček 202600168 [vakcíny] [6070.00 CZK].pdf +2026-01-15 Faktura Poliklinika Prosek 91251957 [telefon a sterilizace] [827.28 CZK].pdf +2026-01-29 Faktura QuickSeal 120600292 [kontrola kvality QSK 1. cyklus 2026] [6970.00 CZK].pdf +2026-01-20 Faktura Microsoft G136228996 [licence] [942.95 CZK].pdf +2026-01-04 Faktura OpenAI 3OXD6KWG-0006 [ChatGPT plus subscription] [558.88 CZK].pdf +2026-01-31 Faktura Mediately 80fae [předplatné] [175.12 CZK].pdf +2026-01-16 Faktura MEDATRON 2261100086 [coagucheck 2x] [5677.40 CZK].pdf +2026-02-11 Opravný doklad Alza 3260384509 [vratka faktury 4009941955] [-12941.00 CZK].pdf +2026-04-28 Faktura CLIMPROFI 900260026 [servis a čištění klimatizace] [2178.00 CZK].pdf +2026-01-19 Paragon [parkování Poliklinika Prosek 2026] [4800.00 CZK].pdf +2026-03-18 Paragon [papír do tiskárny] [180.00 CZK].pdf +2026-01-13 Mzdy MUDr. Buzalkové 202512 [Jarmila Kusinová].pdf +2025-03-31 Platba Michaela Buzalkové ČLK 2025 [4000.00 CZK].pdf +2025-01-24 Zálohová faktura Stormware 2512805657 [program Pohoda mini].pdf +2025-11-11 Faktura Avenier 425160437 [vakcíny] [28180.00 CZK].pdf +2025-04-03 Faktura CLIMPROFI 900250020 [servis a čištění klimatizace] [2057.00 CZK].pdf +2026-01-22 Dodatek Poliklinika Prosek [nájemní smlouva č.3].pdf +2025-12-31 Smlouva Kooperativa 8604142932 [profesní pojištění odpovědnosti 2026].pdf + +DŮLEŽITÁ PRAVIDLA: +1. Prefix [POHODA] nikdy nepřidávej. +2. Používej datum vystavení dokladu, ne datum splatnosti. +3. Typ dokladu vyber podle dokumentu: + - Faktura + - Dobropis + - Opravný doklad ← pro storno/vrátky (ne Dobropis, pokud dokument říká "Opravný daňový doklad") + - Paragon + - Dodací list + - Zálohová faktura + - Smlouva + - Dodatek ← pro dodatky ke smlouvám + - Platba ← pro členské příspěvky a podobné platby bez faktury + - Poplatek + - Mzdy ← pro výplatní / mzdové dokumenty + - Výdajový pokladní doklad +4. Pokud je v dokumentu napsáno "Dodací list není daňový doklad - nehraďte", typ musí být "Dodací list", ne "Faktura". +5. Dodavatel zapisuj krátce a konzistentně podle tohoto seznamu — použij přesně tato jména: + - MEDIPOS (Medipos, MEDIPOS s.r.o.) + - MEDEVIO (Medevio) + - MEDATRON (MEDATRON s.r.o., Medatron) + - ASKER (Asker) + - QuickSeal (QuickSeal International s.r.o.) + - Poliklinika Prosek (i pro "Lékárna Poliklinika Prosek a.s." — viz pravidlo 14) + - Alza + - Microsoft + - OpenAI + - Ptáček (i pro "Distribuce CZ" — viz pravidlo 6) + - Avenier + - Stormware (STORMWARE s.r.o., Pohoda software) + - CompuGroup (CompuGroup Medical, Medicus software) + - CLIMPROFI (CLIMPROFI s.r.o.) + - SEIVA (SEIVA s.r.o.) + - DrMAX + - Mediately (číslo je krátký hash, např. 80fae, bcd33) + - Kooperativa + - ICA (ICA a.s., První certifikační autorita — certifikáty) + - Česká pošta + - SOLDIERBOY + - OMNIPRAX + - Medicross (MediCross s.r.o.) +6. SPECIÁLNÍ PRAVIDLO: pokud je dodavatel/firma "Distribuce CZ", v názvu souboru použij dodavatele "Ptáček". +7. SPECIÁLNÍ PRAVIDLO: u faktur MEDIPOS použij jako číslo dokladu variabilní symbol nebo hlavní číslo faktury bez mezer, například 10195703. Nepoužívej interní evidenční číslo typu FV-5703/2026. +8. SPECIÁLNÍ PRAVIDLO: u faktur MEDEVIO přidej za číslo faktury i měsíční kód, například "2600616 JAN2026". +9. Částku piš vždy s desetinnou tečkou a měnou, například [5578.97 CZK]. Pokud je částka záporná (dobropis/storno), piš ji jako [-12941.00 CZK]. +10. Pokud je částka v EUR, měna je EUR, pokud v Kč/CZK, měna je CZK. +11. Popis drž krátký, praktický a česky. +12. Popis dávej do hranatých závorek [popis]. +13. Nepoužívej dvojtečky, lomítka, uvozovky ani znaky nevhodné pro Windows názvy souborů. +14. Pokud je dodavatel "Lékárna Poliklinika Prosek", "Lékárna Prosek" nebo podobně, použij jako dodavatele "Poliklinika Prosek" a jako popis [lékárna] nebo [léky do ordinace]. +15. Paragon: pokud dokument nemá číslo dokladu, vynech ho. Popis musí popisovat co bylo nakoupeno. +16. Pokud jde jen o dodací list bez daňového dokladu, částku můžeš uvést, ale typ musí zůstat Dodací list. +17. Pokud si nejsi jistý popisem, použij obecný popis: [materiál do ordinace], [lékárna], [vakcíny], [testy], [licence], [předplatné]. +18. Výstup musí být pouze validní JSON, nic jiného. + +JSON FORMÁT: +{ + "filename": "YYYY-MM-DD Faktura Dodavatel 123456 [popis] [123.45 CZK].pdf" +} +""" + + +# ========================= +# POMOCNÉ FUNKCE +# ========================= + +def ocr_pdf(pdf_path: Path) -> str: + doc = fitz.open(str(pdf_path)) + texts = [] + matrix = fitz.Matrix(OCR_DPI / 72, OCR_DPI / 72) + + for page_num, page in enumerate(doc, start=1): + pix = page.get_pixmap(matrix=matrix, colorspace=fitz.csRGB) + img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) + page_text = pytesseract.image_to_string(img, lang=TESSERACT_LANG) + texts.append(f"--- Strana {page_num} ---\n{page_text}") + + doc.close() + return "\n\n".join(texts) + + +def sanitize_windows_filename(name: str) -> str: + name = re.sub(r'[<>:"/\\|?*]', " ", name) + name = re.sub(r"\s+", " ", name).strip() + name = name.rstrip(" .") + if not name.lower().endswith(".pdf"): + name += ".pdf" + return name + + +def unique_path(target: Path) -> Path: + if not target.exists(): + return target + stem = target.stem + suffix = target.suffix + parent = target.parent + i = 2 + while True: + candidate = parent / f"{stem} ({i}){suffix}" + if not candidate.exists(): + return candidate + i += 1 + + +def extract_json_object(text: str) -> dict: + text = text.strip() + try: + return json.loads(text) + except json.JSONDecodeError: + pass + match = re.search(r"\{.*\}", text, flags=re.DOTALL) + if not match: + raise ValueError(f"Model nevrátil JSON:\n{text}") + return json.loads(match.group(0)) + + +def calculate_cost(input_tokens: int, output_tokens: int) -> dict: + input_cost_usd = input_tokens / 1_000_000 * PRICE_INPUT_USD_PER_1M + output_cost_usd = output_tokens / 1_000_000 * PRICE_OUTPUT_USD_PER_1M + total_cost_usd = input_cost_usd + output_cost_usd + return { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "total_tokens": input_tokens + output_tokens, + "input_cost_usd": input_cost_usd, + "output_cost_usd": output_cost_usd, + "total_cost_usd": total_cost_usd, + "total_cost_czk": total_cost_usd * USD_TO_CZK, + } + + +def ask_openai_for_filename(client: OpenAI, ocr_text: str) -> tuple[str, dict]: + prompt = f"OCR text z faktury:\n\n{ocr_text}\n\n{NAMING_RULES}" + + response = client.chat.completions.create( + model=MODEL, + max_completion_tokens=256, + temperature=0, + messages=[ + {"role": "user", "content": prompt} + ], + ) + + text = response.choices[0].message.content.strip() + obj = extract_json_object(text) + filename = obj.get("filename", "").strip() + if not filename: + raise ValueError(f"JSON neobsahuje filename:\n{text}") + + cost = calculate_cost(response.usage.prompt_tokens, response.usage.completion_tokens) + return sanitize_windows_filename(filename), cost + + +def log_line(text: str) -> None: + print(text) + with LOG_FILE.open("a", encoding="utf-8") as f: + f.write(text + "\n") + + +# ========================= +# HLAVNÍ BĚH +# ========================= + +def main() -> None: + if not FOLDER.exists(): + raise FileNotFoundError(f"Adresář neexistuje: {FOLDER}") + + pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD + + load_dotenv(ENV_FILE) + + if not os.getenv("OPENAI_API_KEY"): + raise RuntimeError(f"Chybí OPENAI_API_KEY. Zkontroluj soubor {ENV_FILE}") + + client = OpenAI() + + pdfs = sorted(FOLDER.glob(PDF_PATTERN)) + pdfs = [p for p in pdfs if p.is_file() and p.suffix.lower() == ".pdf"] + + if not pdfs: + print("Nenalezeno žádné PDF.") + return + + total_input_tokens = 0 + total_output_tokens = 0 + total_tokens = 0 + total_cost_usd = 0.0 + total_cost_czk = 0.0 + + log_line("") + log_line("=" * 80) + log_line(f"START: {time.strftime('%Y-%m-%d %H:%M:%S')}") + log_line(f"Adresář: {FOLDER}") + log_line(f"Hotové faktury: {PROCESSED_FOLDER}") + log_line(f"Počet PDF: {len(pdfs)}") + log_line(f"DRY_RUN: {DRY_RUN}") + log_line(f"MODEL: {MODEL} (lokální OCR, do API jde jen text)") + log_line(f"OCR DPI: {OCR_DPI}, jazyk: {TESSERACT_LANG}") + log_line(f"Kurz: 1 USD = {USD_TO_CZK:.2f} CZK") + log_line("=" * 80) + + for i, pdf in enumerate(pdfs, start=1): + log_line(f"\n[{i}/{len(pdfs)}] Původní název: {pdf.name}") + + try: + log_line(" OCR...") + ocr_text = ocr_pdf(pdf) + log_line(f" OCR hotovo: {len(ocr_text)} znaků") + + new_name, cost = ask_openai_for_filename(client, ocr_text) + + total_input_tokens += cost["input_tokens"] + total_output_tokens += cost["output_tokens"] + total_tokens += cost["total_tokens"] + total_cost_usd += cost["total_cost_usd"] + total_cost_czk += cost["total_cost_czk"] + + log_line(f" Návrh: {new_name}") + log_line( + f" Tokeny: input={cost['input_tokens']}, " + f"output={cost['output_tokens']}, " + f"total={cost['total_tokens']}" + ) + log_line( + f" Cena volání: ${cost['total_cost_usd']:.6f} " + f"≈ {cost['total_cost_czk']:.2f} Kč" + ) + + target = unique_path(PROCESSED_FOLDER / new_name) + if target.name != new_name: + log_line(f" Cíl po vyřešení konfliktu: {target.name}") + + if DRY_RUN: + log_line(f" Cíl: {target}") + log_line(" Stav: DRY-RUN, nepřejmenováno/nepřesunuto") + else: + PROCESSED_FOLDER.mkdir(exist_ok=True) + pdf.rename(target) + if pdf.name == new_name: + log_line(" Stav: PŘESUNUTO") + else: + log_line(" Stav: PŘEJMENOVÁNO A PŘESUNUTO") + + except Exception as e: + log_line(f" CHYBA: {type(e).__name__}: {e}") + + log_line("") + log_line("=" * 80) + log_line("SOUHRN CENY") + log_line(f"Tokeny celkem: input={total_input_tokens}, output={total_output_tokens}, total={total_tokens}") + log_line(f"Cena celkem: ${total_cost_usd:.6f} ≈ {total_cost_czk:.2f} Kč") + log_line("=" * 80) + + log_line("\nHOTOVO") + + +if __name__ == "__main__": + main()