当前位置:AIGC资讯 > AIGC > 正文

<Project-3 Video2SubTitle> Python coding Flask应用:从视频中,提取对白,生成独立的字幕文件 浏览器页面交互 调用cuda, Whisper模型

原因:

在网上看到一个视频没有字幕。

记者问小泉纯一郎 (前日本首相),我只是好奇,想知道Y说的是什么。

上面这个帖子里的视频:https://x.com/i/status/1834489208398115295   

视频没有字幕,那就自己做一个

计划实现的功能:

转录音频内容(语音转文字)
翻译转录的文本
生成双语字幕文件
在网页上显示字幕内容,并提供下载和复制功能
使用进度条显示处理进度

注:字幕效果不好,每次运行,内容都不一样,可能是我的laptop陈旧了? 复制了一个复杂代码,想实现进度条功能,只能说有个颜色条而已,如果您完善了,请更新。

程序结构

主程序: app.py
子目录: ./static
                存放 script.js
子目录: ./templates
                存放 index.html
子目录: ./subtitle
                存放 视频文件名的字幕文件

#updated on Sep14. added last 2 rows and modified .. to .  and javascript.js to script.js

代码篇

准备:

以下是复制于我做的笔记,因为是速记目的,最终可能有变化,以"代码"为准。

安装必要的库

首先,您需要安装以下库:

ffmpeg:用于处理音视频文件。 https://ffmpeg.org/download.html
openai-whisper:用于语音识别。
pytorch:Whisper 模型需要。
numpy:处理数组。
os # 用于操作文件和目录
tempfile 创建临时文件和目录
srt 处理 SRT 字幕文件
GoogleTranslator  # 导入 GoogleTranslator
...

pip3 install git+https://github.com/openai/whisper.git
pip3 install torch numpy  # 如果你有NVIDA显卡,需要安装 匹配的pytorch 至少能提高性能

笔记本电脑配备有 NVIDIA GPU

1:验证 NVIDIA GPU 并安装驱动程序

检查 GPU 型号和驱动程序版本:

按下 Win + R,输入 dxdiag,然后按下回车。
在“显示”或“显示 1”选项卡下,您可以查看显卡型号和驱动程序版本。
更新 NVIDIA 驱动程序:

访问 NVIDIA 驱动程序下载页面。
输入您的 GPU 型号和操作系统信息,然后下载最新的驱动程序。
安装驱动程序,并根据提示重新启动计算机。

2:安装 CUDA Toolkit
下载 CUDA Toolkit  https://developer.nvidia.com/cuda-downloads

安装 CUDA Toolkit:
在安装过程中,选择“Express(默认)”或“Custom”安装。
确保安装了 CUDA Toolkit 和 CUDA Samples。
完成安装,并根据需要重新启动计算机。

3:安装 cuDNN 库(可选推荐)

下载 cuDNN:
https://developer.nvidia.com/cudnn
注册或登录 NVIDIA 开发者账户。
选择与您安装的 CUDA 版本兼容的 cuDNN 版本。

下载 Windows 版本的 cuDNN。
安装 cuDNN:
解压下载的 cuDNN 文件。
将 bin、include 和 lib 文件夹中的文件复制到对应的 CUDA 安装目录中,通常为:
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\

4:安装支持 CUDA 的 PyTorch

卸载现有的 PyTorch(如果已安装)
pip uninstall torch torchvision torchaudio

安装 CUDA 版本的 PyTorch
https://pytorch.org/
在“Get Started”页面,选择以下选项
如:pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124

完成后,会看到:
“Installing collected packages: torch, torchvision, torchaudio
Successfully installed torch-2.4.1+cu124 torchaudio-2.4.1+cu124 torchvision-0.19.1+cu124”

验证 PyTorch 是否能使用 CUDA
python coding:
import torch
print(torch.cuda.is_available())

">>> import torch
>>> print(torch.cuda.is_available())
True"

5:调整 Python 代码以使用 GPU

#transcribe_audio 函数
def transcribe_audio(audio_path, include_timecodes=True, language=None):
    # 加载 Whisper 模型,指定设备为 'cuda'
    model = whisper.load_model("base", device="cuda")
    options = {}
    if language:
        options['language'] = language
    if include_timecodes:
        options.update({'task': 'transcribe', 'verbose': False})
    result = model.transcribe(audio_path, **options)
    return result

确保音频处理在 GPU 上进行

6:测试程序

监控 GPU 使用情况
cmd: nvidia-smi

主程序: app.py

1. 导入必要的库

from flask import Flask, request, send_file, jsonify, render_template
import os
import tempfile
import whisper
import srt
import subprocess
from datetime import timedelta
import shutil
from deep_translator import GoogleTranslator  # 导入 GoogleTranslator
import threading
import uuid

