用 Index-Translate 批量翻文档 / 术语一致性得自己维护

文章导读
用 Index-Translate 这类流程批量翻文档,慢点通常不在翻译本身,而在两头:一是几十上百份文件没法只跑一次就完事,中途报错、超长段落、接口超时都会打断;二是同一术语在不同段落、不同文档里被译成不同说法,读者看到前后不一致,比漏译还难修。比较稳的做法是把批处理拆成四步:把待翻文件扫成任务清单、按段落分片并保留位置标记、用术语表在译前替换译后还原、最后跑一遍译后校验。每一步都能单独验证,出
📋 目录
  1. 一 把待翻文件扫成一份任务清单
  2. 二 按段落分片并保留原文位置标记
  3. 三 用术语表在译前替换、译后还原
  4. 四 写译后校验脚本比对段落数与占位符
  5. 五 记录失败片段并单独重跑
A A

用 Index-Translate 这类流程批量翻文档,慢点通常不在翻译本身,而在两头:一是几十上百份文件没法只跑一次就完事,中途报错、超长段落、接口超时都会打断;二是同一术语在不同段落、不同文档里被译成不同说法,读者看到前后不一致,比漏译还难修。比较稳的做法是把批处理拆成四步:把待翻文件扫成任务清单、按段落分片并保留位置标记、用术语表在译前替换译后还原、最后跑一遍译后校验。每一步都能单独验证,出问题也容易定位到具体文件和段落。Index-Translate 只负责“翻译”这一段,术语一致性和断点续跑要自己在外面维持。

批量翻文档的核心不是把翻译接口调通,而是把任务状态留在磁盘上:任务清单管来源与进度,分片标记管回填,术语表管一致性,校验脚本管漏译。适用场景是几十到上百份结构化文档的离线批翻;操作上先跑任务清单生成、再分片、再替换翻译、再校验;验证方式是比对段落数与占位符、检查异常清单;边界是术语表只能约束你列出的词,遇到多义词、嵌套术语仍需要人工抽检,别指望脚本一次到位。

把待翻文件扫成一份任务清单

批量任务要能断点续跑,前提是每份文件的处理状态落盘,而不是放在内存里。用一个 JSONL 或 CSV 任务文件,字段至少包含源路径、目标路径、状态、错误信息和更新时间。状态字段是续跑的锚点:每次启动时只挑状态为 pending 或 failed 的记录。脚本骨架可以这样写:

# task_scan.py
import os, json, hashlib

SRC_DIR = "./docs_in"
DST_DIR = "./docs_out"
TASK_FILE = "./tasks.jsonl"

def scan(src_dir):
    seen = set()
    if os.path.exists(TASK_FILE):
        with open(TASK_FILE, encoding="utf-8") as f:
            for line in f:
                seen.add(json.loads(line)["src"])
    with open(TASK_FILE, "a", encoding="utf-8") as out:
        for root, _, files in os.walk(src_dir):
            for name in files:
                if not name.lower().endswith((".md", ".txt", ".html")):
                    continue
                src = os.path.join(root, name)
                if src in seen:
                    continue
                rel = os.path.relpath(src, src_dir)
                dst = os.path.join(DST_DIR, rel)
                rec = {
                    "src": src,
                    "dst": dst,
                    "status": "pending",
                    "error": "",
                    "updated": ""
                }
                out.write(json.dumps(rec, ensure_ascii=False) + "\n")
                seen.add(src)

if __name__ == "__main__":
    os.makedirs(DST_DIR, exist_ok=True)
    scan(SRC_DIR)

续跑时只需遍历 tasks.jsonl,跳过 status 为 done 的记录。注意不要在扫描时用文件哈希做去重键,改内容后路径不变会让旧记录被误判为已完成;如果确实需要识别内容变化,单独加 mtime 或 hash 字段,由你决定是否重跑。

按段落分片并保留原文位置标记

整篇丢给翻译接口容易超长截断,也让译文回填失去对齐依据。建议按空行或标题切段,每段生成唯一标记,例如 [[S0001]],标记单独成行,紧贴段落上方。分片时把 {marker, text} 写入一个 sidecar 文件,译后按 marker 顺序回填。标记本身不要放进要翻译的文本里,否则模型可能改写、丢弃或翻译标记。检查方式很简单:翻译前后各跑一次正则统计 \[\[S\d{4}\]\] 的出现次数与顺序,数量或顺序不一致就说明分片或回填环节出错了。

用 Index-Translate 批量翻文档 / 术语一致性得自己维护

如果文档本身含有类似方括号的语法,换个不太可能出现在正文里的标记格式,例如 <<SEG001>>,并在校验脚本里同步改正则。段落过长的(比如超过接口单次限制)先按句号或换行做二级切分,标记用 S0001-01 这种带父级编号的形式,回填时再拼回原段。

用术语表在译前替换、译后还原

