z230
This commit is contained in:
@@ -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 Kč, 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 Kč/CZK, měna je CZK.
|
||||||
11. Popis dávej do hranatých závorek.
|
11. Popis drž 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} 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()
|
||||||
@@ -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} 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()
|
||||||
Reference in New Issue
Block a user