├── .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 |
154 |
155 |
156 |
157 |
158 |
213 |
214 |
215 |
--------------------------------------------------------------------------------