| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- from __future__ import annotations
-
- import functools
- import logging
- import os
- import shutil
- from pathlib import Path
- from typing import TYPE_CHECKING
-
- from src.env import load_env
- from src.image_utils import calc_scale_dims
- from src.models import OcrPageResult, ParsedBlock
-
- if TYPE_CHECKING:
- from PIL import Image as PILImage
-
- logger = logging.getLogger(__name__)
-
- # CPU optimizations
- _SURYA_CTX_PER_SLOT: int = 8192
- _SURYA_PARALLEL: int = 1
- _IMAGE_MAX_WIDTH: int = 1056
-
-
- def _setup_surya_env() -> None:
- load_env()
- os.environ.setdefault("SURYA_INFERENCE_BACKEND", "llamacpp")
- os.environ.setdefault("SURYA_INFERENCE_KEEP_ALIVE", "true")
- os.environ.setdefault("SURYA_GUIDED_LAYOUT", "false")
- os.environ.setdefault("SURYA_INFERENCE_PARALLEL", str(_SURYA_PARALLEL))
- os.environ.setdefault("SURYA_INFERENCE_CTX_PER_SLOT", str(_SURYA_CTX_PER_SLOT))
- if "LLAMA_CPP_BINARY" not in os.environ:
- binary = shutil.which("llama-server") or os.path.expanduser(
- "~/.local/bin/llama-server"
- )
- if Path(binary).exists():
- os.environ["LLAMA_CPP_BINARY"] = binary
- cpu_count = os.cpu_count() or 4
- os.environ.setdefault(
- "LLAMA_CPP_EXTRA_ARGS", f"-t {cpu_count} --threads-batch {cpu_count}"
- )
-
-
- class SuryaEngine:
- """OCR engine using Surya 2 VLM via llama.cpp.
-
- Optimizations for CPU:
- - llama-server kept alive between calls (SURYA_INFERENCE_KEEP_ALIVE=true)
- - reduced context per slot (8192 vs 12288) saves memory
- - single parallel slot (no batch needed for single page)
- - image pre-scaled to 1056px wide (≈96 DPI)
- - explicit thread count matching CPU cores
- """
-
- def __init__(self) -> None:
- _setup_surya_env()
- try:
- from surya.inference import SuryaInferenceManager
- from surya.recognition import RecognitionPredictor
-
- self._manager = SuryaInferenceManager()
- self._predictor = RecognitionPredictor(self._manager)
- except ImportError as e:
- msg = (
- "Surya engine not available. Install: pip install surya-ocr torch"
- )
- raise ImportError(msg) from e
-
- def process(self, image_path: str) -> OcrPageResult:
- from PIL import Image
-
- image = Image.open(image_path)
- return self._process_image(image)
-
- def _health_check(self) -> bool:
- import requests
-
- port = os.environ.get("SURYA_INFERENCE_PORT", "")
- host = os.environ.get("SURYA_INFERENCE_HOST", "127.0.0.1")
- if not port:
- return True
- try:
- r = requests.get(f"http://{host}:{port}/health", timeout=10)
- return r.status_code == 200
- except requests.RequestException:
- logger.warning("llama-server health check failed")
- return False
-
- def _process_image(self, image: PILImage.Image) -> OcrPageResult:
- scaled = _pre_scale(image)
- results = self._predictor([scaled])
- if results:
- return self._to_result(results[0], scaled)
- return OcrPageResult()
-
- @staticmethod
- def _to_result(page, image: PILImage.Image) -> OcrPageResult:
- blocks: list[ParsedBlock] = []
- for blk in getattr(page, "blocks", []):
- html = getattr(blk, "html", "") or ""
- label = getattr(blk, "label", "text")
- bbox = getattr(blk, "bbox", [0, 0, 0, 0])
- confidence = float(getattr(blk, "confidence", 0.9))
- blocks.append(
- ParsedBlock(
- label=label,
- content=html,
- bbox=(int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])),
- confidence=confidence,
- )
- )
- return OcrPageResult(
- blocks=blocks,
- raw_json={},
- width=image.width,
- height=image.height,
- )
-
-
- def _pre_scale(image: PILImage.Image) -> PILImage.Image:
- new_w, new_h = calc_scale_dims(*image.size, _IMAGE_MAX_WIDTH)
- if (new_w, new_h) != image.size:
- return image.resize((new_w, new_h), 1) # PIL.Image.LANCZOS
- return image
-
-
- @functools.lru_cache(maxsize=1)
- def _get_surya_engine() -> SuryaEngine:
- return SuryaEngine()
-
-
- def surya_ocr_image(image_path: str) -> OcrPageResult:
- engine = _get_surya_engine()
- if not engine._health_check():
- logger.warning("llama-server не отвечает, перезапуск...")
- _get_surya_engine.cache_clear()
- engine = _get_surya_engine()
- if not engine._health_check():
- raise RuntimeError(
- "llama-server недоступен после перезапуска. "
- "Проверьте: `ps aux | grep llama-server`. "
- "Попробуйте: `llama-server --version`"
- )
- return engine.process(image_path)
|