实用程序:无需付费软件!自制音视频转字幕工具,复制代码直接运行

前言

在多媒体内容爆炸的时代,为音视频添加字幕成为提升内容可访问性、传播效率的重要手段。无论是自媒体创作者、教育工作者还是普通用户,都可能面临手动制作字幕耗时费力的问题。基于此,我开发了一款"音视频转字幕工具",借助OpenAI的Whisper语音识别模型,实现从音视频文件到标准SRT字幕的自动化转换。

这款工具结合了moviepy的音视频处理能力、whisper的语音识别能力和tkinter的可视化界面,让用户无需专业知识即可快速生成字幕。下面将详细介绍工具的实现原理与使用方法。

已在github开源:https://github.com/ChenAI-TGF/Audio_And_Video_Transcription

一、工具介绍

这款音视频转字幕工具具备以下核心功能:

多格式支持:兼容MP4、AVI、MOV等视频格式及MP3、WAV等音频格式

灵活参数配置:可选择Whisper模型(tiny/base/small/medium/large)、调整线程数和温度值

实时进度反馈:双进度条分别显示音频提取和字幕识别进度

时间估算:实时显示处理耗时和预计剩余时间

繁简转换:自动将识别结果转换为简体中文

标准字幕输出:生成符合SRT格式的字幕文件,可直接用于视频编辑

工具的优势在于可视化操作降低了技术门槛,同时保留了参数调整的灵活性,兼顾了普通用户和进阶用户的需求。

二、代码核心部分详解

  1. 音频提取模块
    音视频文件首先需要提取音频轨道,这一步由video_to_audio方法实现:

def video_to_audio(self, video_path: str, audio_path: str) -> bool:

try:

with VideoFileClip(video_path) as video:

total_duration = video.duration

audio = video.audio

复制代码
        # 记录音频转换开始时间
        self.audio_start_time = datetime.now()
        
        # 写入音频(logger=None关闭冗余输出)
        audio.write_audiofile(audio_path, logger=None)
        # 强制进度到100%
        self.update_audio_progress(100.0)
    return True
except Exception as e:
    messagebox.showerror("错误", f"音视频转音频失败:{str(e)}")
    return False

核心逻辑:使用moviepy的VideoFileClip读取视频文件,提取音频轨道后写入WAV格式文件。对于本身就是音频的文件(如MP3),会直接跳过提取步骤

  1. Whisper模型加载与语音识别
    语音转文字是工具的核心功能,基于OpenAI的Whisper模型实现:

def load_whisper_model(self) -> Optional[whisper.Whisper]:

try:

model_name = self.model_var.get()

self.update_transcribe_progress(10)

加载指定模型,使用CPU运行(可改为"cuda"启用GPU加速)

model = whisper.load_model(model_name, device="cpu")

self.update_transcribe_progress(20)

return model

except Exception as e:

messagebox.showerror("错误", f"模型加载失败:{str(e)}")

return None

def transcribe_audio(self, audio_path: str) -> Optional[dict]:

global is_running

self.model = self.load_whisper_model()

if not self.model or not is_running:

return None

复制代码
try:
    # 记录字幕识别开始时间
    self.transcribe_start_time = datetime.now()
    
    # 分段识别模拟进度
    self.update_transcribe_progress(30)
    result = self.model.transcribe(
        audio_path,
        language="zh",  # 指定中文识别
        temperature=self.temp_var.get(),  # 控制输出随机性
    )
    self.update_transcribe_progress(80)
    
    if not is_running:
        return None
    
    self.update_transcribe_progress(100)
    return result
except Exception as e:
    messagebox.showerror("错误", f"字幕识别失败:{str(e)}")
    return None

核心逻辑:

先加载用户选择的Whisper模型(模型越小速度越快,精度越低)

通过transcribe方法处理音频,指定language="zh"优化中文识别效果

temperature参数控制输出随机性(0表示确定性输出,适合字幕生成)

  1. SRT字幕格式化

识别结果需要转换为标准SRT格式,包含序号、时间轴和文本:

def format_srt(self, result: dict) -> str:

