# 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()