| | import os |
| | import json |
| | import pytesseract |
| | from pathlib import Path |
| | from PIL import Image |
| | from pdf2image import convert_from_path |
| | from langchain_ollama import OllamaLLM |
| |
|
| | |
| | pytesseract.pytesseract.tesseract_cmd = r'/opt/homebrew/bin/tesseract' |
| |
|
| | |
| | INPUT_DIR = "scans" |
| | HISTORY_FILE = "processed_real_scans_files.txt" |
| | MODEL_NAME = "llama3" |
| |
|
| | |
| | TARGET_LANGUAGES = { |
| | "pl": "Polish", |
| | "en": "English", |
| | "de": "German", |
| | "fr": "French", |
| | "es": "Spanish", |
| | "it": "Italian", |
| | "uk": "Ukrainian" |
| | } |
| |
|
| | llm = OllamaLLM(model=MODEL_NAME, temperature=0) |
| |
|
| | |
| | ALLOWED_TYPES = [ |
| | |
| | "taxDocument", "invoice", "receipt", "utilityBill", "bankStatement", |
| | "loanAgreement", "insurancePolicy", |
| |
|
| | |
| | "notarialDeed", "courtDocument", "powerOfAttorney", "contract", |
| |
|
| | |
| | "idCard", "passport", "birthCertificate", "marriageCertificate", |
| | "deathCertificate", "officialCertificate", "drivingLicense", |
| | "educationDocument", "cv", |
| |
|
| | |
| | "medicalDocument", "prescription", "referral", "vaccinationCard", |
| | "sanitaryBooklet", |
| |
|
| | |
| | "propertyDeed", "rentalAgreement", "vehicleDocument", "technicalInspection", |
| |
|
| | |
| | "documentScan", "application", "certificate", "other" |
| | ] |
| |
|
| |
|
| | |
| | def load_history(): |
| | """Wczytuje listę przetworzonych plików do setu (dla szybkiego wyszukiwania).""" |
| | if not os.path.exists(HISTORY_FILE): |
| | return set() |
| | with open(HISTORY_FILE, 'r', encoding='utf-8') as f: |
| | return set(line.strip() for line in f if line.strip()) |
| |
|
| |
|
| | def mark_as_done(rel_path): |
| | """Dopisuje plik do historii.""" |
| | with open(HISTORY_FILE, 'a', encoding='utf-8') as f: |
| | f.write(f"{rel_path}\n") |
| |
|
| |
|
| | |
| | def perform_ocr(file_path): |
| | text = "" |
| | try: |
| | langs = 'pol+eng' |
| | if file_path.suffix.lower() == ".pdf": |
| | pages = convert_from_path(file_path) |
| | for page in pages: |
| | text += pytesseract.image_to_string(page, lang=langs) |
| | else: |
| | text = pytesseract.image_to_string(Image.open(file_path), lang=langs) |
| | except Exception as e: |
| | print(f" [!] Błąd OCR: {file_path.name}: {e}") |
| | return text |
| |
|
| |
|
| | def ask_llm_json(prompt): |
| | try: |
| | response = llm.invoke(prompt) |
| | clean = response.replace("```json", "").replace("```", "").strip() |
| | start, end = clean.find('{'), clean.rfind('}') + 1 |
| | return json.loads(clean[start:end]) |
| | except Exception: |
| | return None |
| |
|
| |
|
| | def ask_llm_text(prompt): |
| | try: |
| | response = llm.invoke(prompt) |
| | return response.strip().strip('"').strip("'") |
| | except Exception: |
| | return "Translation Error" |
| |
|
| |
|
| | |
| | def get_core_metadata(text, hinted_type=None): |
| | print(" 🧠 Analiza struktury dokumentu (Core Metadata)...") |
| |
|
| | |
| | hint_str = "" |
| | if hinted_type in ALLOWED_TYPES: |
| | hint_str = f"Strong Hint: The document is likely located in folder '{hinted_type}'." |
| |
|
| | prompt = f""" |
| | Analyze the following document text. |
| | {hint_str} |
| | |
| | Extract structured data. |
| | RULES: |
| | 1. 'summary_base': Write a factual summary in ENGLISH (5 sentences). |
| | 2. 'title_base': Write a title in ENGLISH format: "[Specific Type] - [Entity] - [Date]". |
| | (e.g., "Tax Document (PIT-11) - Employer Name - 2023") |
| | 3. 'category': Must be one of: financial, legal, personal, health, property, other. |
| | 4. 'type': Choose the BEST MATCH from this specific list: {", ".join(ALLOWED_TYPES)}. |
| | 5. 'info': Specific details (e.g. "PIT-11", "Umowa o pracę", "Prąd"). |
| | |
| | Return ONLY JSON: |
| | {{ |
| | "title_base": "...", |
| | "summary_base": "...", |
| | "category": "...", |
| | "type": "...", |
| | "info": "..." |
| | }} |
| | |
| | TEXT: |
| | {text[:4000]} |
| | """ |
| | return ask_llm_json(prompt) |
| |
|
| |
|
| | def translate_section(text, target_lang, content_type="text"): |
| | prompt = f""" |
| | Translate the following {content_type} into {target_lang}. |
| | Output ONLY the translation. No explanations. No markdown. |
| | |
| | TEXT TO TRANSLATE: |
| | {text} |
| | """ |
| | return ask_llm_text(prompt) |
| |
|
| |
|
| | def save_file(root_folder, lang_code, sub_dir, filename, content): |
| | path = Path(root_folder) / lang_code / sub_dir |
| | path.mkdir(parents=True, exist_ok=True) |
| | with open(path / filename, "w", encoding="utf-8") as f: |
| | f.write(str(content)) |
| |
|
| |
|
| | def save_meta(root_folder, sub_dir, filename, content): |
| | path = Path(root_folder) / sub_dir |
| | path.mkdir(parents=True, exist_ok=True) |
| | with open(path / filename, "w", encoding="utf-8") as f: |
| | f.write(str(content)) |
| |
|
| |
|
| | def process_file(file_path, input_root): |
| | rel_path = file_path.relative_to(input_root) |
| | rel_path_str = str(rel_path) |
| |
|
| | base_filename = rel_path.stem + ".txt" |
| | sub_dir = rel_path.parent |
| | hinted_type = sub_dir.name if sub_dir.name != input_root.name else None |
| |
|
| | |
| | raw_text = perform_ocr(file_path) |
| |
|
| | if not raw_text.strip(): |
| | print(" ⚠️ Pusty OCR - oznaczam jako przetworzony (bez wyników).") |
| | mark_as_done(rel_path_str) |
| | return |
| |
|
| | |
| | save_meta("content", sub_dir, base_filename, raw_text) |
| |
|
| | |
| | core_data = get_core_metadata(raw_text, hinted_type) |
| |
|
| | if not core_data: |
| | print(" ❌ Błąd analizy AI. Przerywam dla tego pliku.") |
| | return |
| |
|
| | |
| | save_meta("category", sub_dir, base_filename, core_data.get("category", "other")) |
| | save_meta("type", sub_dir, base_filename, core_data.get("type", "other")) |
| | save_meta("info", sub_dir, base_filename, core_data.get("info", "none")) |
| |
|
| | base_title = core_data.get("title_base", "Document") |
| | base_summary = core_data.get("summary_base", "No summary.") |
| |
|
| | |
| | print(" 🌍 Rozpoczynam generowanie etykiet (tytuły/podsumowania)...") |
| |
|
| | for code, lang_name in TARGET_LANGUAGES.items(): |
| | print(f" -> [{code.upper()}] {lang_name}...", end="", flush=True) |
| |
|
| | |
| | if code == "en": |
| | final_title = base_title |
| | else: |
| | final_title = translate_section(base_title, lang_name, "title") |
| | save_file("titles", code, sub_dir, base_filename, final_title) |
| |
|
| | |
| | if code == "en": |
| | final_summary = base_summary |
| | else: |
| | final_summary = translate_section(base_summary, lang_name, "summary") |
| | save_file("summary", code, sub_dir, base_filename, final_summary) |
| |
|
| | |
| | |
| | print(" OK.") |
| |
|
| | |
| | print(f"✅ Zakończono: {file_path.name}") |
| | mark_as_done(rel_path_str) |
| |
|
| |
|
| | def main(): |
| | input_root = Path(INPUT_DIR) |
| | if not input_root.exists(): |
| | print(f"Brak folderu wejściowego: {INPUT_DIR}") |
| | return |
| |
|
| | |
| | processed_files = load_history() |
| | print(f"📂 Załadowano historię: {len(processed_files)} plików już przetworzonych.") |
| |
|
| | all_files = [f for f in input_root.rglob("*") if |
| | f.is_file() and f.suffix.lower() in [".pdf", ".jpg", ".png", ".jpeg"]] |
| | print(f"🚀 Znaleziono łącznie {len(all_files)} plików do analizy.") |
| |
|
| | for f in all_files: |
| | rel_path_str = str(f.relative_to(input_root)) |
| |
|
| | |
| | if rel_path_str in processed_files: |
| | print(f"⏩ Pomijam (już w historii): {rel_path_str}") |
| | continue |
| |
|
| | print(f"\n📄 Przetwarzanie: {rel_path_str}") |
| | try: |
| | process_file(f, input_root) |
| | except KeyboardInterrupt: |
| | print("\n🛑 Zatrzymano przez użytkownika. Postęp zapisany.") |
| | break |
| | except Exception as e: |
| | print(f"\n❌ Krytyczny błąd dla {rel_path_str}: {e}") |
| |
|
| |
|
| | if __name__ == "__main__": |
| | main() |