【音频字幕】构建一个离线视频字幕生成系统:使用 WhisperX 和 Faster-Whisper 的 Python 实现

发布于:2025-09-08 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、背景介绍

  • 对于一端没有字幕外国视频、字幕,在不懂外语的情况下,怎么获取相关内容?
  • 作为技术宅,怎么自建搭建一个语音转文字的环境
  • 当前AI技术这么发达? 试试

二、系统设计

  • 音频提取(仅仅是视频需要该逻辑、本身就是音频不需要)
  • 优化(去掉静音、噪声等)
  • 语音识别
  • 对齐
  • 合并和 生成最终文字信息

系统主要依赖 Faster-Whisper(语音识别加速版)、WhisperX(时间戳对齐工具)、以及音频处理模块(如 LUFS 标准化和高通滤波)。

Faster-Whisper(语音识别加速版)

Whisper large-v3(推荐,多语言,性能最佳):https://huggingface.co/openai/whisper-large-v3
Whisper medium(代码中默认,平衡速度和精度):https://huggingface.co/openai/whisper-medium
Whisper medium.en(英语专用,速度更快):https://huggingface.co/openai/whisper-medium.en
Whisper small(轻量,适合低资源设备):https://huggingface.co/openai/whisper-small
Whisper tiny(最小模型,速度最快):https://huggingface.co/openai/whisper-tiny

WhisperX(时间戳对齐工具)

中文对齐模型(wav2vec2-large-xlsr-53-chinese-zh-cn):https://huggingface.co/jonatasgrosman/wav2vec2-large-xlsr-53-chinese-zh-cn
日语对齐模型(wav2vec2-large-xlsr-53-japanese):https://huggingface.co/jonatasgrosman/wav2vec2-large-xlsr-53-japanese
多语言备用模型(wav2vec2-large-xlsr-53):https://huggingface.co/facebook/wav2vec2-large-xlsr-53

音频处理模块(如 LUFS 标准化和高通滤波)

系统整体设计思路

系统的设计核心是“端到端”处理视频到字幕的流程:提取音频 → 优化信号 → 转录文本 → 对齐时间戳 → 合并并输出 SRT。离线运行是关键需求,因此优先使用本地模型和量化加速,避免网络依赖。

  • 设计方案:采用模块化架构,每个步骤独立函数,便于调试和扩展。预加载模型减少重复计算,支持 GPU 加速(Torch 和 CUDA)。回退逻辑(如模型缺失时使用未对齐结果)确保鲁棒性。
  • 思路:结合监督学习(Whisper 的多语言训练)和自监督表示(Wav2Vec2),实现高精度 ASR。音频优化先行,以提升识别准确率(噪音减少可降低字错误率 WER 达 10-20%)。

Whisper 模型:语音识别的核心算法与原理

Whisper 是 OpenAI 开发的通用语音识别模型,Faster-Whisper 是其加速实现。Whisper 的设计思路是构建一个多任务、多语言的序列到序列(seq2seq)模型,能够处理转录、翻译和语言识别等多项任务。

原理与设计思路

  • 核心思路:Whisper 使用 Transformer 架构,将音频梅尔谱图(Mel-spectrogram)作为输入,输出文本序列。训练于 680,000 小时多语言数据,支持 99 种语言,强调鲁棒性(处理噪音、口音)。 多任务学习允许模型同时预测时间戳和文本,减少了单独的时间对齐步骤。
  • 算法流程
    1. 音频预处理:将原始音频转换为梅尔谱图。
    2. 编码器:提取音频特征。
    3. 解码器:生成文本序列,使用 beam search 优化输出(beam_size=5 在代码中)。
  • 设计方案:Transformer 的自注意力机制捕捉长距离依赖,适合长音频。模型大小从 tiny 到 large,平衡准确率和速度。

Faster-Whisper:加速原理与优化方案

Faster-Whisper 是 Whisper 的再实现,使用 CTranslate2 引擎加速推理。

