个人电脑玩AI-02让5060 Ti给你打工——Whisper语音识别篇(下)

by 雪隐_上班了 from juejin.cn/user/143341...

欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权。

whisper_v3 核心代码解析:如何从视频自动生成字幕

前言

上一篇文章我们聊了 Whisper 是什么、能干什么、以及各模型怎么选。

这一篇不废话,直接开膛破肚------看看 whisper_v3 项目的核心代码,怎么把一个没字幕的烂视频,变成带时间戳的 SRT 字幕文件

整个项目就两个模块,分工明确:

  1. VideoSubtitleGenerator --- 负责"听写":视频 → 音频 → 语音识别 → SRT 字幕
  2. SubtitleCorrector --- 负责"翻译":把英文字幕变成中文(可选,非必须)

友情提示:看不懂代码没关系,能看懂段子就行。反正你最后也是复制粘贴跑。


一、环境准备:先把工具磨好

跑代码之前,有几个东西必须先装上,否则后面会报错报到你想砸电脑。

1.1 FFmpeg:音视频界的瑞士军刀

字幕生成的第一步是提取音频,这活儿全靠 FFmpeg。

Windows 安装(三选一,哪个顺手用哪个):

bash 复制代码
# 方法一:winget(最省心)
winget install Gyan.FFmpeg

# 方法二:chocolatey(如果你装了)
choco install ffmpeg

# 方法三:手动下载,扔进 PATH
# 下载地址自己搜,解压后把 bin 目录加到系统环境变量

装完验证一下:

bash 复制代码
ffmpeg -version
# 如果输出版本号而不是"不是内部命令",恭喜你,成功迈出第一步。

有些人觉得 FFmpeg 难装。其实不难,就是环境变量里添个路径。比追女生简单多了------至少 FFmpeg 不会猜你为什么没放对位置。

1.2 下载 Whisper 模型(记得选对格式)

本项目用的是 Faster-Whisper(CTranslate2 格式),不是原生 PyTorch 模型。别下错了。

bash 复制代码
# 推荐 large-v3,效果最好,你的 5060 Ti 16G 刚好装下
https://huggingface.co/Systran/faster-whisper-large-v3

# 如果你显卡只有 8G,选 medium 或 small
https://huggingface.co/Systran/faster-whisper-medium
https://huggingface.co/Systran/faster-whisper-small

下载后解压,得到一个文件夹,里面是 model.bin 之类的文件。

把文件夹路径记下来,待会儿填进 config.pyMODEL_PATH

下载模型的时候可以去泡杯咖啡。如果泡完咖啡还没下完,说明你网速不如 56K 猫,建议换时间再下。

1.3 安装 Python 依赖

bash 复制代码
pip install faster-whisper pysrt openai
  • faster-whisper:语音识别引擎,比原版快 2-4 倍(显卡表示很欣慰)
  • pysrt:读写 SRT 字幕文件(不用自己正则硬拆,少掉头发)
  • openai:对接 DeepSeek API 或本地 LLM(翻译用)

1.4 翻译模型配置(二选一,非必须)

如果你只需要英文字幕,这步可以跳过。但大多数人都想转中文,那就接着看。

方式一:DeepSeek API(推荐,省心)

  1. DeepSeek 官网 注册,获取 API Key(新用户送额度,真香)
  2. 填到 config.pyDEEPSEEK_API_KEY
  3. 设置 LOCAL_LLM_ENABLED = False