srt_content = ""

for i, segment in enumerate(result["segments"], 1):

start = self.format_time(segment["start"])

end = self.format_time(segment["end"])

繁简转换(将可能的繁体转为简体)

text = self.cc.convert(segment["text"].strip())

srt_content += f"{i}\n{start} --> {end}\n{text}\n\n"

return srt_content

@staticmethod

def format_time(seconds: float) -> str:

"""将秒数格式化为SRT时间格式(hh:mm:ss,fff)"""

hours = math.floor(seconds / 3600)

minutes = math.floor((seconds % 3600) / 60)

secs = math.floor(seconds % 60)

millis = math.floor((seconds % 1) * 1000)

return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"

核心逻辑:

解析Whisper返回的分段结果(包含开始时间、结束时间和文本)

将时间戳转换为SRT要求的hh:mm:ss,fff格式

使用OpenCC进行繁简转换,确保输出统一为简体中文

  1. 多线程与进度管理

为避免UI卡顿,核心处理逻辑在独立线程中运行:

def start_convert(self):

"""启动转换线程"""

thread = threading.Thread(target=self.convert_thread, daemon=True)

thread.start()

def convert_thread(self):

"""转换线程(避免UI卡顿)"""

global is_running

is_running = True

self.start_btn.config(state=tk.DISABLED)

self.stop_btn.config(state=tk.NORMAL)

复制代码
# 重置进度和时间
self.update_audio_progress(0.0)
self.update_transcribe_progress(0.0)
self.result_text.delete(1.0, tk.END)
self.total_start_time = datetime.now()

# 核心处理流程
input_path = self.file_path_var.get()
# 音频提取 -> 语音识别 -> 字幕格式化 -> 保存文件
# ...(省略具体步骤)

核心逻辑:

将耗时的音频处理和识别任务放入子线程

通过全局变量is_running实现主线程与子线程的通信

实时更新进度条和时间显示,提升用户体验

三、完整代码

import tkinter as tk

from tkinter import ttk, filedialog, messagebox

import os

from moviepy import VideoFileClip

import whisper

import threading

import time

from datetime import datetime, timedelta

from typing import Optional

import math

from opencc import OpenCC

全局变量用于控制进度和线程

audio_convert_progress = 0.0

transcribe_progress = 0.0

is_running = False

class AudioVideoToSubtitle:

def init (self, root):

self.root = root

self.root.title("音视频转字幕工具")

self.root.geometry("900x650") # 扩大窗口以容纳时间显示

复制代码
    # 初始化繁简转换器
    self.cc = OpenCC('t2s')
    
    # 时间跟踪变量
    self.total_start_time = None  # 总处理开始时间
    self.audio_start_time = None  # 音频转换开始时间
    self.transcribe_start_time = None  # 字幕识别开始时间
    self.time_update_id = None  # 时间更新定时器ID
    
    # 初始化模型
    self.model = None
    self.model_path = None
    
    self.init_ui()