原理与设计思路

  • 核心思路:针对 Whisper 的高计算成本(Transformer 的 O(n^2) 复杂度),Faster-Whisper 通过量化、批处理和内核优化加速。设计方案聚焦于生产环境:支持 INT8/FP16 量化,减少内存占用 50%,推理速度提升 4-12 倍。
  • 算法流程
    1. 模型转换:将 PyTorch 模型转换为 CTranslate2 格式。
    2. 量化:权重从 FP32 降到 INT8,代码中 COMPUTE_TYPE=“int8” 是默认。
    3. 并行处理:支持 batch_size>1 和多线程。
  • 设计方案:静态缓存和内核融合减少冗余计算。例如,在长音频中,分段处理并合并结果。

三、系统实现

系统概述和技术栈

在开始拆解代码前,让我们先了解系统的整体架构:

  • 输入:视频文件(例如 MP4 格式)。
  • 输出:对应的 SRT 字幕文件。
  • 关键步骤
    1. 提取音频。
    2. 优化音频(音量标准化和高通滤波)。
    3. 语音识别和时间戳对齐。
    4. 字幕合并。
    5. 生成 SRT 文件。
  • 技术栈
    • 语音识别:Faster-Whisper(Whisper 模型的加速版,支持 INT8 量化以减少内存占用)。
    • 时间戳对齐:WhisperX(基于 Wav2Vec2 的对齐工具)。
    • 音频处理:MoviePy(提取音频)、Pydub(优化音频)。
    • 其他:Torch(GPU 加速)、Tqdm(进度条)、Logging(日志记录)。
    • 环境配置:强制离线模式,通过环境变量避免网络访问。

这个系统优化了资源管理(如 GPU 内存清理),并支持配置文件和命令行参数,提高了灵活性。现在,让我们逐模块拆解代码。

全局配置和环境设置

代码开头定义了全局参数,这是系统的“控制中心”。这里设置了模型路径、设备(GPU 或 CPU)和计算类型。

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.FileHandler("subtitle_generator.log"), logging.StreamHandler()]
)

# 加载配置文件(如果存在)
CONFIG_FILE = "config.json"
config = {}
if os.path.exists(CONFIG_FILE):
    try:
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            config = json.load(f)
        logging.info("已加载配置文件: config.json")
    except Exception as e:
        logging.warning(f"加载配置文件失败: {e},使用默认配置")

# 全局参数与模型路径配置
MODELS_DIR = config.get("models_dir", "/path/to/your/models")  # 示例路径,请替换为实际路径
os.environ["WHISPERX_MODEL_DIR"] = MODELS_DIR
os.environ["HF_HUB_OFFLINE"] = "1"  # 强制使用本地模型
os.environ["TRANSFORMERS_OFFLINE"] = "1"  # 禁用transformers的网络访问

WHISPER_MODEL_PATH = config.get("whisper_model_path", f"{MODELS_DIR}/faster-whisper-medium")
ALIGN_MODEL_PATHS = config.get("align_model_paths", {
    "zh": f"{MODELS_DIR}/wav2vec2-large-xlsr-53-chinese-zh-cn",
    "ja": f"{MODELS_DIR}/wav2vec2-large-xlsr-53-japanese",
})
OTHER_LANGUAGES_MODEL_PATHS = config.get("other_languages_model_path", f"{MODELS_DIR}/wav2vec2-large-xlsr-53")

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
COMPUTE_TYPE = config.get("compute_type", "int8")
MAX_GAP_MS = config.get("max_gap_ms", 1000)
MERGE_IDENTICAL = config.get("merge_identical", True)
MAX_LENGTH_CHARS = config.get("max_length_chars", 80)
TARGET_LUFS = config.get("target_lufs", -20.0)
HIGH_PASS_FREQ = config.get("high_pass_freq", 100)

