第一次提交
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
"""OCR 引擎抽象接口。"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
@dataclass
|
||||
class OcrResult:
|
||||
markdown: str
|
||||
confidence: float # 0-1,未知时给 1.0
|
||||
engine: str # "pix2text" / "vlm"
|
||||
|
||||
|
||||
class OcrEngine(Protocol):
|
||||
async def recognize(self, image_bytes: bytes, mime: str) -> OcrResult: ...
|
||||
@@ -0,0 +1,35 @@
|
||||
"""OCR 编排:Pix2Text 优先,失败/低置信/未部署时降级到多模态 VLM。"""
|
||||
import logging
|
||||
|
||||
from app.config import settings
|
||||
from app.services.llm.client import LLMClient
|
||||
from app.services.ocr.base import OcrResult
|
||||
from app.services.ocr.pix2text_client import Pix2TextClient
|
||||
from app.services.ocr.vlm_engine import VlmOcrEngine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Pix2Text 结果置信度低于此值则降级到 VLM
|
||||
_CONFIDENCE_THRESHOLD = 0.5
|
||||
|
||||
|
||||
async def run_ocr(image_bytes: bytes, mime: str, llm: LLMClient) -> OcrResult:
|
||||
"""返回识别结果,engine 字段标明最终使用的引擎。"""
|
||||
# 1. 若配置了 Pix2Text 服务且健康,优先用
|
||||
if settings.ocr_service_url:
|
||||
client = Pix2TextClient(settings.ocr_service_url)
|
||||
if await client.healthy():
|
||||
try:
|
||||
result = await client.recognize(image_bytes, mime)
|
||||
if result.markdown.strip() and result.confidence >= _CONFIDENCE_THRESHOLD:
|
||||
return result
|
||||
logger.info(
|
||||
"Pix2Text 置信度低(%.2f)或结果为空,降级 VLM", result.confidence
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("Pix2Text 识别失败,降级 VLM:%s", e)
|
||||
else:
|
||||
logger.info("Pix2Text 服务不健康,降级 VLM")
|
||||
|
||||
# 2. 降级 / 默认:多模态大模型
|
||||
return await VlmOcrEngine(llm).recognize(image_bytes, mime)
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Pix2Text 独立微服务的 HTTP 客户端(后续阶段部署 OCR 服务时启用)。"""
|
||||
import httpx
|
||||
|
||||
from app.services.ocr.base import OcrResult
|
||||
|
||||
|
||||
class Pix2TextClient:
|
||||
def __init__(self, service_url: str, timeout: float = 60.0):
|
||||
self._url = service_url.rstrip("/")
|
||||
self._timeout = timeout
|
||||
|
||||
async def healthy(self) -> bool:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as c:
|
||||
r = await c.get(f"{self._url}/health")
|
||||
return r.status_code == 200
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
async def recognize(self, image_bytes: bytes, mime: str) -> OcrResult:
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as c:
|
||||
r = await c.post(
|
||||
f"{self._url}/ocr",
|
||||
files={"file": ("image", image_bytes, mime)},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
return OcrResult(
|
||||
markdown=data["markdown"],
|
||||
confidence=float(data.get("confidence", 1.0)),
|
||||
engine="pix2text",
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""多模态大模型 OCR 引擎:直接看图转写题目为 Markdown。"""
|
||||
from app.services.llm.client import LLMClient
|
||||
from app.services.llm.prompts import VLM_OCR_SYSTEM, VLM_OCR_USER
|
||||
from app.services.ocr.base import OcrResult
|
||||
|
||||
|
||||
class VlmOcrEngine:
|
||||
def __init__(self, llm: LLMClient):
|
||||
self._llm = llm
|
||||
|
||||
async def recognize(self, image_bytes: bytes, mime: str) -> OcrResult:
|
||||
result = await self._llm.chat_vision(
|
||||
VLM_OCR_SYSTEM, VLM_OCR_USER, image_bytes, mime=mime
|
||||
)
|
||||
return OcrResult(markdown=result.content, confidence=1.0, engine="vlm")
|
||||
Reference in New Issue
Block a user