# 需求:EasyScreenshot OCR 引擎升级 v2(PP-OCRv5 本机推理,系统引擎兜底)

状态: 进行中(2026-10-31,代码已接线,未打包)
归属: 产品功能(easy_screenshot)
来源: 承接 `2026-10-31-EasyScreenshot-OCR导出表格.md` 的引擎侧欠账(boss 2026-10-31 口述方向:把已经跑通的 PP-OCRv5 接进应用)
日期: 2026-10-31
建议: 做。v1 的 Windows.Media.Ocr 逐字断行(实测把「姓名」拆成「姓 名」),表格导出只能靠词框兜;PP-OCRv5 已凭 Rust+onnxruntime 与 Python RapidOCR 对拍通过,接进应用是纯接线活。

## 1. 现状(改动前实测)

| 环节 | 改动前 | 位置 |
| --- | --- | --- |
| 引擎 | Windows=`Windows.Media.Ocr`(系统语言包驱动),mac=Apple Vision | `src-tauri/src/ocr.rs` |
| 识别质量 | 中文按字切行:表头 `姓名` → `姓 名`,`1,280.50` → `1 ， 280 ． 50` | 本轮回退实测输出 |
| 命令 | `snip_ocr_text(png)` → `spawn_blocking` 里调 `recognize_png_with_lines`,返回 `{found,text,lines}` | `src-tauri/src/snip_commands.rs:321` |
| 付费墙 | `ensure_allowed("ocr")` 在命令壳最先执行 | `snip_commands.rs:322` |
| 前端 | `controller.ocrText()` 调 `window.snipApi.ocrText`,成功弹结果浮窗 | `src/renderer/overlay/controller.ts:1032` |

已经跑通、但没接进应用的资产:

- `src-tauri/src/ocr_models.rs`:`ensure_models(progress_cb)` 下载/缓存/sha256 校验四件模型(合计 21.5MB),`%LOCALAPPDATA%\EasyScreenshot\ocr\ppocrv5` 缓存命中不联网;
- `src-tauri/src/ocr_onnx.rs`:`recognize_with_models(png, &ModelPaths) -> Result<Vec<OcrTextLine>, OcrEngineError>`,det/rec/cls/dict 四件模型纯 Rust 推理,会话按模型路径进程级缓存。

## 2. 方案(PP-OCRv5 按需下载)

### 2.1 运行时 dll(不接这段,装完就崩)

- `onnxruntime.dll`(18.4MB)走 Tauri resource 随包发:`src-tauri/resources/onnxruntime.dll` + `bundle.resources`;二进制**不入库**(`.gitignore`),由 `scripts/fetch-onnxruntime.ps1` 现取(幂等:已就绪且 sha256 命中就跳过;支持 `-Source <路径>` 复制或 `-FromPip` 拉 `onnxruntime==1.31.1` wheel 抽 dll;校验基线 `e45f61e5…0306` / 19,288,376 B)。
- 启动期 `lib.rs:61 bind_ort_dylib()`:`ORT_DYLIB_PATH` 已设则尊重(开发机/排障覆盖),未设则经 `app.path().resource_dir()` 解析 `<resource_dir>/resources/onnxruntime.dll`(dev 平铺到 resource 根也认),**不写死任何机器绝对路径**;找不到只 `warn`。

### 2.2 命令层编排

`snip_ocr_text`(签名/返回字段向后兼容,只多一个 `engine`):

1. `ensure_allowed("ocr")` **仍最先执行**(付费墙口径不动),拦截返回 `engine:"none"` + `paywall-blocked`;
2. `spawn_blocking`(重活不上主线程):dataURL → PNG 字节;
3. `ocr_engine::recognize()`:`ensure_models`(首次真下 21.5MB)→ `recognize_with_models` → `{text, lines, engine:"ppocrv5"}`;
4. 任何失败(**下载/校验失败、dll 缺失、推理失败、ort panic**)→ 回退系统引擎 `recognize_png_with_lines` → `engine:"system"`,**不报错给用户**,只落一条 debugEvent(`ocr-fallback` + stage/code/detail)和一条 `warn` 日志;
5. 命中即写剪贴板(v1 行为不变),返回 `{found,text,lines,engine}`。

下载进度:`ocr-model-progress` 事件(overlay 窗全局广播),payload `{file,downloaded,total,index,count}`(camelCase);Rust 侧节流 ~10Hz,每件首帧(`downloaded=0`)与末帧(`downloaded>=total`)必发。

### 2.3 前端