def init_ui(self):
    # 1. 文件选择区域
    file_frame = ttk.LabelFrame(self.root, text="文件设置")
    file_frame.pack(fill=tk.X, padx=10, pady=5)
    
    self.file_path_var = tk.StringVar()
    ttk.Entry(file_frame, textvariable=self.file_path_var, width=70).pack(side=tk.LEFT, padx=5, pady=5)
    ttk.Button(file_frame, text="选择文件", command=self.select_file).pack(side=tk.LEFT, padx=5, pady=5)

    # 2. 速度调节参数区域
    param_frame = ttk.LabelFrame(self.root, text="速度调节参数")
    param_frame.pack(fill=tk.X, padx=10, pady=5)
    
    ttk.Label(param_frame, text="模型选择:").pack(side=tk.LEFT, padx=5, pady=5)
    self.model_var = tk.StringVar(value="base")
    model_options = ["tiny", "base", "small", "medium", "large"]
    ttk.Combobox(param_frame, textvariable=self.model_var, values=model_options, width=10).pack(side=tk.LEFT, padx=5, pady=5)
    
    ttk.Label(param_frame, text="线程数:").pack(side=tk.LEFT, padx=5, pady=5)
    self.thread_var = tk.IntVar(value=4)
    ttk.Spinbox(param_frame, from_=1, to=16, textvariable=self.thread_var, width=5).pack(side=tk.LEFT, padx=5, pady=5)
    
    ttk.Label(param_frame, text="温度值:").pack(side=tk.LEFT, padx=5, pady=5)
    self.temp_var = tk.DoubleVar(value=0.0)
    ttk.Spinbox(param_frame, from_=0.0, to=1.0, increment=0.1, textvariable=self.temp_var, width=5).pack(side=tk.LEFT, padx=5, pady=5)

    # 3. 进度条和时间显示区域
    progress_frame = ttk.LabelFrame(self.root, text="处理进度与时间")
    progress_frame.pack(fill=tk.X, padx=10, pady=5)
    
    # 音视频转音频进度条
    ttk.Label(progress_frame, text="音视频转音频:").pack(side=tk.LEFT, padx=5)
    self.audio_progress = ttk.Progressbar(progress_frame, orient=tk.HORIZONTAL, length=300, mode='determinate')
    self.audio_progress.pack(side=tk.LEFT, padx=5, pady=5)
    self.audio_progress_label = ttk.Label(progress_frame, text="0%")
    self.audio_progress_label.pack(side=tk.LEFT, padx=5)
    
    # 字幕识别进度条
    ttk.Label(progress_frame, text="字幕识别:").pack(side=tk.LEFT, padx=5)
    self.transcribe_progress = ttk.Progressbar(progress_frame, orient=tk.HORIZONTAL, length=300, mode='determinate')
    self.transcribe_progress.pack(side=tk.LEFT, padx=5, pady=5)
    self.transcribe_progress_label = ttk.Label(progress_frame, text="0%")
    self.transcribe_progress_label.pack(side=tk.LEFT, padx=5)

    # 时间显示区域
    time_frame = ttk.LabelFrame(self.root, text="时间信息")
    time_frame.pack(fill=tk.X, padx=10, pady=5)
    
    self.elapsed_time_var = tk.StringVar(value="已处理时间: 00:00:00")
    ttk.Label(time_frame, textvariable=self.elapsed_time_var).pack(side=tk.LEFT, padx=20, pady=5)
    
    self.estimated_time_var = tk.StringVar(value="预计剩余时间: --:--:--")
    ttk.Label(time_frame, textvariable=self.estimated_time_var).pack(side=tk.LEFT, padx=20, pady=5)

    # 4. 控制按钮区域
    btn_frame = ttk.Frame(self.root)
    btn_frame.pack(pady=10)
    
    self.start_btn = ttk.Button(btn_frame, text="开始转换", command=self.start_convert)
    self.start_btn.pack(side=tk.LEFT, padx=10)
    self.stop_btn = ttk.Button(btn_frame, text="停止转换", command=self.stop_convert, state=tk.DISABLED)
    self.stop_btn.pack(side=tk.LEFT, padx=10)

    # 5. 结果显示区域
    result_frame = ttk.LabelFrame(self.root, text="识别结果预览")
    result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
    
    self.result_text = tk.Text(result_frame, height=15)
    scrollbar = ttk.Scrollbar(result_frame, command=self.result_text.yview)
    self.result_text.configure(yscrollcommand=scrollbar.set)
    self.result_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
    scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=5, pady=5)

def select_file(self):
    """选择音视频文件"""
    file_types = [
        ("音视频文件", "*.mp4 *.avi *.mov *.mkv *.flv *.mp3 *.wav *.m4a"),
        ("所有文件", "*.*")
    ]
    file_path = filedialog.askopenfilename(filetypes=file_types)
    if file_path:
        self.file_path_var.set(file_path)

def update_audio_progress(self, value: float):
    """更新音频转换进度条"""
    global audio_convert_progress
    audio_convert_progress = min(value, 100.0)
    self.audio_progress["value"] = audio_convert_progress
    self.audio_progress_label.config(text=f"{int(audio_convert_progress)}%")
    self.root.update_idletasks()