拆解解释

  • 日志配置:使用 Python 的 logging 模块记录信息、警告和错误,支持控制台和文件输出。这有助于调试和监控系统运行。
  • 配置文件加载:通过 JSON 文件(如 config.json)动态加载参数,提高可配置性。如果文件不存在,使用默认值。
  • 环境变量:设置 HF_HUB_OFFLINETRANSFORMERS_OFFLINE 确保 Hugging Face 模型库不从网络下载,强制本地运行。
  • 模型路径:支持特定语言的对齐模型(如中文和日语),并提供多语言备用。DEVICECOMPUTE_TYPE 优化 GPU 使用,INT8 量化可将内存需求降低 50% 以上。
  • 其他参数:如字幕合并的最大间隔(1000ms)和字符长度限制(80),这些是经验值,可根据实际需求调整。

这个部分体现了系统的灵活性:用户可以通过修改 config.json 轻松自定义路径和参数,而无需更改代码。

工具函数

接下来是辅助函数,用于格式化时间戳。

def format_time(seconds):
    """将秒数转换为SRT标准时间格式 (HH:MM:SS,ms)"""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    secs = int(seconds % 60)
    millis = int((seconds - int(seconds)) * 1000)
    return f"{hours:02}:{minutes:02}:{secs:02},{millis:03}"

拆解解释

  • 这个函数将浮点秒数转换为 SRT 所需的字符串格式(如 “00:01:23,456”)。
  • 使用整数除法和模运算处理小时、分钟、秒和毫秒,确保输出始终是两位数(使用 f-string 格式化)。
  • 这是 SRT 文件的标准要求,避免了手动字符串拼接的错误。

核心处理流程

系统的“心脏”部分,包括音频提取、优化、转录、对齐、合并和 SRT 生成。

音频提取

def extract_and_prepare_audio(video_path, output_audio_path):
    """从视频中提取音频"""
    logging.info("[步骤 1/5] 正在从视频中提取音频...")
    try:
        os.makedirs(os.path.dirname(output_audio_path), exist_ok=True)
        with AudioFileClip(video_path) as audio_clip:
            audio_clip.write_audiofile(
                output_audio_path,
                codec='pcm_s16le',
                fps=16000,
                ffmpeg_params=['-ac', '1']
            )
        logging.info(f"  音频已成功提取并保存至: {output_audio_path}")
        return True
    except Exception as e:
        logging.error(f"  [错误] 音频提取失败: {e}")
        raise

拆解解释

  • 使用 MoviePy 的 AudioFileClip 从视频提取音频,转换为 WAV 格式(采样率 16000Hz,单声道)。
  • FFmpeg 参数确保兼容 Whisper 模型的输入要求。
  • 异常处理:如果失败,记录错误并抛出异常,防止后续步骤执行。

音频优化

def optimize_audio(input_audio_path, output_audio_path, target_lufs=TARGET_LUFS, high_pass_freq=HIGH_PASS_FREQ):
    """音频优化 (不使用AI降噪)"""
    logging.info("[步骤 2/5] 正在进行音频优化...")
    try:
        audio = AudioSegment.from_wav(input_audio_path)
        normalized_audio = audio.apply_gain(target_lufs - audio.dBFS)
        filtered_audio = normalized_audio.high_pass_filter(high_pass_freq)
        filtered_audio.export(output_audio_path, format="wav")
        logging.info(f"  优化后的音频已保存至: {output_audio_path}")
        return True
    except Exception as e:
        logging.error(f"  [错误] 音频优化失败: {e}")
        try:
            os.rename(input_audio_path, output_audio_path)
            logging.info(f"  使用原始音频作为替代: {output_audio_path}")
            return True
        except:
            logging.error("  [严重错误] 无法保存音频文件")
            return False

拆解解释

  • 使用 Pydub 加载音频,应用音量标准化(目标 -20 LUFS,广播标准)和高通滤波(去除 100Hz 以下低频噪音)。
  • 如果优化失败,回退到原始音频,确保流程不中断。
  • 这步提升了语音识别准确率,尤其在嘈杂或低音视频中。

语音识别与时间戳对齐

