#!/usr/bin/env python3 """Citirea textului din capturile de ecran trimise pe WhatsApp (OCR cu tesseract). De ce OCR si nu un model vizual: modelul de raspuns al Mariei (Qwen3.5-2B pe llama.cpp, LXC 104) e strict text — nu accepta imagini. Utilizatorii insa trimit aproape intotdeauna o captura cu fereastra de eroare, nu textul ei. Tesseract (CPU, ~1-2 s pe o captura de ecran) transforma captura in text, iar restul lantului ramane neschimbat: acelasi RAG, acelasi prompt. Textul recunoscut e ZGOMOTOS. Doua consecinte tratate aici: - pentru CAUTAREA in index folosim doar liniile care arata a eroare (`retrieval_query`), nu toata fereastra: un ecran intreg de meniuri si coloane diluează embedding-ul si scoate chunk-uri fara legatura; - pentru PROMPT trimitem textul intreg, curatat si plafonat, marcat explicit ca provenit din OCR, ca modelul sa nu trateze greselile de recunoastere ca date exacte. """ from __future__ import annotations import re import shutil import subprocess import config DEFAULT_LANGS = "ron+eng" DEFAULT_MAX_CHARS = 1500 DEFAULT_TIMEOUT_S = 60 # Semne ca o linie e chiar mesajul de eroare, nu decor de interfata. Ordinea nu # conteaza; o singura potrivire e de ajuns. _ERROR_HINTS = re.compile( r"(ORA-\d+|PLS-\d+|TNS-\d+|SP2-\d+|IMP-\d+|EXP-\d+" r"|\b(?:eroare|erori|eronat)\b|\berror(?:s|ul)?\b|\bexception\b|\besuat\b|\bfailed\b|\binvalid\b" r"|\bnu se poate\b|\bnu exista\b|\bnu a fost\b|\bcod\s*[:=]?\s*-?\d+" r"|\bE\d{3,}\b|\bwarning\b|\batentie\b)", re.IGNORECASE, ) # Linii care nu aduc nimic: prea scurte, sau numai simboluri/rame de fereastra. _JUNK_LINE = re.compile(r"^[\W_]*$") def available() -> bool: return shutil.which("tesseract") is not None def clean(raw: str, max_chars: int | None = None) -> str: """Normalizeaza iesirea tesseract: fara linii goale, fara zgomot, plafonat.""" if max_chars is None: max_chars = config.get_int("OCR_MAX_CHARS", DEFAULT_MAX_CHARS) lines: list[str] = [] for raw_line in raw.splitlines(): line = re.sub(r"[ \t ]+", " ", raw_line).strip() if not line or _JUNK_LINE.match(line): continue if len(line) < 2: continue if lines and lines[-1] == line: # tesseract repeta uneori aceeasi linie continue lines.append(line) text = "\n".join(lines) if len(text) > max_chars: text = text[:max_chars].rstrip() + "\n[…text trunchiat]" return text def error_lines(text: str, limit: int = 6) -> list[str]: """Liniile care arata a mesaj de eroare, in ordinea din imagine.""" out: list[str] = [] for line in text.splitlines(): if _ERROR_HINTS.search(line) and line not in out: out.append(line) if len(out) >= limit: break return out def retrieval_query(caption: str, ocr_text: str) -> str: """Ce se trimite la cautarea in index: legenda + liniile de eroare, sau tot. Cand nu se distinge nicio linie de eroare, cade inapoi pe primele linii ale capturii — mai bine o cautare aproximativa decat niciuna. """ parts: list[str] = [] if caption.strip(): parts.append(caption.strip()) errs = error_lines(ocr_text) if errs: parts.extend(errs) else: parts.extend(ocr_text.splitlines()[:8]) return "\n".join(p for p in parts if p.strip()).strip() def run(image_path: str, langs: str | None = None) -> str: """Textul curatat dintr-o imagine. Ridica RuntimeError daca tesseract lipseste/cade.""" if not available(): raise RuntimeError("tesseract nu e instalat (apt-get install tesseract-ocr tesseract-ocr-ron)") langs = langs or config.get("OCR_LANGS", DEFAULT_LANGS) timeout = config.get_int("OCR_TIMEOUT_S", DEFAULT_TIMEOUT_S) try: proc = subprocess.run( ["tesseract", image_path, "stdout", "-l", langs], capture_output=True, text=True, timeout=timeout, check=False, ) except subprocess.TimeoutExpired as exc: raise RuntimeError(f"OCR a depasit {timeout}s") from exc if proc.returncode != 0: raise RuntimeError((proc.stderr or "tesseract a esuat").strip().splitlines()[-1]) return clean(proc.stdout)