python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python音视频转字幕

Python结合moviepy和tkinter自制音视频转字幕工具

作者:ChenAI_TGF

在多媒体内容爆炸的时代,为音视频添加字幕成为提升内容可访问性、传播效率的重要手段,本文将使用Python自制一个音视频转字幕工具,感兴趣的小伙伴可以了解下

前言

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

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

一、工具介绍

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

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

二、代码核心部分详解

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

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

2. 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

核心逻辑

3. 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}"

核心逻辑

4. 多线程与进度管理

为避免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()
    # 音频提取 -> 语音识别 -> 字幕格式化 -> 保存文件
    # ...(省略具体步骤)

核心逻辑

三、完整代码

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格式视频)。

参数配置

开始转换

结果查看

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

五、第三方库安装

1、需要的库

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

1.torch(PyTorch)

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

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

pip install torch

2.moviepy

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

安装命令

pip install moviepy

3.openai-whisper

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

安装命令

pip install openai-whisper

4.ffmpeg-python

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

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

Python库安装

pip install ffmpeg-python

系统级ffmpeg安装

5.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、注意事项

总结

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

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

以上就是Python结合moviepy和tkinter自制音视频转字幕工具的详细内容,更多关于Python音视频转字幕的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文