前阵子整理一批上世纪 80 年代的老动画资源,比如 1984 年的《梦战士银翼超人》(Wingman),发现很多外挂字幕都是英文版。网上中文字幕要么残缺,要么时间轴对不上,手动逐条翻译又完全不现实。后来我直接把 DeepSeek API 接进来,做了一条自动化字幕翻译链路:输入一个英文 SRT 文件,输出一个保留时间轴、风格统一的中文 SRT 文件。整个过程不涉及语音识别,也不需要重新压制视频,成本很低,适合个人整理收藏和老番字幕补全。
本文会把这条链路完整拆开:从 DeepSeek API 的基础调用方法,到 SRT 字幕解析、提示词设计、批量翻译脚本,再到常见报错和工程化建议。如果你也想给老番、纪录片或课程视频做“英转中字幕”,可以直接照着操作。
1. 背景与核心概念
1.1 字幕翻译与视频翻译的差别
很多刚接触字幕处理的朋友会把“字幕翻译”和“视频翻译”混在一起。实际上两者差别很大:
- 视频翻译通常包含语音识别(ASR)、文本翻译、语音合成(TTS)、时间轴对齐,甚至还要考虑人声分离和字幕压制,链路很长。
- 字幕翻译只处理已有字幕文本,输入是 SRT、ASS、SSA 或 VTT 文件,输出仍是同格式的字幕文件。它不改变视频画面,也不重新生成音频,只把文字内容从一种语言换成另一种语言。
本文讨论的是第二种场景,也是最容易用大模型 API 自动化的场景。你只需要保证字幕文件本身存在且时间轴正确,剩下的事情就是把文本提取出来,交给 DeepSeek 翻译,再按原顺序写回文件。
1.2 为什么选择 DeepSeek 做字幕翻译
字幕翻译看起来只是“英译中”,但实际要求并不低。长句要拆分,口语要自然,人名要统一,遇到双关语还得适当意译。DeepSeek 在这个过程中有几个明显优势:
- 中文翻译质量稳定,尤其在口语化和长句理解上优于很多通用机器翻译引擎。
- API 兼容 OpenAI 协议,你既可以用官网 SDK,也可以直接用 requests 调用,代码迁移成本很低。
- 支持一次传入多条字幕,通过 JSON 结构化返回,正好适合批量处理。
- 有 deepseek-chat 和 deepseek-reasoner 两个模型方向,前者适合日常翻译,后者适合需要推理分析的复杂场景。
另外,字幕里经常会出现人名、技能名和世界观专有名词,DeepSeek 对“按术语表翻译”这类指令的理解能力比较强。你可以把术语表直接放进系统提示词,让它在翻译时统一遵循。
1.3 本文适合哪些读者
如果你属于以下情况之一,这篇教程会很有帮助:
- 手上有大量英文 SRT 字幕,希望批量转成中文。
- 正在学习 DeepSeek API 调用,想找一个有真实业务场景的练手项目。
- 使用过网页版在线翻译,但发现它无法保留 SRT 时间轴,想用代码解决。
- 对字幕翻译的工程化、缓存、重试和成本控制感兴趣。
2. 环境准备与版本说明
2.1 运行环境
本文示例以 Python 3 为基础,推荐 3.9 及以上版本。操作系统方面,Windows、macOS、Linux 都可以,只要终端能正常执行 Python 命令即可。
你需要准备:
- Python 3.9+。
- 一个 DeepSeek 开放平台账号,并创建 API Key。
- 一个英文 SRT 字幕文件。
IDE 不强求,VS Code、PyCharm 甚至系统自带编辑器都行。我自己习惯用 VS Code,方便直接对比输入输出文件。
2.2 获取 DeepSeek API Key
DeepSeek 开放平台的使用流程和其他大模型平台类似:
- 登录 DeepSeek 开放平台。
- 在控制台找到 API Keys 管理页面。
- 创建一个新的 API Key,创建后只显示一次,需要立即复制保存。
- 确认账户有足够的余额。字幕翻译虽然是文本任务,但批量处理时仍会消耗 token,建议先充少量金额测试。
需要注意:API Key 是敏感信息,不要提交到 Git 仓库,也不要直接硬编码在线上代码里。本文示例用环境变量读取。
2.3 安装 Python 依赖
字幕翻译脚本主要依赖 OpenAI SDK,因为 DeepSeek 的接口兼容 OpenAI。
pip install openai如果你想先跑一个最小请求测试,也可以只安装requests。但完整脚本里我用的是 OpenAI SDK,所以建议直接安装:
pip install openai requests版本方面,OpenAI SDK 的 1.x 版本都支持自定义base_url,这就是接入 DeepSeek 的关键。具体版本号不需要固定,以 pip 当前解析到的最新稳定版为准。
3. DeepSeek API 调用核心知识
3.1 OpenAI 兼容接口与 Base URL
DeepSeek API 最大的特点是兼容 OpenAI Chat Completions 协议。也就是说,你在 OpenAI SDK 里只需把base_url改成 DeepSeek 的地址,就能把请求发到 DeepSeek 模型上。
常见的两个参数如下:
BASE_URL = "https://api.deepseek.com" MODEL = "deepseek-chat"deepseek-chat适合通用的对话、翻译、文本生成任务。deepseek-reasoner适合需要复杂推理、逻辑分析的任务,但翻译任务通常不需要每次都做深度推理,使用deepseek-chat性价比更高。
需要说明的是,模型名称和接口地址可能会随官方迭代调整,实际使用前建议以官方文档为准。本文代码中的地址是长期可用的基准示例。
3.2 使用 curl 快速测试
在写完整 Python 脚本之前,建议先用 curl 验证 API Key 是否有效。下面是一个最小请求示例:
curl https://api.deepseek.com/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的APIKey" \ -d '{ "model": "deepseek-chat", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Translate this into Chinese: I am Wingman!"} ], "stream": false }'如果 Key 有效且余额充足,会返回一段 JSON,里面包含模型回复内容。你可以在返回结果中看到choices[0].message.content字段,这就是翻译后的文本。
3.3 使用 OpenAI SDK 调用 DeepSeek
用 Python 调用时,只需要把OpenAI客户端的base_url参数指向 DeepSeek:
from openai import OpenAI client = OpenAI( api_key="sk-你的APIKey", base_url="https://api.deepseek.com" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是专业字幕翻译。"}, {"role": "user", "content": "翻译这句话:Let's protect this world."} ], temperature=0.3 ) print(resp.choices[0].message.content)这段代码可以独立运行,也是后续批量翻译脚本的基础。
3.4 关键参数说明
在字幕翻译场景中,以下参数比较重要:
| 参数 | 作用 | 字幕翻译建议 |
|---|---|---|
model | 选择模型 | 优先deepseek-chat |
messages | 构造对话上下文 | System 放翻译规则,User 放字幕 JSON |
temperature | 控制随机性 | 0.3 左右,保证术语稳定 |
max_tokens | 限制生成长度 | 根据批次大小设置 4096 左右 |
response_format | 要求 JSON 输出 | 设为{"type": "json_object"} |
stream | 是否流式输出 | 批量场景建议 False |
温度参数值得多说一句。如果你希望翻译结果稳定,尤其是人名和专有名词不要每批都不一样,temperature不要设太高。0.3 是比较合适的起点。如果发现翻译太“死板”,可以适当提高到 0.5。
3.5 本地部署与第三方封装工具
除了官方 API,DeepSeek 也有开源模型权重,可以在本地部署。社区里已经有不少封装好的桌面工具、插件和客户端,比如你在网上可能看到的 DeepSeek Harness、Hermes 等,它们本质上还是对官方 API 或本地模型做了一层界面封装。
如果只是个人整理字幕,直接用官方 API 最省事。如果字幕内容比较敏感,或者公司要求数据不能出内网,可以考虑本地部署,再让脚本通过http://localhost:8000/v1这一类的 OpenAI 兼容地址接入。本文不展开本地部署的完整步骤,因为依赖 GPU 和模型权重,环境差异太大。只要记住:本地部署后,调用方式仍然可以复用本文的 Python 脚本,只需改BASE_URL和MODEL。
4. 完整实战:DeepSeek 批量翻译 SRT 字幕
4.1 理解 SRT 字幕格式
SRT 是最常见的字幕格式之一。一个标准 SRT 文件由多条字幕组成,每条字幕包含序号、时间轴和文本,中间用空行分隔。
例如:
1 00:00:01,000 --> 00:00:04,000 I am Wingman! 2 00:00:05,000 --> 00:00:08,000 Let's protect this world.翻译时,最核心的原则是:时间轴和序号不能动,只替换文本内容。如果把时间轴也交给模型处理,很容易出现格式错误。
所以我建议在脚本中先解析 SRT,把文本内容提取成结构化 JSON,翻译完成后再把时间轴拼回去。
4.2 提示词设计
字幕翻译的提示词和普通“帮我翻译一句话”完全不同。你需要告诉模型几条约束:
- 保持口语化、自然。
- 不要合并或拆分字幕条目。
- 人名和专有名词保留原文,除非有公认译名。
- 只输出 JSON,不输出多余解释。
- 原文本为空时,译文也返回空字符串。
下面是我常用的系统提示词:
你是专业的字幕翻译,负责将英文字幕翻译为简体中文。 要求: 1. 翻译口语化、自然,保留角色语气。 2. 人名字名等专有名词保留原文,除非有公认中文译名。 3. 不要合并/拆分字幕条目,必须保持原 id 一一对应。 4. 只输出 JSON,不要输出解释。 可接受格式为: [{"id":1,"translation":"..."}] 或 {"data":[{"id":1,"translation":"..."}]} 5. 原文本为空时,translation 返回空字符串。把翻译规则放在 System 提示词里,把待翻译内容放在 User 提示词里。这样模型每次都能按照同一套规则工作。
4.3 编写完整脚本
下面是一个可以直接运行的 Python 脚本。它支持批量翻译、自动缓存进度、失败重试,适合处理一整集甚至一整季的字幕。
# -*- coding: utf-8 -*- """ DeepSeek 英转中字幕批处理脚本 用法: python translate_srt.py 输入.srt 输出.srt """ import json import os import re import sys import time from openai import OpenAI API_KEY = os.getenv("DEEPSEEK_API_KEY", "") BASE_URL = "https://api.deepseek.com" MODEL = "deepseek-chat" BATCH_SIZE = 10 MAX_RETRY = 3 CACHE_SUFFIX = ".trans_cache.json" SYSTEM_PROMPT = """你是专业的字幕翻译,负责将英文字幕翻译为简体中文。 要求: 1. 翻译口语化、自然,保留角色语气。 2. 人名字名等专有名词保留原文,除非有公认中文译名。 3. 不要合并/拆分字幕条目,必须保持原 id 一一对应。 4. 只输出 JSON,不要输出解释。 可接受格式为: [{"id":1,"translation":"..."}] 或 {"data":[{"id":1,"translation":"..."}]} 5. 原文本为空时,translation 返回空字符串。""" def parse_srt(content): """解析 SRT 字幕文件内容,返回包含 id、start、end、text 的列表。""" blocks = [] pattern = re.compile( r"(\d+)\s*\n(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})\s*\n(.*?)(?=\n\s*\d+\s*\n|\Z)", re.S, ) for m in pattern.finditer(content): blocks.append({ "id": int(m.group(1)), "start": m.group(2), "end": m.group(3), "text": m.group(4).strip(), }) return blocks def load_cache(cache_path): """加载断点续传缓存。""" if os.path.exists(cache_path): with open(cache_path, encoding="utf-8") as f: return json.load(f) return {} def save_cache(cache_path, cache): """保存翻译进度缓存。""" with open(cache_path, "w", encoding="utf-8") as f: json.dump(cache, f, ensure_ascii=False, indent=2) def translate_batch(client, batch, cache, cache_path): """翻译一批字幕,并把结果写入缓存。""" to_translate = [b for b in batch if str(b["id"]) not in cache] if not to_translate: return payload = [{"id": b["id"], "text": b["text"]} for b in to_translate] user_content = json.dumps(payload, ensure_ascii=False) for attempt in range(MAX_RETRY): try: resp = client.chat.completions.create( model=MODEL, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_content}, ], temperature=0.3, max_tokens=4096, response_format={"type": "json_object"}, ) content = resp.choices[0].message.content data = json.loads(content) if isinstance(data, dict): data = data.get("data") or data.get("translations") or [] for item in data: if isinstance(item, dict) and "id" in item and "translation" in item: cache[str(item["id"])] = item["translation"] save_cache(cache_path, cache) return except Exception as e: print(f"[WARN] 批次重试 {attempt + 1}/{MAX_RETRY}: {e}") time.sleep(2 ** attempt) raise RuntimeError("翻译批次失败: " + user_content[:100]) def merge_srt(blocks, cache): """把翻译结果写回 SRT 格式。""" out_lines = [] for b in blocks: translated = cache.get(str(b["id"]), b["text"]) out_lines.append(f"{b['id']}\n{b['start']} --> {b['end']}\n{translated}\n") return "\n".join(out_lines) def main(): if len(sys.argv) < 3: print("用法: python translate_srt.py 输入.srt 输出.srt") return in_path, out_path = sys.argv[1], sys.argv[2] cache_path = out_path + CACHE_SUFFIX with open(in_path, encoding="utf-8") as f: content = f.read() blocks = parse_srt(content) print(f"共解析到 {len(blocks)} 条字幕") client = OpenAI(api_key=API_KEY, base_url=BASE_URL) cache = load_cache(cache_path) for i in range(0, len(blocks), BATCH_SIZE): batch = blocks[i:i + BATCH_SIZE] translate_batch(client, batch, cache, cache_path) print(f"进度: {min(i + BATCH_SIZE, len(blocks))}/{len(blocks)}") with open(out_path, "w", encoding="utf-8") as f: f.write(merge_srt(blocks, cache)) print(f"翻译完成: {out_path}") if __name__ == "__main__": main()这段脚本有几个设计点值得说明:
parse_srt负责解析时间轴,翻译过程从不修改 start 和 end。- 缓存文件保存的是“字幕 id 到译文”的映射。如果脚本中途断了,再次运行时会跳过已经翻译过的条目,只处理剩余部分。
translate_batch每次传入多条字幕,减少请求次数,也降低费用。- 重试逻辑使用指数退避,遇到网络抖动或临时限流时可以自动恢复。
4.4 运行与验证
首先设置环境变量:
export DEEPSEEK_API_KEY="sk-你的APIKey"然后运行脚本:
python translate_srt.py wingman_ep29.en.srt wingman_ep29.zh.srt假设输入文件内容如下:
1 00:00:01,000 --> 00:00:04,000 I am Wingman! 2 00:00:05,000 --> 00:00:08,000 Let's protect this world.正常运行后,输出文件应类似:
1 00:00:01,000 --> 00:00:04,000 我是银翼超人! 2 00:00:05,000 --> 00:00:08,000 让我们守护这个世界。你需要在播放器里载入原始视频,挂上这个中文字幕,重点检查两点:时间轴是否和原来的英文字幕一致,人名和关键术语是否符合预期。
4.5 进阶:如何避免上下文割裂
字幕是按条翻译的,但台词之间存在上下文。比如角色前面说“我要去那里”,后面才说“那里就是 Wingman 的基地”。如果模型只看单条字幕,可能会把“那里”翻译得不够准确。
一个简单的改进思路是:在 User Prompt 里把当前批次的前几条字幕也带进去,但只要求模型对目标 id 生成译文。这样模型能看到上下文,又不会误解输出范围。
例如:
user_content = json.dumps({ "context": previous_last_5_texts, "to_translate": payload }, ensure_ascii=False)对应 Prompt 里再加一句:
用户输入格式为 JSON,其中 context 是上下文,to_translate 是待翻译列表。 你只需要翻译 to_translate 中的条目。这个方案适合剧情连贯性强的老番,效果比完全独立翻译更自然。
5. 常见问题与排查思路
5.1 常见报错对照表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 返回 401 Authentication Fails | API Key 错误或未设置环境变量 | 检查 Key 是否复制完整,重新 export |
| 返回 402 Insufficient Balance | 账户余额不足 | 到 DeepSeek 平台充值后重试 |
| 提示 model 不存在 | 模型名称写错 | 使用deepseek-chat或查询官方文档 |
| 请求超时 | 网络不稳定或请求体过大 | 增加超时时间,减小 BATCH_SIZE |
| JSON 解析失败 | 模型输出被截断或格式混乱 | 增加 max_tokens,检查 response_format |
| 译文与字幕 id 不对应 | Prompt 约束不够明确 | 强调保持 id 一一对应,调整返回 JSON 结构 |
| 输出内容只有英文 | 模型没有理解翻译要求 | 在 System Prompt 中增加“必须输出简体中文” |
5.2 翻译质量不理想怎么办
如果你发现翻译结果太直译、术语不统一,不要急着换模型,先调整 Prompt:
- 在 System Prompt 中加入术语表。
- 把
temperature调低到 0.2 左右。 - 对同一批字幕多跑几次,对比结果。
- 如果单条字幕太长,先按句号切分再翻译,避免长句截断。
5.3 网络超时与限流
批量任务经常遇到“偶尔一次请求超时”的情况。本文脚本已经包含重试逻辑,但如果你在别的脚本中复制代码,建议也加上指数退避。注意运行环境需要能正常访问api.deepseek.com,如果公司网络有白名单限制,需要联系网络管理员放行。
5.4 字幕时间轴丢失问题
很多在线网页翻译工具会把 SRT 当普通文本处理,输出后时间轴全没了。本文脚本通过先解析、后回写的方式彻底规避这个问题。前提是输入的 SRT 文件本身时间轴格式正确。如果你的输入文件是 ASS 或 SSA,建议先用pysubs2这类库转换成 SRT,再走本文流程。
6. 最佳实践与工程建议
6.1 推荐项目结构
处理多集字幕时,建议用统一的目录结构:
subtitle_project/ ├── input/ │ ├── wingman_ep01.en.srt │ └── wingman_ep29.en.srt ├── output/ │ ├── wingman_ep01.zh.srt │ └── wingman_ep29.zh.srt ├── glossary.json ├── translate_srt.py └── requirements.txt这样输入输出分离,缓存文件也可以统一放在 output 目录,不会污染原始字幕。
6.2 术语表与风格统一
翻译一个系列作品,最重要的就是术语统一。你可以把专用名词整理成 JSON 文件:
{ "Wingman": "银翼超人", "Aoi": "葵", "Dream Fighter": "梦战士" }然后在系统提示词中追加:
翻译时参考以下术语表: Wingman -> 银翼超人 Aoi -> 葵这样即使分多批翻译,也能保证后续校验时不会出现“第一集叫银翼超人,第二集叫翼人”这种不一致。
6.3 成本控制与缓存策略
字幕翻译的 token 消耗取决于字幕条数和文本长度。要控制成本,可以从三方面入手:
- 单次请求尽量合并多条字幕,减少请求次数。
- 使用缓存文件断点续传,避免失败后重新消耗 token。
- 先拿一集测试,统计消耗,再决定是否批量处理整季。
DeepSeek 的定价会随官方策略变化,具体费用以平台账单为准。但思路是一样的:批处理比逐条请求便宜得多,缓存比重复翻译便宜得多。
6.4 API Key 安全与生产环境注意
不管是在本地脚本还是服务器任务中使用,API Key 都不要硬编码。推荐的做法是:
export DEEPSEEK_API_KEY="sk-你的APIKey"脚本中从环境变量读取。如果使用 CI/CD 或定时任务,可以把 Key 放到密钥管理服务中,并配置最小权限。
字幕内容如果涉及版权素材,建议只用于个人学习和备份,不要公开发布翻译后的字幕文件,更不要用于商业传播。技术本身是工具,合规使用才能长久。
6.5 从单集脚本到批量工具
当你觉得单集翻译脚本稳定之后,可以再加一层循环,批量处理整个文件夹:
import glob for srt_path in sorted(glob.glob("input/*.en.srt")): out_path = srt_path.replace("input/", "output/").replace(".en.srt", ".zh.srt") # 在这里复用 translate_srt 的核心函数做好缓存、日志和失败告警后,这套流程基本可以无人值守跑完一整季老番。
老番字幕补全是一件很耗耐心的事情,但用 DeepSeek API 把“翻译”这步自动化后,剩下的主要工作就是术语表维护和质量抽检。建议你从一集开始跑通流程,记录消耗和效果,再决定要不要扩大到整个系列。如果你手头也有积压的英文 SRT,不妨照着这份教程试一次。