2. 初始化 Flask 应用和全局变量

app = Flask(__name__, static_folder='static', template_folder='templates')

# 设置字幕输出目录
base_dir = os.path.abspath(os.path.dirname(__file__))
subtitle_dir = os.path.join(base_dir, '.', 'subtitle') #注意是一个 点, 别跟我一样,写成2点,目录在上一级了。
os.makedirs(subtitle_dir, exist_ok=True)

# 全局字典存储任务进度
tasks_progress = {}

3. 定义辅助函数
3.1 提取音频

def extract_audio(video_path, audio_path):
    command = f'ffmpeg -i "{video_path}" -ac 1 -ar 16000 -vn "{audio_path}" -y'
    subprocess.run(command, shell=True, check=True)
#使用 FFmpeg 从视频文件中提取音频,转换为单声道、16kHz 采样率的 WAV 文件

3.2 转录音频

def transcribe_audio(audio_path, language=None):
    model = whisper.load_model("base", device="cuda")
    options = {'task': 'transcribe', 'verbose': False}
    if language:
        options['language'] = language
    result = model.transcribe(audio_path, **options)
    return result
#使用 OpenAI 的 Whisper 模型将音频文件转录为文本

3.3 翻译文本

def translate_text(text, target_language='zh-CN'):
    """
    使用 deep-translator 的 GoogleTranslator 翻译文本。
    """
    try:
        translated_text = GoogleTranslator(source='auto', target=target_language).translate(text)
        return translated_text
    except Exception as e:
        app.logger.error(f'Error translating text: {e}')
        return text  # 如果翻译失败,返回原文
# 将输入文本翻译为中文

3.4 合并字幕

def merge_subtitles(original_subtitles, translated_subtitles):
    merged_subtitles = []
    for original, translated in zip(original_subtitles, translated_subtitles):
        merged_subtitle = srt.Subtitle(
            index=original.index,
            start=original.start,
            end=original.end,
            content=f"{original.content}\n{translated.content}"
        )
        merged_subtitles.append(merged_subtitle)
    return merged_subtitles
#形成双语字幕

3.5 保存 SRT 字幕文件

def save_srt(subtitles, output_path):
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(srt.compose(subtitles))
#将字幕列表保存为 SRT 格式的文件

4. 定义路由和处理逻辑
4.1 首页路由

@app.route('/')
def index():
    return render_template('index.html')
#打开index.html,供使用上传视频

4.2 处理视频上传和任务创建

@app.route('/process_video', methods=['POST'])
def process_video():
    language = request.form.get('language')
    video_file = request.files.get('video')
    if not video_file:
        return jsonify({'error': 'No video file uploaded.'}), 400

    # 生成唯一的任务ID
    task_id = str(uuid.uuid4())
    tasks_progress[task_id] = {'progress': 0}

    # 在主线程中创建临时目录
    temp_dir = tempfile.mkdtemp()

    # 在主线程中保存上传的文件
    video_path = os.path.join(temp_dir, 'input_video.mp4')
    video_file.save(video_path)

    # 在后台线程中处理视频,传递文件路径和临时目录路径
    thread = threading.Thread(target=process_task, args=(task_id, video_path, language, temp_dir))
    thread.start()

    # 返回任务ID给前端
    return jsonify({'task_id': task_id})
#处理上传的视频文件,创建任务并启动后台线程进行处理

4.3 后台处理任务

