ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用DeepSeek API打造自动化字幕翻译流水线:Python实现SRT批量翻译指南

用DeepSeek API打造自动化字幕翻译流水线:Python实现SRT批量翻译指南 拿到一部 1995 年的旧动画 OVA只有英文字幕想快速转成中文字幕时第一反应是什么用在线翻译一段段贴找字幕组大佬手工翻还是放弃在线翻译对碎片字幕缺乏上下文错译频繁手工翻译质量高但时间成本太高。DeepSeek 这类大模型出来后这件事出现了一条更务实的路径写好脚本解析字幕格式把字幕按语义分段发给模型翻译再把结果写回规范的时间轴文件。这篇文章要解决的问题不是“模型能不能翻译”而是“怎么把字幕翻译做成一条稳定、可复用、质量可控的工程流水线”。无论你手里是 1995 年的老 OVA还是自己录制的视频、播客、课程这套思路都可以直接落地。文中的代码以 Python 编写重点演示 SRT 格式的解析、DeepSeek API 的调用、分段策略、重试机制、术语一致性和格式校验。最终你会得到一份可直接运行的脚本并理解每一步为什么要这么做。1. AI 字幕翻译的边界与机会先给一个明确判断DeepSeek 字幕翻译的瓶颈从来不是“翻译得准不准”而是工程链路的完整性。很多人以为用大模型翻译字幕就是把字幕文本丢给模型模型返回中文就完事了。但在实际处理中会遇到几个非常现实的问题字幕是分段存储的每一条有时间轴、序号、文本直接丢给模型会把格式搞乱。字幕翻译需要上下文只翻译单句会出现前后矛盾人名、术语在不同帧之间不一致。长字幕超出模型单次输入上下文上限需要分段但分段策略如果太粗暴会在句子中间断开导致翻译结果断裂。模型输出偶尔会带有额外的说明文字比如“以下是翻译结果”这些内容不能混入字幕文件。API 调用偶尔会成功但返回空内容或者因为网络问题超时脚本必须重试。这五条每一条都能让一个看起来“挺简单”的脚本翻车。这篇文章的价值就是把这五条逐项拆开给出可执行的代码方案。另一个关键判断字幕翻译本质上是结构化文本转换任务非常适合大模型但必须给它保留格式边界的约束能力。换句话说人负责设计任务框架模型负责语言转换脚本负责格式保真。三者分工才是工程化的正确姿势。2. 字幕翻译的核心难点拆解先理解 SRT 字幕的典型结构。一个 SRT 文件由多条字幕组成每条字幕包含序号从 1 递增。时间轴00:00:01,000 -- 00:00:04,000表示该条字幕的起始和结束时间。正文可以是单行或多行。示例1 00:00:00,500 -- 00:00:03,000 Hello everyone, welcome to my channel. 2 00:00:03,500 -- 00:00:06,000 Today we are talking about AI subtitle translation.如果直接把整个文件的内容交给模型模型可能会把序号、时间轴也翻译掉或者把时间轴格式从逗号改成点导致字幕无法被播放器正确识别。字幕翻译的第二个难点是上下文依赖。比如英文人名或特殊缩写在字幕第 10 条出现一次第 50 条又出现一次如果单条翻译前一次译成“约翰”后一次译成“约翰尼”观看体验就会很差。第三个难点是语言风格一致性。动画字幕通常需要口语化、自然一些语气词、称呼语需要统一。旧 OVA 的台词更是如此不同角色有不同说话习惯如果模型没有足够上下文很难维持人物口吻。第四个难点是成本和效率。字幕条数少则几百条多则上千条。逐条调用 API耗时和费用都需要控制全部塞进一次调用上下文放不下输出也容易失控。所以一个完整的字幕翻译系统至少要包含五个模块字幕解析器读取 SRT拆分成结构化数据。分段器把相邻且有语义关联的字幕组成批次批次大小可控。翻译器调用 DeepSeek API要求模型按指定 JSON 或纯文本格式返回保留序号和时间轴。写回器将模型返回的数据解析、校验、写回 SRT。兜底机制重试、失败标记、日志记录。后面每个模块都会给出代码。3. DeepSeek API 环境准备3.1 获取 API KeyDeepSeek 开放了 API兼容 OpenAI 的接口风格。你需要先在官网注册账号创建一个 API Key。这个 Key 只显示一次创建后要立即保存。从材料看社区里大量讨论集中在 DeepSeek 的 API 调用、本地部署和各类第三方工具集成这说明 DeepSeek 的接入方式已经比较成熟。代码里只需要设置两个核心参数api_key: 你自己的密钥。base_url: DeepSeek 官方接口地址本文不写死具体值以官网文档为准。本文示例使用环境变量方式读取避免把密钥写进代码仓库。3.2 Python 环境建议使用 Python 3.9 及以上版本。需要安装两个库openai: DeepSeek 兼容 OpenAI SDK可以直接用openai库调用也可以使用requests直接发 HTTP 请求。tqdm: 用于显示进度条方便看批量翻译进度。安装命令pip install openai tqdm如果网络受限也可以只用标准库urllib.request发送请求但可读性和错误处理会差一些本文使用openai库演示。3.3 环境变量配置在项目根目录创建.env文件用于存放密钥建议加入.gitignore然后使用os.getenv读取。为了让示例更通用本文直接使用os.environ不引入python-dotenv你可以根据团队习惯调整。4. 完整代码实现DeepSeek 批量翻译 SRT 字幕这一节的目标是提供一个可运行的最小闭环脚本。流程如下读取 SRT 文件。解析出字幕对象列表。将字幕分组合并成批次。每组批次调用 DeepSeek API 翻译返回 JSON 格式结果。解析结果写回新的 SRT 文件。4.1 解析 SRT 文件先实现一个最简单的 SRT 解析函数。它把 SRT 文本解析成SubtitleItem列表。# 文件路径srt_parser.py from dataclasses import dataclass import re dataclass class SubtitleItem: index: int start: str end: str text: str def parse_srt(content: str) - list[SubtitleItem]: 解析 SRT 字幕内容返回字幕对象列表。 只做基础解析序号、时间轴、文本。 blocks re.split(r\n\s*\n, content.strip()) items [] for block in blocks: lines [line.strip() for line in block.strip().split(\n) if line.strip()] if len(lines) 2: continue # 第一行通常为序号 try: index int(lines[0]) except ValueError: # 某些 SRT 文件第一行不一定是序号这里只处理标准格式 index len(items) 1 # 第二行通常为时间轴 time_line lines[1] match re.search(r(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*--\s*(\d{2}:\d{2}:\d{2}[,.]\d{3}), time_line) if match: start match.group(1).replace(., ,) end match.group(2).replace(., ,) else: start 00:00:00,000 end 00:00:00,000 text \n.join(lines[2:]) items.append(SubtitleItem(indexindex, startstart, endend, texttext)) return items关键点用空行分割字幕块。时间轴允许点号或逗号作为毫秒分隔符统一转成逗号。文本部分保留多行内容方便后续合并上下文。4.2 字幕分组与批次构造字幕条目数量多逐个翻译效率低上下文也不连续。更好的做法是按每批最多字符数进行分组一个批次里包含多条字幕模型一次翻译完一个批次。这里用max_chars控制批次大小默认 1500 字符。这个值需要根据模型的上下文能力调整字幕文本较简单1500 字符在安全范围内。# 文件路径batching.py from srt_parser import SubtitleItem def build_batches(items: list[SubtitleItem], max_chars: int 1500) - list[list[SubtitleItem]]: 将字幕条目按最大字符数分批。 保留原有顺序避免跨批次乱序。 batches [] current_batch [] current_len 0 for item in items: item_len len(item.text) if current_len item_len max_chars and current_batch: batches.append(current_batch) current_batch [] current_len 0 current_batch.append(item) current_len item_len if current_batch: batches.append(current_batch) return batches这里真正容易踩坑的地方是不能只按条数分配比如每 10 条一批因为字幕长短差异很大。有的字幕只有两三个词有的则是大段独白。按字符数分配更均匀也更接近模型的 token 消耗。4.3 调用 DeepSeek API 翻译批次这是整个流程的核心部分。为了让结果稳定、可解析我要求模型返回 JSON 数组每一条包含index、start、end、text四个字段。同时用response_format{type: json_object}提示模型输出 JSON 对象。注意不同模型对 JSON 输出的支持程度不同。如果模型不支持强制 JSON可以退回到“只输出纯文本按序号和分隔符解析”的策略。本文以 DeepSeek 兼容 OpenAI 接口为例。# 文件路径translator.py import json import os import time from openai import OpenAI from srt_parser import SubtitleItem def create_client(): 创建 OpenAI 兼容客户端。 base_url 从环境变量读取避免硬编码。 api_key os.getenv(DEEPSEEK_API_KEY) base_url os.getenv(DEEPSEEK_BASE_URL) if not api_key: raise RuntimeError(请设置环境变量 DEEPSEEK_API_KEY) return OpenAI(api_keyapi_key, base_urlbase_url) def build_prompt(batch_index: int, total_batches: int, batch: list[SubtitleItem]) - str: 构造翻译提示词。 明确要求模型保留序号、时间轴只翻译文本部分。 subtitle_data [] for item in batch: subtitle_data.append({ index: item.index, start: item.start, end: item.end, text: item.text, }) prompt f 你是一名专业的字幕翻译。当前是第 {batch_index} / {total_batches} 批次。 请将以下英文字幕翻译成简体中文。要求 1. 翻译自然、口语化符合字幕阅读习惯。 2. 保留原始序号和时间轴不要修改。 3. 只翻译 text 字段不要添加解释。 4. 保持人名、术语在不同批次之间一致。 5. 输出 JSON 对象格式如下 {{translations: [{{index: 1, start: 00:00:00,500, end: 00:00:03,000, text: 翻译后的文本}}]}} 字幕内容 {json.dumps(subtitle_data, ensure_asciiFalse, indent2)} return prompt def translate_batch(client, batch_index: int, total_batches: int, batch: list[SubtitleItem], model: str) - dict: 翻译一个批次返回解析后的 JSON 对象。 prompt build_prompt(batch_index, total_batches, batch) response client.chat.completions.create( modelmodel, messages[ {role: system, content: 你是专业的字幕翻译引擎只输出 JSON。}, {role: user, content: prompt}, ], temperature0.3, response_format{type: json_object}, ) content response.choices[0].message.content parsed json.loads(content) return parsed几个设计细节temperature0.3是为了减少创造性发挥字幕翻译更强调准确性。response_format能显著提高 JSON 输出的稳定性但如果你的 API 版本不支持可以去掉。提示词里写清楚了index和start/end必须保留模型基本不会乱改。4.4 结果写回 SRT模型返回的 JSON 数组可能顺序不一致也可能出现个别条目缺失。写回时需要转成{index: SubtitleItem}的字典。按照原始 SRT 的序号顺序写回。如果某个序号缺失标注原文本避免静默丢字幕。# 文件路径writer.py from srt_parser import SubtitleItem def items_to_srt(items: list[SubtitleItem]) - str: 将字幕对象列表序列化成 SRT 文本。 lines [] for item in items: lines.append(str(item.index)) lines.append(f{item.start} -- {item.end}) lines.append(item.text) lines.append() return \n.join(lines) def merge_translations(original_items: list[SubtitleItem], translations: list[dict]) - list[SubtitleItem]: 将翻译结果按原始序号顺序合并。 translations 来自模型返回的 JSON 数组。 trans_map {} for t in translations: try: idx int(t.get(index)) trans_map[idx] t except (KeyError, TypeError, ValueError): continue merged [] for item in original_items: t trans_map.get(item.index) if t and t.get(text): merged.append(SubtitleItem( indexitem.index, startitem.start, enditem.end, textt[text], )) else: # 缺失翻译时保留原文并加标记方便排查 merged.append(SubtitleItem( indexitem.index, startitem.start, enditem.end, textf[未翻译] {item.text}, )) return merged这里必须保留原始字幕的start和end不要使用模型返回的时间轴。因为即使提示词要求保留模型仍可能出现微小的格式变化比如把逗号改成点。以原始文件为准最稳妥。4.5 主流程串联接下来把上面的模块串联成main.py。这个脚本读取输入文件、翻译、输出目标文件。# 文件路径main.py import os import sys from openai import OpenAI from srt_parser import parse_srt from batching import build_batches from translator import create_client, translate_batch from writer import items_to_srt, merge_translations def main(input_path: str, output_path: str, model: str deepseek-chat, max_chars: int 1500): with open(input_path, r, encodingutf-8) as f: content f.read() items parse_srt(content) print(f解析到 {len(items)} 条字幕) batches build_batches(items, max_charsmax_chars) print(f共分为 {len(batches)} 个批次) client create_client() all_translations [] for idx, batch in enumerate(batches, start1): print(f正在翻译第 {idx}/{len(batches)} 批...) try: result translate_batch(client, idx, len(batches), batch, model) translations result.get(translations, []) all_translations.extend(translations) except Exception as e: print(f批次 {idx} 翻译失败: {e}) # 失败时把原始条目也加入避免丢失 for item in batch: all_translations.append({ index: item.index, text: item.text, start: item.start, end: item.end, }) merged_items merge_translations(items, all_translations) srt_text items_to_srt(merged_items) output_dir os.path.dirname(output_path) if output_dir and not os.path.exists(output_dir): os.makedirs(output_dir, exist_okTrue) with open(output_path, w, encodingutf-8) as f: f.write(srt_text) print(f完成输出文件{output_path}) if __name__ __main__: if len(sys.argv) 3: print(用法: python main.py input.srt output.srt [model] [max_chars]) sys.exit(1) input_file sys.argv[1] output_file sys.argv[2] model_arg sys.argv[3] if len(sys.argv) 3 else deepseek-chat max_chars_arg int(sys.argv[4]) if len(sys.argv) 4 else 1500 main(input_file, output_file, model_arg, max_chars_arg)运行命令示例export DEEPSEEK_API_KEY你的密钥 export DEEPSEEK_BASE_URL你的base_url python main.py input_en.srt output_zh.srt deepseek-chat 1500如果你不方便设置环境变量可以在main.py里临时改成直接赋值但不建议提交到仓库。5. 增强方案重试机制与并发控制上面这个版本能跑通但生产环境使用时有三个明显不足没有重试机制。网络抖动或 API 限流会导致批次翻译失败。没有并发控制。逐批串行调用速度慢。没有日志记录。翻译失败后不知道具体哪个批次、哪个条目出了问题。实际字幕文件常有上千条字幕逐批调用如果每批耗时 10 秒100 批就是 1000 秒大约 17 分钟。通过并发能把时间压缩到 3~5 分钟但并发过高可能触发 API 限流需要设置合理上限。5.1 带重试的调用逻辑使用tenacity库可以优雅实现重试pip install tenacity改造后的翻译函数# 文件路径translator_with_retry.py import json from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from openai import OpenAI, RateLimitError, APIConnectionError def translate_batch_with_retry(client, batch_index, total_batches, batch, model, max_retries3): retry( stopstop_after_attempt(max_retries), waitwait_exponential(multiplier1, min2, max10), retryretry_if_exception_type((RateLimitError, APIConnectionError, json.JSONDecodeError)), reraiseTrue, ) def _call(): # 这里复用前面 translate_batch 的逻辑略 pass return _call()wait_exponential会在重试时逐渐增加等待时间避免在限流时仍然高频请求。5.2 通过 ThreadPoolExecutor 实现并发# 文件路径concurrent_translator.py from concurrent.futures import ThreadPoolExecutor, as_completed def translate_all_batches_concurrently(client, batches, model, max_workers3): 并发翻译多个批次保持结果顺序。 max_workers 默认 3避免并发过高触发限流。 results [None] * len(batches) with ThreadPoolExecutor(max_workersmax_workers) as executor: future_map { executor.submit(translate_batch, client, idx, len(batches), batch, model): idx for idx, batch in enumerate(batches, start0) } for future in as_completed(future_map): idx future_map[future] try: results[idx] future.result() except Exception as e: print(f批次 {idx 1} 失败: {e}) results[idx] None return results需要注意并发只是提速不会自动解决上下文问题。字幕上下文依赖通过批次内部的合并已经解决跨批次的人名一致性依赖提示词约束如果需要更强的一致性可以使用术语表把它放在每个批次的提示词里。6. 运行结果与效果验证6.1 预期输出假设输入input_en.srt内容如下1 00:00:00,500 -- 00:00:03,000 Hello everyone, welcome to my channel. 2 00:00:03,500 -- 00:00:06,000 Today we are talking about AI subtitle translation.运行脚本后output_zh.srt应该类似1 00:00:00,500 -- 00:00:03,000 大家好欢迎来到我的频道。 2 00:00:03,500 -- 00:00:06,000 今天我们来聊聊 AI 字幕翻译。判断成功的标准时间轴与输入文件完全一致。序号从 1 连续递增。没有多余的空行和 JSON 字段。播放器可以正常加载并显示中文。6.2 验证方法除了肉眼检查建议加一个简单的校验函数def validate_srt(content: str) - bool: 校验 SRT 基本结构返回是否符合要求。 import re blocks re.split(r\n\s*\n, content.strip()) for block in blocks: lines [line.strip() for line in block.strip().split(\n) if line.strip()] if len(lines) 2: return False # 序号必须是整数 try: int(lines[0]) except ValueError: return False # 时间轴格式 if -- not in lines[1]: return False return True把这个函数放到写回文件之前调用可以在生成阶段就发现格式问题。6.3 失败时先看哪里如果运行失败按下面顺序排查网络错误检查是否能正常访问 API 接口确认网络策略是否允许调用。鉴权错误检查 API Key 是否正确是否过期是否有余额。JSON 解析错误查看模型返回的原始 content确认是否被截断或包含 Markdown 代码块。格式校验失败查看输出 SRT 的前几行确认时间轴格式是否正确。字幕缺失查看日志中[未翻译]标记定位是模型漏译还是 API 调用失败。7. 常见问题与排查方法问题现象可能原因排查方式解决方案模型返回内容不是 JSON响应被截断或包含额外说明打印response.choices[0].message.content原始内容启用response_formatjson_object或增加提示词约束翻译结果中时间轴改变模型修改了start/end字段对比输入输出时间轴写回时一律使用原始字幕的时间轴翻译出现[未翻译]标记API 调用失败或模型漏译某条查看日志中失败批次增加重试机制单独补译缺失条目输出去重后条目减少模型返回去重了相同文本检查翻译后的 JSON 数组长度写入时按原始序号补全缺失的用原文标记并发过高被限流超过了 API 速率限制查看返回的限流错误码降低max_workers增加重试等待时间字幕顺序错乱多线程结果组装时未按序号排序检查结果数组索引用index映射后按原始顺序写回中文引号或标点变成乱码文件编码不一致检查输入输出文件编码统一使用encodingutf-8且确保编辑器保存为 UTF-88. 最佳实践与工程建议8.1 不要迷信一次翻译成功字幕翻译是一个多轮迭代过程。第一版翻译解决“有没有”后续需要人工校对解决“好不好”。脚本应该支持增量翻译已经人工校对过的字幕不要再次调用 API避免覆盖修改。工程上的做法是在字幕条目里增加一个状态字段pending、translated、reviewed。只处理pending的条目。8.2 使用术语表保证一致性人名、专有名词、机构名是字幕翻译中最容易翻车的地方。建议在项目目录维护一个glossary.txt格式为英文原词TAB中文译名然后在build_prompt里动态读取术语表并拼接到提示词末尾def load_glossary(path: str) - str: if not os.path.exists(path): return with open(path, r, encodingutf-8) as f: return f.read().strip() # 在 prompt 末尾追加 glossary_text load_glossary(glossary.txt) if glossary_text: prompt f\n术语表\n{glossary_text}\n请严格遵守术语表译名。这样能显著改善多批次之间的人名一致性。8.3 控制批次大小与上下文策略批次大小不是越大越好。太大的批次会让模型输出变长增加截断风险太小的批次又会让上下文信息不足。对于 SRT 字幕每个批次包含 8~15 条、总字符 1000~2000 是比较常用的范围。如果源字幕是 ASS 格式它比 SRT 多了样式信息、特效标签解析时还需要处理{\an8}这类标签。建议在进入翻译前先把 ASS 转为纯文本翻译完成后再把标签塞回去或者在解析阶段排除花括号内容。本文不展开 ASS 处理核心思路相同。8.4 把字幕文件纳入版本管理字幕文件本质上是文本资产应该纳入 Git 管理。每次翻译、校对都形成新的 commit方便回溯。同时注意不要把 API Key 提交进仓库。推荐项目结构subtitle-translator/ ├── main.py ├── srt_parser.py ├── batching.py ├── translator.py ├── writer.py ├── glossary.txt ├── input/ │ └── episode01_en.srt ├── output/ │ └── episode01_zh.srt └── .gitignore.gitignore至少包括.env *.srt.bak __pycache__/8.5 尊重版权与合法授权这是必须提醒的一点。字幕翻译工具本身没有问题但使用场景必须合法。确保你处理的内容符合相关版权规定只翻译自己有授权或有权处理的内容不要在公开渠道传播未经授权的版权内容。8.6 关注成本控制批量翻译的成本主要由 token 消耗决定。因为提示词里包含完整的时间轴、字幕 JSON 结构每批次会有一部分固定 token 开销。字幕条数越多批次越多成本越高。建议设置脚本中批次大小避免过小批次浪费 token。只翻译有意义的字幕过滤重复的内容。对长字幕先做预清洗去掉多余空格和特殊字符。9. 总结与后续学习方向这篇文章真正讲清楚的事情是用 DeepSeek 做字幕翻译重点不在模型本身而在字幕格式的解析、批次的合理构造、结果的可解析和可校验以及失败时的兜底处理。这个思路可以迁移到任何“大模型 结构化文本”的任务中。比如批量翻译 JSON 配置文件、本地化文档、游戏对白表核心逻辑完全一致。下一步可以做三件事拿一个真实的 SRT 文件跑通上面的脚本先小批量验证输出质量。增加人工校对环节记录常见翻译错误并补充到提示词或术语表。深入优化上下文管理。当前是“批次内连续”如果你希望模型记住整段剧情需要引入摘要或角色关系说明让模型在翻译后续批次时参考。字幕翻译是一个典型的“看起来简单、做起来细节密集”的任务。只要把解析、分批、翻译、写回、校验这条链路打磨好你不仅是在处理这一部老 OVA而是拥有了一套可复用的字幕生产线。建议先把脚本保存好下次需要翻译任何英文内容时直接改个文件路径就能用。
返回列表