在实际开发中,DeepSeek 除了用作聊天助手,也经常被接到自动化流程里做文本处理。一个典型场景是英转中文字幕:从 SRT 字幕中提取英文文本,调用 DeepSeek API 批量翻译,再写回对应的时间轴,得到一份播放器可以直接加载的中文字幕文件。整个过程不需要 GPU,不需要训练模型,只需要 Python 脚本、API Key 和对字幕格式的基本了解。
这篇文章面向已经会基础 Python、想用 DeepSeek 完成一个真实批量任务的开发者。读完之后,你可以自己实现从 SRT 解析、API 调用、批量翻译、缓存续传到结果校验的完整链路,也可以把同样思路迁移到 JSON、Markdown、配置文件的批量翻译任务。
1. 先想清楚字幕翻译链路,再动手写代码
1.1 字幕翻译为什么适合交给 DeepSeek API,而不是本地模型
字幕翻译看似只是一个翻译任务,实际会涉及文本提取、批量请求、结果对齐和文件回写四件事。人工逐句复制粘贴到网页翻译再贴回来,效率低且容易搞丢格式。本地模型翻译虽然数据更安全,但没有 GPU 时的速度很慢,7B 以下模型的长句翻译质量也不稳定,还要处理显卡显存、量化、推理框架等问题。
DeepSeek API 的优势在于可以按需求短时间并发调用,不需要本地推理环境。对字幕这类短文本为主的场景,API 返回速度快,一次请求可以塞多条句子,整体吞吐量远高于本地小模型。更重要的是,API 模型在语义理解、专有名词处理和长句结构保持上做得更稳,翻译结果更容易直接被播放器加载。
三种方式的差异可以用一张表来看:
| 方案 | 优势 | 主要问题 | 适合场景 |
|---|---|---|---|
| 人工逐句翻译 | 质量最高,能保留语气和风格 | 成本高、速度慢 | 少量重要内容 |
| 本地小模型翻译 | 数据不出内网,无按量计费 | 部署复杂、小模型质量不稳定 | 对隐私有硬性要求 |
| DeepSeek API 翻译 | 速度快、无需显卡、质量稳定 | 按 token 计费、依赖网络和调用审计 | 批量字幕、多语言文案 |
字幕文本通常短,单条只有十几个单词,这种结构非常适合批量请求。一次请求塞入 10 到 20 条文本,模型按编号返回译文,比逐条请求节省大量时间,也更容易控制成本。
1.2 DeepSeek API 的定位和调用方式
这里把 DeepSeek API 当作一个“按 token 计费的文本生成接口”来理解。调用时,客户端把系统提示、上下文和用户消息发给服务端,服务端返回模型生成的回答。接口格式与 OpenAI Chat Completions 兼容,因此可以直接使用 OpenAI 官方 Python SDK,把 base_url 指向 DeepSeek 的 API 地址。
不同模型的定位有差异。DeepSeek-chat 面向常规对话、翻译、改写和代码生成,适合字幕翻译;DeepSeek-reasoner 会额外输出推理过程,更适合复杂推理题,做字幕翻译时输出文字更长、响应更慢,不是优先选择。实际项目中应先用小样本对比,再确定使用哪个模型。
调用时需要关注的关键参数有这几个:
temperature:控制随机性。字幕翻译建议 0.2 到 0.4,过高会导致句子偏离原文。max_tokens:限制返回长度。字幕单批通常 2048 足够,如果输入是长文本需要按源文本字符数估算。top_p:和 temperature 类似,一般保持默认即可,不建议同时调两个。model:目前优先使用 deepseek-chat,reasoner 模型不适合批量短文本翻译。
参数值在本地脚本里可以用环境变量管理。字幕翻译项目往往要跑好几批,如果每改一次 temperature 都要改代码,后面调整参数时会很痛苦。
1.3 一个最小链路中要包含哪些模块
字幕翻译不是“把字符串发给模型再拿回来”这么简单,至少要拆成五个模块:
- 字幕解析器:读取 SRT/ASS,提取序号、时间轴和文本。
- 明文提取与标签保护:把 ASS 里的样式标签临时替换成占位符,避免被翻译破坏。
- 翻译调度器:把多条字幕文本按批次发给 API,处理重试、限流和缓存。
- 结果回写器:把译文按序号放回原时间轴,生成新字幕文件。
- 校验器:检查原文条数、译文条数、时间轴和时间格式是否完整。
把这个链路想清楚后,后面的代码虽然长,但每一块都能单独测试,排查问题时也不用整条流程一起猜。
2. 环境准备:Python、依赖和 API 配置
2.1 基础环境和 Python 版本
建议使用 Python 3.9 或更高版本,主要原因是 dataclass、类型注解和标准库 re 在这些版本上表现更稳定。字幕翻译不需要 GPU,普通办公电脑即可。还需要能正常访问 API 服务,网络不通时所有调用都会超时,这是先要确认的前置条件。
推荐新建虚拟环境,避免污染其他项目。如果请求 API 时出现 SSL 或超时问题,优先检查系统时间、网络出口和防火墙设置,而不是盲目更换依赖版本。
2.2 获取 API Key 和确认调用地址
登录 DeepSeek 开放平台后,在控制台创建 API Key。这个 Key 相当于账号密码,应该通过环境变量读取,不要直接硬编码到脚本或提交到 Git 仓库。
在 Python 项目中,可以把 Key 写入项目根目录的.env文件:
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxx DEEPSEEK_BASE_URL=https://api.deepseek.com DEEPSEEK_MODEL=deepseek-chat.env文件默认应该加入.gitignore,避免误提交。不同环境下使用不同 Key,可以在部署配置中单独设置环境变量,而不需要修改代码。
2.3 安装依赖并验证连通性
安装依赖:
pip install openai python-dotenv然后写一个最小调用脚本:
import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.getenv("DEEPSEEK_API_KEY"), base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"), ) resp = client.chat.completions.create( model=os.getenv("DEEPSEEK_MODEL", "deepseek-chat"), messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Say hello in Chinese."}, ], temperature=0.3, ) print(resp.choices[0].message.content)这段脚本的作用是验证三件事:Key 是否有效、网络是否连通、模型名是否可用。如果这里报错,后面所有字幕翻译代码都不会正常工作。
3. 解析字幕文件:把 SRT 变成可翻译的文本块
3.1 SRT 的基本结构
SRT 是最常用的字幕格式,结构很简单:
1 00:00:01,000 --> 00:00:03,000 Hello, world. 2 00:00:04,000 --> 00:00:06,000 This is a test subtitle.第一条是序号,第二条是时间轴,第三条以后是字幕文本,文本可能有多行,最后用空行分隔下一条。解析时要特别小心:文本内容可能包含-->这样的字符串,也可能包含空行,不能简单按行 split。
3.2 用 Python 解析 SRT 的示例代码
定义一个 dataclass 保存整条字幕:
from dataclasses import dataclass @dataclass class SrtBlock: index: int start: str end: str text: str def format(self) -> str: return f"{self.index}\n{self.start} --> {self.end}\n{self.text}\n"解析函数可以按\n\n切分整个文件:
import re def parse_srt(content: str) -> list[SrtBlock]: raw_blocks = re.split(r"\n\s*\n", content.strip()) blocks = [] for raw in raw_blocks: lines = raw.strip().split("\n", 2) if len(lines) < 3: continue index = int(lines[0]) time_part, text = lines[1], lines[2].strip() start, end = [x.strip() for x in time_part.split("-->")] blocks.append(SrtBlock(index=index, start=start, end=end, text=text)) return blocks这里使用split("\n", 2)只拆前两行,可以保留字幕文本内部原有的换行。time_part.split("-->")在大多数 SRT 文件中都能工作,但严谨一点应该用正则校验时间格式,避免某些播放器导出时把分隔符两侧空格去掉造成解析错误。
不建议在解析阶段就把文本里的标签删除。SRT 中常见<i>、<b>、<font color="...">等标签,ASS 中还有{\an8}这样的控制标签。这些标签如果删掉,播放器显示时会丢失样式。正确做法是提取文本时临时替换,翻译完成后再还原。
3.3 ASS 字幕的标签处理和扩展
ASS 文件比 SRT 复杂。ASS 按[Events]段里的Dialogue行组织字幕,一行字段用逗号分隔,格式大致是:
Dialogue: 0,0:00:01.00,0:00:03.00,Default,,0,0,0,,{\an8}Hello world前两个数字是图层和开始时间,接下来是结束时间、样式、说话人、边距和文本,最后逗号后面的内容是实际字幕文本。解析时不能简单地用split(",")全拆,否则文本内容里如果有逗号,字段就对不齐。
推荐做法是先按前 9 个逗号把前段字段切开,剩下的部分全部作为文本。提取文本后,用正则把{...}里的控制命令替换成占位符:
ASS_TAG_RE = re.compile(r"\{[^}]*\}") def protect_ass_tags(text: str) -> tuple[str, list[str]]: placeholders = [] def replace(match: re.Match) -> str: placeholders.append(match.group(0)) return f"\x00TAG{len(placeholders)-1}\x00" protected = ASS_TAG_RE.sub(replace, text) return protected, placeholders def restore_ass_tags(text: str, placeholders: list[str]) -> str: for i, tag in enumerate(placeholders): text = text.replace(f"\x00TAG{i}\x00", tag) return text把{\an8}这类字符串从翻译内容中隔离出来,可以避免模型把它们当成普通文本翻译,也可以避免翻译结果里缺少标签导致字幕位置错乱。这个保护思路同样适用于 SRT 里的<i>标签,可以先把它们替换成占位符,翻译后再还原。
4. 调用 DeepSeek API 完成批量翻译
4.1 设计一个可复用的调用封装
翻译调用的核心是稳定的请求参数。字幕翻译希望句子忠实于原文,不希望模型自由发挥,temperature建议设在 0.2 到 0.4 之间,top_p保持默认即可。max_tokens需要根据输入量估算,一般取源文本字符数的 1.2 倍左右比较稳妥,但字幕文本通常较短,设置 1024 到 2048 已经足够。
基础调用封装:
from openai import OpenAI, RateLimitError, APITimeoutError, APIConnectionError def call_deepseek(client: OpenAI, system_prompt: str, user_content: str) -> str: try: resp = client.chat.completions.create( model=os.getenv("DEEPSEEK_MODEL", "deepseek-chat"), messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}, ], temperature=0.3, max_tokens=2048, ) return resp.choices[0].message.content or "" except RateLimitError: raise except (APITimeoutError, APIConnectionError) as exc: raise RuntimeError(f"API connection failed: {exc}") from exc这里把异常继续抛到上层,是因为要不要重试、等多久重试,应该由翻译调度器决定,而不是每一层都擅自处理。RateLimitError 表示配额或限流,通常需要等待一段时间后重试;超时和连接错误可能是临时的,也可以重试,但不能无限重试。
参数选择的速查表如下:
| 参数 | 字幕翻译推荐值 | 调大影响 | 调小影响 |
|---|---|---|---|
| temperature | 0.2~0.4 | 更灵活,但容易偏离原文 | 更保守,基本忠实原文 |
| top_p | 默认或不设置 | 采样范围更大 | 采样范围更小 |
| max_tokens | 1024~2048 | 可处理更长输出 | 可能截断译文 |
| model | deepseek-chat | reasoner 输出更长但更慢 | 无 |
实际项目中不建议同时调整 temperature 和 top_p,保持一个变量作为主要控制即可。
4.2 Prompt 设计:让模型返回稳定的编号结构
翻译字幕最怕模型自由发挥。如果不加约束,模型可能返回一小段解释,或者把 20 条字幕合并成一段话,导致后续无法按序号回写。所以在 Prompt 里必须给出“编号对齐”的要求。
一个稳定的批量 Prompt 示例:
你是影视字幕翻译助手。下面每行以 [序号] 开头,内容是一条英文字幕。 请把它们翻译成简体中文,保持每行的 [序号] 不变,只替换后面的文本。 不要增加解释,不要合并或拆分条目,不要输出序号以外的内容。 [1] I think we should go now. [2] There is no time to waste. [3] He left the room without saying a word.对应的解析代码可以这样写:
import re def parse_translation_result(raw: str) -> dict[int, str]: result = {} for line in raw.strip().splitlines(): m = re.match(r"\[(\d+)\]\s*(.*)", line.strip()) if m: result[int(m.group(1))] = m.group(2).strip() return result如果模型偶尔漏行,需要在上层做数量校验,而不是直接把结果写回文件。漏行时可以将缺失批次重新请求,或者把缺失序号单独拼接成一个新批次继续翻译。
4.3 主流程:按批次翻译并写回新字幕
批次大小的选择取决于上下文长度和稳定性。字幕单条文本一般很短,一批 10 到 20 条比较合适。批次太大,模型容易丢失中间的条目,偶尔还会超过单次请求的最大 token;批次太小,请求次数太多,限流概率更高。
主流程代码:
import os import time from openai import OpenAI, RateLimitError, APITimeoutError, APIConnectionError def translate_srt_blocks( client: OpenAI, blocks: list[SrtBlock], batch_size: int = 12, ) -> list[SrtBlock]: translated = [] for i in range(0, len(blocks), batch_size): batch = blocks[i:i + batch_size] user_lines = [ f"[{b.index}] {b.text.strip()}" for b in batch ] user_content = "\n".join(user_lines) system_prompt = ( "你是影视字幕翻译助手。下面每行以 [序号] 开头,内容是字幕文本。\n" "请把它们翻译成简体中文,保持 [序号] 不变,只替换后面的文本。\n" "不要增加解释,不要合并或拆分条目。" ) max_retry = 3 raw_result = "" for attempt in range(max_retry): try: raw_result = call_deepseek(client, system_prompt, user_content) break except (RateLimitError, APITimeoutError, APIConnectionError) as exc: if attempt == max_retry - 1: raise RuntimeError(f"batch {i // batch_size} failed") from exc time.sleep(2 * (attempt + 1)) parsed = parse_translation_result(raw_result) for b in batch: text = parsed.get(b.index, "").strip() translated.append(SrtBlock( index=b.index, start=b.start, end=b.end, text=text, )) return translated值得注意,这里没有直接用parsed的顺序,而是按原始batch顺序遍历,可以保证输出的translated列表顺序和原字幕一致。时间轴直接复用原值,不回写时重新格式化。
写文件时,字幕文件建议保存为 UTF-8。如果目标播放器比较老,中文可能乱码,可以额外加 BOM。
def write_srt(path: str, blocks: list[SrtBlock], with_bom: bool = True): content = "\n".join(b.format() for b in blocks) with open(path, "w", encoding="utf-8-sig" if with_bom else "utf-8") as f: f.write(content)utf-8-sig是 Python 标准库对“UTF-8 with BOM”的写法,Windows 下的某些播放器更容易识别。
5. 验证结果:不要只看“字幕文件生成了”
5.1 自动校验项
翻译完成后,至少要检查四个方面:
- 条目数量一致:原文块数和译文块数是否相同。
- 序号完整:原序号是否都出现在译文中,是否有重复。
- 时间轴完整:每条译文的起止时间是否和原文件一致。
- 文本非空:不应出现整条译文为空或者仍然是英文的情况。
一个简单的校验函数:
def validate_blocks(original: list[SrtBlock], translated: list[SrtBlock]) -> list[str]: errors = [] if len(original) != len(translated): errors.append(f"block count mismatch: {len(original)} -> {len(translated)}") orig_indices = [b.index for b in original] trans_indices = [b.index for b in translated] if orig_indices != trans_indices: errors.append("index order mismatch") for o, t in zip(original, translated): if o.start != t.start or o.end != t.end: errors.append(f"timecode changed at index {o.index}") if not t.text.strip(): errors.append(f"empty text at index {o.index}") return errors不要只验证脚本能跑通。字幕翻译的结果最终要交给观众看,哪怕是影视剧里的专有名词、语气词翻译得不自然,也会明显影响观看体验。所以技术校验后,还需要人工抽读至少前 50 条,检查有没有出现语义错误、句子不完整或专有名词被硬翻的情况。
5.2 常见失败现象和排查方向
| 现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 时间轴全部丢失 | 解析 SRT 时用错了分隔符 | 打印第一条原始文本 | 按\n\n切分,并确认时间轴原样回写 |
| 译文顺序错乱 | 模型返回顺序和原文不一致 | 打印某一批次原始返回 | 使用 [序号] 解析,并按原始索引回填 |
| 字幕里有未翻译的英文标签 | 标签没有做占位符保护 | 打开生成文件检查{、<标签 | 翻译前把标签替换成占位符 |
| 部分序号缺失 | 批次过大或模型丢行 | 统计缺失序号集合 | 缩小批次,单独补翻缺失行 |
| 播放器里中文显示乱码 | 文件编码被识别为非 UTF-8 | 用文本编辑器查看编码 | 使用 UTF-8 with BOM 输出 |
| 请求频繁失败 | API Key 无效或触发限流 | 查看返回状态码和错误 body | 确认 Key 和余额,增加退避重试 |
5.3 最容易被忽视的坑:不要破坏字幕的多行文本
很多字幕文件里,一条字幕可能包含两行文本。翻译后,有时模型会把两行合并成一行。这不影响时间轴,但会影响播放器显示节奏。如果项目对排版有要求,可以在 prompt 中显式说明“保持原文的换行数量”,并在解析结果时对同一序号的多段落文本做换行保留处理。
6. 成本、缓存和生产化建议
6.1 用缓存和断点续传避免重复计费
字幕翻译第一批次如果翻译了一半就断网,重跑所有批次会浪费前一半的费用。最简单的方法是用文件缓存记录已经成功的批次。批次可以用内容哈希作为键。
import hashlib import json def batch_cache_key(lines: list[str]) -> str: raw = "\n".join(lines) return hashlib.md5(raw.encode("utf-8")).hexdigest()维护一个 JSON 文件:
def load_cache(path: str) -> dict: try: with open(path, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return {} def save_cache(path: str, cache: dict): with open(path, "w", encoding="utf-8") as f: json.dump(cache, f, ensure_ascii=False, indent=2)每次翻译前先查缓存,命中就直接使用缓存的译文,不调用 API。这样断点续传就变成“跳过已完成批次”的简单逻辑。
6.2 学习环境与生产环境的差异
在本地脚本里,所有配置都可以写在.env中,错误处理也可以临时打印日志。生产环境需要追加以下几项:
- 配置外置:API Key、模型名、批次大小、并发数放在配置中心或环境变量中,不写死在代码里。
- 日志审计:记录每个批次的请求时间、token 消耗、重试次数、失败原因,方便对账和定位问题。
- 并发和限流:如果多台机器同时翻译,需要控制对 API 的总请求速率,必要时引入 Redis 计数器或任务队列。
- 人工抽检:机器翻译结果要建立抽检流程,不能直接自动发布到线上视频平台。
- 回滚方案:历史上翻译失败会影响字幕发布时间,建议保留原始字幕和每轮翻译结果,支持快速回退。
如果使用了社区或第三方封装工具,比如本地拉取的所谓“harness”“桌面版”插件,需要先确认仓库来源、授权协议、是否长期维护,以及在什么网络环境下运行。很多第三方工具本质是套壳调用,安全审计成本并不低,生产环境使用前必须做封装层审查。
6.3 可复用的发布前检查清单
字幕翻译脚本发布前,建议走一遍下面的清单:
- API Key 从环境变量或配置中心读取,不进入 Git 仓库。