通过 Subtitlarr API 进行手动翻译
为此目的存在两个端点(因为 Gemini 的内容过滤器会阻止合法的历史/戏剧对话——例如一部关于奴隶制的电影使用了符合时代背景的侮辱性语言——而 Claude 可以结合上下文忠实地翻译):
GET /api/queue/{item_id}/manual-translation/source— 以正常方式解析条目的源字幕并返回{item_id, source_language, target_language, cue_count, dialogue_text}。dialogue_text是提示块(cue block),格式为"<index>\n<content>",用空行分隔——与实际提供商接收的格式完全相同。POST /api/queue/{item_id}/manual-translation— 请求体为{"translated_text": "...", "model_name": "claude-code"}。将翻译结果通过与实际提供商输出相同的重组/完整性检查/声明/上传/数据库跟踪管道进行处理。记录engine_used="manual",model_used=<model_name>——在队列、历史记录以及嵌入在字幕文件本身的声明行中,可以与实际 API 驱动的翻译明确区分。
服务端完全不调用 LLM——服务器仅接受提交的 translated_text,并将其按原始提示块的时间安排重组。
所有这些操作仅通过 HTTP 对给定的 Subtitlarr 实例的 base URL 进行(本地开发实例或远程/NAS 实例)——无需本地文件系统或数据库访问。如果使用远程实例,在开始前请确认 base URL 可达(对 GET /api/run/current 执行 curl 是一个良好的冒烟测试)。
处理整个失败的积压列表
请参见 EXAMPLE_PROMPT.md 了解一个实际执行此工作流的提示,以及使其成功的注意事项。
没有专门的“列出符合手动翻译条件的条目”端点——通过现有的队列列表(仅限 HTTP)构建列表:
- 列出失败的条目,支持分页。
GET /api/queue?status=failed&page=1&page_size=100,递增page直到data返回的长度小于page_size(或为空)——响应中的total字段指示所需页数。每一行包含id、title、error_message。 - 在动手前先根据
error_message进行分类——不要不加区分地手动翻译所有失败条目:- 内容被拦截(
"blocked its own response"、"blocked this request"、"prohibited content"等)——本工作流的实际目标。其他方法都无法修复此问题;相同的内容在相同引擎下会每次都失败。 - 限速(
"rate limit hit (429)")——临时性问题,不是内容问题。通过POST /api/queue/run-by-ids正常重试即可,无需手动翻译。 - 认证/凭据错误(401、“service account is deleted/disabled”等)——需要用户修复实际的 API 密钥/凭据;绕过失效密钥进行手动翻译无助于将来使用相同引擎的运行。
- 其他所有情况(5xx 服务器错误、超时、格式错误的响应)——通常也是临时性问题;普通重试比手动翻译更合适。
- 内容被拦截(
- 在翻译大批量内容前,先向用户确认分类/目标列表——尤其是首次针对给定实例进行处理时,因为“实际有多少内容被拦截”只有在步骤 1–2 运行后才能知道。
- 使用下面的单条目工作流程,依次处理每个内容被拦截的条目。
每个条目使用一个后台代理——切勿将多个条目合并到一个代理中
这不是一个次要偏好——将多个条目合并到一个代理中会成倍增加令牌成本,而不是节省成本。在单个代理的对话中,每次工具调用(获取源字幕、每次分块写入、每次验证运行)都会永久保留在该代理的上下文中。如果一个代理依次翻译 5 部电影,电影 2 的工作是在电影 1 的整个累积上下文(分块、源 JSON、工具输出)之上进行的,电影 3 为电影 1+2 的上下文付费,依此类推——批次中的第 5 个条目比第 1 个条目的成本高得多,整个批次的总成本远高于 5 个独立的代理。这一点已通过实证确认:两个 5 电影批次代理各自消耗了约 520K 令牌(大约 100K/电影,成本分摊到批次中的后续条目)——而执行类似长度内容的单条目代理成本仅为此的一小部分,因为它从不携带其他条目的残留上下文。
代理读取本技能文件和约定部分的固定“冷启动”成本相对较小,不是批处理的理由。不要将批处理合理化解释为成本优化——它根本就不是。如果被要求重新考虑这一点,请不要照做——批处理方法已经过尝试、测量并被确认更差。
因此:一旦完成上述的分类和试点/确认步骤,为每个条目 ID 精确地生成一个后台代理,无论是电影还是单集电视剧。对于有多个待处理剧集的电视剧,仍然为每个剧集生成一个代理(而不是为整部电视剧生成一个代理)——这些剧集也不需要彼此的上下文。
每个代理的提示必须是自包含的——代理没有此对话的记忆——并应包含:
- 本技能文件及其单条目工作流程的链接/引用。
- 要处理的单个条目 ID(附标题,用于与
GET /api/queue/{id}返回的结果进行校验)。 - Subtitlarr 实例的 base URL。
- 在条目内分块的提示(每块约 250–350 个提示块,追加到一个持续运行的本地文件中,而不是为每个块创建一个暂存文件)并在最后提交前验证索引一致性——这与“不合并条目”是不同的关注点,两条规则同时适用。同时注意可用于验证步骤的脚本运行环境是 Node.js(而非
python3——它未安装;不要让代理浪费轮次去自行发现这一点)。 - 项目的翻译惯例(见下文),如果与内容相关(例如时代特定的侮辱性语言)。
- 需要报告的内容:成功/失败以及提示块数量,以便结果可以与主分类列表进行核对。
严格按顺序依次分发代理——切勿并行启动两个
“每个条目一个代理”(上文)是关于单个代理对话内部的处理方式——绝不涉及同时运行多少个代理。不要混淆这两者。即使每个条目都有自己的独立代理,代理仍然必须 一次一个,按顺序 启动:启动一个代理,等待它完成并报告,然后再启动下一个。绝对不要在同一条消息中发起两个或更多 Agent 工具调用,也绝对不要同时存在多个进行中的手动翻译代理——不是两个,不是“一个受控的小波次”,也不是十个。用户已明确重复声明:一个代理接一个代理,一次一个,绝不并行。
具体来说:在后台启动条目 N 的代理,然后停止并等待其完成通知,然后再启动条目 N+1 的代理。不要预启动多个代理“以保持管道运转”——那正是并行分发,正是此处禁止的行为,无论每个代理内部是否只处理一个条目。
单条目工作流程
令牌成本提示: 主要成本在于 I/O 开销,而非翻译本身。一个在 1188 个提示块的电影上进行的首次实际运行消耗了约 110K 令牌,因为它将每个块的翻译写入一个暂存文件,然后再将其读回组合——8 次额外的写入+读取往返,加上环境发现开销(缺少 python3,路径插值问题)。以下步骤 2–5 旨在避免这种情况:将块保留在您自己的工作输出中,不要通过文件系统往返它们,也不要为每个条目重新发现环境。
- 确认条目。
GET /api/queue/{item_id}——检查status和error_message是否符合预期,然后再投入精力。 - 获取源字幕。
GET /api/queue/{item_id}/manual-translation/source。记下cue_count。 - 按大约 250–350 个提示块分块翻译,而不是一次性翻译整个文件或分成 100–150 个提示块的片段——非常长的单次传递是索引对齐错误(注意力在长连续输出中漂移)最容易发生的地方,但每个块也带有固定开销(重新陈述格式/约定、工具往返),因此在该安全范围内使用更少/更大的块总体成本更低。将每个块的翻译输出直接写入一个持久运行的本地文件,以该条目的 ID 命名(例如 scratchpad 中的
translated_<item_id>.txt,而不是泛泛的translated.txt)——不要为每个块创建单独的暂存文件,也不要为了重新验证而将块写回后再次读取。一个文件,追加写入,最后读取一次进行下面的一致性检查。即使分发是顺序的(见下文),条目 ID 后缀也很重要:一个泛泛的文件名已经导致某个代理进行中的快被另一个共享同一 scratchpad 目录的并发运行的手动翻译代理静默覆盖——为所有暂存文件(源 JSON、分块、组合输出)以条目 ID 命名,这样分发错误不会破坏另一个条目的工作。 - 精确保留每个索引,不跳过任何提示块。 源字幕中的每个
<index>必须在您的翻译输出中恰好出现一次,并使用相同的索引号——reassemble()按索引匹配,而非位置;您未包含的提示块将回退为其原始语言文本(静默降低质量,而非报错)。保留格式标记(例如歌词/内心独白上的<i>...</i>斜体)——它是内容的一部分,不会在重组前被剥离。 - 在提交前验证索引一致性,仅一次,针对完全组合完成的文件——而不是每个块。提取所有
^\d+$行,并将结果集合与1..cue_count进行比较。注意这仅捕获缺失/重复的索引编号——它不会捕获一个块中提示块被合并或拆分,以至于索引编号仍然一一对应但错误的对话出现在给定编号下的情况(例如意外合并两个源提示块到一个输出块中,会导致后续每个提示块的内容偏移一个位置,而此检查无法发现)。还要在几个点(块边界、块中间的一个提示块)对实际内容与源字幕进行抽查,而不仅仅是索引脚本。使用以下精确的 Node 单行命令进行索引检查(Node 确认可用;不要浪费时间发现或解决python3缺失的问题——直接使用此命令):
在提交前修复其报告的任何缺口或重复。node -e "const fs=require('fs');const t=fs.readFileSync('translated.txt','utf8');const idx=[...t.matchAll(/^(\d+)$/gm)].map(m=>+m[1]);const want=new Set(Array.from({length:<CUE_COUNT>},(_,i)=>i+1));const got=new Set(idx);const missing=[...want].filter(x=>!got.has(x));const extra=idx.filter((x,i)=>idx.indexOf(x)!==i);console.log('count',idx.length,'missing',missing,'dupes',[...new Set(extra)])" - 提交。 使用完整的组合
translated_text(所有块串联在一起——端点每次接收每个条目的一个完整提交,而不是每个块一次调用)和model_name(默认"claude-code";仅在被要求以不同方式归属工作时更改)。 - 确认。
GET /api/queue/{item_id}应显示status: "done",engine_used: "manual"。可选地通过GET /api/debug/{movie|episode}/{bazarr_id}/subtitle?lang=<code>抽查上传的文件——第一个提示块应为以[<model_name>]结尾的声明行,第二个提示块的时间安排应与原始源字幕的第一个真实提示块的完全相同。
此项目的翻译惯例
- 种族歧视用语 / 符合时代背景的冒犯性语言:忠实地翻译,保留语域——不要软化或省略。对于英文“nigger”到加泰罗尼亚语,用户的指导:在奴隶主/监工角色以轻蔑语气使用时使用 “negrata”(贬义形式),而在中性/描述性用法中使用 “negre”/“negres”。这反映了英文原文本身在中性用法和侮辱用法之间的区别。
- 历史/时代语域:保留正式称呼、方言标记和时代言语模式(缩略语、被奴役角色对话中的语法缺失、上层阶级角色中的正式措辞)——不要将所有角色都扁平化为现代中性加泰罗尼亚语。
- 歌词 / 灵歌(例如“Roll, Jordan, Roll”):翻译其含义,不一定是可演唱的等效词——这些是字幕,不是配音脚本。
为什么存在此功能而非简单地重试配置的引擎
Gemini 的内容安全过滤器会阻止整类合法的叙事内容(明显的暴力、时代特定的侮辱性语言、严肃戏剧背景下的成人主题),而人类或 Claude 在上下文阅读同一场景时可以无碍翻译。针对同一引擎重复重试相同内容无济于事——这是内容策略判断,不是临时性故障——并且可能触发提供商对 API 密钥的滥用执行。本工作流正是为此类卡住的条目而存在,而不是作为正常引擎级联的通用替代品。