def update_transcribe_progress(self, value: float):
    """更新字幕识别进度条"""
    global transcribe_progress
    transcribe_progress = min(value, 100.0)
    self.transcribe_progress["value"] = transcribe_progress
    self.transcribe_progress_label.config(text=f"{int(transcribe_progress)}%")
    self.root.update_idletasks()

def format_time_display(self, seconds: float) -> str:
    """将秒数格式化为时分秒显示"""
    hours, remainder = divmod(int(seconds), 3600)
    minutes, seconds = divmod(remainder, 60)
    return f"{hours:02d}:{minutes:02d}:{seconds:02d}"

def update_time_display(self):
    """更新时间显示信息"""
    if not is_running or not self.total_start_time:
        return

    # 计算已处理时间
    elapsed_seconds = (datetime.now() - self.total_start_time).total_seconds()
    self.elapsed_time_var.set(f"已处理时间: {self.format_time_display(elapsed_seconds)}")

    # 计算预计剩余时间
    try:
        if audio_convert_progress < 100:
            # 音频转换阶段
            if self.audio_start_time and audio_convert_progress > 0:
                audio_elapsed = (datetime.now() - self.audio_start_time).total_seconds()
                total_audio_estimated = audio_elapsed / (audio_convert_progress / 100)
                audio_remaining = total_audio_estimated - audio_elapsed
                
                # 假设转录时间与音频时间相当(简单估算)
                total_estimated = total_audio_estimated * 2
                remaining = total_estimated - elapsed_seconds
                self.estimated_time_var.set(f"预计剩余时间: {self.format_time_display(remaining)}")
        else:
            # 字幕识别阶段
            if self.transcribe_start_time and transcribe_progress > 0 and transcribe_progress < 100:
                transcribe_elapsed = (datetime.now() - self.transcribe_start_time).total_seconds()
                total_transcribe_estimated = transcribe_elapsed / (transcribe_progress / 100)
                transcribe_remaining = total_transcribe_estimated - transcribe_elapsed
                self.estimated_time_var.set(f"预计剩余时间: {self.format_time_display(transcribe_remaining)}")
            elif transcribe_progress >= 100:
                self.estimated_time_var.set(f"预计剩余时间: 00:00:00")
    except (ZeroDivisionError, Exception):
        self.estimated_time_var.set(f"预计剩余时间: 计算中...")

    # 继续定时更新
    self.time_update_id = self.root.after(1000, self.update_time_display)

def video_to_audio(self, video_path: str, audio_path: str) -> bool:
    """音视频转音频,带进度更新"""
    try:
        with VideoFileClip(video_path) as video:
            total_duration = video.duration
            audio = video.audio

            # 记录音频转换开始时间
            self.audio_start_time = datetime.now()
            
            # 写入音频
            audio.write_audiofile(audio_path, logger=None)
            # 强制进度到100%
            self.update_audio_progress(100.0)
        return True
    except Exception as e:
        messagebox.showerror("错误", f"音视频转音频失败:{str(e)}")
        return False

def load_whisper_model(self) -> Optional[whisper.Whisper]:
    """加载whisper模型"""
    try:
        model_name = self.model_var.get()
        self.update_transcribe_progress(10)
        model = whisper.load_model(model_name, device="cpu") 
        self.update_transcribe_progress(20)
        return model
    except Exception as e:
        messagebox.showerror("错误", f"模型加载失败:{str(e)}")
        return None

def transcribe_audio(self, audio_path: str) -> Optional[dict]:
    """音频转字幕,带进度更新"""
    global is_running
    self.model = self.load_whisper_model()
    if not self.model or not is_running:
        return None

    try:
        # 记录字幕识别开始时间
        self.transcribe_start_time = datetime.now()
        
        # 分段识别模拟进度
        self.update_transcribe_progress(30)
        result = self.model.transcribe(
            audio_path,
            language="zh",
            temperature=self.temp_var.get(),
        )
        self.update_transcribe_progress(80)
        
        if not is_running:
            return None
        
        self.update_transcribe_progress(100)
        return result
    except Exception as e:
        messagebox.showerror("错误", f"字幕识别失败:{str(e)}")
        return None

