主题
22 — RVH 新增本地 cloze 采集(OCR 语境句):跨端评审请求
物理位置:本文件在 RVH 仓库
~/reading_vocab_helper/docs/cross-end/22-rvh-ocr-cloze-context-review-request.md。 关联:红线 #5g(已改写为双端共写)·docs/cross-end/17-rvh-cloze-context-sync-confirmation.md(RVH 侧镜像完成确认)· RB~/reading-browser/docs/cross-end/16-rb-cloze-context-sync-handoff.md(RB 先行交接)创建:2026-08-15 · 补实测 2026-08-17 · 状态:✅ 已裁决(08-17)+ ✅ RVH 侧已落地(08-26);⏳ 跨端真机实测待做 上位计划:
rvh/docs/plans/scan-ocr-positioning-enhancement-plan.mdTask H
⬆️ 裁决结果(2026-08-17,RB 侧)—— 先读这一段
回执全文:~/reading-browser/docs/cross-end/23-rb-ocr-cloze-context-decision.md
| 本文件的问题 | 裁决 |
|---|---|
| Q1 池子归属 | ✅ 双端共写,互相挤占。池子按 word 单键组织,从设计上就不存在「RB 的池」 |
| Q2 噪声容忍度 | ✅ 方案 A + §5 三规则闸门,附四条硬条件 C1–C4。不退 D |
Q3 source_url NULL | ✅ 不影响 RB 渲染,且 NULL 是唯一正确取值。🚫 禁止造合成 URL |
| Q4 条文改写 | RB CLAUDE.md 已改;RVH 红线 #5g 已于 2026-08-26 改写 |
§2.1 / §2.2 两处本文件自行排除的风险,RB 复核同意排除。但复核发现本文件未列出的一条硬伤:
🔴 C3 ——
created_at时区偏置。它是两端 reconcile 的排序键兼「留新删旧」的唯一依据。 RB 写 UTC RFC3339,而 RVH 采集层(filter_confirmation_notifier.dart:98/788)用的是裸DateTime.now()→ 无偏移的本地时区串。UTC+8 下字典序18:>10:,RVH 的行恒定显得比 同时刻 RB 的行晚 8 小时,淘汰规则悄悄变成「留 RVH、删 RB」,且无任何报错或不一致可被观测。 修法:cloze 写入走lib/core/utils/datetime_utils.dart:8的nowUtc(),不要走 notifier 层那个。
四条硬条件全文见回执 §2.2,已镜像进 RVH CLAUDE.md 红线 #5g。缺任何一条则退回方案 D(不采集), 而不是「先上线再补」。
✅ RVH 落地回执(2026-08-26)
代码见 ocr_cloze_gate.dart(闸门)/ filter_confirmation_notifier::_saveOcrClozeContexts(收集 + 跨页择优) / cloze_pool_writer.dart::insertAll(池语义,逐条镜像 RB crud.rs::insert_cloze_context)。 C1–C4 逐条对账、以及实现中比条文更细的三处(断词的两个方向 / 主列阈值基准 / line_x 列不可直接用), 写在 rvh/docs/plans/scan-ocr-positioning-enhancement-plan.md Task H「落地记录」。
回归 47 例(闸门 32 + 池语义 15),三处注入缺陷验证会红。push 侧如评审所料零改动。
✅ 本端真机验证(2026-08-27,Android):同一页 CNN 文章 4 轮收割 → 27 词入库 / 27 词拿到语境, 30 条行逐条复核 C1(sentence 含 surface)、长度 ≥25、created_at UTC、source_url NULL 全部合格; 第 4 轮重跑 新增 0 条,幂等去重生效。过程中挖出并修掉两个闸门缺陷(破折号被当断词、主列规则 输入信号不可信 → 默认关闭),详见 rvh/docs/plans/scan-ocr-positioning-enhancement-plan.md Task H。 ⚠️ RB 侧知情项:三规则闸门中的「主列」一条已默认关闭,长度 + 连字符 + C1 两条断言仍在。
📄 正式回执已发出:35-rvh-ocr-cloze-context-confirmation.md (C1–C4 对账 + 4 条 RB 需知情项 + 数据形态 + 仍欠事项)。
⏳ 仍欠回执 §7 的三步跨端真机实测(需双端设备 + 同一账号),完成后回写本节与 doc 35。
⚠️ 下文 §3「需要 RB 侧裁决的问题」与 §6「若否决的退路」保留原样仅供追溯,不再是待决状态。
0. 一句话
RVH 想让 OCR 采集的词也带上真实语境句(写 word_cloze_contexts),这直接违反红线 #5g 的 「RVH pull-only:不新增本地采集路径」。本文件把动机、现状核实、风险点摊开,请 RB 侧裁决是 放开、还是走「本地 only 不上行」的退路。评审通过前 RVH 不动代码。
2026-08-17 修订:补入真机实测(§2.4)。实测把两件事钉死了 —— ① 提取方案换掉了:不做整页切句,直接用逐词已存的
ocr_word_positions.line_text, 工作量小一个量级(§5 已按此重写); ② Q2「OCR 噪声会不会污染池子」不再是空口担忧:拍照场景 80% 行文本可通过三规则闸门, 被挡掉的 20% 有明确可判定特征。
1. 为什么要做
RVH 复习卡已分裂成双范式(context_drawer_content.dart):
| 词的来源 | 卡面(正面) | 抽屉(背面) |
|---|---|---|
| RB 双击查词 | web 快照 | ✅ 语境范式:原句 carousel + 挖空 + sense_gloss 联动义项页 |
| RVH OCR 采集 | 原图聚光灯(不弱) | ❌ 词典范式:策展例句 / 裸词 |
差距只在背面这一处,根因是 word_cloze_contexts 在 RVH 全库只有一个写入点 —— sync pull (sync_repository_impl.dart:1289)。历史上 v10 删过 context_sentence 提取(注释写「功能效果 不理想」),但那时 cloze 还不存在,代价看不见;v57 上 cloze 后代价显形:用相机采的词在复习端 系统性差一档。
RVH 侧数据其实已就位:SourceOcrData 带 ocrText + 带位置的 ocrWords;createReadingPage() 已接受 ocrText 参数;入池闸门 8..220 字符与 RB insert_cloze_context 同值 (cloze_builder.dart:29-30)。
2. 现状核实(含两个被推翻的猜想)
立项时的两处担心,读双端代码后均不成立,先排除以免评审跑偏:
2.1 ❌「client 时钟漂移会让两端选出不同幸存行」—— 不成立
两端 reconcile 都是 ORDER BY created_at ASC, id ASC (RVH sync_repository_impl.dart::_reconcileClozePool / RB pull.rs::reconcile_cloze_pool:870)。 排序键是已存储的值,两端读到的是同一批字符串 → 排序结果必然一致 → 幸存行一致。
时钟漂移影响的是「哪句语义上更早」,不影响两端是否一致。这与红线 #5d 的 watermark 场景不同 (那里比的是两端各自的 now)。
2.2 ❌「新采的句会被 reconcile 立刻删掉」—— 不成立,方向反了
两处封顶都是留新删旧:
- RB
insert_cloze_context(crud.rs:75-83):满池 → 软删最旧LIMIT count - cap + 1→ 再插新行 - 两端 reconcile:
ORDER BY created_at ASC LIMIT count - cap删掉最旧的超额行,留下最新 5 条
所以新采的 OCR 句会正常入池,被挤掉的是最旧的。两处策略一致。
2.3 ✅ push 侧无需改动(这条影响工作量估算)
_pushWordClozeContexts 的 dirty-check(sync_repository_impl.dart:1181-1188)没有任何来源 过滤:synced_at IS NULL 的行一律上行。RVH 自建的行会自动被推上去,零 push 侧代码改动。
⇒ 红线 #5g 里「push 只传播 reconcile / 删除产生的墓碑」是对当前行为的描述(因为 RVH 从不 建行),不是一道被代码强制的闸门。这也意味着:一旦本地采集上线,上行是默认发生的, 不存在「先本地试试看」的中间态 —— 除非显式加过滤(见 §4 方案 B)。
2.4 ⭐ 真机实测:提取方案应该换一个(本节推翻本文档原先的实现设想)
样本:开发机相机实拍的装订书页(The House on Mango Street p63),走完整生产 OCR 管线 (ML Kit + 相机增强 + 手工裁剪),收割 49 个词位 / 30 条不同行文本。该样本集齐了拍照场景的 全部难点:书脊弯曲导致行倾斜、左侧邻页文字侵入、真实连字符断词(base- / ball)、 页脚 running head + 页码、斜体外文词。
测量结果:
| 指标 | 数值 |
|---|---|
目标词字面出现在自己的 line_text 里 | 49/49 = 100% |
line_text 通过现有 8..220 字符闸门 | 48/49 = 98% |
| 行末连字符断词 | 3/49 = 6% |
| 来自左侧邻页 | 2/49 = 4% |
| 字符级识别错误 | 30 行里 2 处(beer→heer、多出一个 Palm 1 Sunday) |
结论一:字符级质量不是瓶颈,版面才是。 弯曲页面上的词位框依然精准落位,识别错误率低。 真正的问题是三类版面结构问题,其中两类可确定性解决、一类是硬伤:
| 问题 | 实测证据 | 可解性 |
|---|---|---|
| 连字符断词 | ...crazy base- / ball bats with thorns... → 收进词表的是 ball 而非 baseball | 好解:行末 - + 下行小写开头 → 合并 |
| 邻页侵入 | le with a rag be、drink your Koo 进了词表 | 好解:line_x 干净分离(邻页 −2 / 302,正文 700+) |
| 弯曲致 y 序不可靠 | a wedding 的 line_y=3693,却属于 line_y=3611 那一行 | 硬伤,解不动 |
结论二(关键):整页切句这个问题根本不需要解。
cloze 只需要目标词所在的那一句,不需要全页 reading order。而 ocr_word_positions.line_text 已经逐词存了所在行的完整文本(schema 里一直有,NOT NULL), 现成可用:
ordinary → "They're not like ordinary playing cards, these cards." ← 完整句
recognize → "I saw it before and recognize the music and wish I could"
plastic → "go sit on the plastic couch with Ernie and the baby, but"
mattress → "bees and this a mattress of luxury. You will go to a"坏样本也很清楚,且都能被闸门挡掉:
ball → "ball bats with thorns. ..." ← 连字符断词,词本身就错了
rag → "le with a rag be" ← 邻页碎片,16 字符
wedding → "a wedding" ← 9 字符,过了 8 字符闸门但零语境价值加一道三规则闸门(length ≥ 25 ∧ line_x 落在正文列 ∧ 行末非连字符):
39/49 = 80% 通过,且通过的那些质量都能直接用。
⚠️ 样本局限:单本书、单张照片、单次拍摄。上述比例是指示性的,不是统计结论。但 「目标词 100% 出现在自己的 line_text 里」这条是结构性的(ML Kit 的 word 就是从 line 里抽的), 不依赖样本。
3. 需要 RB 侧裁决的问题
Q1 —— 池子的语义归属:RB 的池,还是双端共享的池?
现状是「RB 采集、RVH 消费」。放开后变成双端共写同一个 5 条上限的池子。 这是个产品问题不是技术问题:用户在 RB 读到的句子和在 RVH 拍到的句子,是否应该互相挤占?
RVH 侧倾向:应该。语境池的价值是「你在哪读到它的」,两端都是真实阅读现场。
Q2 —— OCR 句质量会不会污染池子?(已有实测数据,见 §2.4)
立项时这是 RVH 侧最没把握的一条。跑完真机实测后,它变成了一个有数可谈的问题:
- 拍照场景下 80% 的行文本可通过三规则闸门,通过的部分质量可直接用
- 被挡掉的 20% 有明确、可判定的特征(太短 / 邻页 / 连字符断词),不是"说不清的噪声"
- 字符级识别错误率低(30 行 2 处),且不影响目标词本身的可读语境
- 截图 / 分享正文场景没有上述任何一类问题(无弯曲、无邻页、屏幕文字不断字)
RB 的句子来自 DOM 文本,仍然更干净。需要 RB 侧表态的是容忍度:
| 选项 | 说明 | RVH 侧评价 |
|---|---|---|
| 应用层质量闸门(推荐) | §2.4 的三规则,纯客户端,不改 schema | 成本最低;80% 通过率可量化、可回归测试 |
| 来源标记 | 加列区分 OCR 来源 | ⚠️ 远端 user_word_cloze_contexts 无 source_platform 列,加列 = 三端 schema 变更 + migration,成本显著 |
| 不缓解 | 接受噪声,靠 §2.2 的「留新删旧」自然轮换 | 不推荐:坏句同样会占掉 5 个坑位之一 |
| 仅放开截图 / 分享正文,拍照不入池 | 按 source_type 区分,image 来源不写 cloze | 折中方案。代价是把价值最大的场景(纸质书)排除在外 |
Q3 —— source_url 为 NULL 是否影响 RB 侧渲染?
OCR 句没有 URL。RVH 侧写 NULL。RB 的 review/popup 是否有 source_url 非空假设?
Q4 —— 红线 #5g 条文改写
若放开,#5g 的「RVH pull-only:不新增本地采集路径(RVH 无阅读/双击查词流)」一句失效,需双端 CLAUDE.md 同步改写。RVH 侧不单方面改跨端红线,等 RB 侧确认措辞。
4. 方案选项
| 方案 | 说明 | RVH 侧评价 |
|---|---|---|
| A. 放开,双端共写 | RVH 采集 + 正常上行,与 RB 句同池竞争 | 价值最大;Q2 的风险现已可量化(§2.4) |
| B. 本地 only,不上行 | RVH 采集但 push 侧显式过滤掉本地建的行(需新增来源标记或本地列) | 拿到本端体验提升,不污染 RB;但「本地列」本身是新的跨端不对称,且 §2.3 说明这需要额外加过滤代码 |
| D. 分场景放开 | 仅截图 / 分享正文入池,拍照(source_type='image')不入 | 噪声风险接近零,但排除了价值最大的纸质书场景 |
| C. 否决 | 维持 pull-only,OCR 词继续走词典范式 | 复习端不对称保持现状 |
RVH 侧倾向:A + §2.4 的三规则质量闸门(不加列、不改 schema,纯应用层过滤)。 理由:成本最低、不引入三端 schema 变更,且闸门效果已在真机样本上量化(80% 通过, 拒收项特征明确)。若 RB 侧对噪声零容忍,优先退 D 而非 B —— D 不需要新增任何列或过滤 代码,只是按已有的 source_type 分流。
5. 若批准(方案 A)的 RVH 侧待办
⚠️ 本节已按 §2.4 的实测结论重写。 早先的设想是「新增
OcrSentenceExtractor,从整页ocrText里切句」——实测表明那是在解一个不必要且解不动的问题(整页 reading order 在 弯曲书页上不可靠)。改用逐词已存的line_text,工作量小一个量级,质量反而更可控。
- 取语境 = 读
ocr_word_positions.line_text,不做整页切句。该列 schema 里一直存在且NOT NULL,收割时已随词位一起写入 —— 无需新的提取管线,也无需给 image 来源补reading_pages.ocr_text。 - 质量闸门(新增,纯函数,可单测):
length ≥ 25(比现有 8..220 更严:8 字符能过但零语境价值,实测有"a wedding"这类)line_x落在正文主列(按本页所有line_x聚类取主簇,滤掉邻页侵入)- 行末非连字符(断词行的目标词本身就是错的,如
base-/ball) - 可选增强:与下一行合并以补全句子(
line_y相邻 + 当前行无句末标点)
- 收割 commit 时为每个入库词写
word_cloze_contexts:word走 lemmatizer 归一(红线 #9)、surface存该行里的原始形(C1:不用拼写纠错后的形式,且写前断言sentence整词含surface)、sentence= 闸门通过的line_text、source_urlNULL、created_at走nowUtc()(C3,裁决新增:裸DateTime.now()会让 RVH 的行恒定挤掉 RB 的行)。 每 word 每次收割最多 1 条(C2,按闸门得分取最优行)。 闸门未通过 → 不写(宁可没有语境,也不要坏语境占掉 5 个坑位之一)。 - 双端
CLAUDE.md#5g 条文改写(Q4)。 - 回归测试:
- 闸门的接受/拒收(用 §2.4 的真实样本做 fixture:
ordinary通过、ball/rag/wedding拒收) ClozeBuilder能在line_text上挖空目标词(实测 100% 命中,需锁住)- 双端 reconcile 后收敛到同一 ≤5 池
- 闸门的接受/拒收(用 §2.4 的真实样本做 fixture:
相比原方案省掉的:整页 rawText 切句、reading order 重建、去连字符拼接、页眉页脚剔除 —— 这些在 line_text 方案下全部不需要。
6. 若否决(方案 C)的退路
RVH 侧不做任何事,rvh/docs/plans/scan-ocr-positioning-enhancement-plan.md Task H 标记为 「跨端评审否决」并保留分析,供日后重议。复习端双范式不对称作为已知且已接受的产品现状 记录在案 —— 至少不再是「没人注意到」的状态。