def transcribe_and_align(audio_path, whisper_model, align_models):
    """语音识别与时间戳对齐"""
    logging.info("[步骤 3/5] 正在进行高精度语音识别与对齐...")
    try:
        # 阶段1: 语音识别
        logging.info(f"  - 正在使用 Whisper 模型: {WHISPER_MODEL_PATH}")
        audio = whisperx.load_audio(audio_path)
        segments, info = whisper_model.transcribe(
            audio,
            language=None,  # 自动检测语言
            task="transcribe",
            beam_size=5,
            word_timestamps=True
        )
        segments = list(tqdm(segments, desc="转录音频"))

        result = {
            "segments": [],
            "language": info.language
        }
        for segment in segments:
            result["segments"].append({
                "start": segment.start,
                "end": segment.end,
                "text": segment.text,
                "words": [{
                    "word": word.word,
                    "start": word.start,
                    "end": word.end,
                    "probability": word.probability
                } for word in segment.words] if segment.words else None
            })

        # 检测语言
        detected_language = info.language
        logging.info(f"  - 检测到语言: '{detected_language}'")

        # 阶段2: 时间戳对齐
        logging.info("  - 准备时间戳对齐...")
        align_model_path = ALIGN_MODEL_PATHS.get(detected_language)
        if align_model_path is None or not os.path.exists(align_model_path):
            align_model_path = OTHER_LANGUAGES_MODEL_PATHS
            if not os.path.exists(align_model_path):
                logging.warning(f"  [警告] 多语言对齐模型路径不存在: {align_model_path}")
                logging.info("  使用未对齐的识别结果")
                return result
            logging.info(f"  - 未找到语言 '{detected_language}' 的本地对齐模型,切换到多语言模型")

        required_files = ["preprocessor_config.json", "pytorch_model.bin", "config.json", "vocab.json"]
        missing_files = [f for f in required_files if not os.path.exists(os.path.join(align_model_path, f))]
        if missing_files:
            logging.warning(f"  [警告] 对齐模型缺少文件: {', '.join(missing_files)}")
            logging.info("  使用未对齐的识别结果")
            return result

        logging.info(f"  - 加载本地对齐模型: {align_model_path}")
        if detected_language in align_models:
            model_a, metadata = align_models[detected_language]
        else:
            model_a, metadata = whisperx.load_align_model(
                language_code=detected_language,
                device=DEVICE,
                model_dir=align_model_path
            )
            align_models[detected_language] = (model_a, metadata)  # 缓存模型

        aligned_result = whisperx.align(
            result["segments"],
            model_a,
            metadata,
            audio,
            DEVICE,
            return_char_alignments=False
        )

        logging.info("  - 语音识别与对齐完成。")
        return aligned_result
    except Exception as e:
        logging.error(f"  [错误] 语音识别失败: {e}")
        traceback.print_exc()
        return None

拆解解释

  • 语音识别:使用 Faster-Whisper 转录音频,自动检测语言,支持单词级时间戳。Tqdm 添加进度条,提升用户体验。
  • 结果转换:将转录结果格式化为 WhisperX 兼容的字典。
  • 时间戳对齐:加载 Wav2Vec2 模型进行精确对齐,支持语言特定模型和缓存(避免重复加载)。
  • 回退逻辑:如果模型缺失,使用未对齐结果,确保系统鲁棒性。
  • 技术亮点:Beam search (beam_size=5) 提升准确率,单词级时间戳便于后续合并。

字幕合并