方式二:本地 LLM(完全离线,装X专用)

  1. 下载 LM Studio
  2. 找一个支持中文翻译的模型,比如 Qwen2-7B-InstructLlama3-8B 或者翻译特化的 hy-mt2-7b
  3. 在 LM Studio 里加载模型,开启本地 API 服务(默认 http://127.0.0.1:1234/v1
  4. 修改 config.pyLOCAL_LLM_ENABLED = True,填入模型名称

本地 LLM 的优点是隐私,缺点是慢。你花 10 分钟生成字幕,再花 1 小时翻译------正好可以去吃个午饭。

1.5 最后的配置文件:core/config.py

修改几个关键参数:

python 复制代码
# 模型路径(解压后的文件夹,不是 .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

# 或者本地 LLM 方式(上面那个要改成 True)
# LOCAL_LLM_ENABLED = True
# LOCAL_LLM_API_BASE = "http://127.0.0.1:1234/v1"
# LOCAL_LLM_MODEL_NAME = "qwen2-7b-instruct"

配置好了就别再动,除非你换了显卡。


二、视频字幕生成引擎:core/video_to_subtitle.py

这个模块负责把视频变成 SRT 字幕。核心就一个类:VideoSubtitleGenerator,里面几个方法各司其职。

2.1 整体流程(四步走)

python 复制代码
def generate_subtitle(self, video_path, output_srt=None, language=None, keep_audio=False):
    # 1. 音频提取
    audio_path = self.extract_audio(video_path)

    # 2. Whisper 语音识别,返回带时间戳的片段
    result = self.transcribe_with_timestamp(audio_path, language)

    # 3. 合并重复字幕(Whisper 有时会重复说同一句话)
    result["segments"] = self.merge_duplicate_subtitles(result["segments"])

    # 4. 生成 SRT 文件
    self.create_srt(result["segments"], output_srt)

你可以把这四步想象成:剥壳 → 听写 → 纠错 → 抄正。小学生改作文都没这么认真。

2.2 音频提取:extract_audio() ------ 先检查视频有没有声音

很多新手踩的第一个坑:视频本身没音轨,Whisper 对着空气识别半天,输出空字幕。

python 复制代码
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, check=True)
    audio_streams = result.stdout.strip()
    if not audio_streams:
        raise ValueError(f"文件 '{os.path.basename(video_path)}' 中没有音频流...")

如果这一步报错,别怪代码。是你的视频不够"声情并茂"

检查通过后,用 ffmpeg 提取音频:

python 复制代码
    cmd = [
        "ffmpeg", "-i", video_path,
        "-vn",           # 不要视频流
        "-acodec", "pcm_s16le",   # PCM 16bit
        "-ar", "16000",           # 16kHz 采样率
        "-ac", "1",               # 单声道
        "-y", output_audio
    ]

16kHz 单声道是 Whisper 训练时用的格式,必须匹配。你非要用 44.1kHz 立体声,Whisper 也能跑,但准确率会下降,就像让一个学英语的人去考法语四级。

2.3 语音识别:transcribe_with_timestamp() ------ 真正的核心

这里用的是 Faster-Whisper,不是 OpenAI 原版。为什么?因为快。

python 复制代码
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"
    )

float16 半精度计算,显存占用减半,速度翻倍------5060 Ti 的 16G 显存在这件事上显得格外帅气

然后开始识别:

python 复制代码
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_filter=True:Voice Activity Detection。没有它,Whisper 连背景里的猫叫都会识别成"喵喵喵"。
  • speech_pad_ms=200:防止语音被截断,相当于给每句话加了个安全气囊。

返回的 segments 是一个迭代器,每个元素是 {start, end, text}

2.4 字幕去重合并:merge_duplicate_subtitles() ------ 专治 Whisper 的碎碎念

Whisper 有时候会把同一句话切分成多个相似的片段,比如:

csharp 复制代码
[0.0-1.0] 今天我们来聊聊
[1.0-2.0] 我们来聊聊 Python
[2.0-3.0] Python 装饰器

这显然是病,得治。合并算法基于 最长公共子序列(LCS)

python 复制代码
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 not is_identical else 1.0

            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% 的字符都一样,就认为是在重复

你可能会问:为什么要手写 LCS,不用 difflib

答:因为手写显得我比较硬核。实际上就是懒得加依赖。

2.5 SRT 文件生成:create_srt() ------ 最后一步,也是最稳的一步

