跳转至

翻译

应用级可插拔翻译:内置 provider + 免费 MT + 商用 BYOK + BYOA Agent。

设置

Settings → 翻译:

  • 默认服务 下拉:内置 provider(构建里注入了 key 时)、免费 MT 与 Agent 始终可选;商用仅列出已配置者。打开下拉时对内置(可用时)、免费 MT 与已配置商用并行 probe。
  • 内置 provider(id agentero):凭证由构建期环境变量编入 Host,卡片没有任何凭证字段(无 key / baseUrl / model)。可见性来自 Host 命令 builtin_provider_status 的 available;可用时与其他免费引擎一样参与 probe(真的发一次 "Hi" 翻译请求,顺带验证网关连通),不可用(构建里没有 key)时不 probe、选项禁用或隐藏,当前已选中它时仍保留在列表里。构建里没有 key 时选中它会拿到 translate.no_builtin_key 标记,由 displayTranslateError(src/lib/translate/errors.ts,仿 displayAgentError:子串匹配标记 → i18n.t(...),否则原样返回)在划词翻译与全文翻译的 notifyError 调用点转成文案,不裸露标记串。标记能到前端是因为 invokeTranslateText(src/lib/translate/api.ts)只对已知翻译标记逐字抛出 error.code,其余情况抛 Host 的人类可读 message——AppError::code() 还会返回 io / json / sqlite 等通用码,按"非 message 即标记"判断迟早会把裸码弹给用户。注入 key 的构建里它是新装默认服务——新装没有 settings.json,Host read_file 返回 AppSettings::default(),所以首次安装的默认值由 Rust default_translate_provider() 决定;前端 DEFAULT_TRANSLATE_SETTINGS 只在浏览器 dev(不可能有 key)里生效。
  • 目标语言、划词自动翻译;开启后,PDF 选区文本提取完成即自动启动翻译并打开结果卡,关闭时仍可从选区菜单手动翻译。
  • CNKI 翻译助手(免费引擎,id cnki):知网 dict.cnki.net 逆向接口,学术术语翻译质量好、校园网可用,但仅中英互译;超过 800 字符自动按句切分串行翻译(块间约 2s 防风控,长文耗时相应拉长);CNKI 边缘会随机直接断连(非 HTTP 错误),Host 复用带 cookie 的浏览器客户端(第一次成功响应后即持有 CNKI 的放行 cookie)并对每个请求自动退避重试、无需用户干预,仅连续失败才报错;触发验证码时按提示到 dict.cnki.net 手动过一次验证码后重试;海外 IP 通常不可用(404)。详见 ../backend/translate.md。
  • 商用 API 卡片仅填写 key / endpoint / region / model;点「确定」后:
  • 将 API key 写入 Host settings.json(Unix 权限 0600);WebView 只保留同长度 * 掩码,不再回显明文。
  • Host settings_get / settings:changed 对 key 按字符 redact 为 *;settings_set 收到纯 * 串时保留原密钥。
  • translate_text 在 key 缺省或为 * 掩码时从 Host 配置解析真实密钥。
  • 随后做一次连通性 probe。卡片不承担「设为默认」选择。
  • OpenAI 兼容翻译要求服务支持 Chat Completions 格式:POST {baseUrl}/chat/completions,返回 choices[0].message.content。设置里填写 Base URL(例如 https://api.openai.com/v1 或服务商自己的 /v1 根地址),不要填写 Responses / Completions 端点;应用会自动追加 /chat/completions。 用户配置说明见 配置 OpenAI 兼容翻译。
  • 默认服务为 Agent 时展示 Agent / 模型座。
  • 导入后自动翻译:开启后导入的论文在版面解析完成后后台翻译全文;并发数控制同时翻译的论文数。只有导入 / 下载链路触发,打开论文补解析和手动重新解析不触发。实现见下文「导入后自动翻译」。
  • 自定义翻译提示词(translate.customSystem / translate.customUser,旧 translate.customPrompt 仍可读):仅对 Agent 与 OpenAI 兼容两条 LLM 路径生效(免费引擎与内置 provider 无提示词概念)。设置里分系统提示词和用户模板两栏。引擎规则(只输出译文、不反问、公式 / 引用 / ⟦n⟧ 原样)始终生效。系统栏里多行或带占位符的内容只替换学术风格块;一行不超过 180 字、且不含 {{…}} 的口语(如「翻译得自然一点」)作为附加偏好追加。占位符只认 {{targetLang}} / {{sourceLang}} / {{text}},其它在失焦时校验失败、不写入;手改 settings.json 里的未知占位符会在归一化时被剥离。用户模板留空时原文仍用 """…""" 包裹;非空则必须包含 {{text}},由它决定原文插入位置。[[n]] 批量规则写在 system。每栏上限 8000 字符。「填入默认」把学术风格块写入系统栏并清掉旧的单段 customPrompt,「恢复默认」三栏都清空。OpenAI 兼容路径由 Host 在 translate_text 内注入(WebView 调用方无感)。保存系统栏时写入 customSystem 并清空 customPrompt,避免两段叠在一起。

导入后自动翻译

设置 → 翻译 →「导入后自动翻译」默认关闭;开启后在其下方显示「同时翻译的论文数」(1–5,默认 1)。持久化为 TranslateSettings.autoTranslateImport / autoTranslateImportConcurrency;旧 settings.json 缺字段时按关闭 / 1 处理。关闭开关只停止新任务,已排队的翻译任务仍会执行。

触发点是导入 / 下载链路入队 layoutAnalyze 时:设置开启时 Host 同时入队一个 layoutTranslate 任务,用 JobCenter depends_on 指向那次版面任务(DepPolicy::AllSucceeded)。覆盖本地 PDF、魔棒 / lookup、广场(PWC / papers.cool)与 Connector 附件——广场和 Connector 的 PDF 由 Host 直接落盘、不经过 DownloadAssets,所以这些入口在 Host 侧显式调用同一个 helper(jobs::spawn_layout_after_import):广场只对新建且有本地 PDF 的提交入队(广场提交不会推迟到元数据识别);Connector 在附件写完时入队,另在会话首次 finalize、以及每次论文移动到目标分类后按最终路径补一次,并跳过已交给元数据识别的论文(识别任务会在改名 / 合并后的最终路径入队)。

只有导入来源的入队才带 autoTranslate 标记:打开论文后的补解析与手动重新解析不带,因此不触发。打开一篇没有本地 PDF 的论文会先入队 DownloadAssets,属于下载链路,下载完成后同样触发。

  • LayoutTranslate 改为渲染端执行(ExecHost::Renderer),registerTaskExecutor("layoutTranslate", …) 在后台任务面板显示逐篇进度、可取消;layoutTranslate 已加入面板投影。
  • LayoutTranslateCap 由 autoTranslateImportConcurrency 播种(默认 1),设置变化时更新 cap 并 drain_and_spawn,控制自动与手动全文任务共同占用的论文并发数;单篇内部仍沿用原有批次并发。
  • 版面失败 / 取消时依赖任务标为 Skipped,不发翻译请求。job_cancel 和终止态 job_report 都会触发 drain,依赖任务随即结算;cancel_for_paper / cancel_for_vault 直接取消同论文或同 Vault 的所有任务,不再单独 drain。
  • Headless 流程(src/lib/pdf/layout/headless-translate.ts):读取论文根 {id}.pdf → source/layout.json(raw regions)→ listTranslatableLayoutRegions → prepareReflowItems(IR 缓存受 cacheReflowIr 控制)→ 用 layout-translate.json hydrate → 只翻译 pending 块(后台失败不自动重试)→ 写回 sidecar,已完成块保留为 partial。完整命中缓存时 no-op;无 PDF / 无 translatable 区域时跳过且不报错。
  • cacheReflowFinal 开启时额外构建译文 PDF 并写入现有 final build 缓存(命中则跳过重建);关闭时只写 sidecar,页面留到打开论文后生成。构建失败只记日志、任务仍按成功结算(sidecar 已写入,下次打开重建页面)。
  • 查看器订阅:点击全文或打开双栏译文面板时,先按当前翻译配置读取 layout-translate.json,准备重排 IR 并按 id + source 与所需句对校验缓存。命中可用译文后直接生成页面,不创建或订阅翻译任务,也不占用翻译并发名额;存在缺译块时显示「部分译文」提示,未译位置保留原文,页面生成和阅读继续进行。版面 store 尚未加载时先读取已有 layout.json。没有可用译文,或用户主动补全 / 重译时,自动导入与手动全文翻译共用 JobCenter 的 layoutTranslate 任务。同一 Vault、同一论文只保留一个排队 / 运行任务。运行中的任务保持 jobId、执行器和在途请求;排队任务及其尚未完成的版面依赖提升到 Focus,优先获得下一个空闲位,已运行的其它论文继续翻译。没有活动任务时创建 Focus 任务,复用缓存并补全缺译块。显式整篇重译同样入队,失败块保留上次成功译文。
  • 进度与窗口生命周期:同窗口直接共享执行器的 prepared items;Host 将不含字符 IR / 页面预览的翻译快照存入 job params,通过现有 job:changed 同步,其它窗口按自己的 PDF 准备重排 IR。订阅前先监听事件,再查询 / 请求任务,避免快速完成时漏掉最终结果。查看器关闭或切换论文只解除订阅;全文停止入口与后台面板取消按钮才调用 job_cancel。跨窗口收到同一 offer 时,Host 只允许一个窗口领取执行权。
  • 单页入口:该论文已有全文任务时直接订阅对应页的 items,不另发请求;点击页签只影响该页显示,全文继续运行。没有活动全文任务时,单页按缓存翻译本页。单页运行中点击全文入口会请求整篇任务。
  • 缓存写入:单页和后台完成块按相同 cache key 增量合并;防抖期间汇总两边的完成块,结束时立即 flush 并等待写入。桌面 Host 串行执行读取、合并与原子替换,避免跨窗口覆盖;单页增量写入保留同页其它完成块。

消费方

  • RSS、论文摘要、Markdown 笔记、Agent 消息、文本编辑器和代理网页的划词翻译通过通用划词组件接入;共享翻译服务、来源信息和窗口单卡规则,结果为瞬态卡片。卡片正文只显示译文,复制按钮及其悬停提示提供 C 快捷键,编辑文字、输入法组字或按住修饰键时不触发。卡片按选区矩形在原文外打开;代理网页里按 Esc 由选区桥转发后收起。

  • PDF 划词菜单「翻译」(首要入口)。

  • 结果卡优先打开在选区下方,空间不足时改放上方或侧边,尽量让选区保持可见。卡片锚定在原文位置,随页面一起滚动或缩放;滚出阅读区时按 PDF 滚动视口裁剪,从面板边缘滑出,不会盖住标签栏或查看器工具栏。卡片高度随译文内容增长、上限为阅读区可用高度;超过可用高度才在卡片内滚动,且不显示滚动条(滚轮 / 触控板仍可滚动)。可抓住顶部居中的把手移动,拖动右下角调整大小,移动后的位置同样跟随页面。复制按钮将译文写入剪贴板。
  • 卡片正文只显示译文,不显示选中的原文或来源标题;译文没有独立边框,仅保留内边距。标题栏不显示 logo。卡片使用不透明背景,避免叠在白色纸面上时被透成灰底。卡片不提供「定位原文」按钮。
  • 每个应用窗口同时只显示一张译文卡。开始下一次翻译时立即关闭上一张;切出当前 PDF 时也关闭,切回时不自动打开。这个规则也适用于分屏。
  • 通过卡片关闭按钮或 Esc 可收起,按 P 可钉住或取消钉住,有译文时按 C 可复制当前译文。编辑文字、输入法组字或按住修饰键时,按键仍交给当前输入区。关闭未钉住的卡片会丢弃译文;如果仍在生成,会尝试取消 Agent 任务并忽略迟到结果。已经开始的保存会先写完,再删除对应文件。已钉住的译文关闭卡片后仍会保留。原文浅黄高亮在卡片收起后保留。
  • 有本地论文路径时,打开期间的结果和 pinned 状态写入 {paper}/marks/<id>.json;新译文默认未钉住,旧 JSON 缺少 pinned 时按已钉住读取,保留升级前的页边入口,不涉及 SQLite 迁移。
  • 磁盘刷新保留正在生成、等待保存或删除的本地译文,避免焦点切换或外部文件更新覆盖当前结果;刷新期间发生的本地修改也优先保留。保存或删除失败通过 Toast 提示,并保留当前内存状态。
  • 卡片在本地存储路径和选区定位都有效时显示钉住开关,提示为「钉住 / 取消钉住」。钉住会同时保留译文记录和页边重开入口;取消钉住会立即移除页边标记,但卡片仍保持打开,关闭后才丢弃译文。卡片关闭后没有单独的删除按钮。每次翻译建立独立记录,流式更新不会清掉当前钉住状态。同一位置的页边标记会纵向错开;多行选区的标记锚定在选区末行,空间不足时仍保持在页面内。
  • PDF 全文翻译(工具栏 Languages,在视觉批注旁):
  • 完整文本与源样式:公共入口与底层共用一个提取器,保存完整 source / raw。版面文字回填记录按有效文字字符数加权的字号众数(相对页面高度)及段落主体粗斜体;译文从源字号开始,仅在实际溢出时缩小,短译文不再为填满框而放大。旧块无样式时保留估算回退;段内混合样式、公式原对象移动和字体嵌入尚未实现。
  • 请求完整性:保护公式/链接后按实际 payload 组批;超长单句在传输层分块(不切开 Unicode 代理对或占位符),保留原始段落 id、raw 和句子锚点,不再伪造 header 块。编号多余/乱序或带前言时逐句回退。可靠性层保留两轮逐块重试;连续 3 次传输级失败(DNS / TLS / 连接 / 超时,isTranslateTransportError)即判定服务不可达,剩余批次不再发请求,也跳过两轮重试,直接报错并在 toast 中提示换服务。HTTP 状态码、空结果等仍按原逻辑重试。
  • 依赖版面分析 + PDF 文字层;翻译 text / abstract / header / figure_title(图题·表题)区域(score ≥ 30%)。
  • 不翻译:版面标为 doc_title 的论文标题;缺少 doc_title 的 MinerU 旧格式中,首页首个非编号 paragraph_title 在摘要、单位或明显更大的字号佐证下按论文标题处理;首页标题与摘要 / 首个章节标题之间的作者、单位和联系方式区域;footnote / vision_footnote 脚注;页边 header 中的刊名、会议名等出版信息,以及同一页边位置至少重复两页的短刊名,footer、number、seal、aside_text;算法框及其内部文字;reference / reference_content 参考文献区域和独立的 References / Bibliography / Works Cited / 参考文献标题。显式结构标签不受 30% 置信度门槛限制。正文、摘要、图题 / 表题和 paragraph_title 章节标题继续翻译;参考文献后的附录继续翻译,正文中的行内引用照常保留并参与正文翻译。
  • 保真区域:上述原样区域不进入翻译请求,也不从源页删除。重排时把它们作为障碍框,字符提取、fallback 段落、表格单元和 IR 缓存恢复都跳过这些字符。缺译进度、补全范围和重译范围只统计需要翻译的块;已有 sidecar 中这些区域的旧译文不再应用。整段译文 PDF 继续保留原始标题、作者信息、脚注、页眉页脚和参考文献内容及其链接。
  • 译前归一化(normalizeLayoutSourceText):文字层是空白折叠后的单行串,先合并行末连字符断词(repre- sentation → representation,pre- and 这类并列保留)、展开 ligature / 去 soft hyphen、清掉落在正文 bbox 里的 arXiv 戳与会议 boilerplate、剥掉句末后粘着的页码与续段前的行号(Table 2 这类交叉引用不动,header 不做数字剥离)。
  • 跨页/跨栏段落合并(#340):一个段落被分栏、分页或图表切开时是多个 region。末尾无句末标点、下一片段以小写开头则判为续段,先拼成一个 chain(≤ 4 片段 / 4000 字符)。切句在归一化之前,用文字层原文(raw,没有则退回 source),连字符保留,所以 quote 仍是 repre- sentation。拉丁 . ! ? 要后面有空白或已经到结尾才切句;。 ! ? 单独即可。et al.、Fig.、e.g.、i.e. 和小数不切。发给引擎的是归一化后的句子。一句跨栏、跨页仍是一个编号;译文只在这一句内部按各片段原文长度切回 bbox。块上的 translated 是该块各句译文切片按目标语言拼接(中文直接相连)。图题不打断 chain,header 打断。chain 内任一片段缺译文即整条重译。
  • 段落边界与安全扩框:文字层会按真实行间距将被模型合并的相邻正文段落拆成独立 bbox。标题和图表 caption 的译文估算行宽超过原文 10% 时,覆盖层才会向无检测内容的右侧或下侧留白扩展(最多原框的 1.5× 宽或 1.75× 高);遇到任何版面检测框即停止。标题另按原文框对应的字号排版,译文更长时继续向下加高(最多约 2.75× 原高,同样停在检测框前),而不是把标题缩成正文。单行标题按未扣段落内边距的框高取字号,避免贴着字形的扁框落到最小字号。正文不借用周围空间,避免不完整的版面检测遮住邻栏内容。
  • 占位符保护(src/lib/translate/mask.ts):行内公式 / LaTeX 命令 / URL / DOI,以及方括号引用([12]、[1, 3]、PDF 里带空格的 [ 6 , 13 ])、作者-年份(含 PDF 括号内空格,以及译后的全角括号、中文逗号和「等人」)、Figure / Table / Section / Equation 等交叉引用、Markdown 链接,先换成 ⟦n⟧ 再发引擎,回填时还原;占位符缺失、重复或未知时保留保护标记重试;持续失败则该 chain 保留原文,不用部分译文覆盖整块。带保护标记且含可译文字的句子遇到空译文或腾讯未返回译文的响应时,只翻译标记之间的文字,再按原顺序回填并校验公式、链接和样式结构;替换标记后的重试同样可进入这条恢复路径。批量请求遇到同类响应时先逐句处理,恢复文字的分批响应仍缺译时再逐段处理。取消、网络、鉴权和限流错误不增加这类恢复请求。译文覆盖原文后,这些标记在字形层重新挂上原来的 Link 目标(只包住标记本身,沿用原文链接字形的颜色(字形是正文黑时用可见边框的颜色;当前可见页的填色在打开译文前就读好),不再另加下划线;[17,38] 这类一组多个编号,只拆中心落在该标记字形上的 Link;字形还没读到时,一根已经盖住整组的宽标注不会把同一行后面的引用借进来。同一行上矩形顶边略有高低时,仍按从左到右配对。中心还在这段标记上方或左方的 Link 先跳过。个数对不上时整段只挂第一个 Link,每个拆开的编号单独悬停和点击(同一矩形的重复 Link 只算一条)。从句对调后仍按原文里的同一处标记对回 Link。字形已经对上、但这段上没有链接中心时,这段保持纯文本,不占用下一个 Link。裸 URL 末尾成对的括号保留,只剥掉多出来的句末括号和标点),英文坐标的整块命中区仍留在下层,不挡住译文划选。
  • 按阅读顺序把句子分批翻译(buildTranslateBatches):批内 payload ≤ 4500 字符(约一页双栏正文),用 [[n]] 编号,一个标记一句。标记解析失败时该批改为逐句重译;某一句仍失败则该块不写 sentences。成功时每块写入句对 { quote, source, translated },跨框切片另记 display。并发 2(Agent 串行);每个有效段落完成后在 bbox 上盖译文层(非整页等齐)。这里的并发 2 是前端批次并发,与内置 provider 在 Host 内对单批做的句级 fan-out(并发 3)正交:选内置时同时在飞的请求最多 2 × 3。
  • 每页纸张内侧左上角在 hover 时出现窄页签,可只翻译本页;已有全文任务时复用它的本页 items,保留全文执行。没有全文任务时先按当前 cache key 读 layout-translate.json,只请求本页缺译块。页签 hover 不弹出额外文字;本页已有可见译文时切换为隐藏本页译文。隐藏只影响当前 UI 覆盖层,不删除磁盘缓存。单页失败提示为「本页翻译失败」。
  • 译文按论文写入 {paper}/source/layout-translate.json(schemaVersion 2)。版本不对的文件整份视为未命中,按块重译。缓存命中需匹配 provider / 源语言 / 目标语言 / 非密钥服务配置,并逐块校验 region id + 原文(存的是归一化后的原文,归一化规则变化时旧缓存会 miss 一次并重译);版面或目标语言变化时只复用仍匹配的块。句对随块写入;没有句对的块只复用整段译文。customPrompt / customSystem / customUser 里任一非空时,service key 追加这三段去空白后拼接的 FNV-1a 指纹——改提示词即重译;三者都空时 key 与旧版字节一致,存量缓存升级后仍命中。
  • 单页翻译写缓存时按同一 cache key 增量合并,避免只翻译一页时覆盖其它页已经落盘的译文。
  • 主翻译按钮 / Alt+A 的作用对象是整篇:单页任务期间点击它会进入整篇链路。首次点击论文的全文入口先校验缓存,命中可用译文就直接生成页面,缺译块通过「部分译文」入口提示;没有可用译文时复用、提优或创建共享任务。再次点击运行中的全文入口会停止任务。已有译文时保留原文 / 译文显示切换;部分结果的补全与整篇重译通过旁边的「部分译文」入口执行。停止后再次点击会隐藏已有译文。运行结束后若仍有失败块,点击主按钮会先打开部分译文恢复卡。单页显示由页签控制;全文运行期间点页签不会停止整篇。右上角为中性色状态胶囊:等待翻译 → 提取文本 → 翻译 → 生成页面;独立 PDF 等待版面时显示准备版面。阶段与百分比用与应用 Progress 一致的文字层级(caption + tabular-nums),下方内嵌一条 4px 圆角进度条(foreground/10 轨道、primary 填充),两端留白、不贴着胶囊边缘,也不压到右侧控制图标下方,未知进度使用往返光带。展开用减速曲线、不做回弹,减少动态效果时直接切换。翻译结果在校验通过后立即发布,不等同批其他段落的保护标记重试;段落尚未完成时也计入已处理句子的份额。全部文本完成后继续等待重排预览,页面未生成时不显示完成。只有带重排 IR 的块才可能产出译文页:prepare 时文档还没注册导致整批块都没有 IR 时,不再等永远不会开始的最终构建,进度直接按文本完成收尾。
  • 全文翻译、补全缺译和重译整篇的片段失败(如内置 provider 返回 502)统一通过「部分译文」入口提示,不再额外弹报错 toast;弹层提供「打开翻译设置」按钮,直接进入翻译设置。单页翻译失败仍显示「本页翻译失败」toast、错误描述和「打开翻译设置」动作。版面解析、任务启动、缓存写入或页面生成等独立失败保留原有错误提示。
  • 覆盖层按当前 PDF 页面背景 tone 绘制纸面底色(深字)。有句对且各句切片能拼回块译文时,每句是一个 data-sentence span,句间空格只在块译文本身带空格时插入;拼不回去则整段仍是一个文本节点。暗色下套用与页面栅格相同的 invert filter(PDF_PAGE_RASTER_DARK_CLASS),使盖住原文的底色与反转后的纸面一致。纸面在划词高亮和已有英文高亮之下,字形在高亮之上。主阅读器的译文划选可以高亮和批注。译文开着时标出划中的译文;关闭后,标记按原文字形合并成连续的行内色带,停在对应英文句子的末尾。可编辑 PDF 大纲时也可添加书签,默认标题取对应英文。已有句对但找不到完整原文位置时,仍可复制和发起对话。翻译前已经做好的英文高亮仍铺在对应译文上:有页比例坐标的画那套坐标,不画英文文字层的框;没有这套坐标的高亮是翻译前在英文上做的。译文打开时,翻译前的英文高亮用自己的文字层字形框去对每一句英文的字形框,重叠的那一句才铺原来的颜色,不写回批注。文字层还没读到时只铺整句对得上的高亮,一段文字不会套到同页的每一句上。在译文 span 上划选时,半句扩成该句整句英文,连续几句只保留这几句。贴着上一句的边界起笔、但没有划中上一句的字时,不把上一句算进去;这一块没有句对时退回整段英文。复制仍是划中的译文。拖选过程中直到选区菜单出现,不弹出参考文献、交叉引用或 URL 卡片,与未开译文时一致;引用标记按正文文字选中,不会把拖选抢走。见 译文划选与参考文献卡片。译文正文按起始对齐。重排后每个汉字是图上的一个透明小框,划选不再给每个框单独铺蓝色,同一行只画一条,字缝不会叠深;还没重排时也不画底下的英文文字层。松手后的选区仍按行并成一条。提问和加入对话把配对译文当作当次上下文,不写进批注。英文文字层上的划选仍按字形。笔记里的批注嵌入对得上时在原文下显示译文,见 wiki.md。选中译文时墨色跟纸面走(浅纸黑字、暗纸浅字),不用界面的前景色,避免深色主题把选中的黑字反成浅色。这条要同时写在段落和句 span 上:WebKit 不把段落的 ::selection 传给里面的 span,span 会落回界面前景色。排版先以原文尺度估算、再用真实浏览器度量校验:正文译文膨胀时依次收紧行距(1.25 → 1.10)、缩小字号;标题的度量校验最多收到初始字号的 90%,避免长译文把标题二进制搜索到 1px。遵循严格 CJK 断行,只有不可断的 URL/标识符仍溢出时才允许词内断行。因此普通段落不会过早缩成极小字,标题仍跟原文层级,并尽量避免裁掉译文。
  • 译文选区生命周期:选区底色按查看器 docId 隔离。同一页码出现在多篇 PDF 中时,各自只读取和清理自己的底色,后台标签页的选区变化、译文开关或卸载不会清掉当前论文的选区。查看器隐藏后释放拖选状态、待执行帧和选区底色,并使尚未完成的原文定位失效;再次显示后可以继续划选。可见的并排阅读器保留划选能力,不以工作区焦点限制另一栏。
  • 双栏翻译(Settings → 翻译 →「翻译显示模式」选「双栏对照」):全文翻译按钮在原文右侧打开只读译文 PDF 面板(同页栈 + 译文覆盖层,隐藏工具栏/选区菜单)。左右各是独立 EmbedPDF 实例。手指下的那一栏由浏览器自己滚动(src/lib/pdf/scroll-sync.ts),滚动过程中另一栏保持不动。滚动停稳后另一栏直接跳到同一内容位置(不播放滚动动画),并取消它尚未落地的程序化跳转。没有 scrollend 时,以最后一次滚动后约 180ms 为准。纵向滚动不改对方的横向位置。滚轮监听保持 passive,只认领正在滚的那一栏;单栏不走这条路径。缩放仍由 usePdfScrollSync 在手势停稳后镜像。译文面板走精简 PdfTranslationViewerInner:只挂 raster/tiling/zoom/search 等核心插件(不挂 ONNX 版面分析、批注、PDF 选区插件,也不调用对应 capability hooks)。⌘F 在译文面板搜索生成的译文 PDF;译文 PDF 尚未就绪时回退到原 PDF。译文覆盖层文字可以选中,并显示选区底色;页面层只渲染纸面 + 译文覆盖;打开论文的译文面板时先校验翻译缓存,命中可用译文后直接生成页面,存在缺译块时显示「部分译文」提示。没有可用译文时订阅共享全文任务;运行中的自动翻译继续执行,已完成块立即显示,缺译块沿原链路补齐。没有活动任务时通过 Focus 队列启动全文翻译。脱离论文目录的 PDF 仍先 hydrate layout-translate.json,部分缓存保留原文与部分译文入口。侧栏不挂批注插件,主阅读器上的英文高亮淡底不会出现在侧栏。打开译文面板时若版面分析尚未完成,译文面板会优先准备版面并在进度胶囊显示当前阶段(可取消),不再静默空面板或弹排队提示;译文面板订阅源文档 key 的布局结果做跨面板补种(mirrorLayoutResultToTranslation),源面板稍后完成解析时会自动把区域补种到译文 key 并开始翻译。译文面板不挂批注插件,链接来自主阅读器已经解析的 citation / 文本 URL;点击引用在译文面板内跳到目标页,URL 经系统浏览器打开。悬停译文里的引用、图/表/公式交叉引用或 URL 时,显示与主阅读器相同的引用卡片、交叉引用预览和 URL 提示。跳转前用主阅读器解析出的页面可见框原点做 destinationInPageBox。当前可见页在空闲时就读取链接字形和填色,不等到点翻译;译文打开后,主阅读器再补已经挂载、且同时有译文和链接的页。旁边页的链接表换成新对象时,这次读取不取消。每读完一页就交给译文面板,译文面板不再自己扫全文。译文栏不画整页底图,只画瓦片。瓦片使用上游取消与 URL 生命周期,各栏独立出图;不再维护跨栏共享位图缓存。段落的浏览器度量有缓存,并摊到后续帧里,页面滚出就取消。
  • 进度控件只显示当前阶段与真实百分比,点击方块可停止;完成短暂停留后收回图标。适配浅色 / 深色与减少动态效果,不按时间伪造百分比。阶段文字后同时显示本阶段页数(翻译阶段是本轮涉及页面的已完成页,排版阶段是已生成页),分母在阶段开始时固定。
  • 进度采用工作量加权:需要翻译时,文本处理占 70%、页面生成占 30%;全部命中译文缓存时,只计算本次页面生成的工作,不从 86% 起跳。页面生成按公式处理、逐页写入、PDF 保存和预览渲染上报,最终输出发布前最多 99%。数字与进度条共用 Motion 无回弹弹簧,连续追随实际进度,减少动态效果时直接显示目标值。它不是剩余时间估计,服务等待或文件保存期间仍可能暂时停留。
  • 缺少版面数据时,点击翻译使用 JobCenter focus lane,并通过 jobFocusPaper 提升已排队的同论文任务;复用已有解析,不重复启动或强行中断其他运行中的任务。版面是段落顺序、公式保护与页面重排的必要输入,所以仍需准备完成后翻译。入口不再弹「已排队」及「解析完成」Toast,失败仍用通知。
  • 等待期间会主动读取已完成文本提取的 layout.json 并补入当前文档的版面 store,也订阅本进程的版面写入通知;不依赖解析面板处于激活状态或下一次文件变化事件。阅读器的旧加载时间戳不能阻止缺失 store 的重新加载。入队失败或知识库路径无效时退出等待并显示错误,取消 / 切换文档后的异步结果不得启动翻译。
  • API:runTranslate(task)(src/lib/translate/)。

缓存优先与部分译文(#1029)

全文翻译有两类缓存,职责不同:reflow-ir 不存译文,{paper}/source/layout-translate.json(schemaVersion 2)只存已完成块的译文。两条入口(覆盖层与双栏译文面板)现在共用 hydrateLayoutTranslateItems():按 id + source 应用 sidecar,返回 { items, cachedCount, pendingRequired, fromCache }。

  • 有缓存且无缺块:查看器状态直接 done,只生成页面,不进入后台翻译队列,不发翻译请求。
  • 有缓存但有缺块:主阅读器和双栏译文面板都直接展示已有译文并生成页面,保留 partial 状态与「部分译文」提示,不自动请求补全,不进入翻译队列。只有用户主动点击「补全 N 段」或「重译整篇」时才请求翻译;本地论文通过共享队列执行。未译位置保留原文,提示不阻塞页面生成和阅读。
  • 没有任何可用译文:保持原有首次翻译流程(含等待版面分析)。
  • 缓存 key、sidecar schema 与翻译 provider / 提示词都不变。

补全和重译期间保留已经生成的译文 PDF 与各页预览,变化页生成成功后才替换,未变化页继续使用现有译文、字号与排版。变化页先显示新预览,等对应的译文 PDF 加载成功后再启用原生清晰渲染;加载失败时继续显示预览。失败或取消的段落保留旧译文。生成期间保留的旧结果用于阅读,等本轮页面构建完成后恢复导出。增量替换保留原页序与页间跳转,并复用相同字体数据、释放废弃页面资源,避免反复补全累积旧资源。

页面变化检测使用不可变排版对象的短标识,避免把逐字符 IR 序列化后嵌入页面与全文比较键。重新读取缓存得到新对象时,仅首次比较其内容,内容相同则复用原标识,保持未变化页面的译文与排版。

译文 PDF 字节仅供加载和导出使用,页面组件间通过独立的小型版本标识判断预览与已加载文档是否一致。不要把整份 PDF 的 Uint8Array 放进页面组件 props;React 开发版的性能记录会展开数组的每一个字节,造成大量分配并阻塞显示。

全文翻译进度进入「已完成」时,百分比与进度条立即同步为 100%,随后短暂保留再收起;部分缺译继续通过黄色恢复图标提示。

任务未运行且 pendingRequired > 0 时,覆盖模式的主按钮旁、双栏译文面板的右上角均显示黄色恢复图标(悬停与读屏文案为「部分译文 · N」)。双栏操作直接交给右侧面板持有的翻译任务,不在原文面板重复启动请求;页面生成期间恢复入口仍可使用。全文翻译失败、读取部分缓存或重新打开文档时,弹层均保持关闭,只显示黄色恢复图标;点击图标后展开,页面构建在后台继续。图标未展开时背景透明;悬停、按下和展开时沿用工具栏的浅中性底色,图标始终保持黄色。弹层标题为「部分译文缺失」,第一行最右侧是关闭图标;正文一段说明已译/总数、未译数量和页码范围,以及未译段落显示原文。操作区左侧为「打开翻译设置」,右侧为「补全 N 段」和「重译整篇」,按可用宽度换行,并提供:

  • 补全 N 段:只把未译块作为本次范围,已完成块保持 done,不重复请求。进度分母只算本次 pending(runScope: "pending"),不再从整篇 98% 长时间停留。完成后只重建有变化的页面并重写 sidecar;仍失败则保留图标,弹层正文追加「上次补全后仍有 N 段未译」,不自动重新打开弹层。
  • 重译整篇:次要操作,二次确认后重新请求整篇(runScope: "all")。执行前保存已有译文快照,成功的块写新译文,失败的块恢复旧译文,随后重建页面并更新 sidecar,避免一次失败清空缓存。
  • 打开翻译设置:直接进入设置窗口的翻译页。
  • 关闭:收起弹层,黄色恢复图标保留,可再次手动打开。

未译块不进入 translations map,覆盖层和双栏面板都保留源页栅格,所以未译位置显示英文原文而不是空白。标题、首页作者信息、脚注、页眉页脚、页码、印章、侧栏文字和参考文献在共享筛选、字符补提取和 IR 缓存恢复时排除,不发翻译请求,也不计入进度分母、缺译提示、补全及重译范围。已有 sidecar 中这些区域的译文不再应用;译文和双语 PDF 保留原始文字、编号、链接及完整页序。手动划词翻译仍可使用。

Prompt

翻译 API 层仅透传已知错误码;错误展示由 displayTranslateError 统一本地化。API 层不导入 i18n 初始化模块,避免经 settings / services 形成循环依赖。

OpenAI 兼容服务卡片增加「额外参数」JSON 输入框,与 API Key / 模型一起在「确定」时保存。留空采用自动模型适配;填写对象后完全替代自动采样和思考配置,{} 不发送附加参数。model、messages、stream 保留给应用控制;格式错误通过 notifyError 提示并保留草稿。probe 与实际翻译传递相同配置;translateServiceKey 包含 JSON 指纹,留空保持旧缓存 key。完整规则与最新模型行为见 OpenAI 兼容翻译配置。

buildTranslateMessages / buildTranslatePrompt(Agent 路径)与 Host openai_translate_messages(OpenAI-compatible 路径)共用同一套约束,改一处要同步另一处:

  • system 是翻译引擎,不是聊天助手:focusing solely on translating… Only return the translated text。不解释、不润色、不总结、不反问「翻译还是解释」。
  • user 只有原文,默认用 """…""" 包裹。Agent 的 ACP runOnce 只有一条字符串,所以 buildTranslatePrompt 把 system 放在最前并写明 OUTPUT ONLY THE TRANSLATION;Host 翻译信封不再加 User request:。
  • 学术风格(按意思重组语序、术语首次可附原文,如 注意力机制(attention))和少量英↔中论文例句放在 system。公式 / 符号 / 变量 / 单位 / 行内代码 / URL / 引用标记 / 图表公式编号 / ⟦n⟧ 占位符原样保留。
  • 词或短短语(约 1–4 个词,或不超过 8 个汉字,且没有句末标点;[[n]] 批量不算)追加词典式约束:只给译文或一行词性释义,禁止反问意图。长句仍走翻译引擎。
  • 批量 payload 里每个 [[n]] 是一句。规则在 system:保留全部标记、顺序和编号,不跨编号搬运、不合并、不丢弃。一个编号内部可以调整语序,也可以把一句原文拆成多句译文。
  • OpenAI-compatible 的 temperature 用 0.2(0.0 的直译感太强)。能关 thinking 才关,而且只对认得出的模型:o1/o3/o4 发 reasoning_effort: low 并不发 temperature;gpt-5(非 *-chat*)发 reasoning_effort: none 并不发 temperature;混合思考型号使用对应关闭参数,强制思考的新型号只降低 effort,不发送不支持的 disabled。Kimi 新系列和 MiniMax M3 系列不发送 temperature。具体型号及例外见配置说明。填写额外 JSON 后这些自动参数全部被用户配置替代。Agent 翻译把会话 effort 调到已声明档位里最低的一档(有 none 就用 none)。

内置 provider(agentero)不适用以上整套约束:tencent/Hunyuan-MT-7B 是专用 MT 模型而非 instruct 模型,只认它自己的单行模板,Host 改发单条 user message(无 system message),并且不把 [[n]] 喂给模型——批量对齐依赖指令遵循,对它无效,所以标记由 Host 拆分、逐句请求、按序重组。⟦n⟧ 占位符仍由前端 mask.ts 插入并原样透传。详见 ../backend/builtin-provider.md §翻译:Hunyuan-MT。

自定义提示词:customSystem 非空时优先于旧的 customPrompt,都不再整段替换 system。引擎规则始终保留。多行或含占位符的系统内容替换学术风格块(DEFAULT_TRANSLATE_PROMPT_TEMPLATE);一行短口语只作为附加偏好,默认风格和 few-shot 仍在。customUser 含 {{text}} 时成为 user 消息,否则原文仍用引号包裹。[[n]] 批量规则仍由应用写进 system。Host 与前端一起改。

路径

类型 路径
内置 provider Host translate_text(agentero → Hunyuan-MT,构建期凭证,无凭证卡片)
免费 MT Host translate_text(腾讯交互翻译 / 火山 Web / DeepLX / 知网 CNKI / Google gtx)
商用 BYOK Host translate_text(DeepL / Azure / Google Cloud / OpenAI-compatible)
Agent agent_run_once + 翻译 prompt;同一篇文献的多次翻译复用同一个 ACP provider session

结果可写入 marks/(划词)。Host 细节:../backend/translate.md。

限制与后续

字词对齐,以及把滚动同步从页面比例改成句子对齐,留在后续。译文上的高亮用划选时记下的页比例坐标,不是事后按矩形反查英文。侧栏译文面板不共享主阅读器的高亮,英文高亮的淡底只画在主阅读器。

与 BabelDOC 的差异和后续

全文翻译现已接入 src/lib/pdf/reflow/ 的字符级 IR、公式/富文本占位、字体度量排版和 PDF 输出。 有可用 PDF 引擎时,先从字符索引与几何生成段落 IR。原文按句编号翻译,保留公式与样式占位,再按段落排版;透明选区层用排版前的字符偏移保留句界。 样式可跨句保留;若句界穿过不可拆分的公式,该块保留段落级对齐,划完整块才可写入标记。该路径不进行旧路径的跨段句链合并。 缓存按带结构标记的 source 匹配;主阅读器再次发起翻译时,只有缺少所需句对的重排块会重译。 没有引擎上下文时仍保留旧 DOM 路径。

翻译完成后生成隔离 PDF 副本,再由 PDFium 渲染译文预览;透明选择层使用同一排版坐标,选区只画底色、不重绘字形,避免与栅格译文叠成重影。流式阶段用单页栅格作占位;完成后把译文 PDF 以隐藏文档(autoActivate: false)注册进文档管理器,译文页改由与原文页相同的渲染层按当前缩放/DPR 重绘,缩放后不再拉伸固定图片。 工具栏下载菜单提供译文 PDF 与原文/译文交替页 PDF。字体首次从固定版本公共资源下载、校验并缓存, 因此首次使用需要联网。当前完整嵌入中文字体,导出文件可能约 20 MB;不修改源 PDF。 出错使用 Toast,无法容纳或不支持的页面不会静默裁切。

这一路径支持有文字层的中英论文;未旋转页面支持偏移 CropBox/MediaBox(含负坐标),文字、公式片段和链接均按实际可见区域转换坐标,保留原始页面边界。扫描和旋转页尚未支持完整导出。 复杂表格、公式分组和链接迁移仍有差异。源码对照与真实论文验证不能宣称与 BabelDOC 算法等价。

全文验收补充(2026-10-26)

字符 IR 路径在保护标记校验失败时,直接改用非数字占位别名,避免重复相同请求;继续失败则只翻译保护边界之间的文字, 并记录 reflowFallback: protected-spans,供验收审计。结构完整不代表句子流畅,持续校验失败仍报告错误。 重叠块按未归属字符重新定位;表格纯数学单元保持原 PDF;导出只遮盖公式笔画,不再整段清白擦断表格线。

Attention 15 页 / ResNet 12 页已执行真实 Google 非 LLM 全文链路和 BabelDOC 对照,正文、摘要、图题及表格文字纳入翻译;标题、作者信息、脚注、页眉页脚和参考文献按保真区域处理。 图内标签、复杂表结构、部分未翻译英文和人名误译仍未通过;不是算法等价实现。

字符覆盖与图内标签(第四轮)

翻译页内补查未被检测文字框覆盖的水平字符,按文本对象及空间行生成 fallback;读取 PDFium 字符矩阵、旋转角、绘制模式和局部裁剪信息。 图内局部字号转换到页面字号;旋转、不能可靠复原裁剪的内容,以及复杂公式对象保留原样。原页渲染辅助排除不可见的变换文字,仍是保守启发式。 IR 提取结果保存每个非空白字符的归类用于审计,不能把全部单元 done 等同于全部可见文字翻译完成。 表格排版框可利用相邻单元前的空白,源文字删除框独立保持;全文字号众数纳入标题和短段落并按输出字符加权。

延迟与结构回退(2026-10-27)

全文耗时包含字符 IR 提取、网络翻译、PDF 重排及预览渲染;首次使用还需下载字体。 新增表格、参考文献与检测框外文字覆盖会增加翻译工作量。翻译批次默认并发为 2(Agent 为 1), 批次中的结构回退与可靠性层两轮逐块重试会延长尾部耗时。

结构回退现在跳过相同数字占位请求,直接尝试非数字别名。仍失败时,保护边界间的文字 复用 4500 字符预算组批,按严格编号回填,公式和样式标记留在本地;编号损坏才逐片段翻译。 没有增加服务并发,也没有放宽结构校验。两个文字片段的确定性回归用例从原来的 5 次请求降至 3 次; 若编号损坏则为 5 次。该数字是模拟服务的请求计数,不是整篇真实网络耗时或速度提升比例。

字符 IR 持久化缓存(重启命中)

prepareReflow 的 PDFium 字符 IR 提取结果按页写入应用缓存目录,重启后再次全文翻译可直接复用,不再重新抽 glyph / text run / 字符级几何:

  • 位置:appCacheDir()/reflow-ir/<论文目录名>-<paper key 哈希>/<页码>-<缓存键哈希>.json.gz。只放在应用缓存目录,不写 paper/source,不进入 vault。缓存目录按论文键隔离,单篇最多保留 96 个文件(按 mtime 淘汰更旧的键)。
  • 键:PDF 内容 sha256 + source/layout.json 文件内容 sha256 + 当前页的 items 几何指纹 + 当前页的 regions 几何指纹 + 缓存格式版本 + reflow IR schema 版本 + 算法版本 + paper key。改 PDF、重新版面分析、换论文或调整提取算法都会 miss;命中时不会复用其它 PDF/版面的旧位置。提取算法或段落归组变化时,需要提升 src/lib/pdf/reflow/cache.ts 的 REFLOW_IR_ALGORITHM_VERSION。
  • 内容:只存源几何 ReflowPage(段落、字符、公式、样式、table/fallback 框、coverage、obstacles),不存译文。译文仍在 layout-translate.json;preview / 最终 PDF 仍按现有流程重建。
  • 格式:整页 JSON 经 gzip(CompressionStream);运行环境没有压缩流时写普通 JSON,读取按 gzip 魔数自动识别。
  • 写入:先写同目录临时文件,再 rename 覆盖,失败只记 warn,不影响翻译。页面级 key 让整篇翻译和单页翻译互不覆盖。
  • 读取:校验 header 的 format / 版本 / key / paper / PDF 与 layout 哈希,校验页面结构、页码、段落归属和页面尺寸;任一不符或 gzip/JSON 损坏都按 miss 处理,回到 extractReflowDocument 重抽。部分页 miss 时只重抽这些页,全部页都 miss 时等价于原有整篇抽取。
  • 开关:设置 → 翻译 → 缓存 提供两个开关。cacheReflowIr(默认开)控制 IR 页缓存;cacheReflowFinal(默认关,单篇约 25 MB)控制最终译文 PDF 缓存。开关只控制是否写入新文件:关闭后不再新增缓存,已有文件仍会命中复用,需要释放空间时到「设置 → 关于 → 翻译缓存」清除。
  • 日志:pdf.reflow.prepare 是 prepare 总耗时;pdf.reflow.prepare.cache(phase=read|write)是缓存 IO,pdf.reflow.prepare.extract 是 PDFium 抽取,pdf.reflow.cache 记录 result=hit|miss 与页数;pdf.reflow.page / pdf.reflow.final 仍是预览和导出耗时。命中后 prepare 只剩缓存解压与 item 映射,页构建与最终 PDF 仍会按页重算。
  • 构建结果缓存(仅整篇):受 cacheReflowFinal 开关控制(默认关);开启后最终译文 PDF 持久化在 appCacheDir()/reflow-build/<pdf 哈希前缀>-<内容键>.pdf,按原样存为可直接打开的 PDF。键覆盖整篇 IR 内容、全部实际译文、字体字节长度与资源 revision、构建管线版本(REFLOW_BUILD_PIPELINE_VERSION,layout/export 输出变化时必须提升);键内容规范化——段落/公式/障碍按 id 排序后参与指纹,页级运行重新合并 items 造成的绘制顺序抖动(内容语义不变)不会导致 miss。命中时 pdf.reflow.final 不再重跑 PDFium 去字、公式片段抽取、字体嵌入和 pdf-lib 序列化,读写走 pdf.reflow.buildCache(phase=read|write、result=hit|miss),命中后排版度量在内存中按同一字体重算,逐页渲染照旧。逐页预览构建不落盘:每页 PDF 都完整嵌入 CJK 字体对(~20 MB/页),缓存它们会让每篇占用数百 MB,因此页构建仍每次执行(pdf.reflow.page 照常出现),只有 final 复用。任何一层校验不符(换 PDF、译文变化、字体更新、管线升级)都按 miss 重建,构建缓存失败从不阻断翻译。单页隐藏 / 恢复会改变译文集合,final 随之 miss 一次属预期;同一篇的 final 按 mtime 只保留最新 2 份,更旧的与早期逐页缓存遗留文件在写入时清扫,磁盘占用约 25–60 MB/篇。

逐页重排预览(#838)

翻译开始准备字符 IR 时即预取固定字体。某页所有条目结束后,独立串行队列提取单页 PDF, 仅处理该页的文字删除和公式片段,并以局部字号排版、渲染;其他页仍可继续翻译。 自动重试期间的 pending/running 条目会阻止该页预览;最终失败/跳过的条目保留原文, 其 IR 框仍参与避让。停止后的部分结果沿用原有可导出语义。

预览按页保存,与翻译批次状态分离。只有该页译文、状态、IR 或版面区域变化时才失效, 其他页的批次不会取消正在构建的页;清空、重试和关闭文档会丢弃过期结果并释放 URL。 全文结束后执行文档级 common scale 校正和完整 PDF 构建,只重新渲染布局变化的页, 每页渲染完即显示;全部完成后才开放译文/双语 PDF 导出。导出对话框期间若结果失效, 不会继续写出旧结果。

诊断日志包含 pdf.layout.complete、pdf.translate.first-result、pdf.translate.complete, 以及 pdf.reflow.prepare、pdf.reflow.prepare.cache、pdf.reflow.prepare.extract、pdf.reflow.cache、 pdf.reflow.buildCache、pdf.reflow.fonts/page/final/render 的 duration_ms。首次缓存和热缓存应分开比较。 页级构建与最终导出复用同一排版、公式和链接处理;保持 subset: false 防止 CJK 缺字。 字体字节与下载 Promise 已缓存,嵌入对象仍属于各自 PDF,不能跨文档直接复用。

限制与后续:整篇版面分析及字符 IR 准备仍在首批翻译之前;完整字体嵌入、逐公式裁剪仍有 CPU 开销, 2x 预览图片暂未按可视区域淘汰。仍需同 PDF、同服务的端到端阶段占比与桌面交互耗时实测, 据此评估字体写出 Worker、公式缓存及可视页内存预算。