def process_task(task_id, video_path, language, temp_dir):
    try:
        tasks_progress[task_id]['progress'] = 5

        audio_path = os.path.join(temp_dir, 'extracted_audio.wav')
        output_srt_filename = f"{os.path.splitext(os.path.basename(video_path))[0]}_subtitles.srt"
        output_srt_path = os.path.join(subtitle_dir, output_srt_filename)

        # 提取音频
        extract_audio(video_path, audio_path)
        tasks_progress[task_id]['progress'] = 30

        # 转录音频
        result = transcribe_audio(audio_path, language)
        tasks_progress[task_id]['progress'] = 60

        # 生成原文字幕列表
        original_subtitles = []
        for segment in result['segments']:
            subtitle = srt.Subtitle(
                index=segment['id'],
                start=timedelta(seconds=segment['start']),
                end=timedelta(seconds=segment['end']),
                content=segment['text'].strip()
            )
            original_subtitles.append(subtitle)
        tasks_progress[task_id]['progress'] = 70

        # 翻译字幕内容
        texts_to_translate = [subtitle.content for subtitle in original_subtitles]
        translated_texts = []
        for text in texts_to_translate:
            translated_text = translate_text(text, target_language='zh-CN')
            translated_texts.append(translated_text)
        tasks_progress[task_id]['progress'] = 85

        # 创建翻译后的字幕列表
        translated_subtitles = []
        for subtitle, translated_text in zip(original_subtitles, translated_texts):
            translated_subtitle = srt.Subtitle(
                index=subtitle.index,
                start=subtitle.start,
                end=subtitle.end,
                content=translated_text
            )
            translated_subtitles.append(translated_subtitle)

        # 合并双语字幕
        merged_subtitles = merge_subtitles(original_subtitles, translated_subtitles)
        tasks_progress[task_id]['progress'] = 90

        # 保存合并后的双语字幕文件到指定目录
        save_srt(merged_subtitles, output_srt_path)
        tasks_progress[task_id]['progress'] = 95

        # 读取字幕内容
        with open(output_srt_path, 'r', encoding='utf-8') as f:
            subtitles_content = f.read()

        # 更新进度为100,并保存结果
        tasks_progress[task_id]['progress'] = 100
        tasks_progress[task_id]['result'] = {
            'message': 'Subtitle generated successfully.',
            'subtitles_content': subtitles_content,
            'download_url': f'/download_subtitle/{output_srt_filename}'
        }
    except Exception as e:
        app.logger.error(f'Error processing video: {e}')
        tasks_progress[task_id]['progress'] = -1  # 用于表示处理失败
    finally:
        # 清理临时目录
        shutil.rmtree(temp_dir)
#后台线程中执行视频处理任务,包括提取音频、转录、翻译、生成字幕等

4.4 查询任务进度

@app.route('/progress/<task_id>')
def progress(task_id):
    if task_id in tasks_progress:
        progress = tasks_progress[task_id]['progress']
        if progress == 100:
            # 返回处理结果
            result = tasks_progress[task_id].get('result', {})
            # 删除任务进度信息
            del tasks_progress[task_id]
            return jsonify({'progress': progress, 'result': result})
        elif progress == -1:
            # 处理失败
            del tasks_progress[task_id]
            return jsonify({'progress': progress, 'error': 'An error occurred during processing.'})
        else:
            return jsonify({'progress': progress})
    else:
        return jsonify({'error': 'Invalid task ID.'}), 404
# 通过任务 ID 查询处理进度和结果, 这里有很大的提升空间,没去研究

4.5 提供字幕文件下载

@app.route('/download_subtitle/<filename>')
def download_subtitle(filename):
    return send_file(
        os.path.join(subtitle_dir, filename),
        as_attachment=True,
        download_name=filename,
        mimetype='text/plain'
    )

5. 启动Flask应用程序

if __name__ == '__main__':
    app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024  # 1GB
    app.run(host='0.0.0.0', port=5000)
MAX_CONTENT_LENGTH:设置上传文件的最大大小为 1GB,防止过大的文件导致服务器压力过大。
监听端口还是5000 更以前的coding一样,浏览器里:  127.0.0.1:5000 

网页文件 index.html

用于视频转字幕的网页前端,用户可以在网页上上传视频文件,选择语言,然后提交表单生成字幕。网页还提供了显示字幕内容、下载字幕文件和复制全部内容的功能。

<!DOCTYPE html>

<html lang="zh-CN">

<head>

    <meta charset="UTF-8">

    <title>视频转字幕</title>

    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

    <style>

        #subtitle-content {

            width: 100%;

            height: 300px;

            margin-top: 20px;

            white-space: pre-wrap;

            overflow-y: scroll;

            border: 1px solid #ccc;

            padding: 10px;

        }

        #buttons {

            margin-top: 10px;

        }

    </style>

</head>

<body>

    <h1>视频转字幕</h1>

    <form id="upload-form">

        <div style="display:none; margin-top: 20px;">

        <div style="width: 0%; height: 20px; background-color: green;"></div></div>

        <label for="video">选择视频文件:</label>

        <input type="file" name="video" accept="video/*" required><br><br>

        <label for="language">选择语言:</label>

        <select name="language">

            <option value="">自动检测</option>

            <option value="en">英语</option>

            <option value="zh">中文</option>

            <option value="jp">日文</option>

            <!-- 添加其他语言选项 -->

        </select><br><br>

        <button type="submit">上传并生成字幕</button>

    </form>

    <div style="display:none;"></div>

    <div style="display:none;">

        <button id="download-btn">下载字幕文件</button>

        <button id="copy-btn">复制全部内容</button>

    </div>

    <script src="https://blog.csdn.net/static/script.js"></script>

</body>

</html>

网页够简洁?可以直接看懂的。