python 复制代码
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 秒(通常是识别错了)
        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 → 补 0.5 秒,避免播放器崩溃
  • end - start > 30 → 直接丢弃,这种超长片段 99% 是识别错误(比如把整段背景音乐当成语音)

有些人问:为什么不调大阈值到 60 秒?

答:30 秒一句话,你是播音员还是唐僧?


三、字幕翻译校正器:core/subtitle_corrector.py

如果你只需要英文字幕,这段可以不看。但如果你像我一样,看英文就想睡觉,那翻译模块就是你的救星。

3.1 双后端初始化:支持 DeepSeek 和本地 LLM

python 复制代码
def __init__(self, api_key=None, api_base=None, use_local_llm=None):
    if use_local_llm is None:
        self.use_local_llm = config.LOCAL_LLM_ENABLED
    else:
        self.use_local_llm = use_local_llm

    if self.use_local_llm:
        self.client = OpenAI(api_key="dummy", base_url=config.LOCAL_LLM_API_BASE)
        self.model_name = config.LOCAL_LLM_MODEL_NAME
    else:
        self.client = OpenAI(api_key=api_key, base_url=api_base)

本地 LLM 需要填一个假的 api_key,因为 LM Studio 不验证 key。

这个设计很像某些小区的门禁------刷什么都行,只要刷了就行

3.2 批量翻译:translate_srt_file() ------ 既快又稳

python 复制代码
def translate_srt_file(self, input_srt, output_srt, progress_callback=None, batch_size=200):
    subs = pysrt.open(input_srt, encoding='utf-8')
    total_count = len(subs)

    # 本地 LLM 每次只能处理 5 条,云端 API 可以 200 条
    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, total_count, 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:
            # 翻译失败保留原文,不中断流程
            print(f"批次翻译失败,保留原文: {e}")
            translated_subs.extend(batch)

    translated_subs.save(output_srt, encoding='utf-8')

核心思想:分批翻译 + 失败保底

不指望 LLM 每次都完美,但至少别让整个流程崩掉。就像考试不会的题先空着,别把整张卷子撕了。

3.3 批量翻译核心:_translate_batch() ------ 和 LLM 斗智斗勇

python 复制代码
def _translate_batch(self, batch):
    texts_to_translate = [sub.text for sub in batch]
    combined_text = "\n".join([f"[{i+1}] {text}" for i, text in enumerate(texts_to_translate)])

    prompt = f"""请将以下{len(batch)}条字幕翻译成中文。
要求:
1. 严格按顺序翻译,每条字幕对应一行翻译结果
2. 必须输出恰好{len(batch)}行翻译,一行不多一行不少
3. 只返回翻译结果,不要任何解释、序号、前缀
4. 保持口语化风格,专业术语准确

待翻译:
{combined_text}

请输出恰好{len(batch)}行翻译:"""

这里的技巧是用 [序号] 原文 告诉 LLM 每条的顺序,并且强制要求输出行数等于输入行数

这就好比你跟服务员说:"我要三个菜,你给我端三个盘子来,不许合并,不许少,不许多。"------LLM 就是那个经常试图合并菜品的服务员。

python 复制代码
    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=120 if self.use_local_llm else 30
    )

    # 验证行数
    translated_lines = [line.strip() for line in response.choices[0].message.content.split('\n') if line.strip()]
    if len(translated_lines) != len(texts_to_translate):
        # 重试,用更低温度
        retry_prompt = f"请严格输出{len(texts_to_translate)}行翻译,每行对应一条字幕,不要序号。\n{combined_text}"
        retry_response = self.client.chat.completions.create(
            model=self.model_name,
            messages=[{"role": "system", "content": "你是专业字幕翻译,必须输出指定行数。"}, {"role": "user", "content": retry_prompt}],
            temperature=0.1,
            max_tokens=8000
        )
        translated_lines = [line.strip() for line in retry_response.choices[0].message.content.split('\n') if line.strip()]

