File size: 4,245 Bytes
e0ea7df e5e756a e0ea7df e5e756a e0ea7df ae8b600 e0ea7df e5e756a ae8b600 e0ea7df ae8b600 e0ea7df ae8b600 cc826a1 ae8b600 e5e756a e0ea7df ae8b600 e0ea7df e5e756a ae8b600 e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a cc826a1 e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a e0ea7df e5e756a e0ea7df | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | """OCR 插件 HTTP 接口。
所有识别请求走 FastAPI BackgroundTasks 真正异步执行:上传/解码完成后立即返回
run_id,OCR 在后台任务中完成并通过 run/log 服务写事件,前端轮询日志面板即可。
"""
import base64
import logging
import tempfile
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, HTTPException, UploadFile
from pydantic import BaseModel
from .core import extract_text, get_model_info_with_providers
from app.plugins.run_log import get_run_log_service, RunStatus
logger = logging.getLogger(__name__)
router = APIRouter()
plugin = None
def set_plugin_instance(plugin_instance):
global plugin
plugin = plugin_instance
@router.get("/status")
async def get_status():
"""三态状态:插件启用 / 引擎就绪 / 模型 ID。"""
if plugin is None:
return {"name": "ocr", "enabled": False, "engine_ready": False}
return plugin.get_status()
@router.get("/model-info")
async def model_info():
return get_model_info_with_providers()
class OCRBase64Request(BaseModel):
image_base64: str
suffix: str = ".jpg"
@router.post("/upload")
async def ocr_upload(file: UploadFile, background: BackgroundTasks):
"""上传图片并异步识别,立即返回 run_id。"""
run = _create_run(f"图片已上传: {file.filename}")
content = await file.read()
background.add_task(
_run_ocr_task, content, _suffix_from_filename(file.filename), run.run_id,
)
return {"run_id": run.run_id, "status": "running"}
@router.post("/image-base64")
async def ocr_image_base64(request: OCRBase64Request, background: BackgroundTasks):
"""Base64 图片异步识别。"""
try:
content = base64.b64decode(request.image_base64)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Base64 解码失败: {e}")
run = _create_run("Base64 图片已接收")
background.add_task(
_run_ocr_task, content, _normalize_suffix(request.suffix), run.run_id,
)
return {"run_id": run.run_id, "status": "running"}
# ---- 后台任务 ----
def _create_run(upload_msg: str):
"""创建 run 并写上传事件;前置校验 plugin 是否启用。"""
if plugin is None or not plugin.enabled:
raise HTTPException(status_code=400, detail="插件未启用")
run_service = get_run_log_service()
run = run_service.create_run("ocr")
run_service.add_event(run_id=run.run_id, stage="upload", message=upload_msg)
return run
def _run_ocr_task(content: bytes, suffix: str, run_id: str) -> None:
"""后台识别任务:落临时文件 → 调引擎 → 写 run 结果 → 清理临时文件。
整段 try/finally 保证临时文件最终被清理;run_service.finish_run 一定被调用。
"""
run_service = get_run_log_service()
tmp_path: str | None = None
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp.write(content)
tmp_path = tmp.name
result = extract_text(tmp_path, run_id=run_id)
if result["success"]:
run_service.finish_run(
run_id=run_id,
status=RunStatus.SUCCEEDED,
result={
"text": result["text"],
"lines": result["lines"],
"boxes": result["boxes"],
"model_info": result["model_info"],
"timings": result["timings"],
},
)
else:
run_service.finish_run(
run_id=run_id, status=RunStatus.FAILED, error=result["error"],
)
except Exception as e:
logger.exception("后台 OCR 异常")
run_service.add_event(run_id, "ocr", f"OCR 异常: {e}", level="error")
run_service.finish_run(run_id=run_id, status=RunStatus.FAILED, error=str(e))
finally:
if tmp_path:
Path(tmp_path).unlink(missing_ok=True)
# ---- 工具 ----
def _suffix_from_filename(filename: str) -> str:
return "." + filename.rsplit(".", 1)[-1] if "." in filename else ".jpg"
def _normalize_suffix(suffix: str) -> str:
return suffix if suffix.startswith(".") else f".{suffix}"
|