├── .gitignore ├── LICENSE ├── README.md ├── app.py ├── bin └── clear_outputs.sh ├── config ├── __init__.py ├── paths.py └── settings.py ├── prompts └── 字幕校正.md ├── requirements.txt ├── src ├── __init__.py ├── audio_processing.py └── video_processing.py └── static └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 111 | .pdm.toml 112 | .pdm-python 113 | .pdm-build/ 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # Media files 159 | ./outputs/ 160 | 161 | # PyCharm 162 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 163 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 164 | # and can be added to the global gitignore or merged into this file. For a more nuclear 165 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 166 | #.idea/ 167 | 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 温州程序员劝退师 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Processing Service 2 | 3 | ## Slogan 4 | 从此再无生番! 5 | 6 | ## 项目介绍 7 | 8 | **Video Processing Service** 是一个用于自动化视频处理的服务,支持从视频中提取音频、生成字幕并将字幕嵌入到视频中的功能。 9 | 10 | 该项目利用 FFmpeg 和 Whisper 等开源工具,提供高效、可扩展的处理流程,适用于各种视频和音频处理需求 11 | 12 | ## 功能特性 13 | 14 | * **音频提取:** 从视频中提取音频并保存为 wav 格式,支持自定义采样率 15 | * **字幕生成**:使用 Whisper 模型自动生成字幕,支持中文和其他语言 16 | * **嵌入字幕**:使用 FFmpeg 将生成的字幕嵌入到视频中,生成带有字幕的最终视频文件 17 | * **支持多种格式**:支持常见的视频格式(如 MP4)及音频格式(如 WAV) 18 | 19 | ## 项目结构 20 | 21 | ``` 22 | video_processing_service/ 23 | ├── config/ 24 | │ └── paths.py # 配置文件,定义文件路径和目录 25 | ├── src/ 26 | │ └── video_processing.py # 主要的视频处理逻辑 27 | ├── uploads/ # 上传的视频文件存储目录 28 | ├── outputs/ # 处理后的文件输出目录 29 | │ ├── audio/ # 提取的音频文件 30 | │ ├── subtitles/ # 生成的字幕文件 31 | │ ├── frames/ # 视频帧 32 | ├── app.py # Flask API 服务入口 33 | └── requirements.txt # 项目的依赖列表 34 | ``` 35 | 36 | ## 安装及运行 37 | 38 | ### 克隆项目 39 | 40 | ```bash 41 | git clone https://github.com/GeekyWizKid/video_processing_service.git 42 | ``` 43 | 44 | ```bash 45 | cd video_processing_service 46 | ``` 47 | 48 | ### 创建虚拟环境并安装依赖 49 | 50 | 注意:Python 版本 3.9.12,建议使用 Python 虚拟环境来隔离项目依赖。 51 | 52 | 创建虚拟环境(可以使用 venv 或 conda) 53 | 54 | ```bash 55 | python -m venv venv 56 | ``` 57 | 58 | #### Mac/Linux 59 | 60 | ```bash 61 | source venv/bin/activate 62 | ``` 63 | 64 | #### Windows 65 | 66 | ```bash 67 | venv\Scripts\activate 68 | ``` 69 | 70 | ### 安装依赖 71 | 72 | ```bash 73 | pip install -r requirements.txt 74 | ``` 75 | 76 | ### 配置路径 77 | 78 | 确保项目中的文件路径(如 uploads 和 outputs 文件夹)已创建。 79 | 80 | 你可以根据需要修改 config/paths.py 文件中的路径配置。 81 | 82 | ### 运行 Flask 服务 83 | 84 | ```bash 85 | python app.py 86 | ``` 87 | 88 | **注意:** Flask 服务将在 http://0.0.0.0:5000 启动,你可以使用该服务进行视频上传和处理。 89 | 90 | ### 使用 API 91 | 92 | 测试服务,检查服务是否运行 93 | 94 | ```bash 95 | curl http://127.0.0.1:5000/test 96 | ``` 97 | 98 | ### 上传视频并处理 99 | 100 | 使用 POST 请求上传视频,服务会自动提取音频、生成字幕并将字幕嵌入到视频中。 101 | 102 | ### 请求示例 103 | 104 | ```bash 105 | curl -X POST -F "file=@path_to_video.mp4" http://127.0.0.1:5000/upload 106 | ``` 107 | 108 | ### 响应示例 109 | 110 | ``` 111 | { 112 | "message": "视频处理完成", 113 | "download_url": "/download/85_1734421479_with_subtitles.mp4" 114 | } 115 | ``` 116 | 117 | ### 下载处理后的视频 118 | 119 | 通过下载 URL 获取处理后的文件 120 | 121 | ```bash 122 | curl -O http://127.0.0.1:5000/download/85_1734421479_with_subtitles.mp4 123 | ``` 124 | 125 | ## 依赖 126 | 127 | * **Flask**:用于构建 API 服务 128 | * **Whisper**:OpenAI 的自动语音识别模型,用于生成字幕 129 | * **FFmpeg**:用于视频和音频处理 130 | 131 | ## 贡献 132 | 133 | 如果你想贡献代码,可以通过以下步骤: 134 | 135 | 1. Fork 本项目 136 | 2. 创建一个新的分支 (git checkout -b feature-branch) 137 | 3. 提交更改 (git commit -am 'Add new feature') 138 | 4. 推送到分支 (git push origin feature-branch) 139 | 5. 创建一个新的 Pull Request 140 | 141 | 142 | ### 贡献者 143 | 144 | 145 | 146 | 147 | 148 | 149 | ## 许可 150 | 151 | 该项目遵循 **MIT** 许可协议。 152 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from flask import Flask, request, jsonify, send_from_directory 4 | from flask_cors import CORS 5 | 6 | from config.paths import PathConfig 7 | from src.video_processing import extract_audio_from_video, generate_subtitles, embed_subtitles, \ 8 | generate_subtitles_with_translation 9 | import os 10 | import subprocess 11 | 12 | app = Flask(__name__) 13 | CORS(app) 14 | 15 | def get_unique_filename(directory, filename): 16 | """ 17 | 生成唯一文件名,避免重名文件覆盖 18 | :param directory: 保存的目录路径 19 | :param filename: 原始文件名 20 | :return: 一个不重复的文件名 21 | """ 22 | base, extension = os.path.splitext(filename) 23 | # 移除 base 末尾已有的 (数字) 后缀,避免重复叠加 24 | base = re.sub(r'\(\d+\)$', '', base) 25 | 26 | counter = 1 27 | new_filename = f"{base}{extension}" 28 | new_path = os.path.join(directory, new_filename) 29 | 30 | while os.path.exists(new_path): 31 | counter += 1 32 | new_filename = f"{base}({counter}){extension}" 33 | new_path = os.path.join(directory, new_filename) 34 | return new_filename 35 | 36 | 37 | @app.route('/test', methods=['GET']) 38 | def test(): 39 | return jsonify({'message': 'Server is running'}) 40 | 41 | 42 | @app.route('/upload', methods=['POST']) 43 | def upload_video(): 44 | # 确保相关目录已存在 45 | PathConfig.ensure_dirs( 46 | [PathConfig.UPLOAD_DIR, PathConfig.OUTPUT_DIR, PathConfig.AUDIO_DIR, PathConfig.SUBTITLE_DIR]) 47 | 48 | if 'file' not in request.files: 49 | return jsonify({'error': 'No file part'}), 400 50 | file = request.files['file'] 51 | if file.filename == '': 52 | return jsonify({'error': 'No selected file'}), 400 53 | 54 | # 处理上传文件名,确保唯一性 55 | unique_filename = get_unique_filename(PathConfig.UPLOAD_DIR, file.filename) 56 | video_path = PathConfig.get_upload_path(unique_filename) 57 | file.save(video_path) 58 | 59 | # 从唯一文件名提取稳定的 base 名称(去掉扩展名) 60 | base = os.path.splitext(unique_filename)[0] 61 | 62 | # 音频提取 63 | audio_path = PathConfig.get_audio_path(f"{base}.wav") 64 | extract_audio_from_video(video_path, audio_path) 65 | 66 | # 检查请求中是否包含 translate 参数 67 | translate_target_language = request.form.get('translate') 68 | 69 | # 生成字幕 70 | subtitle_path = PathConfig.get_subtitle_path(f"{base}.srt") 71 | if translate_target_language: 72 | success = generate_subtitles_with_translation(audio_path, subtitle_path, 73 | target_language=translate_target_language) 74 | else: 75 | success = generate_subtitles(audio_path, subtitle_path) 76 | 77 | if not success: 78 | return jsonify({'error': '生成字幕时出错'}), 500 79 | 80 | # 获取用户的返回选项 81 | return_option = request.form.get('return_option', 'video') # 默认返回视频 82 | 83 | # 处理返回字幕文件或视频文件 84 | if return_option == 'subtitle': 85 | # 返回字幕文件的 JSON 格式链接 86 | return jsonify({ 87 | 'message': '字幕文件处理完成', 88 | 'download_url': f'/download/{os.path.basename(subtitle_path)}' 89 | }) 90 | else: 91 | # 嵌入字幕并返回视频文件的 JSON 格式链接 92 | output_video_path = PathConfig.get_output_path(f"{base}_with_subtitles.mp4") 93 | embed_subtitles(video_path, subtitle_path, output_video_path) 94 | 95 | return jsonify({ 96 | 'message': '视频处理完成', 97 | 'download_url': f'/download/{os.path.basename(output_video_path)}' 98 | }) 99 | 100 | 101 | @app.route('/burn', methods=['POST']) 102 | def burn_subtitles(): 103 | # 确保请求体是 JSON 104 | if not request.is_json: 105 | return jsonify({'error': 'Invalid content type. Please use application/json'}), 400 106 | 107 | # 从 JSON 请求体中获取 'filename' 108 | data = request.get_json() 109 | filename = data.get('filename') 110 | if not filename: 111 | return jsonify({'error': 'Filename is required'}), 400 112 | 113 | # 确保 PathConfig 的目录已经存在 114 | PathConfig.ensure_dirs( 115 | [PathConfig.UPLOAD_DIR, PathConfig.OUTPUT_DIR, PathConfig.AUDIO_DIR, PathConfig.SUBTITLE_DIR] 116 | ) 117 | 118 | # 获取上传目录中的视频文件和字幕文件 119 | video_path = PathConfig.get_upload_path(f"{filename}.mp4") 120 | subtitle_path = PathConfig.get_subtitle_path(f"{filename}.srt") 121 | 122 | # 检查视频文件和字幕文件是否存在 123 | if not os.path.exists(video_path): 124 | return jsonify({'error': f'Video file "{filename}.mp4" not found'}), 404 125 | if not os.path.exists(subtitle_path): 126 | return jsonify({'error': f'Subtitle file "{filename}.srt" not found'}), 404 127 | 128 | # 输出带字幕的视频路径 129 | output_video_path = PathConfig.get_output_path(f"{filename}_with_subtitles.mp4") 130 | 131 | try: 132 | # 嵌入字幕 133 | embed_subtitles(video_path, subtitle_path, output_video_path) 134 | except Exception as e: 135 | return jsonify({'error': f'Failed to burn subtitles: {str(e)}'}), 500 136 | 137 | # 返回下载地址 138 | return jsonify({ 139 | 'message': '字幕烧录完成', 140 | 'download_url': f'/download/{os.path.basename(output_video_path)}' 141 | }) 142 | 143 | @app.route('/download/', methods=['GET']) 144 | def download_file(filename): 145 | # 根据文件扩展名判断是否是字幕文件 146 | if filename.endswith('.srt'): 147 | directory = PathConfig.SUBTITLE_DIR 148 | else: 149 | directory = PathConfig.OUTPUT_DIR 150 | 151 | return send_from_directory(directory, filename) 152 | 153 | @app.route('/') 154 | def index(): 155 | return send_from_directory('static', 'index.html') 156 | 157 | if __name__ == '__main__': 158 | app.run(debug=True, host='0.0.0.0', port=5000) -------------------------------------------------------------------------------- /bin/clear_outputs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 获取当前脚本所在目录的路径 4 | BASE_DIR=$(cd "$(dirname "$0")/.." && pwd) 5 | OUTPUT_DIR="$BASE_DIR/outputs" 6 | 7 | # 检查目录是否存在 8 | if [ ! -d "$OUTPUT_DIR" ]; then 9 | echo "目录 $OUTPUT_DIR 不存在。" 10 | exit 1 11 | fi 12 | 13 | # 删除目录及其子目录中的所有文件,但保留文件夹结构 14 | find "$OUTPUT_DIR" -type f -exec rm -f {} \; 15 | 16 | echo "已删除 $OUTPUT_DIR 内容" 17 | 18 | 19 | UPLOAD_DIR="$BASE_DIR/uploads" 20 | 21 | # 检查目录是否存在 22 | if [ ! -d "$UPLOAD_DIR" ]; then 23 | echo "目录 $UPLOAD_DIR 不存在。" 24 | exit 1 25 | fi 26 | 27 | # 删除目录及其子目录中的所有文件,但保留文件夹结构 28 | find "$UPLOAD_DIR" -type f -exec rm -f {} \; 29 | 30 | echo "已删除 $UPLOAD_DIR 内容" -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekyWizKid/video_processing_service/0e999787c2886a9b25994ae2c6f2e5d8cd4d87fa/config/__init__.py -------------------------------------------------------------------------------- /config/paths.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class PathConfig: 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | # 上传文件目录 7 | UPLOAD_DIR = os.path.join(BASE_DIR, 'uploads') 8 | # 输出目录 9 | OUTPUT_DIR = os.path.join(BASE_DIR, 'outputs') 10 | # 临时音频存储目录 11 | AUDIO_DIR = os.path.join(OUTPUT_DIR, 'audio') 12 | # 临时字幕文件存储目录 13 | SUBTITLE_DIR = os.path.join(OUTPUT_DIR, 'subtitles') 14 | # 视频帧存储目录 15 | FRAMES_DIR = os.path.join(OUTPUT_DIR, 'frames') 16 | 17 | @classmethod 18 | def ensure_dirs(cls, dirs): 19 | if isinstance(dirs, list): 20 | for dir in dirs: 21 | cls.ensure_dir(dir) 22 | else: 23 | cls.ensure_dir(dirs) 24 | 25 | @classmethod 26 | def get_upload_path(cls, filename): 27 | return os.path.join(cls.UPLOAD_DIR, filename) 28 | 29 | @classmethod 30 | def get_output_path(cls, filename): 31 | return os.path.join(cls.OUTPUT_DIR, filename) 32 | 33 | @classmethod 34 | def get_audio_path(cls, filename): 35 | return os.path.join(cls.AUDIO_DIR, filename) 36 | 37 | @classmethod 38 | def get_subtitle_path(cls, filename): 39 | return os.path.join(cls.SUBTITLE_DIR, filename) 40 | 41 | @classmethod 42 | def get_frames_path(cls, filename): 43 | return os.path.join(cls.FRAMES_DIR, filename) 44 | 45 | @classmethod 46 | def ensure_dir(cls, path): 47 | """ 48 | 确保目录存在,不存在则创建 49 | """ 50 | if not os.path.exists(path): 51 | os.makedirs(path) 52 | pass -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | # 配置日志 4 | logging.basicConfig( 5 | level=logging.INFO, 6 | format="%(asctime)s - %(levelname)s - %(message)s", 7 | handlers=[ 8 | logging.StreamHandler(), 9 | logging.FileHandler("app.log", mode='w') 10 | ] 11 | ) -------------------------------------------------------------------------------- /prompts/字幕校正.md: -------------------------------------------------------------------------------- 1 | 你现在是字幕文本校对员,请认真校对以下字幕。不能改变原文含义、段落结构,只修改错别字、标点符号、去掉多余的字和加上漏的字,没错的不用修改,句尾去除标点符号。保持字幕时间戳不错误,只输出结果,不要对话和询问 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask_Cors==5.0.0 2 | openai_whisper==20240930 3 | flask==3.1.0 4 | jsonify==0.5 -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekyWizKid/video_processing_service/0e999787c2886a9b25994ae2c6f2e5d8cd4d87fa/src/__init__.py -------------------------------------------------------------------------------- /src/audio_processing.py: -------------------------------------------------------------------------------- 1 | import whisper 2 | from config.paths import PathConfig 3 | 4 | def transcribe_audio(audio_path, language='zh'): 5 | """使用 Whisper 进行音频转录""" 6 | try: 7 | model = whisper.load_model("large-v3") 8 | result = model.transcribe(audio_path, language=language) 9 | return result["text"] 10 | except Exception as e: 11 | print(f"音频转录时出错: {str(e)}") 12 | return None -------------------------------------------------------------------------------- /src/video_processing.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import whisper 4 | from datetime import timedelta 5 | from config.paths import PathConfig 6 | 7 | def extract_audio_from_video(video_path, audio_path, sample_rate=44000): 8 | """从视频中提取音频并保存为 wav 文件""" 9 | cmd = [ 10 | 'ffmpeg', '-i', video_path, '-vn', '-ar', str(sample_rate), 11 | '-ac', '2', '-b:a', '128k', audio_path 12 | ] 13 | subprocess.run(cmd, check=True) 14 | 15 | def format_timestamp(seconds): 16 | """将秒数转换为 SRT 格式的时间戳""" 17 | td = timedelta(seconds=float(seconds)) 18 | hours = int(td.total_seconds() // 3600) 19 | minutes = int((td.total_seconds() % 3600) // 60) 20 | seconds = td.total_seconds() % 60 21 | milliseconds = int((seconds % 1) * 1000) 22 | return f"{hours:02d}:{minutes:02d}:{int(seconds):02d},{milliseconds:03d}" 23 | 24 | def generate_subtitles(audio_path, output_srt_path, language='zh'): 25 | """使用 Whisper 生成字幕文件""" 26 | try: 27 | # 加载 Whisper 模型 28 | model = whisper.load_model("large-v3") 29 | result = model.transcribe(audio_path) 30 | 31 | with open(output_srt_path, 'w', encoding='utf-8') as f: 32 | for i, segment in enumerate(result["segments"], 1): 33 | start_time = format_timestamp(segment["start"]) 34 | end_time = format_timestamp(segment["end"]) 35 | f.write(f"{i}\n{start_time} --> {end_time}\n{segment['text'].strip()}\n\n") 36 | 37 | return True 38 | except Exception as e: 39 | print(f"生成字幕时出错: {str(e)}") 40 | return False 41 | 42 | 43 | # TODO 这个方法暂时只实现到翻译成英文的功能 44 | def generate_subtitles_with_translation(audio_path, output_srt_path, target_language='zh'): 45 | """使用 Whisper 生成翻译成目标语言的字幕""" 46 | try: 47 | # 加载 Whisper 模型 48 | model = whisper.load_model("large-v3") 49 | 50 | # TODO 使用 Whisper 的内置翻译功能 whisper 暂时只支持翻译成英文,需要接入第三方翻译服务 , 这里不会生效 51 | print(f"Transcribing and translating audio to '{target_language}'...") 52 | result = model.transcribe(audio_path, task="translate") 53 | 54 | # 保存翻译后的字幕 55 | with open(output_srt_path, 'w', encoding='utf-8') as f: 56 | for i, segment in enumerate(result["segments"], 1): 57 | start_time = format_timestamp(segment["start"]) 58 | end_time = format_timestamp(segment["end"]) 59 | text = segment['text'].strip() 60 | 61 | f.write(f"{i}\n{start_time} --> {end_time}\n{text}\n\n") 62 | 63 | return True 64 | except Exception as e: 65 | print(f"生成翻译字幕时出错: {str(e)}") 66 | return False 67 | 68 | 69 | def detect_language_in_audio(audio_path): 70 | """检测音频中的主语言""" 71 | try: 72 | # 加载 Whisper 模型 73 | model = whisper.load_model("large-v3") 74 | 75 | # 转录音频并获取语言信息 76 | result = model.transcribe(audio_path) 77 | 78 | # 提取检测到的语言 79 | detected_language = result['language'] 80 | print(f"检测到的语言是: {detected_language}") 81 | 82 | return detected_language 83 | except Exception as e: 84 | print(f"检测语言时出错: {str(e)}") 85 | return None 86 | 87 | 88 | 89 | 90 | def embed_subtitles(video_path, subtitle_path, output_path): 91 | """使用 FFmpeg 将字幕嵌入到视频中""" 92 | try: 93 | cmd = [ 94 | 'ffmpeg', '-i', video_path, '-vf', f'subtitles={subtitle_path}', '-c:a', 'copy', output_path 95 | ] 96 | subprocess.run(cmd, check=True) 97 | return True 98 | except subprocess.CalledProcessError as e: 99 | print(f"嵌入字幕时出错: {str(e)}") 100 | return False -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 从此再无生番 7 | 128 | 129 | 130 |
131 |

视频字幕处理

132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 151 | 152 | 153 |
154 | 155 |
156 |
157 | 158 | 213 | 214 | 215 | --------------------------------------------------------------------------------