前十轮问的是「这句话是真的吗」「这个答案是什么时候取的」。
这一轮换了个更朴素的问题:
算出来的那个数字,有人接住吗?
派了四位审查员,分别盯着四段链:导出与屏幕、记账与队列、界面与文案、 翻译的核心路径。每一段都跑了一遍自己走。
结果:链上有八处数字被算了出来,然后被丢在原地。
模型读不出整页漫画上的字时,扩展会退一步:OCR 先找出文字区,把每一块 裁下来,竖着拼成一条,送去问模型。
送去的那张图不是漫画页 —— 是一叠互不相关的裁剪。
而提示词是整图那条。那条提示词里写着:
Merge multi-line text in one bubble into ONE item — one box covering all of its lines.
在一个气泡里,这是对的。在一叠裁剪上,这句话是错的:相邻的两块长得 就像一段话,模型于是把两块合成一个大框。
合成的大框的中心落在其中一块上,译文整段盖在那一个气泡里;另一块气泡 什么都没有——还是日文。
而这一轮被记成成功。读者看到半日文半中文的一页,只当自己看漏了。
不是模型不听话,是这张图长得就不像提示词说的那样。两块之间只有 20 像素的白,对模型来说和一段话没有区别。
三件事一起做:
顺带删掉了原来那套编号锚点的画法:模型数块数不准,认编号会把译文画进 另一个块里,比认错中心点更糟。
模型找到了气泡、也给了译文,却没给坐标(或者坐标给了一半)。
没有框就没有地方画。 这一条早就实现了:解析层把这样的条目丢掉,
并且记一个 unplacedCount。
那个数字一路带着:provider → worker → 翻译器。
然后翻译器没读它,worker 把它原样送了回去,页面也没读。
页面上画出来的气泡比模型找到的少。差的那几个还是原文,而这一轮被记成 成功,HUD 写着「翻译完成」,弹窗写着「成功翻译 12 张」。
读者看到的是半日文半中文的一页,和「翻译完成」四个字。
数字接上:解析层数出来的 → worker 带回来 → 翻译器算进「没翻出来」 → 页面说给读者。
现在这一页说的是:
有 2 处文字没有翻出来(仍是原文)
并且弹窗的结果卡片会因此变成警告(琥珀色),而不是成功(绿色)。
同一个根因的另一面:坐标缺失时,代码把缺失当成 0。
于是模型忘了给坐标的那句译文,会以最小字号被画成左上角一列单字 ——盖在画面上,而真正的气泡还留着日文。
这种”画出来了”的假成功,比”没画”更糟。
没有框就没有框。 不再把缺失当 0:缺坐标的条目一律丢掉,并计入 「没翻出来」。
「框越界」也一并改掉:以前 x、y、width、height 各自夹到 [0,1],
x=0.9, width=0.5 这种(右边缘超出图外 40%)会被保留,渲染器再把面板
往左挪——译文就画在了模型没有指的地方。现在夹的是宽度:框小了,
但还在模型说的位置。
运行中,弹窗的错误段落被包在 {!isRunning ? … : null} 里。
用户点「取消当前翻译」,chrome.tabs.sendMessage 拒绝(页面刚跳转,
或扩展刚重载)——错误被捕获了,但渲染它的那一段在运行时不渲染。
弹窗毫无提示。用户以为已经停下来了,页面却还在翻,还在计费。
错误段落常驻在两个分支之外。「本页不再自动翻译」的失败同理——那个按钮 在运行中是可以按的。
扫描的可翻译数不含已经翻过的图(那是”跳过:已翻译”)。整页翻完后, 可翻译数是 0。
于是按钮上方写着「本页没有可翻译的图片。」——一句会把人送去改图片大小的 话;按下去之后的结果卡片却写着「本页 12 张图片已经翻译过了」。
同一页,两句互相打架的话。
弹窗按”总共几张 − 还能翻几张”算出已翻译的张数,为 0 时说:
本页 12 张图片已经翻译过了。想重新翻译,用下面的「忽略缓存重翻」。
弹窗的「重试失败 N 张」按钮要求 status === 'complete'。
但页面在每一个状态都上报 retryTargetCount——而致命错误(比如密钥
在第 8 张过期)走的正是错误分支。
一章翻到第 8 张密钥过期:弹窗给一张错误卡片和「打开设置」,没有重试 按钮。读者修好密钥之后,手里只剩「翻译当前页面」——一次范围不明的 全页重扫,而不是”只重试那 12 张”。
按钮的可见性跟着 retryTargetCount 走,不跟着状态走。
缓存的键里带着 目标语言 和 翻译风格(translator.ts:buildCacheKey)。
改其中任何一个,所有缓存条目都失效,整页会被重新请求一次。
设置页只在「图片分辨率上限」下面写了这条警告。目标语言的下拉框一声 不吭——而换目标语言恰恰是读者翻完第一章最想做的事。
三个设置都说。
worker 里有一整套调度:五个优先级类、二分插入、”急任务可以借一个通道” 的规则。
而页面上每一件任务都被标成了同一类。于是那套机制永远改变不了 顺序——”借通道”的规则判断的是”跑着的都是低优先级”,可低优先级从来 没有出现过。
页面开始标:读者正盯着的是「visible-now」,页面下面的是「next-up」, 读者明确点重试的是最急的「manual-retry」。
精修器要把模型的框和墨迹范围求并集(模型少框了一行时,并集能救 回来)。然后它拿并集去和模型框算重叠比。
并集一定包含模型框,重叠比恒等于 1,阈值 0.35 永远达不到。
真正会走偏的是墨迹:找到的字如果和模型说的几乎不搭,精修就跑到隔壁 那个气泡上去了——不透明的译文面板盖住了邻居的原文,而译文还留在原处。
比的是”包含度”:两个框里小的那个有多大比例落在大的里面。模型框比墨迹 矮(少框了一行)时包含度是 1,墨迹比模型框小(多框了空白)时也是 1 ——这两种都是精修该干的事。只有两者几乎不搭时,包含度才会掉下来。
opts.languages 和模块默认值是同一
个对象,第一次调用之后全局默认的语言顺序就变了。四位审查员分头跑了四段链。值得注意的是两件事:
一、审查员报告的多数问题,其实代码里已经修好了。
它们报告的每一条,都能在这份代码里找到对应的注释——注释写的是「以前 是这样,现在不是了」。注释写得越生动,读代码的人越容易把修好之前的 样子当成现在。逐条核对之后,真正还活着的缺陷只剩下一半。
这本身有代价:注释是给人看的,写成”病灶的样子”比写成”现在的规矩”更 容易让人(和模型)读错。规矩写在那里,理由归档。
二、审查员动手改了代码,而不是只报告。
这让”已修好”和”刚修好”混在了一起,核对时必须一条条看代码本身,不能 信报告里的措辞。有一处因此差点漏掉:一位审查员在重写渲染器时,把 另一位的改动覆盖掉了——
一位审查员重写
renderer.ts时,用的是它几十分钟前读到的版本, 于是同期另一处的改动被回滚了。测试没有立刻报警(剩下的用例仍然通过), 是逐文件比对才发现的。
于是这一轮多了一条规矩:改动落地后要逐文件确认,别信”我改过了”。
| 测试 | 去掉修复后 |
|---|---|
| 拼图条用自己的提示词 | 用整图那条,模型把两块气泡合成一个框 |
| 块与块之间画分隔线 | 两块之间只有 20 像素白,模型看不出边界 |
| 回答按重叠认领块 | 按中心点认,跨块的框认错 |
| 画不上去的译文被计数 | 数字算出来,没人看,页面说「翻译完成」 |
| 缺坐标的条目被丢掉 | 当成 0,画成左上角一列单字 |
| 越界的框夹宽度 | 渲染器把它挪到模型没指的地方 |
| 运行中按取消失败会报错 | 错误段落不渲染,弹窗一声不吭 |
| 翻完的页面说「已经翻过了」 | 说「没有可翻译的图片」,把人送去改图片大小 |
| 致命错误后仍有重试入口 | 读者只剩一次范围不明的全页重扫 |
| 三个设置都说会让缓存失效 | 换目标语言静默地重新计费整页 |
| 页面给任务标优先级 | 五个优先级类永远用不上 |
| 精修器比的是包含度 | 比并集,重叠比恒为 1,护栏永远不响 |
| 用量统计不排序调用方的数组 | 模块默认的语言顺序被改掉 |
| 「翻译风格」有一句解释 | 唯一没有解释的控件 |
| 用量卡片说「本月」 | 散文说「这一页」,和标题打架 |
这一轮改的每一处,都是同一件事:
一个事实被算出来,然后没有第二个人接住它。
数出来的没翻出来的气泡、模型说”这里没有字”和”这里没有框”的区别、 翻过的页面的张数、要重试的张数、会让缓存失效的设置、任务有多急、 精修会不会跑偏——每一样都有人算了,每一样都曾经被丢在原地。
这一轮做的事只有一个:把每个算出来的数字,接上一个真的会读它的人。