def format_srt(self, result: dict) -> str:
    """将识别结果格式化为SRT字幕格式"""
    srt_content = ""
    for i, segment in enumerate(result["segments"], 1):
        start = self.format_time(segment["start"])
        end = self.format_time(segment["end"])
        text = self.cc.convert(segment["text"].strip())
        srt_content += f"{i}\n{start} --> {end}\n{text}\n\n"
    return srt_content

@staticmethod
def format_time(seconds: float) -> str:
    """将秒数格式化为SRT时间格式(hh:mm:ss,fff)"""
    hours = math.floor(seconds / 3600)
    minutes = math.floor((seconds % 3600) / 60)
    secs = math.floor(seconds % 60)
    millis = math.floor((seconds % 1) * 1000)
    return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"

def save_subtitle(self, srt_content: str, input_path: str):
    """保存字幕文件"""
    save_path = os.path.splitext(input_path)[0] + ".srt"
    try:
        with open(save_path, "w", encoding="utf-8") as f:
            f.write(srt_content)
        messagebox.showinfo("成功", f"字幕已保存至:\n{save_path}")
        return save_path
    except Exception as e:
        messagebox.showerror("错误", f"字幕保存失败:{str(e)}")
        return None

def convert_thread(self):
    """转换线程(避免UI卡顿)"""
    global is_running
    is_running = True
    self.start_btn.config(state=tk.DISABLED)
    self.stop_btn.config(state=tk.NORMAL)
    
    # 重置进度和时间
    self.update_audio_progress(0.0)
    self.update_transcribe_progress(0.0)
    self.result_text.delete(1.0, tk.END)
    self.total_start_time = datetime.now()
    self.audio_start_time = None
    self.transcribe_start_time = None
    
    # 启动时间更新
    self.root.after(0, self.update_time_display)

    input_path = self.file_path_var.get()
    if not os.path.exists(input_path):
        messagebox.showwarning("警告", "请选择有效的音视频文件!")
        self.reset_ui()
        return

    # 临时音频文件路径
    temp_audio = "temp_audio.wav"
    try:
        # 步骤1:音视频转音频
        if not is_running:
            return
        if input_path.lower().endswith(("mp3", "wav", "m4a")):
            # 已是音频文件,跳过转换
            self.update_audio_progress(100.0)
            audio_path = input_path
            self.audio_start_time = datetime.now()  # 标记音频处理完成时间
        else:
            if not self.video_to_audio(input_path, temp_audio):
                return
            audio_path = temp_audio

        # 步骤2:音频转字幕
        if not is_running:
            return
        result = self.transcribe_audio(audio_path)
        if not result or not is_running:
            return

        # 步骤3:格式化并显示结果
        srt_content = self.format_srt(result)
        self.result_text.insert(1.0, srt_content)

        # 步骤4:保存字幕
        self.save_subtitle(srt_content, input_path)

    finally:
        # 清理临时文件
        if os.path.exists(temp_audio) and not input_path.lower().endswith(("mp3", "wav", "m4a")):
            os.remove(temp_audio)
        self.reset_ui()

def start_convert(self):
    """启动转换线程"""
    thread = threading.Thread(target=self.convert_thread, daemon=True)
    thread.start()

def stop_convert(self):
    """停止转换"""
    global is_running
    is_running = False
    self.stop_btn.config(state=tk.DISABLED)
    self.result_text.insert(tk.END, "\n\n转换已停止!")

def reset_ui(self):
    """重置UI状态"""
    global is_running
    is_running = False
    self.start_btn.config(state=tk.NORMAL)
    self.stop_btn.config(state=tk.DISABLED)
    
    # 停止时间更新
    if self.time_update_id:
        self.root.after_cancel(self.time_update_id)
        self.time_update_id = None
    
    # 重置时间显示
    self.elapsed_time_var.set("已处理时间: 00:00:00")
    self.estimated_time_var.set("预计剩余时间: --:--:--")

if name == "main ":

提示安装依赖