- `bridge.ts:21 onOcrModelProgress`:`listen('ocr-model-progress')`,返回退订函数(overlay 每屏一窗,不退订会叠监听);
- `controller.ocrText()`:点击先置「识别中」常驻提示条(复用 `showToast` 的同一份深色玻璃样式,原地改文案,**不新开窗**),收到进度改写成 `正在下载识别引擎 12.3 MB / 21.5 MB`,最后一件下完切回「识别中」并自动继续识别(用户不用再点一次);`ocrBusy` 期间工具条 OCR 按钮 `disabled` 防连点;
- i18n 五语齐(en 基准 + zh-CN/de/ja/es):`toolbar.ocrRecognizing`、`toolbar.ocrDownloading`(`{done}` / `{total}` 占位)。

## 3. 对拍数据(照抄 A3/A4 账页,本票不重排、不重算)

Rust + ort 路线与 Python RapidOCR(同四件模型)逐样本对拍:

- 文本一致率:**0.986 / 1.0 / 0.9595**(三张真实截图各一个数,样本与数字的对应关系以 A3/A4 对拍账页为准,本票不复述以免张冠李戴);
- 表格:A_table.png 4 列 × 4 行,**16 个单元格逐格与 Python RapidOCR 一致(16/16)**。

本轮接线的实跑复核(冷起,`engine=ppocrv5`):

- `table.png` → `lines=4 words=16 chars=77`,耗时 1673ms(冷态含会话加载;热态 1.6–1.9s,首下 21.5MB 的时间不计入识别耗时);
- 行/词框一个不丢(4 行 × 4 词框 = 16,与对拍时同数),说明 IPC 接线没把结构压平。

## 4. 回退策略(实测三条)

| 场景 | 触发 | 结果 |
| --- | --- | --- |
| 安装包漏打 dll / `ORT_DYLIB_PATH` 未设 | 启动 warn,识别时首选腿直接回退 | `fallback stage=runtime code=runtime_failed` → `engine=system`,出字 13 行/107 字,耗时 31ms |
| `ORT_DYLIB_PATH` 指到坏文件 | `ort::init_from` 返回 Err | `fallback stage=inference code=runtime_failed` → `engine=system`,出字同上,331ms |
| 模型下载失败(断网/校验不符) | `ensure_models` 返回 `network_failed` / `checksum_failed` / `disk_failed` | 同样回退系统引擎,下次点击会重试下载(不熔断) |

额外加的熔断:运行时/panic 类失败在进程内标 `PPOCRV5_DISABLED`,后续点击直接走系统引擎,不再每拍白等一次 dlopen;**网络类失败不熔断**(可恢复)。
`ort` 在 dylib 加载失败时是 **panic 不是 Err**(本轮实测),首选腿整体包了 `catch_unwind` 转回退。

## 5. 入口与层级

- 入口不变:overlay 工具条「提取文字(OCR)」按钮 → `controller.ocrText()` → IPC `snip_ocr_text`;mac 一期不随包发 dll,直接走系统引擎(Apple Vision)。
- 层级:UI(提示条/浮窗)→ IPC(命令壳,付费墙 + 线程池)→ 编排(`ocr_engine`,选引擎 + 回退)→ 资产层(`ocr_models` 下载校验 / `ocr_onnx` 推理)→ 回退腿(`ocr`,系统引擎)。

## 6. 风险

| 风险 | 说明 | 处置 |
| --- | --- | --- |
| 首下 21.5MB | 首次 OCR 要联网下载,慢网下用户会等 | 进度条给到 MB 级;缓存命中后零联网;失败回退系统引擎(用户仍有字) |
| CPU 推理 1–1.8s | 无 GPU,1080p 整图 det+rec | 已 `spawn_blocking`;识别中态防连点;后续可缩 ROI(只跑选区) |
| 未接 cls | 文本行方向分类模型下载了但一期没接,极端竖排/倒置可能错行 | 二期接 cls;当前靠 det 框坐标顺序 |
| dll 与 ort 版本耦合 | ort 2.0.0-rc.15 配 onnxruntime 1.31.1,换版本要重跑对拍 | 脚本 pin sha256;升级走对拍再改基线 |
| 打包未验证 | 本轮明确不打包,`bundle.resources` 与 NSIS 实际落位(resource_dir 下 `resources\` 还是根)只按 Tauri v2 约定写了双候选 | 下一轮打包后跑一次安装包实测,确认 `bind_ort_dylib` 的 `info` 日志 |

## 7. 二期

1. 接 cls(文本行方向分类)后处理竖排/倒置;
2. VLM 兜底(复杂版面/手写),走云端或本机小模型,按会员档位再定;
3. 识别提速:按选区裁剪 ROI、会话常驻预热、首帧预加载;
4. mac 随包发 onnxruntime.dylib,让 mac 也用 PP-OCRv5(当前 mac 仍是 Apple Vision)。
