零成本在本地跑 Whisper:从视频自动生成双语字幕
很多视频没有字幕,你可能需要花钱用云服务,或者手动一个字一个字敲。
其实完全可以在自己的电脑上跑——一块 RTX 5060 Ti 16G 就够了。Faster-Whisper 比 OpenAI 原版快 2-4 倍,DeepSeek API 翻译全程不到一块钱,数据还不出门。
这篇文章拆解 whisper_v3 项目的核心代码,看看视频是怎么变成带时间戳的 SRT 字幕的。
整体架构:两个模块,各司其职
整个项目就两个核心模块:
- VideoSubtitleGenerator:负责"听写",视频 → 音频 → 语音识别 → SRT 字幕
- SubtitleCorrector:负责"翻译",把英文字幕变成中文(可选)
视频文件
↓ FFmpeg 提取音频
16kHz 单声道音频
↓ Faster-Whisper 语音识别
带时间戳的语音片段
↓ LCS 去重合并
SRT 字幕文件(英文)
↓ LLM 批量翻译(可选)
SRT 字幕文件(中文)
一、环境准备
1.1 FFmpeg:音视频瑞士军刀
Windows 安装三选一:
# 方法一:winget(最省心)
winget install Gyan.FFmpeg
# 方法二:chocolatey
choco install ffmpeg
# 方法三:手动下载,解压后把 bin 目录加到 PATH
验证安装:ffmpeg -version,输出版本号即成功。
1.2 下载 Whisper 模型
本项目用的是 Faster-Whisper(CTranslate2 格式),不是原生 PyTorch 模型,别下错了。
| 显存 | 推荐模型 | 下载地址 |
|---|---|---|
| 16G | large-v3(效果最好) | huggingface.co/Systran/faster-whisper-large-v3 |
| 8G | medium 或 small | huggingface.co/Systran/faster-whisper-medium |
| 无独显 | base | huggingface.co/Systran/faster-whisper-base |
解压后文件夹路径记下来,待会儿填进 config.py。
1.3 Python 依赖
pip install faster-whisper pysrt openai
faster-whisper:语音识别引擎,比原版快 2-4 倍pysrt:读写 SRT 字幕文件,不用自己正则硬拆openai:对接 DeepSeek API 或本地 LLM 翻译
1.4 翻译配置(二选一,非必须)
方式一:DeepSeek API(推荐)
去 DeepSeek 官网 注册获取 API Key(新用户送额度),填入 config.py:
DEEPSEEK_API_KEY = "sk-xxxxxxxxxxxx"
LOCAL_LLM_ENABLED = False
方式二:本地 LLM(完全离线)
用 LM Studio 加载模型(如 Qwen2-7B-Instruct),开启本地 API 服务(默认 http://127.0.0.1:1234/v1),然后:
LOCAL_LLM_ENABLED = True
LOCAL_LLM_API_BASE = "http://127.0.0.1:1234/v1"
LOCAL_LLM_MODEL_NAME = "qwen2-7b-instruct"
本地 LLM 的缺点是慢——生成字幕 10 分钟,翻译可能花 1 小时。
1.5 配置文件 core/config.py
# 模型路径(解压后的文件夹,不是 .bin 文件本身)
MODEL_PATH = r"D:\work\models\faster-whisper-large-v3"
# 有 NVIDIA 显卡写 "cuda",否则 "cpu"
DEVICE = "cuda"
# DeepSeek API 方式
DEEPSEEK_API_KEY = "sk-xxxxxxxxxxxx"
LOCAL_LLM_ENABLED = False
二、视频字幕生成:VideoSubtitleGenerator
核心就一个类,几个方法各司其职。
2.1 整体流程
def generate_subtitle(self, video_path, output_srt=None, language=None):
# 1. 音频提取
audio_path = self.extract_audio(video_path)
# 2. Whisper 语音识别,返回带时间戳的片段
result = self.transcribe_with_timestamp(audio_path, language)
# 3. 合并重复字幕
result["segments"] = self.merge_duplicate_subtitles(result["segments"])
# 4. 生成 SRT 文件
self.create_srt(result["segments"], output_srt)
四步:剥壳 → 听写 → 纠错 → 抄正。
2.2 音频提取:extract_audio()
第一步必须先检查视频有没有音轨——很多人踩的第一个坑就是对着没声音的视频识别半天。
def extract_audio(self, video_path: str, output_audio: str = None) -> str:
# 用 ffprobe 检查音频轨道
cmd_check = [
"ffprobe", "-v", "error", "-select_streams", "a",
"-show_entries", "stream=codec_type",
"-of", "default=noprint_wrappers=1:nokey=1", video_path
]
result = subprocess.run(cmd_check, capture_output=True, text=True)
audio_streams = result.stdout.strip()
if not audio_streams:
raise ValueError(f"文件 '{os.path.basename(video_path)}' 中没有音频流")
# 用 ffmpeg 提取音频
cmd = [
"ffmpeg", "-i", video_path,
"-vn", # 不要视频流
"-acodec", "pcm_s16le", # PCM 16bit
"-ar", "16000", # 16kHz 采样率(Whisper 训练格式)
"-ac", "1", # 单声道
"-y", output_audio
]
subprocess.run(cmd, ...)
关键参数解释:
-ar 16000:Whisper 训练用的是 16kHz,不是 44.1kHz。格式不匹配准确率会下降,就像让学英语的人去考法语四级。-ac 1:单声道,Whisper 不需要立体声。
2.3 语音识别:transcribe_with_timestamp()
用 Faster-Whisper,半精度计算,显存占用减半、速度翻倍。
def _load_model(self):
from faster_whisper import WhisperModel
self.pipe = WhisperModel(
self.model_path,
device=self.device,
compute_type="float16" if self.device == "cuda" else "float32"
)
def transcribe_with_timestamp(self, audio_path: str, language=None):
self._load_model()
segments, info = self.pipe.transcribe(
audio_path,
language=language or config.WHISPER_LANGUAGE or "en",
word_timestamps=True,
vad_filter=True, # 语音活动检测,过滤静音和背景音乐
vad_parameters=dict(
min_silence_duration_ms=500, # 静音超过 500ms 算断句
speech_pad_ms=200, # 每段前后加 200ms 缓冲
min_speech_duration_ms=100
)
)
VAD(Voice Activity Detection) 是关键:
- 没开 VAD:Whisper 会把咳嗽声识别成"嗯哼",背景猫叫也不放过
- 开了 VAD:只录人声,相当于给视频装了"只录取人声"的过滤器
speech_pad_ms=200 是给每句话加个安全气囊,防止语音被截断。
2.4 字幕去重合并:merge_duplicate_subtitles()
Whisper 有时会重复识别同一句话,比如:
[0.0-1.0] 今天我们来聊聊
[1.0-2.0] 我们来聊聊 Python
[2.0-3.0] Python 装饰器
合并算法基于最长公共子序列(LCS):
def merge_duplicate_subtitles(self, segments, similarity_threshold=0.85):
merged = []
i = 0
while i < len(segments):
current = segments[i]
merged_text = current["text"]
merged_start = current["start"]
merged_end = current["end"]
# 往后看,能合并就合并
while i + 1 < len(segments):
next_seg = segments[i + 1]
is_identical = merged_text == next_seg["text"]
similarity = self._calculate_similarity(merged_text, next_seg["text"])
if is_identical or similarity >= similarity_threshold:
merged_end = max(merged_end, next_seg["end"])
i += 1
continue
break
merged.append({"start": merged_start, "end": merged_end, "text": merged_text})
i += 1
return merged
相似度 = LCS长度 / 较长字符串长度,超过 85% 就合并。
2.5 SRT 文件生成:create_srt()
最后一步,用 pysrt 生成标准 SRT 格式。
def create_srt(self, segments, output_srt):
subs = pysrt.SubRipFile()
for i, segment in enumerate(segments, 1):
start, end = segment["start"], segment["end"]
# 防御:end <= start 时强行给 0.5 秒
if end <= start:
end = start + 0.5
# 防御:单条超过 30 秒的直接丢弃(99% 是识别错误)
if end - start > 30:
continue
sub = pysrt.SubRipItem()
sub.index = i
sub.start = timedelta(seconds=start)
sub.end = timedelta(seconds=end)
sub.text = segment["text"]
subs.append(sub)
subs.save(output_srt, encoding='utf-8')
两个防御机制:
end <= start:SRT 格式要求结束时间必须大于开始时间,播放器遇到无效时间戳直接罢工end - start > 30:超过 30 秒一句话,要么是识别错误,要么是唐僧转世
三、字幕翻译:SubtitleCorrector
如果你只需要英文字幕,这部分可以跳过。需要中文的话,继续。
3.1 双后端初始化
def __init__(self, api_key=None, api_base=None, use_local_llm=None):
if self.use_local_llm:
# LM Studio 不验证 key,填个假的就行
self.client = OpenAI(api_key="dummy", base_url=config.LOCAL_LLM_API_BASE)
else:
self.client = OpenAI(api_key=api_key, base_url=api_base)
3.2 批量翻译:translate_srt_file()
def translate_srt_file(self, input_srt, output_srt, batch_size=200):
subs = pysrt.open(input_srt, encoding='utf-8')
effective_batch_size = config.LOCAL_LLM_BATCH_SIZE if self.use_local_llm else batch_size
translated_subs = pysrt.SubRipFile()
for i in range(0, len(subs), effective_batch_size):
batch = subs[i:i + effective_batch_size]
try:
translated_batch = self._translate_batch(batch)
translated_subs.extend(translated_batch)
except Exception as e:
# 翻译失败保留原文,不中断流程
translated_subs.extend(batch)
核心思想:分批翻译 + 失败保底。翻译失败直接保留英文,不让整个流程崩掉。
3.3 批量翻译核心:_translate_batch()
这是整个翻译模块的灵魂——怎么让 LLM 乖乖按顺序翻译、不偷懒?
def _translate_batch(self, batch):
texts = [sub.text for sub in batch]
combined = "\n".join([f"[{i+1}] {text}" for i, text in enumerate(texts)])
prompt = f"""请将以下{len(batch)}条字幕翻译成中文。
要求:
1. 严格按顺序翻译,每条字幕对应一行翻译结果
2. 必须输出恰好{len(batch)}行,一行不多一行不少
3. 只返回翻译结果,不要任何解释
待翻译:
{combined}
请输出恰好{len(batch)}行翻译:"""
response = self.client.chat.completions.create(
model=self.model_name,
messages=[
{"role": "system", "content": "你是专业字幕翻译..."},
{"role": "user", "content": prompt}
],
temperature=0.3,
max_tokens=8000,
timeout=30
)
# 验证行数——LLM 经常自作聪明合并句子或漏行
translated_lines = response.choices[0].message.content.split('\n')
if len(translated_lines) != len(texts):
# 重试,降低温度让它当复读机
retry_response = self.client.chat.completions.create(
..., temperature=0.1 # 几乎等于复读
)
行数验证是这段代码的灵魂。LLM 经常自作聪明合并相似句子,发现行数不对立刻用 temperature=0.1 重试——这个温度下 LLM 基本就是复读机,不敢发挥。
四、技术亮点总结
| 特性 | 作用 |
|---|---|
| VAD 语音活动检测 | 过滤背景音,只识别真实人声 |
| float16 半精度 | 显存减半,速度翻倍 |
| LCS 去重合并 | 解决 Whisper 重复识别问题 |
| 时间戳安全修复 | 防止 SRT 播放器崩溃 |
| 翻译失败保底 | 宁可保留原文,不丢字幕 |
| 行数验证 + 重试 | 防止 LLM 偷工减料 |
五、和云服务对比
| 对比项 | 本文方案 | 付费云服务 |
|---|---|---|
| 费用 | 几乎为零(DeepSeek API 每百万字几毛钱) | 每次几元到几十元 |
| 数据隐私 | 完全本地,数据不出门 | 视频上传到第三方 |
| 速度 | 取决于显卡(5060 Ti 够用) | 依赖网速和服务器排队 |
| 定制化 | 可调整参数、可接本地 LLM | 只能调基础参数 |
| 适用场景 | 定期处理大量视频 | 偶尔用一次 |
全程跑在你的 5060 Ti 上,还避免了"你的教学视频出现在别人训练集里"的社死事件。
项目参考:whisper_v3 源码
依赖项目:OpenAI Whisper · Faster-Whisper · PySRT