try:

import torch

import opencc

except ImportError as e:

missing = str(e).split("'")[1]

messagebox.showwarning("提示", f"请先安装依赖库:\npip install torch moviepy openai-whisper ffmpeg-python opencc-python-reimplemented")

exit()

复制代码
root = tk.Tk()
app = AudioVideoToSubtitle(root)
root.mainloop()

四、效果演示

工具启动:运行程序后显示主界面,包含文件选择、参数配置、进度显示和结果预览区域。

在这里插入图片描述

在这里插入图片描述

文件选择:点击"选择文件"按钮,选择需要转换的音视频文件(如MP4格式视频)。

在这里插入图片描述

在这里插入图片描述

参数配置:

模型选择:根据需求选择(tiny最快,large最精准)

线程数:根据CPU核心数调整(建议4-8)

温度值:默认0.0(适合字幕生成)

在这里插入图片描述

在这里插入图片描述

开始转换:

点击"开始转换",音频提取进度条开始推进

提取完成后,字幕识别进度条启动

实时显示"已处理时间"和"预计剩余时间"

在这里插入图片描述

在这里插入图片描述

结果查看:

识别完成后,结果预览区显示SRT格式字幕

自动保存与原文件同名的SRT文件(如"视频.mp4"生成"视频.srt")

弹窗提示保存路径

在这里插入图片描述

在这里插入图片描述

中途停止:如需中断,点击"停止转换"按钮,工具会清理临时文件并重置状态。

五、第三方库安装

1、需要的库

该音视频转字幕工具的代码依赖以下第三方库,以下是各库的作用及安装方法:

torch(PyTorch)

作用:Whisper模型运行的基础框架,用于加载和运行语音识别模型(Whisper基于PyTorch实现)。

安装命令:

推荐根据系统和是否需要GPU加速,从PyTorch官网获取对应命令,基础CPU版本可直接安装:

pip install torch

moviepy

作用:音视频处理库,用于从视频中提取音频轨道(核心功能之一)。

安装命令:

pip install moviepy

openai-whisper

作用:OpenAI官方的语音识别库,提供Whisper模型(实现语音转文字的核心功能)。

安装命令:

pip install openai-whisper

ffmpeg-python

作用:moviepy处理音视频时依赖的底层工具封装,用于实际执行音视频编解码操作。

注意:除了安装Python库,还需要在系统中安装ffmpeg程序(否则moviepy可能无法正常工作):

Python库安装:

pip install ffmpeg-python

系统级ffmpeg安装:

Windows:从ffmpeg官网下载安装包,解压后将bin目录添加到系统环境变量。

Ubuntu/Debian:sudo apt-get install ffmpeg

macOS:brew install ffmpeg(需先安装Homebrew)

opencc-python-reimplemented

作用:繁简转换库,用于将识别结果中的繁体中文自动转换为简体中文(代码中通过OpenCC('t2s')实现)。

安装命令:

pip install opencc-python-reimplemented

2、一条命令安装所有依赖

可将上述命令整合为一条安装命令(推荐使用国内镜像源如-i https://pypi.tuna.tsinghua.edu.cn/simple加速):

pip install torch moviepy openai-whisper ffmpeg-python opencc-python-reimplemented -i https://pypi.tuna.tsinghua.edu.cn/simple

3、注意事项

首次运行时,Whisper会自动下载选择的模型(如base模型约1GB),请确保网络畅通。

若电脑有NVIDIA显卡且安装了CUDA,可将代码中device="cpu"改为device="cuda",显著提升识别速度(需安装对应CUDA版本的PyTorch)。

总结

这款音视频转字幕工具通过整合moviepy和whisper的强大功能,实现了文字识别和字幕生成的自动化流程。核心优势在于:

易用性:可视化界面降低了技术门槛,无需命令行操作

灵活性:可通过模型选择平衡速度与精度

实用性:生成标准SRT格式,直接适配主流视频编辑软件

无论是自媒体创作者快速制作字幕,还是学习者为教学视频添加字幕,这款工具都能显著提升效率,降低字幕制作的技术门槛