Image-to-Text
Transformers
Safetensors
Turkish
English
ocr
turkish
document-intelligence
vision-language-model
svtrv2
qwen2.5-vl
kvkk
pii-redaction
legal-nlp
ictihat
financial-verification
visual-redaction
Eval Results (legacy)
Instructions to use ersanbil/TurkceOCR with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ersanbil/TurkceOCR with Transformers:
# Use a pipeline as a high-level helper # Warning: Pipeline type "image-to-text" is no longer supported in transformers v5. # You must load the model directly (see below) or downgrade to v4.x with: # pip install "transformers<5.0.0" from transformers import pipeline pipe = pipeline("image-to-text", model="ersanbil/TurkceOCR")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("ersanbil/TurkceOCR", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download turkceocr/postprocess/visual_redactor.py from ersanbil/TurkceOCR: direct link, hf CLI and curl.
- Browser
- Download file 15.8 kB
-
https://huggingface.co/ersanbil/TurkceOCR/resolve/main/turkceocr/postprocess/visual_redactor.py
- Command line
-
hf download hf://ersanbil/TurkceOCR/turkceocr/postprocess/visual_redactor.py
-
curl -L -o visual_redactor.py https://huggingface.co/ersanbil/TurkceOCR/resolve/main/turkceocr/postprocess/visual_redactor.py
15.8 kB
| """ | |
| FAZ 2C — `OffsetMap` (π: norm -> raw) Karakter-Piksel İzdüşümü ve 2B Görsel Karartma Motoru (`VisualRedactor`). | |
| Özellikler: | |
| 1. `OffsetMap`: | |
| - `TurkceVerify` veya Unicode normalizasyonu sonrası oluşan `norm_text` üzerindeki | |
| `[norm_start, norm_end)` varlık ofsetlerini kaynak `raw_text` üzerindeki `[raw_start, raw_end)` | |
| karakter aralığına monotonik dinamik programlama hizalaması ile izdüşürür. | |
| 2. 2B Görsel Karartma (`VisualRedactor`): | |
| - Metin tabanlı PII (`TR_TCKN`, `TR_TCKN_SUSPECT`, `TR_VKN`, `TR_VKN_SUSPECT`, `TR_IBAN`, | |
| `TR_IBAN_SUSPECT`, `PERSON`, `ADDRESS`, `TR_PHONE`, `EMAIL`, `HEALTH_DATA`, `KVKK_SPECIAL` vb.) | |
| ve görsel biyometrik/kimlik unsurlarını (`<signature/>`, `<stamp>`, `VISUAL_PHOTO`, `VISUAL_QRCODE`) | |
| geri döndürülemez tam siyah (`#000000`) dikdörtgen + `[KVKK: <ENTITY_TYPE>]` etiketi ile kapatır. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from dataclasses import dataclass, field | |
| from typing import Any, Dict, List, Optional, Sequence, Set, Tuple | |
| from PIL import Image, ImageDraw | |
| from turkceocr.postprocess.entity_engine import ( | |
| DEFAULT_REDACTABLE_ENTITY_TYPES, | |
| DetectedEntitySpan, | |
| UnifiedTurkishEntityEngine, | |
| ) | |
| class OffsetMap: | |
| """ | |
| π: norm_text [0..N] -> raw_text [0..M] monotonik karakter ofset izdüşüm haritası. | |
| """ | |
| raw_text: str | |
| norm_text: str | |
| norm_to_raw: List[int] | |
| def build(cls, raw_text: str, norm_text: str) -> "OffsetMap": | |
| """ | |
| `raw_text` ile `norm_text` arasında karakter/token düzeyinde monotonik ofset haritası kurar. | |
| Eğer uzunluklar birebir eşitse (1:1 izomorfizm), birim harita (identity map) O(N) sürede üretilir. | |
| """ | |
| m = len(raw_text) | |
| n = len(norm_text) | |
| if m == n: | |
| return cls(raw_text=raw_text, norm_text=norm_text, norm_to_raw=list(range(n + 1))) | |
| if n == 0: | |
| return cls(raw_text=raw_text, norm_text=norm_text, norm_to_raw=[0]) | |
| if m == 0: | |
| return cls(raw_text=raw_text, norm_text=norm_text, norm_to_raw=[0] * (n + 1)) | |
| # İki işaretçili açgözlü (greedy) + yerel pencere hizalaması (uzun metinlerde O(N) hız garantisi) | |
| mapping = [0] * (n + 1) | |
| i = 0 # raw_text indeksi | |
| j = 0 # norm_text indeksi | |
| raw_fold = raw_text.lower() | |
| norm_fold = norm_text.lower() | |
| while j < n and i < m: | |
| mapping[j] = i | |
| if raw_fold[i] == norm_fold[j]: | |
| i += 1 | |
| j += 1 | |
| else: | |
| # 12 karakterlik yerel pencerede eşleşme ara | |
| found_sync = False | |
| for look_raw in range(1, min(12, m - i)): | |
| if raw_fold[i + look_raw] == norm_fold[j]: | |
| i += look_raw | |
| found_sync = True | |
| break | |
| if not found_sync: | |
| for look_norm in range(1, min(12, n - j)): | |
| if raw_fold[i] == norm_fold[j + look_norm]: | |
| for step in range(look_norm): | |
| mapping[j + step] = i | |
| j += look_norm | |
| found_sync = True | |
| break | |
| if not found_sync: | |
| i += 1 | |
| j += 1 | |
| while j <= n: | |
| mapping[j] = min(i, m) | |
| j += 1 | |
| # Monotoniklik garantisi | |
| for k in range(1, n + 1): | |
| if mapping[k] < mapping[k - 1]: | |
| mapping[k] = mapping[k - 1] | |
| mapping[n] = m | |
| return cls(raw_text=raw_text, norm_text=norm_text, norm_to_raw=mapping) | |
| def project_span(self, norm_start: int, norm_end: int) -> Tuple[int, int]: | |
| """`norm_text` üzerindeki `[norm_start, norm_end)` aralığını `raw_text` ofsetlerine izdüşürür.""" | |
| n = len(self.norm_text) | |
| s_clamped = max(0, min(norm_start, n)) | |
| e_clamped = max(s_clamped, min(norm_end, n)) | |
| raw_s = self.norm_to_raw[s_clamped] | |
| raw_e = self.norm_to_raw[e_clamped] | |
| if raw_e <= raw_s and e_clamped > s_clamped and raw_s < len(self.raw_text): | |
| raw_e = min(len(self.raw_text), raw_s + (e_clamped - s_clamped)) | |
| return raw_s, raw_e | |
| class OCRLineBox: | |
| """Satır düzeyinde OCR metni, ham metin karakter aralığı ve 2B piksel kutusu `(x1, y1, x2, y2)`.""" | |
| text: str | |
| bbox: Tuple[int, int, int, int] | |
| raw_start: int = 0 | |
| raw_end: int = 0 | |
| class RedactionBox2D: | |
| """Karartılan 2B piksel bölgesi kaydı.""" | |
| entity_type: str | |
| bbox: Tuple[int, int, int, int] | |
| label_tag: str | |
| text_preview: str = "" | |
| tier: str = "VERIFIED_PII" | |
| source: str = "text_projection" | |
| def to_dict(self) -> Dict[str, Any]: | |
| return { | |
| "entity_type": self.entity_type, | |
| "bbox": list(self.bbox), | |
| "label_tag": self.label_tag, | |
| "tier": self.tier, | |
| "source": self.source, | |
| } | |
| class VisualRedactionResult: | |
| """2B Görsel + Metinsel KVKK Karartma Çıktısı.""" | |
| redacted_image: Image.Image | |
| raw_text: str | |
| redacted_text: str | |
| entities: List[DetectedEntitySpan] = field(default_factory=list) | |
| redaction_boxes: List[RedactionBox2D] = field(default_factory=list) | |
| redaction_coverage: float = 1.0 | |
| def to_dict(self) -> Dict[str, Any]: | |
| return { | |
| "redacted_text": self.redacted_text, | |
| "entity_count": len(self.entities), | |
| "redaction_box_count": len(self.redaction_boxes), | |
| "redaction_coverage": round(float(self.redaction_coverage), 4), | |
| "entities": [e.to_dict() for e in self.entities], | |
| "redaction_boxes": [b.to_dict() for b in self.redaction_boxes], | |
| } | |
| class VisualRedactor: | |
| """ | |
| 2B Görsel Belge Karartma Motoru: | |
| - Metin içindeki tüm PII ve SUSPECT_PII spanlarını `OffsetMap` üzerinden `OCRLineBox` | |
| koordinatlarına izdüşürür. | |
| - `<signature/>` (`VISUAL_SIGNATURE`), `<stamp>...</stamp>` (`VISUAL_STAMP`), | |
| `VISUAL_PHOTO` ve `VISUAL_QRCODE` görsel bölgelerini tespit eder. | |
| - Tüm hassas kutuları geri döndürülemez `#000000` (tam siyah) dolgu ve beyaz | |
| `[KVKK: <ENTITY_TYPE>]` etiketi ile kapatır. | |
| """ | |
| def __init__( | |
| self, | |
| entity_engine: Optional[UnifiedTurkishEntityEngine] = None, | |
| pad_px: int = 3, | |
| ) -> None: | |
| self.entity_engine = entity_engine or UnifiedTurkishEntityEngine(use_ner=False) | |
| self.pad_px = pad_px | |
| def build_synthetic_line_boxes( | |
| raw_text: str, | |
| image_size: Tuple[int, int], | |
| ) -> List[OCRLineBox]: | |
| """ | |
| Eğer dışarıdan açık satır koordinatları (`OCRLineBox`) verilmemişse, | |
| metin satırlarını sayfa yüksekliği ve genişliği boyunca orantısal olarak | |
| 2B satır kutularına eşler. | |
| """ | |
| w, h = image_size | |
| lines = raw_text.splitlines(keepends=True) | |
| if not lines: | |
| return [] | |
| num_lines = max(1, len(lines)) | |
| margin_x = max(16, int(w * 0.05)) | |
| margin_y = max(16, int(h * 0.05)) | |
| usable_w = max(60, w - 2 * margin_x) | |
| usable_h = max(60, h - 2 * margin_y) | |
| line_step = max(16.0, usable_h / float(num_lines)) | |
| boxes: List[OCRLineBox] = [] | |
| cursor = 0 | |
| for idx, line in enumerate(lines): | |
| stripped = line.rstrip("\r\n") | |
| line_start = cursor | |
| line_end = cursor + len(stripped) | |
| cursor += len(line) | |
| if not stripped.strip(): | |
| continue | |
| y1 = int(margin_y + idx * line_step) | |
| y2 = min(h - 4, int(y1 + max(14.0, line_step * 0.82))) | |
| char_ratio = min(1.0, max(0.15, len(stripped) / 75.0)) | |
| x1 = margin_x | |
| x2 = min(w - margin_x, int(margin_x + usable_w * char_ratio)) | |
| boxes.append( | |
| OCRLineBox( | |
| text=stripped, | |
| bbox=(x1, y1, x2, y2), | |
| raw_start=line_start, | |
| raw_end=line_end, | |
| ) | |
| ) | |
| return boxes | |
| def _char_visual_weight(ch: str) -> float: | |
| """Orantısal (proportional) fontlarda (Arial/Calibri/Times) karakter genişlik ağırlığı.""" | |
| if ch in " .,:;|!iıljtfIİ()[]/'\"-": | |
| return 0.48 | |
| if ch in "r1": | |
| return 0.72 | |
| if ch.isdigit(): | |
| return 1.08 | |
| if ch in "mw": | |
| return 1.42 | |
| if ch in "MW@%&": | |
| return 1.55 | |
| if ch.isupper(): | |
| return 1.24 | |
| return 0.92 | |
| def project_char_span_to_boxes( | |
| self, | |
| raw_start: int, | |
| raw_end: int, | |
| line_boxes: Sequence[OCRLineBox], | |
| image_size: Tuple[int, int], | |
| ) -> List[Tuple[int, int, int, int]]: | |
| """ | |
| `[raw_start, raw_end)` karakter aralığını kesiştiği `OCRLineBox` satırları içinde | |
| orantısal tipografik karakter ağırlığıyla 2B piksel kutularına `(bx1, by1, bx2, by2)` dönüştürür. | |
| """ | |
| w, h = image_size | |
| matched_boxes: List[Tuple[int, int, int, int]] = [] | |
| for lb in line_boxes: | |
| if lb.raw_end <= raw_start or lb.raw_start >= raw_end: | |
| continue | |
| line_len = max(1, lb.raw_end - lb.raw_start) | |
| local_s = max(0, min(line_len, raw_start - lb.raw_start)) | |
| local_e = max(local_s, min(line_len, raw_end - lb.raw_start)) | |
| if lb.text and len(lb.text) == line_len: | |
| weights = [self._char_visual_weight(c) for c in lb.text] | |
| total_w = max(1e-6, sum(weights)) | |
| rel_s = sum(weights[:local_s]) / total_w | |
| rel_e = sum(weights[:local_e]) / total_w | |
| else: | |
| rel_s = local_s / float(line_len) | |
| rel_e = local_e / float(line_len) | |
| lx1, ly1, lx2, ly2 = lb.bbox | |
| lw = max(8, lx2 - lx1) | |
| pad_x = max(self.pad_px, 6) | |
| bx1 = max(0, int(lx1 + lw * rel_s) - pad_x) | |
| bx2 = min(w, int(lx1 + lw * rel_e) + pad_x) | |
| by1 = max(0, ly1 - self.pad_px) | |
| by2 = min(h, ly2 + self.pad_px) | |
| if bx2 <= bx1 + 4: | |
| bx2 = min(w, bx1 + 16) | |
| matched_boxes.append((bx1, by1, bx2, by2)) | |
| return matched_boxes | |
| def redact_document( | |
| self, | |
| image: Image.Image, | |
| raw_text: str, | |
| normalized_text: Optional[str] = None, | |
| line_boxes: Optional[Sequence[OCRLineBox]] = None, | |
| visual_regions: Optional[Sequence[Tuple[str, Tuple[int, int, int, int]]]] = None, | |
| redact_types: Optional[Set[str]] = None, | |
| ) -> VisualRedactionResult: | |
| """ | |
| Belge görseli ve OCR metnini birlikte tarayarak: | |
| 1. `normalized_text` (veya `raw_text`) üzerinde `UnifiedTurkishEntityEngine` çalıştırır, | |
| 2. `OffsetMap` (π: norm -> raw) ile tüm PII varlıklarını `raw_text` ofsetlerine izdüşürür, | |
| 3. `<signature/>`, `<stamp>...</stamp>`, `VISUAL_PHOTO`, `VISUAL_QRCODE` görsel bölgelerini ekler, | |
| 4. Görsel üzerinde geri döndürülemez `#000000` (tam siyah) karartma uygular. | |
| """ | |
| target_types = redact_types if redact_types is not None else DEFAULT_REDACTABLE_ENTITY_TYPES | |
| analysis_text = normalized_text if normalized_text is not None else raw_text | |
| offset_map = OffsetMap.build(raw_text=raw_text, norm_text=analysis_text) | |
| report = self.entity_engine.analyze(analysis_text, redact_types=target_types) | |
| img_out = image.convert("RGB").copy() | |
| w, h = img_out.size | |
| active_lines = ( | |
| list(line_boxes) | |
| if line_boxes is not None | |
| else self.build_synthetic_line_boxes(raw_text, (w, h)) | |
| ) | |
| redaction_boxes: List[RedactionBox2D] = [] | |
| # 1. Metin tabanlı PII ve SUSPECT_PII varlıklarının 2B kutulara izdüşümü | |
| for ent in report.entities: | |
| if ent.entity_type not in target_types: | |
| continue | |
| raw_s, raw_e = offset_map.project_span(ent.start, ent.end) | |
| boxes = self.project_char_span_to_boxes(raw_s, raw_e, active_lines, (w, h)) | |
| for bx in boxes: | |
| redaction_boxes.append( | |
| RedactionBox2D( | |
| entity_type=ent.entity_type, | |
| bbox=bx, | |
| label_tag=f"[KVKK: {ent.entity_type}]", | |
| text_preview=ent.text[:16], | |
| tier=ent.tier, | |
| source=ent.source, | |
| ) | |
| ) | |
| # 2. Yapısal VLM etiketleri (<signature/>, <stamp>...</stamp>) için 2B görsel karartma | |
| for m_sig in re.finditer(r"<signature\s*/?>", raw_text, flags=re.IGNORECASE): | |
| for bx in self.project_char_span_to_boxes(m_sig.start(), m_sig.end(), active_lines, (w, h)): | |
| redaction_boxes.append( | |
| RedactionBox2D( | |
| entity_type="VISUAL_SIGNATURE", | |
| bbox=bx, | |
| label_tag="[KVKK: VISUAL_SIGNATURE]", | |
| tier="VERIFIED_VISUAL", | |
| source="structural_tag", | |
| ) | |
| ) | |
| for m_stamp in re.finditer(r"<stamp>.*?</stamp>", raw_text, flags=re.IGNORECASE | re.DOTALL): | |
| for bx in self.project_char_span_to_boxes(m_stamp.start(), m_stamp.end(), active_lines, (w, h)): | |
| redaction_boxes.append( | |
| RedactionBox2D( | |
| entity_type="VISUAL_STAMP", | |
| bbox=bx, | |
| label_tag="[KVKK: VISUAL_STAMP]", | |
| tier="VERIFIED_VISUAL", | |
| source="structural_tag", | |
| ) | |
| ) | |
| # 3. Açıkça verilen görsel biyometrik/kimlik kutuları (VISUAL_PHOTO, VISUAL_QRCODE, VISUAL_SIGNATURE, VISUAL_STAMP) | |
| if visual_regions: | |
| for vtype, vbbox in visual_regions: | |
| vx1 = max(0, min(w, int(vbbox[0]))) | |
| vy1 = max(0, min(h, int(vbbox[1]))) | |
| vx2 = max(vx1 + 4, min(w, int(vbbox[2]))) | |
| vy2 = max(vy1 + 4, min(h, int(vbbox[3]))) | |
| redaction_boxes.append( | |
| RedactionBox2D( | |
| entity_type=vtype, | |
| bbox=(vx1, vy1, vx2, vy2), | |
| label_tag=f"[KVKK: {vtype}]", | |
| tier="VERIFIED_VISUAL", | |
| source="visual_detector", | |
| ) | |
| ) | |
| # 4. Geri döndürülemez #000000 tam siyah dolgu + beyaz [KVKK: ...] etiketi çizimi | |
| draw = ImageDraw.Draw(img_out) | |
| covered_boxes = 0 | |
| for rbox in redaction_boxes: | |
| x1, y1, x2, y2 = rbox.bbox | |
| draw.rectangle([x1, y1, x2, y2], fill=(0, 0, 0), outline=(0, 0, 0)) | |
| if (x2 - x1) >= 75 and (y2 - y1) >= 12: | |
| short_tag = rbox.label_tag[: max(8, (x2 - x1) // 6)] | |
| draw.text((x1 + 3, y1 + 1), short_tag, fill=(255, 255, 255)) | |
| covered_boxes += 1 | |
| coverage = 1.0 if len(redaction_boxes) == covered_boxes else (covered_boxes / max(1, len(redaction_boxes))) | |
| return VisualRedactionResult( | |
| redacted_image=img_out, | |
| raw_text=raw_text, | |
| redacted_text=report.redacted_text, | |
| entities=report.entities, | |
| redaction_boxes=redaction_boxes, | |
| redaction_coverage=coverage, | |
| ) | |