feat(ocr): 图片/扫描件可检索 + 评审阻断项修复(含端到端测试) - #4
Conversation
- ocr.rs:tesseract 外部进程封装(30s 超时强杀、PSM 4 单列排版、 路径探测 OnceLock 缓存、临时文件原子序号防并发冲突) - 扫描 PDF:无文本层回退提取 XObject 图片 OCR,支持 DCTDecode 直写 与 [ASCII85Decode FlateDecode] 链式解码(含 ASCII85/Hex 解码器) - docx 内嵌图片(word/media)OCR 追加,20MB 大小上限 - 独立图片文件(png/jpg/jpeg)进白名单直接 OCR - 找不到 tesseract 时全链路静默降级,既有行为不变 - settings.json 新增 ocr_tesseract_path,server/desktop 启动时注入 环境变量(显式 env 优先);ocr_smoke 调试工具自动读 settings - v2 套件图片/扫描 4 题:文档召回 0/4 → 4/4
评审阻断项/严重项:read_document_text 白名单补 png/jpg/jpeg(图片曾被当 UTF-8 读,OCR 永不执行且每张图都写 last_error);tesseract stdout 管道死锁改为独立线程读管道 + 超时看门狗;DOCX 内嵌图临时目录未创建;ask 期与桌面预览不再触发 OCR(原来会同步阻塞 worker 数分钟);flate 解压加 take(limit+1) 防 OOM;只接受 8bit/通道并跳过 ImageMask/不支持色彩空间;无文本层 PDF 恢复 Some 空串 语义。 复查补充:/Filter 严格解析(解析失败即跳过,不再退化成无过滤器而把压缩字节当 raw 像素解码成乱码);支持 ICCBased/间接引用色彩空间、ASCII85 包裹的 JPEG、仅 ASCII85/ASCIIHex 的 raw 图;tesseract 路径缓存按配置值失效(改配置无需重启)。 配置入口:桌面 设置-模型 可选 tesseract;服务端新增 POST /api/settings/ocr-path(含 OpenAPI 登记与路由计数同步)。 测试:真实扫描件 OCR 端到端(无 tesseract 自动跳过,CI Linux job 安装 tesseract-ocr + tesseract-ocr-chi-sim)、图片路由回归、解码上限、位深、ICCBased、ASCII85+DCT;文档同步 OCR 现状与边界。
当前 workspace/UI/Tauri 版本均为 1.5.2,但 docs/release 只有到 v1.5.0,lint job 的版本一致性检查因此必然失败(上游 collab 本身也缺)。补齐 RELEASE_NOTES_v1.5.2.md,覆盖本次工程硬化与 OCR 变更、已知边界与升级说明。 同时把 docs/README.md 的 Release 索引补上 v1.5.0 与 v1.5.2 两条(此前 v1.5.0 也未登记)。
审查者指南本 PR 将 tesseract OCR 以索引期能力接入图片、扫描 PDF 和 DOCX 内嵌图片的真实检索链路,同时通过严格 PDF 解码与资源上限、无阻塞的进程管道处理、ask/预览隔离、可即时更新的配置入口和真实端到端测试,修复原 OCR 实现的主要评审阻断项并同步产品文档。 索引期 OCR 导入与检索时序图sequenceDiagram
participant File as DocumentOrImage
participant Indexer
participant Parser
participant Tesseract
participant Store
participant Search
File->>Indexer: read_document_text(path)
Indexer->>Parser: extract_document_text(path)
Parser->>Parser: extract_pdf_images(path)
Parser->>Tesseract: ocr_image_file(image)
Tesseract-->>Parser: recognized text
Parser-->>Indexer: document text
Indexer->>Store: chunk and index text
Search->>Store: retrieve(query)
Store-->>Search: OCR-backed chunks
无 OCR 的 ask 和预览路径时序图sequenceDiagram
participant User
participant Ask as AskOrPreview
participant Parser
participant Store
User->>Ask: request answer or file preview
Ask->>Parser: extract_document_text_without_ocr(path)
Parser-->>Ask: text-layer content or empty text
Ask->>Store: use existing indexed chunks
Store-->>Ask: stored OCR-backed evidence
Ask-->>User: answer or preview
保守式 PDF 图片 OCR 解码流程图flowchart TD
A[PDF page XObject image] --> B{Valid image stream?}
B -- No --> Z[Skip with debug log]
B -- Yes --> C{8 bit per component?}
C -- No --> Z
C -- Yes --> D{Supported color space?}
D -- No --> Z
D -- Yes --> E{Filter chain parses strictly?}
E -- No --> Z
E -- Yes --> F[Decode with bounded output]
F --> G{Within pixel and byte limits?}
G -- No --> Z
G -- Yes --> H[Write valid PNG or JPEG]
H --> I[Run OCR at index time]
文件级变更
提示与命令与 Sourcery 交互
自定义使用体验访问你的控制面板,可以:
获取帮助Original review guide in EnglishReviewer's Guide本 PR 将 tesseract OCR 以索引期能力接入图片、扫描 PDF 和 DOCX 内嵌图片的真实检索链路,同时通过严格 PDF 解码与资源上限、无阻塞的进程管道处理、ask/预览隔离、可即时更新的配置入口和真实端到端测试,修复原 OCR 实现的主要评审阻断项并同步产品文档。 Sequence diagram for index-time OCR ingestion and retrievalsequenceDiagram
participant File as DocumentOrImage
participant Indexer
participant Parser
participant Tesseract
participant Store
participant Search
File->>Indexer: read_document_text(path)
Indexer->>Parser: extract_document_text(path)
Parser->>Parser: extract_pdf_images(path)
Parser->>Tesseract: ocr_image_file(image)
Tesseract-->>Parser: recognized text
Parser-->>Indexer: document text
Indexer->>Store: chunk and index text
Search->>Store: retrieve(query)
Store-->>Search: OCR-backed chunks
Sequence diagram for OCR-free ask and preview pathssequenceDiagram
participant User
participant Ask as AskOrPreview
participant Parser
participant Store
User->>Ask: request answer or file preview
Ask->>Parser: extract_document_text_without_ocr(path)
Parser-->>Ask: text-layer content or empty text
Ask->>Store: use existing indexed chunks
Store-->>Ask: stored OCR-backed evidence
Ask-->>User: answer or preview
Flow diagram for conservative PDF image OCR decodingflowchart TD
A[PDF page XObject image] --> B{Valid image stream?}
B -- No --> Z[Skip with debug log]
B -- Yes --> C{8 bit per component?}
C -- No --> Z
C -- Yes --> D{Supported color space?}
D -- No --> Z
D -- Yes --> E{Filter chain parses strictly?}
E -- No --> Z
E -- Yes --> F[Decode with bounded output]
F --> G{Within pixel and byte limits?}
G -- No --> Z
G -- Yes --> H[Write valid PNG or JPEG]
H --> I[Run OCR at index time]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
嘿——我发现了 2 个问题
面向 AI 代理的提示
请处理本次代码审查中的评论:
## 各条评论
### 评论 1
<location path="memori-parser/src/ocr.rs" line_range="209-212" />
<code_context>
+ continue;
+ };
+ let Some(xobjects) = resources
+ .get(b"XObject")
+ .ok()
+ .and_then(|value| value.as_dict().ok())
+ else {
+ continue;
+ };
</code_context>
<issue_to_address>
**issue (bug_risk):** 某些有效 PDF 的页面资源字典会将 `/XObject` 存储为间接引用,但代码在未解析该引用的情况下调用了 `as_dict()`,因此不会发现页面图像,扫描页面的 OCR 最终返回空文本。
**触发条件:** PDF 页面使用 `/XObject 12 0 R`,而不是内联的 XObject 字典时。
**建议修复:** 在将 `/XObject` 值转换为字典之前,先使用 `resolve_object` 对其进行解析。
</issue_to_address>
### 评论 2
<location path="memori-parser/examples/ocr_smoke.rs" line_range="97-105" />
<code_context>
+ }?;
+ let raw = std::fs::read_to_string(settings_path).ok()?;
+ // 轻量解析:只取需要的字段,避免给 parser 引入 serde 依赖。
+ raw.split('\n')
+ .find_map(|line| {
+ let (key, value) = line.split_once(':')?;
+ (key.trim().trim_matches('"') == "ocr_tesseract_path").then(|| {
+ value
+ .trim()
+ .trim_end_matches(',')
+ .trim_matches('"')
+ .to_string()
+ })
+ })
</code_context>
<issue_to_address>
**nitpick (bug_risk):** smoke 工具通过按每行第一个冒号分割并去除引号来解析 `settings.json`,但没有执行 JSON 反转义。因此,存储为转义 JSON 的 Windows 路径(例如 `C:\\Program Files\\Tesseract\\tesseract.exe`)会以包含字面反斜杠的形式传递,导致无法找到该路径。
**触发条件:** 配置的 OCR 路径是包含转义反斜杠的 Windows 路径时。
**建议修复:** 使用一个小型 serde 模型反序列化 `settings.json`,而不是按行解析。
</issue_to_address>Sourcery 评估
需要人工审查。 首先需要处理 1 个发现;如果 OCR 提取或 PDF/图像解码有误,错误识别的文本可能会被持久化到索引中,并在建立索引后影响检索。回滚可以阻止后续 OCR,但已经建立索引的内容需要清除或重建索引;受影响的数据范围有限且可以修复。
阻塞性发现:memori-parser/src/ocr.rs:212
Original comment in English
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="memori-parser/src/ocr.rs" line_range="209-212" />
<code_context>
+ continue;
+ };
+ let Some(xobjects) = resources
+ .get(b"XObject")
+ .ok()
+ .and_then(|value| value.as_dict().ok())
+ else {
+ continue;
+ };
</code_context>
<issue_to_address>
**issue (bug_risk):** Valid PDFs whose page resource dictionary stores `/XObject` as an indirect reference are skipped because `as_dict()` is called on the reference without resolving it, so no page images are discovered and scan-page OCR returns empty text.
**Triggers:** When a PDF page uses `/XObject 12 0 R` instead of an inline XObject dictionary.
**Suggested fix:** Resolve the `/XObject` value with `resolve_object` before converting it to a dictionary.
</issue_to_address>
### Comment 2
<location path="memori-parser/examples/ocr_smoke.rs" line_range="97-105" />
<code_context>
+ }?;
+ let raw = std::fs::read_to_string(settings_path).ok()?;
+ // 轻量解析:只取需要的字段,避免给 parser 引入 serde 依赖。
+ raw.split('\n')
+ .find_map(|line| {
+ let (key, value) = line.split_once(':')?;
+ (key.trim().trim_matches('"') == "ocr_tesseract_path").then(|| {
+ value
+ .trim()
+ .trim_end_matches(',')
+ .trim_matches('"')
+ .to_string()
+ })
+ })
</code_context>
<issue_to_address>
**nitpick (bug_risk):** The smoke tool parses `settings.json` by splitting each line at the first colon and stripping quotes without JSON unescaping, so a Windows path stored as escaped JSON such as `C:\\Program Files\\Tesseract\\tesseract.exe` is passed with literal backslashes and is not found.
**Triggers:** When the configured OCR path is a Windows path containing escaped backslashes.
**Suggested fix:** Deserialize `settings.json` with a small serde model instead of line-based parsing.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 1 finding to address first, and if the OCR extraction or PDF/image decoding is wrong, misrecognized text can be persisted in the index and affect retrieval after indexing. Reverting stops future OCR, but already-indexed content requires clearing or rebuilding the index; the affected data is bounded and repairable.
Blocking findings: memori-parser/src/ocr.rs:212
| raw.split('\n') | ||
| .find_map(|line| { | ||
| let (key, value) = line.split_once(':')?; | ||
| (key.trim().trim_matches('"') == "ocr_tesseract_path").then(|| { | ||
| value | ||
| .trim() | ||
| .trim_end_matches(',') | ||
| .trim_matches('"') | ||
| .to_string() |
There was a problem hiding this comment.
nitpick (bug_risk): smoke 工具通过按每行第一个冒号分割并去除引号来解析 settings.json,但没有执行 JSON 反转义。因此,存储为转义 JSON 的 Windows 路径(例如 C:\\Program Files\\Tesseract\\tesseract.exe)会以包含字面反斜杠的形式传递,导致无法找到该路径。
触发条件: 配置的 OCR 路径是包含转义反斜杠的 Windows 路径时。
建议修复: 使用一个小型 serde 模型反序列化 settings.json,而不是按行解析。
Original comment in English
nitpick (bug_risk): The smoke tool parses settings.json by splitting each line at the first colon and stripping quotes without JSON unescaping, so a Windows path stored as escaped JSON such as C:\\Program Files\\Tesseract\\tesseract.exe is passed with literal backslashes and is not found.
Triggers: When the configured OCR path is a Windows path containing escaped backslashes.
Suggested fix: Deserialize settings.json with a small serde model instead of line-based parsing.
FPSZ
left a comment
There was a problem hiding this comment.
先说结论:上一轮的 11 条全部真修了,而且修法都对路。管道死锁改成 stdout/stderr 独立线程 drain 且超时 kill 路径也 join;allow_ocr 开关把 OCR 彻底赶出问答链路;take(limit+1) 给解压产物设了界;BitsPerComponent != 8 直接跳过并写了理由;探测缓存用配置值做键,改配置不用重启;CI 还补上了 tesseract-ocr + chi-sim,让端到端测试在 CI 真能跑。这批返工质量很高,辛苦了。
但 "OCR 端到端可用" 这个核心结论仍然不成立,而且是上一轮同一类的问题:单测能过、真实文件挂。所以还得再打回一次。
🔴 阻断项
1. extract_pdf_images 丢弃了间接 / 继承的 /Resources — memori-parser/src/ocr.rs:205
对着 lopdf 0.35.0 源码核过了。get_page_resources 的签名是 Result<(Option<&Dictionary>, Vec<ObjectId>)>,实现在 document.rs:429:
resource_dict = page.get(b"Resources").and_then(Object::as_dict).ok(); // ← .0
collect_resources(page, &mut resource_ids, self, &mut HashSet::new())?; // ← .1.0 只有在 /Resources 是直接字典时才是 Some(Object::as_dict 对 Reference 返回 Err)。间接引用的、以及从 /Parent 继承的,全部走 .1 那个 Vec<ObjectId>。
当前写法把 .1 整个丢掉了:
let Ok((Some(resources), _)) = doc.get_page_resources(page_id) else { continue; };Word / Acrobat / Ghostscript / 多数扫描仪驱动导出的 PDF 都用间接 /Resources,这些页会被直接 continue 跳过,扫描件索引成空——正是这个 PR 要解决的场景。
另外 ocr.rs:203 的注释写的是「第一个返回值是页面资源字典(含继承)」,和 lopdf 的实际行为正好相反,建议一并改掉,免得后面的人照着注释理解。
2. /XObject 没有解引用 — memori-parser/src/ocr.rs:211
resources.get(b"XObject").ok().and_then(|value| value.as_dict().ok())as_dict() 同样不做解引用(lopdf/src/object.rs:247,Reference 分支直接 Err),所以 /XObject 15 0 R 这种写法会让整页被跳过。
lopdf 提供了会解引用的 get_dict_in_dict(document.rs:201);这个文件里也已经有 resolve_object 了,只是这一步没用上。
3. 为什么测试没挡住
我把测试用的那份语料拆开看了 —— Memory_Test_V2/special_005_扫描件_苍岭_对账.pdf,ReportLab 生成,obj 4 长这样:
4 0 obj << /Contents 8 0 R /MediaBox [...] /Parent 7 0 R
/Resources << /Font 1 0 R /ProcSet [...] /XObject << /FormXob.93b6... 3 0 R >> >>
/Rotate 0 /Type /Page >>
/Resources 和 /XObject 都是直接字典,所以上面两个 bug 对这份文件完全隐形。
这个端到端测试本身写得是对的(用真实扫描件、结构性断言而不锁措辞、环境缺失自动跳过、CI 装语言包),问题只在语料选型单一。建议再补一份 /Resources 与 /XObject 都是间接引用的 PDF 作为第二个 fixture——用 Ghostscript 或 LibreOffice 导出就能得到,两行代码的 bug 才会被这个测试真正锁住。
🟠 需要修
4. OpenAPI 悬空 $ref — memori-server/src/routes/openapi.rs:318
request: Some("SetOcrTesseractPathRequest") 引用了一个 build_component_schemas() 里没有定义的 schema(对照 SetLocalModelsRootRequest 在 522 行、SetWatchRootRequest 在 526 行都有定义)。这是生成的 spec 里唯一一个解析不了的 $ref,#2 刚加的 Swagger UI 打开这个端点会渲染成坏的。
漂移自检目前只断言 paths.len() >= 25 和 ErrorResponse 存在,抓不到这类问题。建议顺手在自检里加一条:遍历 spec 收集所有 $ref,断言每个都能在 components.schemas 里解析到——这样以后加端点漏 schema 会直接红。
5. /DecodeParms /Predictor 完全未处理 — memori-parser/src/ocr.rs:362
全文搜不到 DecodeParms / Predictor。后果分两种:Predictor 15(PNG predictor,很常见)解出来是带行首 filter 字节的数据,会被当成像素;Predictor 2 的图能通过现有全部护栏,然后把一张错位的图 OCR 成噪声写进索引。
这正是上一轮第 6 条(BitsPerComponent)要防的同一类污染——证据链可信是这个项目的卖点,建议和那条一样处理:只接受没有 /DecodeParms 或 Predictor == 1 的流,其余跳过并 warn。宁可少索引,不能索引噪声。
6. 图片能索引能引用了,但预览白名单没跟上 — memori-desktop/src/commands/scope.rs:189
extracted_text_exts = ["docx", "pdf"],png/jpg 两个列表都不在,点开一条图片 citation 会弹「不支持预览的文件类型: .png」。图片预览按说应该直接显示图,不是走抽取文本那条路,可能需要前端配合;至少不该是现在这个报错。
🟡 建议修
-l chi_sim硬编码,没有eng回退 (ocr.rs:110):而ocr_available()只探--version,所以装了英文版 tesseract 的用户会得到「可用」+ 永远识别不出东西。CI 装了 chi-sim 所以 CI 不受影响,这是终端用户侧的坑。建议探测时顺带跑一次--list-langs,按可用语言包拼-l chi_sim+eng。- 逐页图片全解码落盘后才开始 OCR (
ocr.rs:195):没有总量上限、也没有按 XObject id 去重(同一张图在多页复用时会重复解码 + 重复 OCR)。500 页扫描件会先把临时目录撑满再串行 OCR 数小时。建议边解码边 OCR、加页数/总字节上限、用 ObjectId 去重。 - 单图路径少了尺寸护栏 (
lib.rs:593):DOCX 和 PDF 两条路径都有MAX_OCR_IMAGE_BYTES,独立图片文件这条没有。这里是把路径交给 tesseract、不是自己 buffer,所以风险低于另两条,但一张超大图仍会让 tesseract 吃满内存直到 30s 超时。 - spawn tesseract 没加
CREATE_NO_WINDOW(ocr.rs:107):Windows 桌面端每张图会闪一次控制台窗口。仓库里对 llama.cpp 已经有现成写法,见memori-desktop/src/commands/model_runtime_cmd.rs:545。
✅ 核过没问题的
- stdout/stderr 独立线程 drain + 超时看门狗,kill 路径也 join,管道死锁确实解决了
take(limit + 1)限制的是解压产物,解压炸弹防住了/Filter严格解析、ASCII85/ASCIIHex 解码(含短组补位,不会 u32 溢出)Some("")空文档语义走到process_file_event的空 chunk 分支,不写last_error- ask 期确实不做 OCR:
retrieval_output.rs:366和scope.rs:203都走extract_document_text_without_ocr - tesseract 探测缓存以配置值为键,改配置无需重启;空值回退 PATH
- CI 补了 tesseract + chi-sim,端到端测试在 CI 会真的执行
REST_ROUTE_METHOD_COUNT33→34 同步了
另外:release notes 的耦合问题
docs/release/RELEASE_NOTES_v1.5.2.md 在 #2 / #3 / #4 三个 PR 里内容完全相同(md5 一致),而它明确写了 "adds OCR ingestion for images and scanned documents"。
也就是说只要 #2 或 #3 先合,仓库就发布了一份宣称 OCR 已可用的说明,而 OCR 还在返工。建议把这份 release notes 从 #2 / #3 里摘掉,只留在 #4,等 OCR 真正合入时一起发。
(顺带一提:notes 里主动写了 苍岭 → 苑岭/苔岭 的误读限制,这个坦诚度很好,建议保留。)
阻断项:extract_pdf_images 改用 get_page_resources 的第二个返回值(间接/继承 /Resources 不再被整页跳过,并修正错误注释);/XObject 改用 get_dict_in_dict 解引用;按 ObjectId 去重。必须项:Predictor 非 1 一律跳过;登记 SetOcrTesseractPathRequest schema 并新增 every_ref_in_spec_resolves 守门测试;图片预览放开 png/jpg/jpeg。建议项:--list-langs 组装 chi_sim+eng;PDF 图片张数/总字节双上限 + 边解码边 OCR;独立图片尺寸护栏;Windows CREATE_NO_WINDOW。
|
第三轮复审。两个阻断项确实修好了,我实测验证过,不是只看代码。 实测验证代码审下来 手工构造了一份全间接引用的 PDF(raw bytes 直写,完全控制 xref): 对同一份文件跑
所以修复是真实有效的,探针也确实有鉴别力。 其余 8 项也都核过了:Predictor 保守放行(只接受无 唯一剩下的问题:回归测试被 ignore 了
这个诊断是错的,我查到了真正的根因。 先说 真正的问题在流本身: 原因是手工构造 lopdf::Object::Stream(lopdf::Stream {
dict: image,
content: compressed,
allows_compression: true,
start_position: None,
})这样 dict 里没有 一行修复(我已实测通过)- doc.objects.insert(
- (1, 0),
- lopdf::Object::Stream(lopdf::Stream {
- dict: image,
- content: compressed,
- allows_compression: true,
- start_position: None,
- }),
- );
+ doc.objects.insert(
+ (1, 0),
+ lopdf::Object::Stream(lopdf::Stream::new(image, compressed)),
+ );改完把 顺带一提, 这样就不需要再去找 Ghostscript / LibreOffice 导出的真实 fixture 了——手工构造的这份已经能精确覆盖「间接 把这个测试启用之后我就没有别的意见了。这轮返工做得很扎实——十项逐条落实,注释里还把「为什么这么改」的来龙去脉写清楚了(尤其是 |
合并 FPSZ#2/FPSZ#3/FPSZ#5 后的 collab,三处冲突均为双方各自新增内容,保留双方: - README.md「已实现但仍在优化」:50k 压测条目 + OCR 条目 - docs/qa/RETRIEVAL_BASELINE_V2.md:作答层 LLM-judge 基线 + OCR 接入与边界 - memori-server/src/dto.rs:AppSettings 的 index_filter 与 ocr_tesseract_path 两个字段 同时启用 pdf_with_indirect_resources_is_extracted(原 #[ignore]): 手工构造 lopdf::Stream 用结构体字面量会绕过 /Length 写入(lopdf object.rs:602 的 Stream::new 才会 dict.set("Length", ...)),存盘后流长度为 0、load 回来 content 为空,解码拿不到像素,测试因此假失败——与 get_pages() 无关(实测 get_pages()=1、get_page_resources 也正确返回 inherited)。改用 Stream::new 后 测试通过,可作为间接 /Resources + 间接 /XObject 的回归锁。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
已合并。合并前我往这个分支推了一个提交( 1. 解决与已合并 PR 的冲突(#2/#3/#5 先合入 collab 后产生,三处都是双方各自新增内容,保留双方):
2. 启用了 合并前在本地完整验证过(fork PR 的 workflow 需要批准,Rust CI 一直没在这四个 PR 上跑过,所以这轮是本地把的关):
顺带一提: 这轮三次返工下来质量很好,辛苦了。 |
背景
原 PR 的 OCR 部分被评审打回("OCR 部分目前端到端不工作")。本 PR 保留 OCR 功能提交,逐条修复评审提出的问题,并补上评审要求的"走真实索引链路的端到端测试"。
评审问题 → 修复对照
read_document_text白名单缺图片 → 被当 UTF-8 读,OCR 永不执行,且每张图都写last_error)png/jpg/jpeg;无 OCR/无文字时返回空串走"空文档"正常分支,不再污染last_errortry_wait不读管道,文字密集页必挂且白烧 30s)create_dir_all+ 失败告警extract_document_text_without_ocr();ask 引用摘要与桌面预览一律不触发 OCR(OCR 只在索引期)take(limit + 1)限制解压产物,并按声明尺寸设上限/BitsPerComponent,会把解码噪声当文档内容索引ImageMask与不支持的色彩空间Some("")改成None,误导为"文件读取失败(可能被占用)"Some("")语义OnceLock+ env 不覆盖 → 改配置要重启read_to_end失败绕过尺寸护栏自查中另修(同类"静默失效")
/Filter解析失败会退化成"无过滤器" → 把压缩字节按 raw 像素解码成乱码 PNG,OCR 噪声可能写进知识库(评审 #6 警告的类型)。现改为严格解析:任一元素解析不出名称即整张跳过,并支持间接引用。[/ASCII85Decode /DCTDecode])与仅 ASCII85/ASCIIHex 的 raw 图debug!留痕,不再"静默什么都没发生"新增配置入口
POST /api/settings/ocr-path(operator 角色),已登记 OpenAPI 与路由计数同步MEMORI_OCR_TESSERACT_PATH>settings.json的ocr_tesseract_path> PATH 自动探测测试与验证
scanned_pdf_is_indexed_through_ocr_when_available;无 tesseract 时自动跳过;CI 的 Linux job 安装tesseract-ocr+tesseract-ocr-chi-sim保证真实执行)stream did not contain valid UTF-8)、真实扫描件解码为合法 PNG、解压上限、位深校验、ICCBased、ASCII85+DCT提取成功,40 字符,耗时 755ms→ 识别出…项目的对账窗口为每月 8 号…苍岭→苑岭/苔岭),因此端到端测试只做结构性断言(中文数量 + 索引链路),不锁定措辞;OCR 文本适合做召回辅助,不宜当作精确引用口径cargo test --workspace --all-targets全绿(0 失败);clippy -- -D warnings、fmt --check、UI build 均通过文档同步
README.md/README.en.md(OCR 从"设计中"移到"已实现但仍在优化",写清边界)、plan.md(Q6 移到已做)、IMPROVEMENTS.md、RETRIEVAL_BASELINE_V2.md(新增「OCR 接入与边界」章节:能力 / 不做 OCR 的时机 / 配置路径 / 改配置需重建 / 实测 / 已知边界)已知边界(本 PR 未解决)
ppt/xlsx内嵌图未接入;CCITTFaxDecode(G4 传真压缩)/JPXDecode(JPEG2000)不支持Sourcery 摘要
启用可靠的索引时 OCR,支持图像和扫描文档,同时加强提取安全性、配置管理和端到端验证。
新功能:
错误修复:
改进:
构建:
CI:
文档:
测试:
杂项:
Original summary in English
Sourcery 摘要
通过可靠的索引时 OCR,使图像和扫描文档可供搜索,同时增强提取安全性、配置能力和端到端验证。
新功能:
错误修复:
增强功能:
CI:
文档:
测试:
杂务:
Original summary in English
Sourcery 摘要
启用可靠的索引时 OCR,支持处理图像和扫描文档,同时加强提取安全性、配置能力和端到端验证。
新功能:
错误修复:
改进:
CI:
文档:
测试:
杂项:
Original summary in English
Summary by Sourcery
Enable reliable index-time OCR for images and scanned documents while hardening extraction safety, configuration, and end-to-end validation.
New Features:
Bug Fixes:
Enhancements:
CI:
Documentation:
Tests:
Chores: