#!/usr/bin/env python3 """Offline full-page Persian OCR using bundled detector plus a Bina recognizer.""" from __future__ import annotations import argparse import json import re from pathlib import Path from typing import Any, Iterator from paddleocr import PaddleOCR _LTR_RUN = re.compile(r"[a-zA-Z0-9 :*./%+-]") def pred_reverse(text: str) -> str: segments: list[str] = [] current_ltr = "" for character in text: if _LTR_RUN.search(character): current_ltr += character continue if current_ltr: segments.append(current_ltr) current_ltr = "" segments.append(character) if current_ltr: segments.append(current_ltr) return "".join(reversed(segments)) def _json_payload(result: Any) -> dict[str, Any]: payload = result.json() if callable(result.json) else result.json return payload.get("res", payload) def _box_list(box: Any) -> list[float]: if hasattr(box, "tolist"): box = box.tolist() return [float(value) for value in box] def ordered_lines(payload: dict[str, Any]) -> list[dict[str, Any]]: visual_texts = [str(value) for value in payload.get("rec_texts", [])] texts = [pred_reverse(value) for value in visual_texts] scores = [float(value) for value in payload.get("rec_scores", [0.0] * len(texts))] boxes = payload.get("rec_boxes", []) if len(boxes) != len(texts): return [{"text": text, "score": scores[i], "raw_visual_text": visual_texts[i]} for i, text in enumerate(texts)] items = [] for i, (text, box) in enumerate(zip(texts, boxes)): x0, y0, x1, y1 = _box_list(box) items.append({ "text": text, "score": scores[i], "raw_visual_text": visual_texts[i], "box": [x0, y0, x1, y1], "x": (x0 + x1) / 2, "y": (y0 + y1) / 2, "height": max(y1 - y0, 1.0), }) items.sort(key=lambda item: item["y"]) rows: list[list[dict[str, Any]]] = [] for item in items: if not rows: rows.append([item]) continue row = rows[-1] mean_y = sum(part["y"] for part in row) / len(row) mean_h = sum(part["height"] for part in row) / len(row) if abs(item["y"] - mean_y) <= 0.55 * max(item["height"], mean_h): row.append(item) else: rows.append([item]) output = [] for row_index, row in enumerate(rows): row.sort(key=lambda item: item["x"], reverse=True) for item in row: output.append({key: value for key, value in item.items() if key not in {"x", "y", "height"}} | {"row": row_index}) return output class BinaPageOCR: def __init__(self, model_dir: str | Path | None = None, detector_dir: str | Path | None = None, device: str | None = None, score_threshold: float = 0.0) -> None: base = Path(__file__).resolve().parent model_dir = Path(model_dir) if model_dir else base / "inference" detector_dir = Path(detector_dir) if detector_dir else base / "detector" options: dict[str, Any] = { "text_detection_model_dir": str(detector_dir), "text_recognition_model_dir": str(model_dir), "use_doc_orientation_classify": False, "use_doc_unwarping": False, "use_textline_orientation": False, "text_rec_score_thresh": score_threshold, } if device: options["device"] = device self._ocr = PaddleOCR(**options) def predict(self, inputs: list[str | Path]) -> Iterator[dict[str, Any]]: for source in inputs: for page_index, result in enumerate(self._ocr.predict(str(source))): raw = _json_payload(result) lines = ordered_lines(raw) row_text: dict[int, list[str]] = {} for line in lines: row_text.setdefault(int(line.get("row", len(row_text))), []).append(line["text"]) text = "\n".join(" ".join(row_text[index]).strip() for index in sorted(row_text) if row_text[index]) yield {"input_path": str(source), "page_index": page_index, "text": text, "lines": lines} def main() -> int: parser = argparse.ArgumentParser(description="Run self-contained full-page Persian OCR with Bina 0.2") parser.add_argument("inputs", nargs="+") parser.add_argument("--model-dir", default=str(Path(__file__).resolve().parent / "inference")) parser.add_argument("--detector-dir", default=str(Path(__file__).resolve().parent / "detector")) parser.add_argument("--device", default="cpu", help="cpu, gpu:0, ...") parser.add_argument("--score-threshold", type=float, default=0.0) parser.add_argument("--output", type=Path) args = parser.parse_args() model = BinaPageOCR(args.model_dir, args.detector_dir, args.device, args.score_threshold) predictions = list(model.predict(args.inputs)) for prediction in predictions: print(json.dumps(prediction, ensure_ascii=False)) if args.output: args.output.write_text(json.dumps(predictions, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main())