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}"