def merge_subtitles(transcription_result, max_gap_ms=MAX_GAP_MS, merge_identical=MERGE_IDENTICAL, max_length_chars=MAX_LENGTH_CHARS):
    """合并字幕的核心函数"""
    logging.info("[步骤 4/5] 正在进行字幕合并...")
    segments = []
    for seg in transcription_result["segments"]:
        if 'words' in seg and seg['words']:
            for word_info in seg['words']:
                text = word_info.get('word', '').strip()
                if text:
                    segments.append({
                        'start': word_info['start'],
                        'end': word_info['end'],
                        'text': text
                    })
        else:
            text = seg.get('text', '').strip()
            if text:
                segments.append({
                    'start': seg['start'],
                    'end': seg['end'],
                    'text': text
                })

    if not segments:
        logging.info("  - 没有检测到有效字幕内容,跳过合并。")
        return []

    logging.info(f"  - 合并前字幕条数: {len(segments)}")

    merged_segments = []
    if segments:
        current_segment = segments[0].copy()
        for i in range(1, len(segments)):
            next_segment = segments[i]
            gap_ms = (next_segment['start'] - current_segment['end']) * 1000

            if merge_identical and current_segment['text'] == next_segment['text']:
                current_segment['end'] = next_segment['end']
                continue

            elif gap_ms <= max_gap_ms and len(current_segment['text']) + len(next_segment['text']) + 1 <= max_length_chars:
                current_segment['text'] += " " + next_segment['text']
                current_segment['end'] = next_segment['end']

            else:
                merged_segments.append(current_segment)
                current_segment = next_segment.copy()

        merged_segments.append(current_segment)

    logging.info(f"  - 合并后字幕条数: {len(merged_segments)}")
    return merged_segments

拆解解释

  • 将转录结果拆分成单词或句子级片段。
  • 合并逻辑:如果间隔小于 max_gap_ms 且总长度不超过 max_length_chars,则合并;相同文本直接扩展时间。
  • 这避免了过碎的字幕,提高可读性。日志记录合并前后条数,便于调试。

SRT 文件生成

def generate_srt_file(transcription_result, output_srt_path, max_gap_ms=MAX_GAP_MS, merge_identical=MERGE_IDENTICAL):
    """生成SRT字幕文件 (包含合并逻辑)"""
    merged_transcription = merge_subtitles(
        transcription_result,
        max_gap_ms=max_gap_ms,
        merge_identical=merge_identical
    )

    logging.info("[步骤 5/5] 正在生成SRT字幕文件...")
    try:
        with open(output_srt_path, "w", encoding="utf-8") as srt_file:
            for i, segment in enumerate(merged_transcription):
                start_time = format_time(segment['start'])
                end_time = format_time(segment['end'])
                text = segment['text'].strip()
                if text:
                    srt_file.write(f"{i + 1}\n")
                    srt_file.write(f"{start_time} --> {end_time}\n")
                    srt_file.write(f"{text}\n\n")

        logging.info(f"  字幕文件已成功生成: {output_srt_path}")
        return True
    except Exception as e:
        logging.error(f"  [错误] 生成SRT文件失败: {e}")
        return False

拆解解释

  • 调用合并函数后,遍历片段写入 SRT 文件(序号、时间戳、文本)。
  • UTF-8 编码确保多语言兼容。异常处理防止文件写入失败。

主工作流程和模型加载