设置页面的字符编码为 UTF-8,确保中文字符正确显示
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>:引入 jQuery 库
引入外部的 JavaScript 文件 script.js,包含前端的逻辑处理代码,如表单提交、进度条更新、结果显示等

页面工作流程
打开网页 选择视频文件,并选择语言(或保持默认的自动检测)推荐选择语言,这样更准确些 点击“上传并生成字幕”按钮,提交事件。 JavaScript 接管提交: 使用 AJAX 请求将视频文件和语言参数发送到后端 /process_video 路由。 显示进度条,开始轮询后端的进度接口 /progress/<task_id>。 后端处理视频,更新任务进度。 前端根据任务进度,实时更新进度条的显示。 处理完成后: 前端收到处理结果,显示字幕内容区域和下载、复制按钮。 用户可以在网页上查看字幕内容。 点击“下载字幕文件”按钮,下载生成的字幕文件。 点击“复制全部内容”按钮,将字幕内容复制到剪贴板。
JavaScript 代码 script.js文件

# updated to added script.js filename on above row. 

使用 jQuery 库,处理网页上的提交、与后端进行 AJAX 通信、更新进度条、以及在处理完成后显示结果。

这个网上拼的,只是在这里达到能用。

$(document).ready(function() {

    $('#upload-form').on('submit', function(event) {

        event.preventDefault();

        var formData = new FormData();

        var videoFile = $('#video')[0].files[0];

        var language = $('#language').val();

        if (!videoFile) {

            alert('请选择一个视频文件。');

            return;

        }

        formData.append('video', videoFile);

        formData.append('language', language);

        // 显示进度条

        $('#progress-bar-container').show();

        $('#progress-bar').css('width', '0%');

        $.ajax({

            url: '/process_video',

            type: 'POST',

            data: formData,

            contentType: false,

            processData: false,

            success: function(data) {

                if (data.error) {

                    alert(data.error);

                    $('#progress-bar-container').hide();

                } else {

                    var taskId = data.task_id;

                    // 开始轮询进度

                    pollProgress(taskId);

                }

            },

            error: function(jqXHR, textStatus, errorThrown) {

                alert('处理过程中发生错误,请稍后再试。');

                $('#progress-bar-container').hide();

            }

        });

    });

    function pollProgress(taskId) {

        $.ajax({

            url: '/progress/' + taskId,

            type: 'GET',

            success: function(data) {

                if (data.progress >= 0 && data.progress < 100) {

                    // 更新进度条

                    $('#progress-bar').css('width', data.progress + '%');

                    // 继续轮询

                    setTimeout(function() {

                        pollProgress(taskId);

                    }, 1000);

                } else if (data.progress == 100) {

                    // 处理完成

                    $('#progress-bar').css('width', '100%');

                    // 显示结果

                    var result = data.result;

                    $('#subtitle-content').text(result.subtitles_content).show();

                    $('#buttons').show();

                    // 设置下载按钮的链接

                    $('#download-btn').off('click').on('click', function() {

                        window.location.href = result.download_url;

                    });

                    // 复制按钮功能

                    $('#copy-btn').off('click').on('click', function() {

                        navigator.clipboard.writeText(result.subtitles_content).then(function() {

                            alert('字幕内容已复制到剪贴板。');

                        }, function(err) {

                            alert('复制失败:' + err);

                        });

                    });

                    // 隐藏进度条

                    $('#progress-bar-container').hide();

                } else if (data.progress == -1) {

                    // 处理失败

                    alert('处理过程中发生错误,请稍后再试。');

                    $('#progress-bar-container').hide();

                } else if (data.error) {

                    alert(data.error);

                    $('#progress-bar-container').hide();

                }

            },

            error: function() {

                // 请求失败,继续轮询

                setTimeout(function() {

                    pollProgress(taskId);

                }, 1000);

            }

        });

    }

});

总结:

我的Laptop很旧,运行whisper模型比较慢,我使用的是 model = whisper.load_model("base", device="cuda")    base级别对于我GPU已经是极限

如果需要更快的速度或更高的准确性,可以选择 small, medium, large 等不同大小的模型,最小的是Tiny

Size Parameters English-only model Multilingual model tiny 39 M ✓ ✓ base 74 M ✓ ✓ small 244 M ✓ ✓ medium 769 M ✓ ✓ large 1550 M ✓

whisper/model-card.md at main · openai/whisper · GitHub

下一步还是需移到NAS Docker,NAS有一颗赛扬处理器 J4125 ,内存4GB,GPU 是Intel HD 600,要简单更改代码希望能跑。 要花些时间用在做容器上Container.

总结

更新时间 2024-09-26