行数验证是整段代码的灵魂。LLM 经常自作聪明合并相似句子,或者漏掉某一行。发现行数不对,立刻用更低温度重试------温度 0.1 几乎等于让 LLM 当复读机。

最后用正则把序号前缀去掉:

python 复制代码
    translated = re.sub(r'^\d+[\.\、]\s*', '', translated)
    new_sub.text = translated

这样 1. 今天天气不错 就变成 今天天气不错。干净利落。


四、技术亮点(吹牛时间)

4.1 VAD 语音活动检测:不再把猫叫当对话

没开 VAD 的时候,Whisper 连背景里的咳嗽声都会识别成"嗯哼"。

开启 vad_filter=True 后,只有人声片段才会被识别。

效果就像给你的视频装了个"只录取人声"的过滤器,猫主子再也不能抢戏。

4.2 时间戳安全修复:避免 SRT 播放崩溃

SRT 格式要求每条字幕的结束时间必须大于开始时间。Whisper 偶尔会生成 end <= start 的无效片段,播放器遇到这种直接罢工。

我们的代码会强制修复成 0.5 秒最小长度,并且丢弃超过 30 秒的异常片段。

这叫 "数据清洗,从我做起"

4.3 翻译失败保底:宁可保留原文,不丢字幕

调用 LLM 翻译时,网络抖动、API 限流、模型抽风......都可能失败。

代码捕获异常后,直接把英文原文保留在原位置。

你得到的结果可能中英混杂,但至少每句话都在。比某度翻译那种"丢失半句"要强一百倍。


五、总结

whisper_v3 的核心代码其实就做两件事:

第一件事:视频 → 字幕

  • FFmpeg 检查音轨 → 提取 16kHz 单声道音频
  • Faster-Whisper + VAD 识别 → 带时间戳的片段
  • LCS 去重合并 → pysrt 生成标准 SRT

第二件事:字幕翻译

  • 批量请求 LLM(DeepSeek API 或本地 LM Studio)
  • 行数验证 + 重试机制(防止 LLM 偷工减料)
  • 失败时保留原文,绝不丢字幕

全程跑在你的 5060 Ti 16G 上,数据不出门,隐私有保障。

对比那些需要上传视频到云端的付费服务,你不仅省了钱,还避免了"你的教学视频出现在别人的训练集里"这种社死事件。

本章代码在此,自取不谢


致谢 & 参考:

  • OpenAI Whisper --- 开源语音识别的扛把子
  • Faster-Whisper --- 让 Whisper 跑得像火箭
  • PySRT --- 让你不用手撕 SRT 格式

如果这篇文章让你在写代码时笑出了声,请点赞、评论、转发

你们的笑声是我继续熬夜的唯一动力(咖啡不算,咖啡因已经免疫了)。

谢谢大家 🙏

祝你们的字幕永远对齐,翻译从不抽风。

相关推荐
HIT_Weston1 小时前
110、【Agent】【OpenCode】todowrite 工具提示词(示例)(四)
人工智能·agent·opencode
ECT-OS-JiuHuaShan1 小时前
什么是对和错?——“有针对性定义域的逻辑值的真伪”:认识论终极追问的公理化裁决
数据库·人工智能·算法·机器学习·数学建模
澹锦汐1 小时前
从 0 到 1 构建 AI 创意工具:独立开发者的 LLM 应用实战
人工智能
道友可好1 小时前
Superpowers vs OpenSpec vs Spec Kit:该选哪个?
前端·人工智能·后端
xixingzhe21 小时前
AI运维注意点
运维·人工智能
morning_judger1 小时前
Agent开发系列(十一)-知识库建设(知识地图)
人工智能
zhangfeng11331 小时前
能让不同架构的gpu一起训练 跨芯片统一、异构混合训练、自动并行调优
人工智能·架构·transformer
王牌狮AIen1 小时前
合规生命线——警惕“AI投毒”与算法陷阱,如何为品牌装上“事前免疫”系统?
大数据·人工智能·数据挖掘·geo·ai营销