# Devlog · N3 落地:ONNX 内嵌 OCR(PP-OCRv4 × tract,纯 Rust)——扫描件从 0 文本到 14/14 行全对

> 日期:2026-06-10 · plan:[n3-real-enhancer.md](../plans/n3-real-enhancer.md) · spike:[n3-tract-spike](2026-06-10-n3-tract-spike.md)
> 路线:P4 优先(用户决策)。**身份零妥协**:无 Python/C++/子进程,推理在进程内纯 Rust(tract);页面图来自**抽取嵌入 XObject 原字节**而非渲染——"不光栅化"保持。

## 交付物

| 层 | 内容 |
|---|---|
| IR(0.4.0) | `ImageChunk` 像素载荷(`width_px/height_px/kind/data`,data 不序列化);`TextChunk.source` 元素级 provenance(M7 遗留补齐) |
| docparse-pdf | `images.rs` 惰性收集页面 Image XObject(流不解码);解释器 `Do`:CTM 单位方折 bbox,**覆盖页面 ≥50% 才解码附像素**(数字 PDF 小图零成本)。支持 DCT 直通 + Flate/A85 裸位图(Gray8/Rgb8);JBIG2/CCITT/JPX 仅记位置(TODO) |
| **docparse-ocr**(新 crate) | det(DBNet,960² 定画布单次优化)→ 阈值+连通域成框 → 矩形偏移近似 DB unclip → 裁剪+双线性缩放 h=48、宽度桶缓存 rec(SVTR-LCNet)→ CTC 贪心解码+均值概率置信度 → 像素坐标经图片放置 bbox 折回 PDF 用户空间。`impl core::enhance::Enhancer`,M7 路由原样复用 |
| CLI | `--ocr --ocr-models <dir>`(默认 `models/ppocr`);route report 到 stderr |

## 验收(plan §1 全过)

- `chinese_scan.pdf`:0 文本 → **14/14 行全对**(含标点/数字/混排),conf 0.98–0.99(封顶 0.99 保"模型≠确定性"约定),每行 `source: "ocr:ppocr-v4"` + bbox 引用;text/markdown/chunks 全管线可用(段落聚合正常工作);
- **数字页零模型**:1901 在 `--ocr` 下 route 为空;不开 `--ocr` 完全不加载模型;
- **确定性**:同文件两次 `--ocr` JSON 逐字节一致;
- 回归:79 单测(+12)、clippy 零 warning、三件套与双记分牌(0.764/0.833)不动;
- 全页 OCR 端到端 ~1.3s(det 一次 + 14 行 rec,桶缓存生效)。

## 开发中抓到的 bug(review 同步进行)

1. **置信度二次 softmax**:PP-OCR rec 头已带 softmax,再 softmax 6625 类把概率摊平到 ~0,全部被 MIN_CONFIDENCE 拦下(症状:识别全对但 applied=false)。修:argmax 值即步概率。
2. **错误吞没**:`ocr_rgb(...).ok()?` 静默吞推理错误,违反"不静默"红线。修:eprintln 后 decline,确定性结果仍交付。
3. 连通域高度门把 2 行高的细文本带滤掉(≥3 行改 ≥2 行);双线性插值 `as u8` 截断改四舍五入。

## 已知边界(显式 TODO)

- 方向分类(cls)模型未接——假定扫描件正向;
- JBIG2/CCITT(1-bit 传真类)与 JPX 编码扫描不支持(位置可审计,文本不可得);
- 二进制 19.08MB(tract 占 ~12.5MB),仍在 20MB 门内——若需瘦身可把 docparse-ocr 改 feature-gate;
- 英文/多语种字典未配(当前 ch 模型中英混排可用)。

## 后续增量(同日):MCP/REST OCR 透传——四接口能力对等

- `OcrState` 懒加载共享件:模型在**第一个要 OCR 的请求**时才读取(服务数字文档零模型),加载结果(成败)缓存——坏配置快速失败不重读。
- MCP 三 tool 加可选 `"ocr": true` 参数;REST `POST /parse?ocr=true`;`docparse mcp|serve --ocr-models <dir>`。
- 验收:MCP `ocr:true` 对 chinese_scan 拿到中文 chunks(不带参数仍 0,路由不变);**REST `?ocr=true` 与 CLI `--ocr` 逐字节一致**(cmp);模型缺失 → 结构化 `isError: "ocr models unavailable: ..."`(单测钉死);80 单测、clippy 零、三件套零回归。
