Files
ordinaceprojekt/Faktury/trash/FakturyRenameOllama.py
2026-06-05 15:48:09 +02:00

310 lines
9.8 KiB
Python

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