This commit is contained in:
2026-06-05 15:48:09 +02:00
parent d850486eb9
commit c9f94de286
4 changed files with 1120 additions and 24 deletions
@@ -69,12 +69,27 @@ Vrať pouze JSON s polem "filename".
CÍLOVÝ FORMÁT: CÍLOVÝ FORMÁT:
YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf
PŘÍKLADY: PŘÍKLADY různé typy dokladů:
2026-06-01 Faktura ASKER 261103225 [kontejner Yannick 1.5 l] [339.00 CZK].pdf 2026-01-22 Faktura MEDIPOS 101827406 [materiál do ordinace] [9620.80 CZK].pdf
2026-06-01 Faktura MEDIPOS 10195703 [CRP, kapiláry, písty, rukavice, nádoba] [5578.97 CZK].pdf 2026-01-16 Faktura MEDEVIO 2600616 JAN2026 [1999.00 CZK].pdf
2026-05-29 Faktura Ptáček 202604570 [vakcíny Adacel, Vaqta, Havrix] [9235.20 CZK].pdf 2026-01-07 Faktura Ptáček 202600168 [vakcíny] [6070.00 CZK].pdf
2026-05-29 Faktura Poliklinika Prosek 91260763 [lékárna] [16165.40 CZK].pdf 2026-01-15 Faktura Poliklinika Prosek 91251957 [telefon a sterilizace] [827.28 CZK].pdf
2026-06-01 Dodací list QuickSeal 200609058 [VivaDiag Hydroxyvitamin D3] [2620.00 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: DŮLEŽITÁ PRAVIDLA:
1. Prefix [POHODA] nikdy nepřidávej. 1. Prefix [POHODA] nikdy nepřidávej.
@@ -82,35 +97,54 @@ DŮLEŽITÁ PRAVIDLA:
3. Typ dokladu vyber podle dokumentu: 3. Typ dokladu vyber podle dokumentu:
- Faktura - Faktura
- Dobropis - Dobropis
- Opravný doklad pro storno/vrátky (ne Dobropis, pokud dokument říká "Opravný daňový doklad")
- Paragon - Paragon
- Dodací list - Dodací list
- Zálohová faktura - Zálohová faktura
- Smlouva - Smlouva
- Platba - Dodatek pro dodatky ke smlouvám
- Platba pro členské příspěvky a podobné platby bez faktury
- Poplatek - Poplatek
- Mzdy pro výplatní / mzdové dokumenty
- Výdajový pokladní doklad - 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". 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ě: 5. Dodavatel zapisuj krátce a konzistentně podle tohoto seznamu použij přesně tato jména:
- MEDIPOS - MEDIPOS (Medipos, MEDIPOS s.r.o.)
- MEDEVIO - MEDEVIO (Medevio)
- MEDATRON - MEDATRON (MEDATRON s.r.o., Medatron)
- ASKER - ASKER (Asker)
- QuickSeal - QuickSeal (QuickSeal International s.r.o.)
- Poliklinika Prosek - Poliklinika Prosek (i pro "Lékárna Poliklinika Prosek a.s." viz pravidlo 14)
- Alza - Alza
- Microsoft - Microsoft
- Anthropic - OpenAI
- Ptáček - 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". 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. 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]. 8. SPECIÁLNÍ PRAVIDLO: u faktur MEDEVIO přidej za číslo faktury i měsíční kód, například "2600616 JAN2026".
9. Když je částka v , měna je CZK. 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. Popis drž krátký, praktický a česky. 10. Pokud je částka v EUR, měna je EUR, pokud v /CZK, měna je CZK.
11. Popis dávej do hranatých závorek. 11. Popis d krátký, praktický a česky.
12. Nepoužívej dvojtečky, lomítka, uvozovky ani znaky nevhodné pro Windows názvy souborů. 12. Popis dávej do hranatých závorek [popis].
13. Pokud jde jen o dodací list bez daňového dokladu, částku můžeš uvést, ale typ musí zůstat Dodací list. 13. Nepoužívej dvojtečky, lomítka, uvozovky ani znaky nevhodné pro Windows názvy souborů.
14. Pokud si nejsi jistý popisem, použij obecný popis typu [materiál do ordinace], [lékárna], [vakcíny], [testy]. 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. Výstup musí být pouze validní JSON, nic jiného. 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: JSON FORMÁT:
{ {
@@ -161,6 +195,9 @@ def unique_path(target: Path) -> Path:
def extract_json_object(text: str) -> dict: def extract_json_object(text: str) -> dict:
text = text.strip() 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: try:
return json.loads(text) return json.loads(text)
except json.JSONDecodeError: except json.JSONDecodeError:
@@ -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}"
)
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}")
log_line("=" * 80)
log_line("\nHOTOVO")
if __name__ == "__main__":
main()
+309
View File
@@ -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()
@@ -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}"
)
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}")
log_line("=" * 80)
log_line("\nHOTOVO")
if __name__ == "__main__":
main()