def main_workflow(video_file_path, whisper_model, align_models):
    """主工作流程控制器"""
    if not os.path.exists(video_file_path):
        logging.error(f"[错误] 输入的视频文件不存在: {video_file_path}")
        return False

    logging.info(f"\n{'=' * 50}")
    logging.info(f"开始处理视频: {os.path.basename(video_file_path)}")
    logging.info(f"{'=' * 50}\n")

    start_total_time = time.time()

    temp_dir = "temp_audio"
    os.makedirs(temp_dir, exist_ok=True)

    base_name = os.path.splitext(os.path.basename(video_file_path))[0]
    temp_raw_audio = os.path.join(temp_dir, f"temp_{base_name}_raw.wav")
    temp_final_audio = os.path.join(temp_dir, f"temp_{base_name}_final.wav")
    output_srt = f"{os.path.dirname(video_file_path)}\\{base_name}.srt"

    try:
        extract_and_prepare_audio(video_file_path, temp_raw_audio)
        optimize_audio(temp_raw_audio, temp_final_audio)
        transcription_data = transcribe_and_align(temp_final_audio, whisper_model, align_models)
        if transcription_data is None:
            logging.error("  [错误] 语音识别失败,无法生成字幕")
            return False
        generate_srt_file(transcription_data, output_srt)
        return True
    except Exception as e:
        logging.error(f"\n[!!!] 工作流程中断,发生严重错误: {e}")
        traceback.print_exc()
        return False
    finally:
        for temp_file in [temp_raw_audio, temp_final_audio]:
            if os.path.exists(temp_file):
                try:
                    os.remove(temp_file)
                    logging.info(f"  - 已删除: {temp_file}")
                except OSError as e:
                    logging.warning(f"  [警告] 无法删除临时文件 {temp_file}: {e}")

        end_total_time = time.time()
        total_time = end_total_time - start_total_time
        minutes = int(total_time // 60)
        seconds = int(total_time % 60)

        logging.info(f"\n{'=' * 50}")
        logging.info(f"所有任务完成! 总耗时: {minutes}{seconds}秒")
        logging.info(f"生成的字幕文件: {output_srt}")
        logging.info(f"{'=' * 50}")

        if torch.cuda.is_available():
            torch.cuda.empty_cache()
        gc.collect()
        logging.info("显存和内存清理完成")

def load_models():
    """预加载模型"""
    logging.info("预加载 Whisper 模型...")
    try:
        whisper_model = WhisperModel(WHISPER_MODEL_PATH, device=DEVICE, compute_type=COMPUTE_TYPE)
    except Exception as e:
        logging.warning(f"无法使用 {COMPUTE_TYPE} 量化: {e},尝试 float16")
        whisper_model = WhisperModel(WHISPER_MODEL_PATH, device=DEVICE, compute_type="float16")

    align_models = {}
    for lang, path in ALIGN_MODEL_PATHS.items():
        if os.path.exists(path):
            logging.info(f"预加载 {lang} 对齐模型...")
            align_models[lang] = whisperx.load_align_model(language_code=lang, device=DEVICE, model_dir=path)

    return whisper_model, align_models

拆解解释

  • 模型加载:预加载 Whisper 和对齐模型,支持量化回退,提高批量处理效率。
  • 主流程:顺序调用各步骤,管理临时文件和时间计算。Finally 块确保清理资源,防止内存泄漏。
  • 返回布尔值表示成功,便于批量脚本调用。

程序入口

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="离线字幕生成系统")
    parser.add_argument("input_dir", help="视频文件目录路径")
    args = parser.parse_args()

    dir_path = args.input_dir
    whisper_model, align_models = load_models()

    videos = [vid for vid in os.listdir(dir_path) if vid.lower().endswith(('.mp4', '.mkv', '.avi'))]
    for vid in tqdm(videos, desc="处理视频"):
        input_video = os.path.join(dir_path, vid)
        if main_workflow(input_video, whisper_model, align_models):
            logging.info(f"成功处理: {input_video}")
        else:
            logging.error(f"处理失败: {input_video}")

    sys.exit(0)

拆解解释

  • 使用 Argparse 支持命令行输入目录。
  • 过滤视频文件,Tqdm 显示进度。
  • 循环调用主流程,支持批量处理。

总结与改进建议

这个系统展示了如何将 AI 模型集成到 Python 脚本中,实现高效的离线字幕生成。优势包括:离线隐私保护、GPU 优化和可配置性。潜在改进:添加说话人识别(diarization)、支持更多格式,或集成 GUI 接口。

注意:

  • 如果你想运行这个系统,确保下载相应模型。
  • 注意环境配置,AI大模型对于最新依赖项和代码是对不上的,需要自己看依赖项目,避免出现依赖地狱
# ============== Core AI Libraries (Version Locked) ==============
numpy<2.0
torch==2.1.2
torchaudio==2.1.2
whisperx==3.1.1
speechbrain==1.0.0
ctranslate2==3.24.0
pyannote.audio==3.1.1
pytorch-lightning<2.0.0

# ============== Audio & Video Tools ==============
moviepy
pydub
ffmpeg-python
srt

# ============== Other Dependencies ==============
onnxruntime

网站公告

今日签到

点亮在社区的每一天
去签到