术语一致性靠翻译模型自己保持并不可靠,尤其是同一术语在不同段落里被拆成不同语境时。做法是维护一张术语表,字段至少包含 source、target、alias、case_sensitive、priority。翻译前把原文里的术语替换成不太可能被翻译的占位 token,例如 __T_HTTP2__,翻译后再把 token 还原成目标译法。

用 Index-Translate 批量翻文档 / 术语一致性得自己维护
import json, re

TERMS = [
    {"source": "Index-Translate", "target": "索引翻译",
     "alias": [], "case_sensitive": True, "priority": 100},
    {"source": "pipeline", "target": "流水线",
     "alias": ["pipelines"], "case_sensitive": False, "priority": 50},
]

def build_terms(terms):
    items = []
    for t in terms:
        forms = [t["source"]] + t.get("alias", [])
        for f in forms:
            items.append((f, t))
    # 长词优先,避免子串误替换
    items.sort(key=lambda x: (len(x[0]), x[1]["priority"]), reverse=True)
    return items

def apply_terms(text, items):
    for form, t in items:
        token = "__T_%s__" % t["source"].upper().replace(" ", "_")
        flags = 0 if t["case_sensitive"] else re.IGNORECASE
        text = re.sub(re.escape(form), token, text, flags=flags)
    return text

def restore_terms(text, items):
    for form, t in items:
        token = "__T_%s__" % t["source"].upper().replace(" ", "_")
        text = text.replace(token, t["target"])
    return text

还原的匹配顺序要跟替换一致:长术语和 alias 先处理,短术语后处理,否则 Index-Translate 可能先被 Index 之类的子串规则命中。发生冲突时,比如同一个 target token 对应多条术语,按 priority 最高的一条生效,其余在日志里记一条 warning,别静默丢弃。注意 token 本身如果被翻译模型改写,还原会失败,所以校验脚本里要专门检查 token 是否原样保留。

写译后校验脚本比对段落数与占位符

校验不评估翻译质量,只确认结构完整:段落数量是否一致、占位符是否一一对应、是否存在空译文。建议输出一份异常清单,每行包含文件路径、缺失或多余的标记、异常类型。

import re, json, os

MARKER = re.compile(r"\[\[S\d{4}\]\]")
TOKEN = re.compile(r"__T_[A-Z0-9_]+__")

def check(src_text, dst_text, path):
    issues = []
    sm = MARKER.findall(src_text)
    dm = MARKER.findall(dst_text)
    if len(sm) != len(dm):
        issues.append({"file": path, "type": "marker_count",
                       "src": len(sm), "dst": len(dm)})
    st = sorted(set(TOKEN.findall(src_text)))
    dt = sorted(set(TOKEN.findall(dst_text)))
    if st != dt:
        issues.append({"file": path, "type": "token_mismatch",
                       "missing": list(set(st) - set(dt))})
    for i, seg in enumerate(dst_text.splitlines()):
        if MARKER.match(seg.strip()) is None and seg.strip() == "":
            issues.append({"file": path, "type": "empty_segment",
                           "line": i + 1})
    return issues

if __name__ == "__main__":
    report = []
    for rec in map(json.loads, open("./tasks.jsonl", encoding="utf-8")):
        if rec["status"] != "done":
            continue
        src = open(rec["src"], encoding="utf-8").read()
        dst = open(rec["dst"], encoding="utf-8").read()
        report.extend(check(src, dst, rec["dst"]))
    with open("./issues.jsonl", "w", encoding="utf-8") as f:
        for r in report:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")

这份清单只用来决定哪些文件需要人工看或重跑,别拿它当翻译质量结论。空译文检查容易误报列表项和代码块,如果你的文档里有大量短行,先把判断条件调成“连续两行以上为空”再跑。

用 Index-Translate 批量翻文档 / 术语一致性得自己维护

记录失败片段并单独重跑

超长片段和接口报错不可能靠调大重试次数解决,得先记下来。失败清单建议按 file、marker、reason、retry_count、last_error 五个字段写入 JSONL,每次重跑前先读这个文件,只重试这些 marker 对应的片段。重跑时不要整篇覆盖:把成功片段的译文读进内存,只替换失败 marker 对应的段落,再写回目标文件。

# retry_failed.py 要点
# 1. 读 failed.jsonl,按 file 分组
# 2. 对每个 file,先读已生成的 dst 文件
# 3. 只对 failed 里的 marker 重新翻译
# 4. 用 marker 定位并替换 dst 中的对应段落
# 5. 成功后从 failed 清单移除,追加到 done 清单

要避免覆盖已成功结果,有两处要卡住:一是重跑前用 os.path.exists 和 marker 双重确认,不整篇覆盖;二是重跑脚本只在 marker 级别做替换,写回时保留其它段落不动。处理完后建议把 tasks.jsonl 里对应记录的状态改回 done,并更新时间字段,否则下次续跑又会把它扫进来。单条失败片段可以继续沿用同一个术语表,别在重跑时切换术语表版本,否则跨段落的一致性会被自己打乱。