字幕翻译是个很典型的场景:人工翻译整集速度太慢,直接丢给通用翻译工具又容易翻译腔。这次我们以《恶魔君 1989》第28集为例,讲一条可以直接落地的英转中字幕流水线,核心工具是 DeepSeek 的 API。
这个需求的本质不复杂:把英文字幕文件解析出来,喂给大模型翻译成中文,再按原时间轴写回 SRT。真正的难点在于批量处理稳不稳定、角色名是否一致、分多少条字幕一起翻译、失败之后怎么重试。《恶魔君 1989》是 1989 年的老动画,如果靠人工补字幕或翻译,工作量不低;用 DeepSeek 做初译,再人工校对,效率会明显好于从零开始。
这篇文章不是单集教程,而是把一条可以复用的字幕翻译工作流拆开讲。看完你至少能做三件事:第一,用 DeepSeek 官方 API 翻译单条字幕文本;第二,用一个 Python 脚本批量处理整集 SRT;第三,把脚本扩展到多集批量任务,并做基本的质量校验。整个流程不需要本地 GPU,不需要部署模型,一台普通电脑加一个 API Key 就能跑。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 字幕翻译工作流(英文 -> 中文) |
| AI 服务 | DeepSeek API |
| 输入格式 | SRT 英文字幕 |
| 输出格式 | SRT 中文字幕 |
| 是否需要 GPU | 不需要,纯 API 调用 |
| 是否支持批量 | 支持,可循环处理多集字幕 |
| 术语一致性 | 通过术语表 + 分块上下文控制 |
| 启动方式 | Python 脚本运行 |
| 适合场景 | 老番补全、字幕组初译、双语字幕制作 |
从这张表可以看出来,这不是一个需要高配置硬件部署的项目,而是一个“调接口 + 处理文件”的工程脚本。你想用 DeepSeek 做字幕翻译,最关心的不是显存,而是 API 调用是否稳定、Prompt 是否能把字幕格式约束住、批量任务能不能断点续跑。
2. 适用场景与使用边界
2.1 适合谁用
这套工作流最先适合三类人:
- 个人译者和追番用户:手里有英文字幕,想快速得到中文字幕做参考。
- 字幕组成员:用 DeepSeek 做初译,人工在校对阶段统一术语和润色,可以省不少时间。
- 老番补全整理者:像《恶魔君 1989》这类老动画,如果字幕资源只有英文,批量跑一遍中文字幕是性价比最高的方式。
2.2 不适合什么场景
我不建议把 DeepSeek 机翻字幕直接当成成品发布。字幕翻译涉及口语、语气、文化梗和角色设定,机器结果只能算初稿。如果对译文质量要求很高,或者素材涉及商业版权,必须走完整人工校对流程。
2.3 版权与合规边界
这里要特别提醒:字幕文件本身有版权,视频画面和官方字幕也不能随意传播。你可以把《恶魔君 1989》第28集当作个人学习和技术演示的输入,但公开发布、二次分发或商用前,需要确认片源和字幕的授权情况。涉及人物肖像、声音、角色素材的场景同样要先取得授权。
3. 环境准备与前置条件
这套方案不需要本地显卡,也不需要安装 CUDA、PyTorch 之类的东西。环境要求非常低。
3.1 软件依赖
- Python 3.9 以上
- requests 库发送 HTTP 请求
- 一个可以正常访问 DeepSeek API 的网络环境
- DeepSeek API Key
安装依赖只需要一条命令:
pip install requests如果你习惯用 OpenAI 风格的 Python SDK,也可以安装 openai 库,然后用 base_url 指向 DeepSeek 的接口。不过为了减少依赖,这篇文章使用 requests 直接调用 HTTP 接口。
3.2 获取 API Key
登录 DeepSeek 开放平台,在控制台创建 API Key。创建之后把 Key 存到环境变量里,不要直接硬编码在脚本中,避免提交到 Git 仓库后泄露。
在 Linux / macOS 下可以临时导出:
export DEEPSEEK_API_KEY="sk-xxxxxxxx"Windows PowerShell 下可以写:
$env:DEEPSEEK_API_KEY = "sk-xxxxxxxx"脚本内部通过os.environ.get("DEEPSEEK_API_KEY")读取,没有 Key 时直接报错。
3.3 目录结构
建议按下面的目录结构组织文件:
subtitle_translator/ ├── en_srt/ # 英文原版 SRT ├── zh_srt/ # 生成的中文 SRT ├── output/ # 日志和中间结果 ├── translate.py # 主脚本 └── glossary.txt # 术语表原始英文字幕只保留在en_srt/,生成结果单独输出到zh_srt/,避免覆盖原始文件。
4. 字幕翻译工作流设计
4.1 SRT 文件格式
SRT 是最常见的字幕格式,结构固定:
1 00:00:01,000 --> 00:00:04,000 Hello, world! 2 00:00:04,500 --> 00:00:07,000 This is a subtitle.每个字幕块包含序号、时间轴和文本。翻译脚本要做的事情,就是保留序号和时间轴不变,只把文本替换成中文。
4.2 翻译流程
整体流程分五步:
- 读取 SRT 文件并按块解析。
- 把字幕文本按批次组合成一个翻译请求。
- 调用 DeepSeek API 获取中文结果。
- 将结果按编号映射回原字幕块。
- 重新生成 SRT 文件并输出到
zh_srt/。
4.3 分块策略
分块大小直接影响翻译质量和稳定性。建议每批 10 到 20 条字幕一起发送,而不是把整集字幕一次性塞给模型。
原因有两个:
- 模型输入长度有限,整集字幕量可能超过上下文窗口。
- 分块翻译可以保留上下文,字幕之间如果有对话承接,批量翻译能减少前后不连贯的问题。
分块太小也有问题:比如每次只翻译 1 到 2 条,模型容易丢失前面提到的角色名和设定,而且请求次数会大幅增加,成本变高,限流风险也变大。
5. DeepSeek API 调用与字幕脚本实现
这一章直接给代码。示例中的接口地址和模型名请以 DeepSeek 官方文档为准,我这里使用的是 OpenAI 兼容格式的通用调用方式。
5.1 基础 Chat 接口调用
先写一个最基础的 Chat 接口封装:
import os import requests DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions" API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") if not API_KEY: raise RuntimeError("DEEPSEEK_API_KEY not set") def chat_completion(messages, temperature=0.3, timeout=120): resp = requests.post( DEEPSEEK_API_URL, headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, json={ "model": "deepseek-chat", "messages": messages, "temperature": temperature, }, timeout=timeout, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]temperature 设置到 0.3 左右可以让翻译更稳定,减少模型发挥过头的概率。字幕翻译不是创作型任务,随机性越低越好。
5.2 翻译 Prompt 设计
字幕翻译的 Prompt 要强调三件事:只输出翻译结果、保持编号格式、按术语表处理专有名词。下面是一个推荐写法:
GLOSSARY = """\ Akuma-kun -> 恶魔君 Mephisto -> 梅菲斯托 """ def build_translate_prompt(numbered_text, glossary=GLOSSARY): return f"""你是一名专业的字幕翻译。请把下面的英文字幕翻译成简体中文。 要求: 1. 保持口语化,符合中文表达习惯 2. 专有名词按术语表翻译 3. 保留原文语气和情感 4. 只输出翻译后的文本,格式保持 [编号] 译文 5. 不要添加任何解释 术语表: {glossary} 待翻译内容: {numbered_text} """这里有一个关键点:给模型输入时,先给每条字幕加编号,比如[1] Hello、[2] World,要求模型按同样格式返回。这样脚本解析结果时能精确还原到原字幕块,不会因为翻译顺序错乱导致字幕和台词对不上。
术语表的作用是统一角色名。老动画中的角色名很容易出现“前面翻成恶魔君、后面翻成小恶魔”的问题,提前定义术语表可以明显减少这种错误。具体角色名需要根据《恶魔君 1989》的实际片源补充,脚本里先用示例占位。
5.3 批量翻译与解析
翻译一批字幕时,先把多条字幕拼成一个请求,再解析模型返回值:
import re import time def translate_batch(texts, glossary=GLOSSARY, max_retries=3): numbered_text = "\n".join(f"[{i}] {t}" for i, t in enumerate(texts)) prompt = build_translate_prompt(numbered_text, glossary) messages = [ {"role": "system", "content": "你是一名专业的字幕翻译助手,擅长英文到中文的影视字幕翻译。"}, {"role": "user", "content": prompt}, ] last_error = None for attempt in range(max_retries): try: output = chat_completion(messages) return parse_numbered_output(output) except Exception as e: last_error = e print(f"translate_batch retry {attempt + 1}: {e}") time.sleep(2) raise RuntimeError(f"translate failed after {max_retries} retries: {last_error}") def parse_numbered_output(output: str): result = {} for line in output.strip().split("\n"): line = line.strip() m = re.match(r"^\[(\d+)\]\s*(.*)$", line) if m: result[int(m.group(1))] = m.group(2).strip() return result解析时使用正则^\[(\d+)\]\s*(.*)$提取编号和译文。如果模型没有严格按格式输出,这部分解析可能丢内容,所以parse_numbered_output只返回能解析到的条目,后续脚本再结合原字幕块做填充。
5.4 完整字幕翻译脚本
把解析、翻译、写回整合在一起,就是一个可以直接跑整集字幕的脚本。下面给出完整示例:
import os import re import time import glob import requests DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions" API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") if not API_KEY: raise RuntimeError("DEEPSEEK_API_KEY not set") GLOSSARY = """\ Akuma-kun -> 恶魔君 Mephisto -> 梅菲斯托 """ def parse_srt(content: str): blocks = [] parts = content.strip().split("\n\n") for part in parts: lines = part.strip().split("\n") if len(lines) < 2: continue blocks.append({ "index": lines[0].strip(), "time": lines[1].strip(), "text": "\n".join(lines[2:]).strip(), }) return blocks def build_srt(blocks): return "\n\n".join( f"{b['index']}\n{b['time']}\n{b['text']}" for b in blocks ) + "\n" def chat_completion(messages, temperature=0.3, timeout=120): resp = requests.post( DEEPSEEK_API_URL, headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, json={ "model": "deepseek-chat", "messages": messages, "temperature": temperature, }, timeout=timeout, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] def build_translate_prompt(numbered_text, glossary=GLOSSARY): return f"""你是一名专业的字幕翻译。请把下面的英文字幕翻译成简体中文。 要求: 1. 保持口语化,符合中文表达习惯 2. 专有名词按术语表翻译 3. 保留原文语气和情感 4. 只输出翻译后的文本,格式保持 [编号] 译文 5. 不要添加任何解释 术语表: {glossary} 待翻译内容: {numbered_text} """ def parse_numbered_output(output: str): result = {} for line in output.strip().split("\n"): line = line.strip() m = re.match(r"^\[(\d+)\]\s*(.*)$", line) if m: result[int(m.group(1))] = m.group(2).strip() return result def translate_batch(texts, glossary=GLOSSARY, max_retries=3): numbered_text = "\n".join(f"[{i}] {t}" for i, t in enumerate(texts)) prompt = build_translate_prompt(numbered_text, glossary) messages = [ {"role": "system", "content": "你是一名专业的字幕翻译助手,擅长英文到中文的影视字幕翻译。"}, {"role": "user", "content": prompt}, ] last_error = None for attempt in range(max_retries): try: output = chat_completion(messages) return parse_numbered_output(output) except Exception as e: last_error = e print(f"translate_batch retry {attempt + 1}: {e}") time.sleep(2) raise RuntimeError(f"translate failed after {max_retries} retries: {last_error}") def translate_srt_file(srt_path, output_dir, batch_size=10): with open(srt_path, "r", encoding="utf-8") as f: content = f.read() blocks = parse_srt(content) total = len(blocks) translated_count = 0 for i in range(0, total, batch_size): batch = blocks[i:i + batch_size] texts = [b["text"] for b in batch] result = translate_batch(texts) for j, block in enumerate(batch): if j in result: block["text"] = result[j] else: print(f"warning: missing translation for block {block['index']}") translated_count += len(batch) print(f"progress: {translated_count}/{total}") output_srt = build_srt(blocks) os.makedirs(output_dir, exist_ok=True) name = os.path.splitext(os.path.basename(srt_path))[0] out_path = os.path.join(output_dir, f"{name}.zh.srt") with open(out_path, "w", encoding="utf-8") as f: f.write(output_srt) return out_path if __name__ == "__main__": for srt_file in glob.glob("./en_srt/*.srt"): print(f"processing {srt_file}") output = translate_srt_file(srt_file, "./zh_srt") print(f"saved to {output}")脚本的工作过程是:扫描en_srt/下所有 SRT 文件,逐集解析、逐批翻译,最终在zh_srt/下生成同名.zh.srt文件。运行方式很简单:
python translate.py第一次运行建议先放一个 SRT 文件到en_srt/,不要直接丢几十集进去。先确认单集能跑通,再扩大批量。
6. 批量任务与稳定运行
6.1 多集批量处理
上面的脚本已经能扫描目录下所有 SRT 文件。如果要处理《恶魔君 1989》全集,直接把多集英文字幕放进en_srt/即可。脚本会逐个文件执行,互不干扰。
批量处理时建议在脚本外层加一个简单的计数输出,方便观察进度。更完整的方式是写一个任务清单文件,记录每集状态:
[ {"episode": "ep01", "status": "done", "output": "zh_srt/ep01.zh.srt"}, {"episode": "ep02", "status": "pending", "output": ""} ]脚本每次启动前先读状态文件,已经处理成功的跳过,只处理 pending 和 failed 的任务。这样可以避免中途断网导致全部重跑。
6.2 断点续跑
翻译几十集字幕时,最常见的失败原因是网络超时或 API 限流。在translate_batch中已经加了重试,但整集跑一半的时候进程崩掉,仍然需要断点续跑。
简单做法是在translate_srt_file里,翻译完一批就把当前结果写入临时缓存文件,比如output/ep01.partial.json。重新启动时先检查缓存,已经完成的批次不重新翻译。
6.3 API 限流与成本控制
DeepSeek API 是计费服务,批量任务需要控制请求频率和成本。分块大小是成本的关键变量:每批 10 条字幕比每批 5 条省一半请求数,但单次请求 token 更长。建议第一批测试时用小样本估算成本,再决定每批条数。
如果同时跑多集,可以在每批之间加一个短延时,避免触发限流:
time.sleep(0.5)单集翻译几十批字幕,耗时通常取决于 API 响应速度。具体耗时和费用以实际控制台数据为准,不要在生产流程里假设固定值。
7. 效果验证与质量控制
机器翻译做完,不能直接拿去用。至少要做三轮检查。
7.1 时间轴一致性检查
脚本本身不修改时间轴,但需要确认生成文件里每条字幕的序号和时间轴仍在。最简单的方法是打开生成的中文 SRT,随机挑几个时间点,把中文和英文放在同一个播放器里对比。
也可以用脚本做格式化检查:解析生成后的 SRT,确认块数与原文件一致,并且每个块的时间轴格式正确。
7.2 术语一致性检查
检查角色名、地名、专有名词是否统一。用术语表做一次自动扫描:
def check_glossary(srt_text, glossary_pairs): issues = [] for en_term, zh_term in glossary_pairs.items(): if en_term in srt_text: issues.append(f"{en_term} 未被替换为 {zh_term}") return issues如果术语表中出现英文原文残留,说明术语表没有完全生效,需要在 Prompt 中加强约束或调整术语表写法。
7.3 漏译与错译排查
漏译是最容易发现的问题。脚本在translate_srt_file中已经打印了missing translation警告。如果警告较多,说明模型返回格式不稳定,或分块过大导致部分内容被截断。
错译排查需要人工抽查。建议按下面维度抽检:
- 对话是否通顺,是否存在翻译腔。
- 语气是否符合角色性格。
- 长句是否被截断或丢失信息。
- 文化梗是否被直译成无法理解的内容。
抽查比例不用太高,一集字幕抽 20 到 30 条就足以判断整体质量。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| API 返回 401 | API Key 错误 | 检查环境变量是否设置 | 重新创建 Key 并导出 |
| API 返回 402 或余额不足 | 账户余额不足 | 查看 DeepSeek 控制台 | 充值或更换 Key |
| API 返回 429 | 请求频率过高或限流 | 查看返回 header 和日志 | 增加 sleep 时间,降低并发 |
| 翻译后字幕为空 | 模型没有按 [编号] 格式返回 | 打印原始返回内容 | 调整 Prompt,或增加重试 |
| 时间轴错位 | SRT 解析失败 | 打开原始文件检查格式 | 确保使用标准 SRT 格式,留意空行 |
| 批量任务中途中断 | 网络超时或进程崩溃 | 查看日志和缓存文件 | 加断点续跑,缓存已完成批次 |
| 角色名前后不一致 | 术语表未生效 | 扫描输出中的英文残留 | 补充术语表,并在 Prompt 中强调 |
| 翻译结果口语化不足 | temperature 过高 | 检查参数 | 降低 temperature 到 0.3 以下 |
如果翻译批次总是失败,可以把max_retries提高到 5,并在重试之间加指数退避:
sleep_time = 2 ** attempt time.sleep(sleep_time)太多次重试仍然失败时,记录当前批次到日志文件,不要无限阻塞主流程。
9. 最佳实践与总结
这套 DeepSeek 字幕翻译工作流,最值得复用的地方不是某一段 Prompt,而是“保持时间轴不变 + 编号批量翻译 + 术语表约束 + 断点续跑”的组合方式。用同样的思路,不只是《恶魔君 1989》,任何有英文字幕的视频都能纳入处理。
第一次跑的时候,建议只拿第28集的一小段字幕测试,比如前 50 条。确认能跑通、返回格式稳定,再放整集。整集通过后,再扩展到多集批量处理。这样能把成本浪费控制在最低。
最容易踩的坑有三个:一是模型返回格式不固定,导致解析失败;二是分块太大导致部分内容缺失;三是忽略术语表,导致角色名前后不一致。这三个问题都可以通过脚本里的打印日志尽早发现。
接下来可以做的扩展方向包括:接入术语库文件让非技术人员也能维护角色名;增加双语对照字幕输出;把脚本封装成简单 Web 服务,上传 SRT 就能返回中文 SRT。如果你平时做字幕相关工作,建议把这套脚本保存下来,作为本地字幕初译的标准工具。