felix1968839 commited on
Commit
4aff0b5
·
verified ·
1 Parent(s): c6f9fe6

first commit

Browse files
Files changed (8) hide show
  1. .gitignore +27 -0
  2. .streamlit/config.toml +3 -0
  3. README.md +103 -11
  4. app.py +308 -0
  5. requirements.txt +8 -0
  6. stt_module.py +71 -0
  7. translator.py +36 -0
  8. utils.py +86 -0
.gitignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Environment variables
7
+ .env
8
+
9
+ # Local models
10
+ models/
11
+ models/*
12
+
13
+ # Streamlit config (optional, if you want to ignore local server config)
14
+ #.streamlit/
15
+
16
+ # Video processing temp files
17
+ *.mp4
18
+ *.mp3
19
+ *.wav
20
+ *.srt
21
+ *.mkv
22
+ *.avi
23
+ *.mov
24
+
25
+ # IDE
26
+ .vscode/
27
+ .idea/
.streamlit/config.toml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [server]
2
+ maxUploadSize = 10240
3
+ maxMessageSize = 10240
README.md CHANGED
@@ -1,11 +1,103 @@
1
- ---
2
- title: Translator
3
- emoji: 📊
4
- colorFrom: pink
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- short_description: 音视频翻译
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎬 AI 媒体翻译专家 (Media Translator)
2
+
3
+ 这是一个基于 Python 的视频/音频翻译工具,可以自动提取媒体音频、识别语音生成字幕、通过 AI 翻译字幕,并最终将字幕压制回视频中(仅视频)。支持本地 CPU 运行语音识别,翻译部分支持对接各种主流 AI API。
4
+
5
+ ## ✨ 功能特点
6
+
7
+ - **全媒体支持**:支持视频(mp4, mkv, avi, mov)和音频(mp3, wav, m4a, flac, aac)文件。
8
+ - **本地语音识别 (STT)**:使用 `faster-whisper` 模型,在本地即可完成语音转文字,支持自动检测语言。
9
+ - **直接字幕翻译**:支持直接上传已有的 `.srt` 字幕文件进行翻译,无需重新处理视频。
10
+ - **AI 智能翻译**:兼容 OpenAI 格式的 API(如 SiliconFlow、Zenmux、DeepSeek 等),支持自定义 API 地址和模型。
11
+ - **视频硬压字幕**:自动将翻译后的字幕嵌入到视频中。
12
+ - **多种导出格式**:支持导出原始 SRT 字幕、翻译后的 SRT 字幕以及压制好的 MP4 视频。
13
+ - **低硬件门槛**:经过优化,即使在没有显卡(仅 CPU)的普通电脑上也能流畅运行。
14
+
15
+ ## 🛠️ 环境准备
16
+
17
+ 在运行本项目之前,请确保已安装以下工具:
18
+
19
+ ### 1. FFmpeg (核心依赖)
20
+ FFmpeg 负责音频提取和视频合成,是必须安装的。
21
+ - **Windows**:
22
+ 1. 下载 [FFmpeg 编译版](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z)。
23
+ 2. 解压并将 `bin` 目录路径添加到系统的 **环境变量 (PATH)** 中。(在打开的“编辑环境变量”窗口中,窗口的下半部分“**系统变量**”区域,找到并选中名为 **`Path`** 的变量。点击“新建”,将你电脑上 FFmpeg 的 bin 文件夹的完整路径粘贴进去。例如:D:\ffmpeg\bin 或 C:\Tools\ffmpeg-6.1-full_build\bin)
24
+ 3. 验证:在终端输入 `ffmpeg -version` 确认是否有输出。
25
+ - **macOS**: `brew install ffmpeg`
26
+ - **Linux**: `sudo apt install ffmpeg`
27
+
28
+ ### 2. Python 3.8+
29
+ 建议使用 Python 3.10 或更高版本。
30
+
31
+ ## 🚀 安装与运行
32
+
33
+ 1. **克隆或下载本项目**到本地。
34
+
35
+ 2. **安装依赖库**:
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ ```
39
+
40
+ 3. **启动程序**:
41
+ ```bash
42
+ streamlit run app.py
43
+ ```
44
+ 启动后,浏览器会自动打开 `http://localhost:8501`。
45
+
46
+ **注意**:为了支持大视频上传,本项目已在 `.streamlit/config.toml` 中配置了最大 10GB 的上传限制。
47
+
48
+ ## ⚙️ 自动保存配置 (.env)
49
+
50
+ 为了避免每次运行都要手动输入 API Key,你可以创建一个名为 `.env` 的文件(可以参考项目中的 `.env.example`):
51
+
52
+ 1. 在项目根目录下新建文件 `.env`。
53
+ 2. 填写以下内容:
54
+ ```env
55
+ API_KEY=你的_API_KEY
56
+ BASE_URL=https://api.siliconflow.cn/v1
57
+ MODEL_NAME=THUDM/glm-4-9b-chat
58
+ STT_MODEL_SIZE=base
59
+ TARGET_LANG=中文
60
+ ```
61
+ 3. 下次启动程序时,这些值将自动填充到界面中。
62
+
63
+ ## 📖 使用指南
64
+
65
+ 1. **配置 API**:
66
+ - 在左侧边栏输入你的 **API Key**。
67
+ - 如果使用 SiliconFlow,默认地址为 `https://api.siliconflow.cn/v1`。
68
+ - 输入你想要使用的模型名称(例如 `THUDM/glm-4-9b-chat` 或 `deepseek-chat`)。
69
+
70
+ 2. **上传媒体文件**:
71
+ - 点击或拖拽视频(mp4, mkv, avi, mov)或音频(mp3, wav, m4a 等)文件到上传区。
72
+
73
+ 3. **开始处理**:
74
+ - 选择本地 STT 模型(建议 CPU 使用 `base` 或 `small`)。
75
+ - 选择目标翻译语言(默认为中文)。
76
+ - 点击 **“开始处理”**。
77
+
78
+ 4. **获取结果**:
79
+ - 处理完成后,你可以分别下载:原始字幕、翻译字幕、带字幕的视频。
80
+
81
+ ## � 文件存储与清理
82
+
83
+ 为了确保处理流程的稳定性,程序在运行过程中会产生一些临时文件:
84
+
85
+ - **存储位置**:所有上传的文件和中间产物(音频、临时字幕、合成视频)都保存在**操作系统的默认临时目录**中。
86
+ - **Windows**: `C:\Users\你的用户名\AppData\Local\Temp`
87
+ - **macOS/Linux**: `/tmp` 或 `$TMPDIR`
88
+ - **清理机制**:程序**不会自动删除**这些临时文件。
89
+ - **手动清理**:如果处理的文件较多或较大,建议定期手动清理系统临时文件夹中以 `tmp` 开头的文件,以释放磁盘空间。
90
+
91
+ ## �� 项目结构
92
+
93
+ - `app.py`: Streamlit Web 界面主程序。
94
+ - `stt_module.py`: 封装了 `faster-whisper` 的语音识别逻辑。
95
+ - `translator.py`: 封装了基于 OpenAI 协议的 AI 翻译逻辑。
96
+ - `utils.py`: 封装了 FFmpeg 相关的视频与音频处理工具。
97
+ - `requirements.txt`: 项目所需的 Python 依赖列表。
98
+
99
+ ## ⚠️ 注意事项
100
+
101
+ - **首次运行**:第一次使用某种 STT 模型(如 `base`)时,程序会自动从 HuggingFace 下���模型文件,请确保网络通畅。
102
+ - **性能**:如果电脑配置较低,建议使用 `tiny` 或 `base` 模型以加快识别速度。
103
+ - **翻译额度**:翻译功能依赖外部 API,请确保你的 API 账户有足够余额或额度。
app.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ from utils import VideoUtils
4
+ from stt_module import STTManager
5
+ from translator import Translator
6
+ import tempfile
7
+ from dotenv import load_dotenv
8
+
9
+ # 加载 .env 文件中的配置
10
+ load_dotenv()
11
+
12
+ # 取消 Streamlit 上传限制(虽然主要通过命令行配置,但在脚本中提醒用户)
13
+ # 实际上 Streamlit 的服务器配置需要在运行命令时指定,或者写在 config.toml 中
14
+ # 我们这里先通过 UI 提醒用户
15
+
16
+ st.set_page_config(page_title="AI 媒体翻译专家", layout="wide")
17
+
18
+ st.title("🎬 AI 媒体字幕提取与翻译")
19
+
20
+ # 侧边栏配置
21
+ with st.sidebar:
22
+ st.header("⚙️ 配置参数")
23
+
24
+ # 从环境变量读取默认值,如果没有则为空字符串或预设值
25
+ default_api_key = os.getenv("API_KEY", "")
26
+ default_base_url = os.getenv("BASE_URL", "https://api.siliconflow.cn/v1")
27
+ default_model = os.getenv("MODEL_NAME", "THUDM/glm-4-9b-chat")
28
+ default_stt_size = os.getenv("STT_MODEL_SIZE", "base")
29
+ default_lang = os.getenv("TARGET_LANG", "中文")
30
+ default_device = os.getenv("DEVICE", "cpu")
31
+
32
+ api_key = st.text_input("API Key", value=default_api_key, type="password", help="输入 API Key")
33
+ base_url = st.text_input("API Base URL", value=default_base_url)
34
+ model_name = st.text_input("模型名称", value=default_model)
35
+
36
+ st.divider()
37
+
38
+ # 设备选择
39
+ cuda_available = STTManager.is_cuda_available()
40
+ device_options = ["cpu", "cuda"] if cuda_available else ["cpu"]
41
+ device_index = device_options.index(default_device) if default_device in device_options else 0
42
+ device = st.selectbox(
43
+ "计算设备 (Device)",
44
+ device_options,
45
+ index=device_index,
46
+ help="如果有 NVIDIA 显卡且安装了 CUDA,选择 'cuda' 会大幅提升速度。"
47
+ )
48
+
49
+ # 根据设备推荐精度
50
+ default_compute_type = "float16" if device == "cuda" else "int8"
51
+ compute_type = st.selectbox(
52
+ "计算精度 (Compute Type)",
53
+ ["int8", "float16", "int8_float16"],
54
+ index=1 if device == "cuda" else 0,
55
+ help="CPU 推荐 int8,GPU 推荐 float16。"
56
+ )
57
+
58
+ stt_options = ["tiny", "base", "small", "medium", "large-v3"]
59
+ # 获取已下载模型
60
+ downloaded_models = STTManager.get_downloaded_models()
61
+
62
+ # 构建选项显示名称
63
+ option_labels = []
64
+ for opt in stt_options:
65
+ label = f"{opt} (已下载)" if opt in downloaded_models else opt
66
+ option_labels.append(label)
67
+
68
+ stt_index = stt_options.index(default_stt_size) if default_stt_size in stt_options else 1
69
+ selected_option = st.selectbox(
70
+ "本地 STT 模型大小",
71
+ option_labels,
72
+ index=stt_index,
73
+ help="越大越准,但速度越慢。首次使用未下载的模型时会自动下载。"
74
+ )
75
+ # 从选项中提取真实模型名
76
+ model_size = selected_option.split(" ")[0]
77
+
78
+ lang_options = ["中文", "English", "日本語", "Français"]
79
+ lang_index = lang_options.index(default_lang) if default_lang in lang_options else 0
80
+ target_lang = st.selectbox("目标语言", lang_options, index=lang_index)
81
+
82
+ # 主界面
83
+ tab1, tab2 = st.tabs(["📁 媒体处理", "📜 字幕翻译"])
84
+
85
+ if "process_results" not in st.session_state:
86
+ st.session_state.process_results = {}
87
+ if "srt_results" not in st.session_state:
88
+ st.session_state.srt_results = {}
89
+ if "awaiting_synthesis" not in st.session_state:
90
+ st.session_state.awaiting_synthesis = False
91
+
92
+ with tab1:
93
+ st.info("💡 提示:支持视频和音频文件。如果文件非常大,处理会很慢请耐心等待。")
94
+ uploaded_file = st.file_uploader("选择视频或音频文件", type=["mp4", "mkv", "avi", "mov", "mp3", "wav", "m4a", "flac", "aac"])
95
+
96
+ if uploaded_file:
97
+ # 检测文件类型
98
+ video_extensions = [".mp4", ".mkv", ".avi", ".mov"]
99
+ file_ext = os.path.splitext(uploaded_file.name)[1].lower()
100
+ is_video = file_ext in video_extensions
101
+
102
+ # 如果上传了新文件且与 session_state 中记录的不同,则清除旧结果
103
+ if "last_uploaded_file" not in st.session_state or st.session_state.last_uploaded_file != uploaded_file.name:
104
+ st.session_state.process_results = {}
105
+ st.session_state.awaiting_synthesis = False
106
+ st.session_state.last_uploaded_file = uploaded_file.name
107
+
108
+ # 保存上传的文件到临时目录
109
+ tfile = tempfile.NamedTemporaryFile(delete=False, suffix=file_ext)
110
+ tfile.write(uploaded_file.read())
111
+ file_path = tfile.name
112
+
113
+ if is_video:
114
+ st.video(file_path)
115
+ else:
116
+ st.audio(file_path)
117
+
118
+ if st.button("开始处理", disabled=not api_key or st.session_state.awaiting_synthesis):
119
+ # 清除旧结果
120
+ st.session_state.process_results = {}
121
+ st.session_state.awaiting_synthesis = False
122
+
123
+ with st.status("正在处理中...", expanded=True) as status:
124
+ # 1. 提取/准备音频
125
+ st.write("🎵 正在准备音频...")
126
+ audio_path = os.path.splitext(file_path)[0] + '.wav'
127
+ try:
128
+ # 无论视频还是音频,都通过 prepare_audio (ffmpeg) 转换为标准格式,确保 STT 兼容性
129
+ VideoUtils.prepare_audio(file_path, audio_path)
130
+ except Exception as e:
131
+ st.error(str(e))
132
+ status.update(label="处理出错", state="error", expanded=True)
133
+ st.stop()
134
+
135
+ # 2. 本地 STT
136
+ if model_size not in downloaded_models:
137
+ st.write(f"📥 正在下载 {model_size} 模型,请稍候...")
138
+
139
+ st.write(f"✍️ 正在识别语音 (使用 {model_size} 模型,设备: {device})...")
140
+ stt_manager = STTManager(model_size=model_size, device=device, compute_type=compute_type)
141
+ stt_manager.load_model()
142
+
143
+ segments_gen, info = stt_manager.transcribe(audio_path)
144
+ st.write(f"检测到语言: {info.language} (置信度: {info.language_probability:.2f})")
145
+
146
+ # 增量处理与展示
147
+ st.write("---")
148
+ st.write("实时识别与翻译预览:")
149
+ preview_container = st.empty()
150
+ all_segments = []
151
+ all_translated_segments = []
152
+
153
+ translator = Translator(api_key, base_url, model_name)
154
+
155
+ # 用于展示的表格数据
156
+ display_data = []
157
+
158
+ for segment in segments_gen:
159
+ # 1. 翻译当前段落
160
+ trans_text = translator.translate_text(segment.text, target_lang)
161
+
162
+ # 2. 保存原始和翻译后的段落
163
+ all_segments.append(segment)
164
+ new_trans_seg = type('Segment', (), {
165
+ 'start': segment.start,
166
+ 'end': segment.end,
167
+ 'text': trans_text
168
+ })
169
+ all_translated_segments.append(new_trans_seg)
170
+
171
+ # 3. 更新预览界面
172
+ time_str = f"{VideoUtils.format_timestamp(segment.start)} -> {VideoUtils.format_timestamp(segment.end)}"
173
+ display_data.append({
174
+ "时间轴": time_str,
175
+ "原文": segment.text,
176
+ "翻译": trans_text
177
+ })
178
+ # 仅显示最后 5 条,避免页面过长,但提供滚动查看全部的可能
179
+ preview_container.table(display_data[-5:])
180
+
181
+ # 生成原始字幕
182
+ orig_srt_path = os.path.splitext(file_path)[0] + '_orig.srt'
183
+ VideoUtils.write_srt(all_segments, orig_srt_path)
184
+
185
+ # 生成翻译字幕
186
+ trans_srt_path = os.path.splitext(file_path)[0] + '_trans.srt'
187
+ VideoUtils.write_srt(all_translated_segments, trans_srt_path)
188
+
189
+ status.update(label="处理完成!", state="complete", expanded=False)
190
+
191
+ # 保存结果到 session_state
192
+ st.session_state.process_results = {
193
+ "orig_srt": orig_srt_path,
194
+ "trans_srt": trans_srt_path,
195
+ "output_video": None,
196
+ "is_video": is_video
197
+ }
198
+
199
+ # 保存中间结果用于后续合成 (仅针对视频)
200
+ if is_video:
201
+ st.session_state.temp_video_path = file_path
202
+ st.session_state.temp_trans_srt_path = trans_srt_path
203
+ st.session_state.temp_orig_srt_path = orig_srt_path
204
+ st.session_state.awaiting_synthesis = True
205
+
206
+ st.rerun()
207
+
208
+ # 4. 嵌入视频 (仅视频且在等待状态)
209
+ if st.session_state.awaiting_synthesis and st.session_state.process_results.get("is_video"):
210
+ st.success("✅ 字幕翻译已完成!")
211
+ col_synth_1, col_synth_2 = st.columns(2)
212
+ with col_synth_1:
213
+ if st.button("🚀 开始合成视频字幕", type="primary"):
214
+ with st.status("🎬 正在合成视频字幕...", expanded=True) as status:
215
+ v_path = st.session_state.temp_video_path
216
+ s_path = st.session_state.temp_trans_srt_path
217
+ output_video_path = os.path.splitext(v_path)[0] + '_translated.mp4'
218
+
219
+ video_ready = False
220
+ try:
221
+ VideoUtils.embed_subtitles(v_path, s_path, output_video_path)
222
+ video_ready = True
223
+ st.write("✨ 视频合成成功!")
224
+ except Exception as e:
225
+ st.error(f"视频合成失败 (请确保已安装 FFmpeg): {e}")
226
+
227
+ status.update(label="全部处理完成!", state="complete", expanded=False)
228
+
229
+ # 保存最终结果到 session_state
230
+ st.session_state.process_results["output_video"] = output_video_path if video_ready else None
231
+ st.session_state.awaiting_synthesis = False
232
+ st.rerun()
233
+
234
+ with col_synth_2:
235
+ if st.button("📂 仅保存字幕"):
236
+ st.session_state.awaiting_synthesis = False
237
+ st.rerun()
238
+
239
+ # 结果展示与下载 (移出 button 缩进块,始终根据 session_state 显示)
240
+ if st.session_state.process_results:
241
+ st.divider()
242
+ col_title, col_clear = st.columns([5, 1])
243
+ with col_title:
244
+ st.subheader("🎉 处理结果")
245
+ with col_clear:
246
+ if st.button("🗑️ 清除结果"):
247
+ st.session_state.process_results = {}
248
+ st.session_state.awaiting_synthesis = False
249
+ st.rerun()
250
+
251
+ col1, col2, col3 = st.columns(3)
252
+
253
+ results = st.session_state.process_results
254
+
255
+ if os.path.exists(results.get("orig_srt", "")):
256
+ with col1:
257
+ with open(results["orig_srt"], "rb") as f:
258
+ st.download_button("⬇️ 下载原始字幕", f, file_name="original.srt", key="dl_orig")
259
+
260
+ if os.path.exists(results.get("trans_srt", "")):
261
+ with col2:
262
+ with open(results["trans_srt"], "rb") as f:
263
+ st.download_button("⬇️ 下载翻译字幕", f, file_name="translated.srt", key="dl_trans")
264
+
265
+ if results.get("output_video") and os.path.exists(results["output_video"]):
266
+ with col3:
267
+ with open(results["output_video"], "rb") as f:
268
+ st.download_button("⬇️ 下载翻译视频", f, file_name="video_with_subtitles.mp4", key="dl_video")
269
+
270
+ with tab2:
271
+ st.info("如果你已经有原始语言的字幕文件(SRT),可以在这里直接进行翻译。")
272
+ uploaded_srt = st.file_uploader("上传原始 SRT 字幕", type=["srt"])
273
+
274
+ if uploaded_srt:
275
+ # 如果上传了新字幕且与 session_state 中记录的不同,则清除旧结果
276
+ if "last_uploaded_srt" not in st.session_state or st.session_state.last_uploaded_srt != uploaded_srt.name:
277
+ st.session_state.srt_results = {}
278
+ st.session_state.last_uploaded_srt = uploaded_srt.name
279
+
280
+ srt_content = uploaded_srt.read().decode("utf-8")
281
+ st.text_area("字幕预览", srt_content, height=200)
282
+
283
+ if st.button("开始翻译字幕", disabled=not api_key):
284
+ st.session_state.srt_results = {} # 清除旧结果
285
+ with st.spinner("正在翻译字幕..."):
286
+ segments = VideoUtils.parse_srt(srt_content)
287
+ translator = Translator(api_key, base_url, model_name)
288
+ translated_segments = translator.translate_segments(segments, target_lang)
289
+
290
+ # 保存到临时文件
291
+ temp_trans_srt = tempfile.NamedTemporaryFile(delete=False, suffix='.srt')
292
+ VideoUtils.write_srt(translated_segments, temp_trans_srt.name)
293
+
294
+ st.session_state.srt_results = {
295
+ "trans_srt": temp_trans_srt.name
296
+ }
297
+ st.success("翻译完成!")
298
+
299
+ # 结果下载
300
+ if st.session_state.srt_results:
301
+ st.divider()
302
+ res = st.session_state.srt_results
303
+ if os.path.exists(res.get("trans_srt", "")):
304
+ with open(res["trans_srt"], "rb") as f:
305
+ st.download_button("⬇️ 下载翻译后的字幕", f, file_name="translated_only.srt", key="dl_srt_tab")
306
+
307
+ if not api_key:
308
+ st.warning("请在左侧边栏输入 API Key 后开始。")
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ faster-whisper
2
+ openai
3
+ pydantic
4
+ requests
5
+ tqdm
6
+ streamlit
7
+ python-dotenv
8
+ ffmpeg-python
stt_module.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from faster_whisper import WhisperModel
2
+ import os
3
+
4
+ class STTManager:
5
+ def __init__(self, model_size="base", device="cpu", compute_type="int8"):
6
+ """
7
+ model_size: tiny, base, small, medium, large-v3
8
+ device: cpu or cuda
9
+ compute_type: int8, float16, int8_float16 等
10
+ """
11
+ self.model_size = model_size
12
+ self.device = device
13
+ self.compute_type = compute_type
14
+ self.model = None
15
+
16
+ def load_model(self):
17
+ """延迟加载模型,方便在加载前显示提示"""
18
+ if self.model is None:
19
+ # 自动处理 compute_type,如果 GPU 不支持 float16 则回退
20
+ stt_compute_type = self.compute_type
21
+ if self.device == "cpu" and stt_compute_type == "float16":
22
+ stt_compute_type = "int8"
23
+
24
+ self.model = WhisperModel(
25
+ self.model_size,
26
+ device=self.device,
27
+ compute_type=stt_compute_type,
28
+ download_root=os.path.join(os.getcwd(), "models")
29
+ )
30
+ return self.model
31
+
32
+ def transcribe(self, audio_path):
33
+ """识别音频并返回 segments (生成器) 和 info"""
34
+ model = self.load_model()
35
+ segments, info = model.transcribe(audio_path, beam_size=5)
36
+ return segments, info
37
+
38
+ @staticmethod
39
+ def is_cuda_available():
40
+ """检测 CUDA 是否可用"""
41
+ try:
42
+ import ctranslate2
43
+ return ctranslate2.get_cuda_device_count() > 0
44
+ except:
45
+ return False
46
+
47
+ @staticmethod
48
+ def get_downloaded_models():
49
+ """获取本地已下载的模型列表"""
50
+ model_dir = os.path.join(os.getcwd(), "models")
51
+ if not os.path.exists(model_dir):
52
+ return []
53
+
54
+ models = []
55
+ for d in os.listdir(model_dir):
56
+ if "faster-whisper-" in d:
57
+ name = d.split("-")[-1]
58
+ models.append(name)
59
+ elif os.path.isdir(os.path.join(model_dir, d)):
60
+ models.append(d)
61
+
62
+ valid_names = ["tiny", "base", "small", "medium", "large-v1", "large-v2", "large-v3"]
63
+ found = [m for m in models if any(v in m for v in valid_names)]
64
+ final_models = []
65
+ for f in found:
66
+ for v in valid_names:
67
+ if v in f:
68
+ final_models.append(v)
69
+ break
70
+
71
+ return sorted(list(set(final_models)))
translator.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+
3
+ class Translator:
4
+ def __init__(self, api_key, base_url, model="base"):
5
+ self.client = OpenAI(api_key=api_key, base_url=base_url)
6
+ self.model = model
7
+
8
+ def translate_text(self, text, target_lang="中文"):
9
+ """翻译单段文本"""
10
+ if not text.strip():
11
+ return ""
12
+
13
+ prompt = f"你是一个专业的视频字幕翻译专家。请将以下字幕内容翻译成{target_lang},保持原意,语言自然、简洁。只返回翻译后的内容,不要有任何解释。\n\n内容:{text}"
14
+
15
+ try:
16
+ response = self.client.chat.completions.create(
17
+ model=self.model,
18
+ messages=[{"role": "user", "content": prompt}],
19
+ temperature=0.3
20
+ )
21
+ return response.choices[0].message.content.strip()
22
+ except Exception as e:
23
+ return f"翻译错误: {str(e)}"
24
+
25
+ def translate_segments(self, segments, target_lang="中文"):
26
+ """批量翻译字幕段 (为了演示,这里使用循环翻译,实际可优化为批量处理)"""
27
+ translated_segments = []
28
+ for seg in segments:
29
+ # 创建一个新的对象来保存翻译后的内容,保持原有的时间戳
30
+ new_seg = type('Segment', (), {
31
+ 'start': seg.start,
32
+ 'end': seg.end,
33
+ 'text': self.translate_text(seg.text, target_lang)
34
+ })
35
+ translated_segments.append(new_seg)
36
+ return translated_segments
utils.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import os
3
+ import json
4
+
5
+ class VideoUtils:
6
+ @staticmethod
7
+ def prepare_audio(input_path, output_path):
8
+ """从视频或音频中提取/转换音频为标准格式 (16kHz, mono, wav)"""
9
+ # 使用 wav 格式更通用,不需要 libmp3lame 编码器
10
+ cmd = [
11
+ 'ffmpeg', '-y', '-i', input_path,
12
+ '-vn', '-acodec', 'pcm_s16le', '-ar', '16000', '-ac', '1',
13
+ output_path
14
+ ]
15
+ try:
16
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
17
+ return result
18
+ except subprocess.CalledProcessError as e:
19
+ error_msg = f"FFmpeg 处理音频失败!\n错误代码: {e.returncode}\n错误输出: {e.stderr}"
20
+ print(error_msg) # 控制台打印
21
+ raise Exception(error_msg)
22
+ except FileNotFoundError:
23
+ raise Exception("找不到 FFmpeg 命令。请确保 FFmpeg 已安装并已添加到系统环境变量 PATH 中。")
24
+
25
+ @staticmethod
26
+ def embed_subtitles(video_path, srt_path, output_path):
27
+ """将字幕嵌入视频 (硬压)"""
28
+ # 注意:Windows下路径处理较复杂,ffmpeg 的 subtitles 滤镜需要特殊转义
29
+ abs_srt_path = os.path.abspath(srt_path).replace('\\', '/').replace(':', '\\:')
30
+ cmd = [
31
+ 'ffmpeg', '-y', '-i', video_path,
32
+ '-vf', f"subtitles='{abs_srt_path}'",
33
+ '-c:a', 'copy',
34
+ output_path
35
+ ]
36
+ try:
37
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
38
+ return result
39
+ except subprocess.CalledProcessError as e:
40
+ error_msg = f"FFmpeg 合成视频失败!\n错误代码: {e.returncode}\n错误输出: {e.stderr}"
41
+ print(error_msg)
42
+ raise Exception(error_msg)
43
+ except FileNotFoundError:
44
+ raise Exception("找不到 FFmpeg 命令。")
45
+
46
+ @staticmethod
47
+ def format_timestamp(seconds: float):
48
+ """将秒转换为 SRT 时间格式 00:00:00,000"""
49
+ td_hours = int(seconds // 3600)
50
+ td_mins = int((seconds % 3600) // 60)
51
+ td_secs = int(seconds % 60)
52
+ td_msecs = int((seconds - int(seconds)) * 1000)
53
+ return f"{td_hours:02}:{td_mins:02}:{td_secs:02},{td_msecs:03}"
54
+
55
+ @staticmethod
56
+ def write_srt(segments, output_path):
57
+ """生成 SRT 格式文件"""
58
+ with open(output_path, 'w', encoding='utf-8') as f:
59
+ for i, segment in enumerate(segments, 1):
60
+ start = VideoUtils.format_timestamp(segment.start)
61
+ end = VideoUtils.format_timestamp(segment.end)
62
+ f.write(f"{i}\n{start} --> {end}\n{segment.text.strip()}\n\n")
63
+
64
+ @staticmethod
65
+ def parse_srt(srt_content):
66
+ """简单的 SRT 解析器,返回 segments 列表"""
67
+ import re
68
+ segments = []
69
+ # 正则匹配 SRT 块
70
+ pattern = re.compile(r'(\d+)\n(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\n(.*?)(?=\n\n|\n$|$)', re.DOTALL)
71
+
72
+ def time_to_seconds(t_str):
73
+ h, m, s_ms = t_str.split(':')
74
+ s, ms = s_ms.split(',')
75
+ return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000.0
76
+
77
+ matches = pattern.findall(srt_content)
78
+ for m in matches:
79
+ idx, start_t, end_t, text = m
80
+ seg = type('Segment', (), {
81
+ 'start': time_to_seconds(start_t),
82
+ 'end': time_to_seconds(end_t),
83
+ 'text': text.strip()
84
+ })
85
+ segments.append(seg)
86
+ return segments