TurkceOCR / turkceocr /cli.py
ersanbil's picture
Release TurkceOCR v2.0 (Apache-2.0)
c8b6043
Raw History Blame Contribute Delete
14.3 kB
"""
TürkçeOCR (`turkceocr` / `turkceocr`) Komut Satırı Çıkarım ve Doğrulama Aracı (CLI).
Desteklenen Modlar:
1. `--image <path>` veya `--pdf <path>`:
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 <path>`:
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>{tckn}</tckn>" in corrupted:
bad_tckn = tckn.replace("0", "O", 1) if "0" in tckn else (tckn[:-1] + "O")
corrupted = corrupted.replace(f"<tckn>{tckn}</tckn>", f"<tckn>{bad_tckn}</tckn>", 1)
vkn = sample.expected_kie.get("vkn")
if vkn and f"<vkn>{vkn}</vkn>" in corrupted:
bad_vkn = vkn.replace("0", "O", 1) if "0" in vkn else (vkn[:-1] + "O")
corrupted = corrupted.replace(f"<vkn>{vkn}</vkn>", f"<vkn>{bad_vkn}</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())