""" TürkçeOCR (`turkceocr` / `turkceocr`) Komut Satırı Çıkarım ve Doğrulama Aracı (CLI). Desteklenen Modlar: 1. `--image ` veya `--pdf `: PNG/JPG görüntülerini veya çok sayfalı PDF belgelerini (`PyMuPDF / fitz`) yükler, `HighResDiacriticTiler` ile yüksek çözünürlüklü yamalara böler, `TurkceOCRLine` ve `TurkceVerify` (TCKN/VKN/IBAN/KDV/İ-ı) hattından geçirir. 2. `--demo-doc {invoice,pos,handwritten,gazette,erp_table,born_digital_2col}`: Gerçekçi kaşeli/imzalı/termal Türkçe belge sentezler, satır kırpımlarını `TurkceOCRLine` ile okur ve `TurkceVerify` algoritmik sağlama onarımını uçtan uca çalıştırır. 3. `--markdown-text "..."` veya `--markdown-file `: Ham VLM/OCR Markdown metnini doğrudan `TurkceOCRPipeline.postprocess_document_markdown` ve `TurkishFinancialSolver` hattından geçirerek onarır. """ from __future__ import annotations import argparse import io import json import sys from pathlib import Path from typing import Any, Dict, List, Optional, Sequence from PIL import Image from turkceocr.data.pdf_and_table_engine import ( BornDigitalPDFPairGenerator, ComplexTurkishTableRenderer, ) from turkceocr.data.synthetic_generator import ( SyntheticDocumentSample, TurkishDocumentGenerator, ) from turkceocr.models.vl_model import SUPPORTED_VL_BACKBONES, TURKCEOCR_SPECIAL_TOKENS from turkceocr.pipeline import TurkceOCRPipeline from turkceocr.postprocess.entity_engine import UnifiedTurkishEntityEngine from turkceocr.postprocess.visual_redactor import VisualRedactor DEMO_DOC_TYPES = ( "invoice", "pos", "handwritten", "gazette", "erp_table", "born_digital_2col", ) def synthesize_demo_document( doc_type: str, apply_degradation: bool = True, is_ood: bool = False, seed: int = 2026, ) -> SyntheticDocumentSample: """Belirtilen tipte gerçekçi Türkçe belge sentezler.""" norm = doc_type.strip().lower() gen = TurkishDocumentGenerator(seed=seed) if norm in ("invoice", "corporate_invoice"): return gen.generate_corporate_invoice(apply_degradation=apply_degradation, is_ood=is_ood) if norm in ("pos", "thermal_pos_receipt"): return gen.generate_thermal_pos_receipt(apply_degradation=apply_degradation, is_ood=is_ood) if norm in ("handwritten", "mixed_handwritten_form"): return gen.generate_mixed_handwritten_form(apply_degradation=apply_degradation, is_ood=is_ood) if norm in ("gazette", "official_gazette"): return gen.generate_official_gazette_or_contract(apply_degradation=apply_degradation, is_ood=is_ood) if norm in ("erp_table", "multi_vat_erp_invoice"): table_eng = ComplexTurkishTableRenderer(seed=seed) return table_eng.generate_multi_vat_erp_invoice( apply_degradation=apply_degradation, is_ood=is_ood, table_style="zebra_striped", ) if norm in ("born_digital_2col", "two_column"): pdf_gen = BornDigitalPDFPairGenerator(seed=seed) return pdf_gen.generate_two_column_born_digital_page( apply_degradation=apply_degradation, is_ood=is_ood, ) raise ValueError( f"Desteklenmeyen demo belge tipi: {doc_type!r}. Geçerli tipler: {DEMO_DOC_TYPES}" ) def inject_realistic_ocr_corruptions(sample: SyntheticDocumentSample) -> str: """ Demo akışında `TurkceVerify` algoritmik onarımını göstermek için sentetik belgenin ham OCR çıktısına gerçekçi görsel karışıklıklar (0->O, İ->I, Ş->S) enjekte eder. """ corrupted = sample.ground_truth_markdown tckn = sample.expected_kie.get("tckn") if tckn and f"{tckn}" in corrupted: bad_tckn = tckn.replace("0", "O", 1) if "0" in tckn else (tckn[:-1] + "O") corrupted = corrupted.replace(f"{tckn}", f"{bad_tckn}", 1) vkn = sample.expected_kie.get("vkn") if vkn and f"{vkn}" in corrupted: bad_vkn = vkn.replace("0", "O", 1) if "0" in vkn else (vkn[:-1] + "O") corrupted = corrupted.replace(f"{vkn}", f"{bad_vkn}", 1) corrupted = ( corrupted.replace("BAŞKANLIĞI", "BASKANLIGI") .replace("MÜDÜRLÜĞÜ", "MUDURLUGU") .replace("İSTANBUL", "ISTANBUL") ) return corrupted def load_images_from_path(path: Path) -> List[Image.Image]: """Görüntü (.png/.jpg/.webp) veya PDF (.pdf) dosyasından PIL Image listesi yükler.""" if not path.exists(): raise FileNotFoundError(f"Girdi dosyası bulunamadı: {path}") if path.suffix.lower() == ".pdf": import fitz # PyMuPDF images: List[Image.Image] = [] with fitz.open(path) as doc: for page_index in range(len(doc)): page = doc.load_page(page_index) pix = page.get_pixmap(dpi=200) img = Image.open(io.BytesIO(pix.tobytes("png"))).convert("RGB") images.append(img) return images return [Image.open(path).convert("RGB")] def build_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="turkceocr", description="TürkçeOCR (TürkçeOCR) — İki Katmanlı Türkçe OCR, Doküman VLM ve TurkceVerify CLI", ) input_group = parser.add_mutually_exclusive_group(required=False) input_group.add_argument( "--image", type=str, default=None, help="İşlenecek görüntü (.png, .jpg) veya PDF dosyasının yolu.", ) input_group.add_argument( "--pdf", type=str, default=None, help="İşlenecek PDF dosyasının yolu (PyMuPDF ile yüksek çözünürlüklü render).", ) input_group.add_argument( "--demo-doc", type=str, choices=list(DEMO_DOC_TYPES), default=None, help="Gerçekçi kaşeli/bozulmuş Türkçe belge sentezleyip uçtan uca TürkçeOCR çalıştırır.", ) input_group.add_argument( "--markdown-text", type=str, default=None, help="Doğrudan TurkceVerify ve Finansal Çözücü ile onarılacak ham Markdown metni.", ) input_group.add_argument( "--markdown-file", type=str, default=None, help="Doğrudan TurkceVerify ile onarılacak ham Markdown dosyası.", ) parser.add_argument( "--backbone", type=str, choices=list(SUPPORTED_VL_BACKBONES.keys()), default="lightonocr", help="Kullanılacak TürkçeOCR-VL-1B omurgası (lightonocr veya paddleocr_vl).", ) parser.add_argument( "--seed", type=int, default=2026, help="Sentetik demo belge üretimi için rastgelelik tohumu.", ) parser.add_argument( "--no-degradation", action="store_true", help="Demo belge üretiminde fiziksel bozulma ve kaşe bindirmesini devre dışı bırakır.", ) parser.add_argument( "--output-json", type=str, default=None, help="Yapılandırılmış TürkçeOCR sonucunun kaydedileceği JSON dosya yolu.", ) parser.add_argument( "--output-md", type=str, default=None, help="Onarılmış Markdown çıktısının kaydedileceği .md dosya yolu.", ) parser.add_argument( "--redact-pii", action="store_true", help="Birleşik Hukuk & KVKK PII motorunu çalıştırarak hassas verileri karartır.", ) parser.add_argument( "--redact-image", type=str, default=None, help="2B #000000 KVKK karartması uygulanmış görüntünün kaydedileceği dosya yolu.", ) parser.add_argument( "--quiet", action="store_true", help="Standart çıktıya JSON yazdırmayı bastırır.", ) return parser def execute_cli(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: """ TürkçeOCR CLI argümanlarını çalıştırır ve yapılandırılmış sonuç sözlüğünü döndürür. """ parser = build_arg_parser() args = parser.parse_args(list(argv) if argv is not None else None) pipeline = TurkceOCRPipeline(vl_backbone=args.backbone) # Varsayılan olarak girdi verilmemişse --demo-doc invoice çalıştırılır mode = "demo_doc" image_obj: Optional[Image.Image] = None raw_markdown = "" line_predictions: List[Dict[str, Any]] = [] doc_type_label = "markdown_only" page_count = 1 if args.markdown_text is not None: mode = "markdown_text" raw_markdown = args.markdown_text elif args.markdown_file is not None: mode = "markdown_file" raw_markdown = Path(args.markdown_file).read_text(encoding="utf-8") elif args.image is not None or args.pdf is not None: mode = "pdf" if args.pdf is not None else "image" target_path = Path(args.pdf or args.image) pages = load_images_from_path(target_path) page_count = len(pages) image_obj = pages[0] doc_type_label = target_path.suffix.lstrip(".").lower() or "image" # Satır modeli ile hızlı tepe/orta/alt şerit okuması w, h = image_obj.size strip_coords = [ (0, int(h * 0.04), w, int(h * 0.14)), (0, int(h * 0.18), w, int(h * 0.28)), (0, int(h * 0.70), w, int(h * 0.82)), ] decoded_lines: List[str] = [] for idx, bbox in enumerate(strip_coords): crop = image_obj.crop(bbox) pred_text, conf = pipeline.recognize_line_image(crop) line_predictions.append( { "line_index": idx, "bbox": list(bbox), "predicted_text": pred_text, "confidence": round(float(conf), 4), } ) if pred_text.strip(): decoded_lines.append(pred_text.strip()) raw_markdown = "\n".join(decoded_lines) if decoded_lines else f"# TürkçeOCR Belge ({target_path.name})" else: demo_choice = args.demo_doc or "invoice" sample = synthesize_demo_document( doc_type=demo_choice, apply_degradation=not args.no_degradation, seed=args.seed, ) doc_type_label = sample.doc_type image_obj = sample.image raw_markdown = inject_realistic_ocr_corruptions(sample) for idx, (crop_img, gt_line) in enumerate(sample.line_crops[:4]): pred_text, conf = pipeline.recognize_line_image(crop_img) line_predictions.append( { "line_index": idx, "ground_truth_line": gt_line, "predicted_text": pred_text, "confidence": round(float(conf), 4), } ) page_out = pipeline.postprocess_document_markdown( raw_markdown=raw_markdown, image=image_obj, repair_financials=True, ) fin_dict: Optional[Dict[str, Any]] = None if page_out.financial_check is not None: fc = page_out.financial_check fin_dict = { "is_consistent": fc.is_consistent, "was_repaired": fc.was_repaired, "subtotal_matrah": fc.subtotal_matrah, "vat_kdv": fc.vat_kdv, "grand_total": fc.grand_total, "inferred_vat_rate": fc.inferred_vat_rate, "repair_note": fc.repair_note, } result: Dict[str, Any] = { "brand": "TürkçeOCR", "version": "0.2.0", "mode": mode, "doc_type": doc_type_label, "backbone": args.backbone, "resolved_vl_model_id": pipeline.vl_config.resolved_model_id(), "page_count": page_count, "tile_count": len(page_out.tile_bboxes), "tile_bboxes": [list(b) for b in page_out.tile_bboxes], "raw_markdown": page_out.raw_markdown, "verified_markdown": page_out.markdown, "valid_tckns": page_out.verification_report.valid_tckns, "valid_vkns": page_out.verification_report.valid_vkns, "valid_ibans": page_out.verification_report.valid_ibans, "repaired_fields": page_out.verification_report.repaired_fields, "financial_check": fin_dict, "line_recognitions": line_predictions, "special_tokens_supported": TURKCEOCR_SPECIAL_TOKENS, } if args.redact_pii or args.redact_image: entity_engine = UnifiedTurkishEntityEngine(use_ner=False) visual_redactor = VisualRedactor(entity_engine=entity_engine) target_img = image_obj if image_obj is not None else Image.new("RGB", (900, 600), color=(255, 255, 255)) redaction_res = visual_redactor.redact_document( image=target_img, raw_text=page_out.raw_markdown, normalized_text=page_out.markdown, ) result["redacted_markdown"] = redaction_res.redacted_text result["detected_entities"] = [e.to_dict() for e in redaction_res.entities] result["redaction_boxes"] = [b.to_dict() for b in redaction_res.redaction_boxes] result["redaction_coverage"] = round(float(redaction_res.redaction_coverage), 4) if args.redact_image: out_img_path = Path(args.redact_image) out_img_path.parent.mkdir(parents=True, exist_ok=True) redaction_res.redacted_image.save(out_img_path) result["redacted_image_path"] = str(out_img_path) if args.output_json: out_json_path = Path(args.output_json) out_json_path.parent.mkdir(parents=True, exist_ok=True) out_json_path.write_text( json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8", ) if args.output_md: out_md_path = Path(args.output_md) out_md_path.parent.mkdir(parents=True, exist_ok=True) out_md_path.write_text(page_out.markdown, encoding="utf-8") if not args.quiet: print(json.dumps(result, ensure_ascii=False, indent=2)) return result def main(argv: Optional[Sequence[str]] = None) -> int: """`turkceocr` ve `turkceocr` konsol giriş noktası.""" execute_cli(argv=argv) return 0 if __name__ == "__main__": sys.exit(main())