TNOT commited on
Commit
1c3c064
·
1 Parent(s): 094317f

feat: 重构云端 Web UI,实现上传处理下载流程

Browse files
Files changed (4) hide show
  1. app.py +3 -3
  2. docs/流程文档_AI用.md +24 -8
  3. main_local.py +13 -0
  4. src/gui_cloud.py +685 -0
app.py CHANGED
@@ -103,10 +103,10 @@ def main():
103
  """主入口"""
104
  setup_environment()
105
 
106
- # 导入并启动 GUI
107
- from src.gui import create_ui
108
 
109
- app = create_ui()
110
 
111
  # 云端配置
112
  app.launch(
 
103
  """主入口"""
104
  setup_environment()
105
 
106
+ # 导入并启动云端 GUI
107
+ from src.gui_cloud import create_cloud_ui
108
 
109
+ app = create_cloud_ui()
110
 
111
  # 云端配置
112
  app.launch(
docs/流程文档_AI用.md CHANGED
@@ -37,7 +37,8 @@
37
 
38
  ```
39
  项目根目录/
40
- ├── main.py # 程序入口
 
41
  ├── config.json # 全局配置文件
42
  ├── bank/ # 音源库目录
43
  │ └── [音源名称]/
@@ -220,7 +221,7 @@ MFA 支持两种运行模式:
220
 
221
  ## 使用流程
222
 
223
- ### 方式一: 本地 Web UI
224
 
225
  1. 运行 `python main.py` 启动 Web UI
226
  2. 浏览器自动打开 http://127.0.0.1:7860
@@ -239,9 +240,15 @@ MFA 支持两种运行模式:
239
  - 配置导出选项并执行
240
  - 点击下载按钮获取结果
241
 
242
- > 注: 旧版 CustomTkinter 桌面 GUI 已移至 `src/gui_old.py`
243
 
244
- ### 方式二: 云端部署 (HF Spaces / 魔塔社区)
 
 
 
 
 
 
245
 
246
  1. 使用 `app.py` 作为入口文件
247
  2. 云端环境自动安装 MFA 和下载模型
@@ -251,7 +258,7 @@ MFA 支持两种运行模式:
251
  - Hugging Face Spaces (Gradio SDK)
252
  - 魔塔社区 ModelScope (推荐,国内访问快)
253
 
254
- ### 方式: 命令行/脚本
255
 
256
  ```python
257
  from src.pipeline import PipelineConfig, VoiceBankPipeline
@@ -295,16 +302,25 @@ MFA 环境:
295
 
296
  ```
297
  项目根目录/
298
- ├── app.py # 云端入口 (自动初始化环境)
299
- ├── main.py # 本地入口
300
  ├── requirements.txt
301
  ├── src/
302
- │ ├── gui.py # 支持云端环境检测和下载功能
 
 
303
  │ ├── mfa_runner.py # 跨平台 MFA 调用
304
  │ └── ...
305
  └── ...
306
  ```
307
 
 
 
 
 
 
 
 
308
  ### 平台差异
309
 
310
  | 功能 | 本地 (Windows) | 云端 (Linux) |
 
37
 
38
  ```
39
  项目根目录/
40
+ ├── main.py # 程序入口 (Web UI)
41
+ ├── main_local.py # 本地桌面入口 (CustomTkinter GUI)
42
  ├── config.json # 全局配置文件
43
  ├── bank/ # 音源库目录
44
  │ └── [音源名称]/
 
221
 
222
  ## 使用流程
223
 
224
+ ### 方式一: 本地 Web UI (Gradio)
225
 
226
  1. 运行 `python main.py` 启动 Web UI
227
  2. 浏览器自动打开 http://127.0.0.1:7860
 
240
  - 配置导出选项并执行
241
  - 点击下载按钮获取结果
242
 
243
+ > 注: 旧版 CustomTkinter 桌面 GUI 已移至 `src/gui_old.py`,可通过 `python main_local.py` 启动
244
 
245
+ ### 方式二: 本地桌面 GUI (CustomTkinter)
246
+
247
+ 1. 运行 `python main_local.py` 启动桌面应用
248
+ 2. 使用原生窗口界面操作,功能与 Web UI 相同
249
+ 3. 适合不需要浏览器的本地独立运行场景
250
+
251
+ ### 方式三: 云端部署 (HF Spaces / 魔塔社区)
252
 
253
  1. 使用 `app.py` 作为入口文件
254
  2. 云端环境自动安装 MFA 和下载模型
 
258
  - Hugging Face Spaces (Gradio SDK)
259
  - 魔塔社区 ModelScope (推荐,国内访问快)
260
 
261
+ ### 方式: 命令行/脚本
262
 
263
  ```python
264
  from src.pipeline import PipelineConfig, VoiceBankPipeline
 
302
 
303
  ```
304
  项目根目录/
305
+ ├── app.py # 云端入口 (使用 gui_cloud.py)
306
+ ├── main.py # 本地入口 (使用 gui.py)
307
  ├── requirements.txt
308
  ├── src/
309
+ │ ├── gui.py # 本地 Web UI (完整功能)
310
+ │ ├── gui_cloud.py # 云端 Web UI (上传→处理→下载)
311
+ │ ├── gui_old.py # 旧版桌面 GUI (CustomTkinter)
312
  │ ├── mfa_runner.py # 跨平台 MFA 调用
313
  │ └── ...
314
  └── ...
315
  ```
316
 
317
+ ### 云端 GUI 特点 (gui_cloud.py)
318
+
319
+ - **制作音源**: 上传音频文件 → VAD切片 + Whisper转录 + MFA对齐 → 下载音源包
320
+ - **导出音源**: 上传音源包 → 选择导出插件 → 下载导出结果
321
+ - 使用临时工作空间,处理完成后自动清理
322
+ - 无需本地持久化存储
323
+
324
  ### 平台差异
325
 
326
  | 功能 | 本地 (Windows) | 云端 (Linux) |
main_local.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 人力V助手 (JinrikiHelper) - 本地版入口 (CustomTkinter GUI)
4
+ 作者:TNOT
5
+ 开源协议:MIT
6
+
7
+ 使用旧版 CustomTkinter 界面,适合本地独立运行
8
+ """
9
+
10
+ from src.gui_old import main
11
+
12
+ if __name__ == "__main__":
13
+ main()
src/gui_cloud.py ADDED
@@ -0,0 +1,685 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 人力V助手 (JinrikiHelper) 云端 Web UI
4
+ 基于 Gradio 6.2.0 构建
5
+ 专为云端部署优化:上传 → 处理 → 下载
6
+
7
+ 作者:TNOT
8
+ """
9
+
10
+ import gradio as gr
11
+ import logging
12
+ import os
13
+ import sys
14
+ import json
15
+ import tempfile
16
+ import zipfile
17
+ import shutil
18
+ import uuid
19
+ from pathlib import Path
20
+ from typing import Optional, List, Dict, Tuple
21
+
22
+ # 配置日志
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
26
+ handlers=[logging.StreamHandler(sys.stdout)]
27
+ )
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # 项目根目录
31
+ BASE_DIR = Path(__file__).parent.parent.absolute()
32
+
33
+
34
+ class CloudConfig:
35
+ """云端配置"""
36
+
37
+ # 临时工作目录
38
+ TEMP_BASE = tempfile.gettempdir()
39
+
40
+ # 模型目录(云端使用项目内目录)
41
+ MODELS_DIR = str(BASE_DIR / "models")
42
+ MFA_DIR = str(BASE_DIR / "models" / "mfa")
43
+
44
+ # 支持的音频格式
45
+ AUDIO_EXTENSIONS = ('.wav', '.mp3', '.flac', '.ogg', '.m4a')
46
+
47
+ # Whisper 模型选项
48
+ WHISPER_MODELS = {
49
+ "whisper-small": "openai/whisper-small",
50
+ "whisper-medium": "openai/whisper-medium"
51
+ }
52
+
53
+ # 语言选项
54
+ LANGUAGES = ["chinese", "japanese"]
55
+
56
+
57
+ def create_temp_workspace() -> str:
58
+ """创建临时工作空间"""
59
+ workspace_id = str(uuid.uuid4())[:8]
60
+ workspace = os.path.join(CloudConfig.TEMP_BASE, f"jinriki_{workspace_id}")
61
+ os.makedirs(workspace, exist_ok=True)
62
+ return workspace
63
+
64
+
65
+ def cleanup_workspace(workspace: str):
66
+ """清理工作空间"""
67
+ if workspace and os.path.exists(workspace):
68
+ try:
69
+ shutil.rmtree(workspace)
70
+ logger.info(f"已清理工作空间: {workspace}")
71
+ except Exception as e:
72
+ logger.warning(f"清理工作空间失败: {e}")
73
+
74
+
75
+ def create_zip(source_dir: str, zip_name: str) -> Optional[str]:
76
+ """打包目录为 zip"""
77
+ if not os.path.isdir(source_dir):
78
+ return None
79
+ try:
80
+ zip_path = os.path.join(CloudConfig.TEMP_BASE, f"{zip_name}.zip")
81
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
82
+ for root, dirs, files in os.walk(source_dir):
83
+ for file in files:
84
+ file_path = os.path.join(root, file)
85
+ arcname = os.path.relpath(file_path, source_dir)
86
+ zf.write(file_path, arcname)
87
+ return zip_path
88
+ except Exception as e:
89
+ logger.error(f"打包失败: {e}")
90
+ return None
91
+
92
+
93
+ def extract_zip(zip_path: str, target_dir: str) -> Tuple[bool, str]:
94
+ """解压 zip 文件"""
95
+ try:
96
+ with zipfile.ZipFile(zip_path, 'r') as zf:
97
+ zf.extractall(target_dir)
98
+ return True, "解压成功"
99
+ except Exception as e:
100
+ return False, f"解压失败: {e}"
101
+
102
+
103
+ def scan_mfa_models() -> Dict[str, List[str]]:
104
+ """扫描 MFA 模型"""
105
+ result = {"acoustic": [], "dictionary": []}
106
+ if not os.path.exists(CloudConfig.MFA_DIR):
107
+ return result
108
+ for f in os.listdir(CloudConfig.MFA_DIR):
109
+ if f.endswith('.zip'):
110
+ result["acoustic"].append(f)
111
+ elif f.endswith('.dict') or f.endswith('.txt'):
112
+ result["dictionary"].append(f)
113
+ return result
114
+
115
+
116
+ def check_mfa_available() -> bool:
117
+ """检查 MFA 是否可用"""
118
+ from src.mfa_runner import check_mfa_available as _check
119
+ return _check()
120
+
121
+
122
+ # ==================== 制作音源功能 ====================
123
+
124
+ def validate_audio_upload(files) -> Tuple[bool, str, List[str]]:
125
+ """
126
+ 验证上传的音频文件
127
+
128
+ 返回: (是否有效, 消息, 文件路径列表)
129
+ """
130
+ if not files:
131
+ return False, "请上传音频文件", []
132
+
133
+ valid_files = []
134
+ for f in files:
135
+ if hasattr(f, 'name'):
136
+ path = f.name
137
+ else:
138
+ path = str(f)
139
+
140
+ if path.lower().endswith(CloudConfig.AUDIO_EXTENSIONS):
141
+ valid_files.append(path)
142
+
143
+ if not valid_files:
144
+ return False, f"未找到有效音频文件,支持格式: {', '.join(CloudConfig.AUDIO_EXTENSIONS)}", []
145
+
146
+ return True, f"找到 {len(valid_files)} 个音频文件", valid_files
147
+
148
+
149
+ def process_make_voicebank(
150
+ audio_files,
151
+ source_name: str,
152
+ language: str,
153
+ whisper_model: str,
154
+ progress=gr.Progress()
155
+ ) -> Tuple[str, str, Optional[str]]:
156
+ """
157
+ 制作音源:上传音频 → VAD切片 → Whisper转录 → MFA对齐 → 打包下载
158
+
159
+ 返回: (状态, 日志, 下载文件路径)
160
+ """
161
+ from src.pipeline import PipelineConfig, VoiceBankPipeline
162
+
163
+ logs = []
164
+ def log(msg):
165
+ logs.append(msg)
166
+ logger.info(msg)
167
+
168
+ # 验证输入
169
+ if not source_name or not source_name.strip():
170
+ return "❌ 请输入音源名称", "", None
171
+
172
+ source_name = source_name.strip()
173
+
174
+ valid, msg, file_paths = validate_audio_upload(audio_files)
175
+ if not valid:
176
+ return f"❌ {msg}", "", None
177
+
178
+ log(f"📁 {msg}")
179
+
180
+ # 创建临时工作空间
181
+ workspace = create_temp_workspace()
182
+ log(f"🔧 创建工作空间: {workspace}")
183
+
184
+ try:
185
+ # 准备输入目录
186
+ input_dir = os.path.join(workspace, "input")
187
+ bank_dir = os.path.join(workspace, "bank")
188
+ os.makedirs(input_dir, exist_ok=True)
189
+ os.makedirs(bank_dir, exist_ok=True)
190
+
191
+ # 复制音频文件到输入目录
192
+ progress(0.05, desc="复制音频文件...")
193
+ for src_path in file_paths:
194
+ dst_path = os.path.join(input_dir, os.path.basename(src_path))
195
+ shutil.copy2(src_path, dst_path)
196
+ log(f"📋 已复制 {len(file_paths)} 个文件到工作目录")
197
+
198
+ # 获取 MFA 模型路径
199
+ mfa_models = scan_mfa_models()
200
+ dict_path = None
201
+ acoustic_path = None
202
+
203
+ if mfa_models["dictionary"]:
204
+ # 根据语言选择字典
205
+ for d in mfa_models["dictionary"]:
206
+ if language == "japanese" and "japanese" in d.lower():
207
+ dict_path = os.path.join(CloudConfig.MFA_DIR, d)
208
+ break
209
+ elif language == "chinese" and "mandarin" in d.lower():
210
+ dict_path = os.path.join(CloudConfig.MFA_DIR, d)
211
+ break
212
+ if not dict_path:
213
+ dict_path = os.path.join(CloudConfig.MFA_DIR, mfa_models["dictionary"][0])
214
+
215
+ if mfa_models["acoustic"]:
216
+ for a in mfa_models["acoustic"]:
217
+ if language == "japanese" and "japanese" in a.lower():
218
+ acoustic_path = os.path.join(CloudConfig.MFA_DIR, a)
219
+ break
220
+ elif language == "chinese" and "mandarin" in a.lower():
221
+ acoustic_path = os.path.join(CloudConfig.MFA_DIR, a)
222
+ break
223
+ if not acoustic_path:
224
+ acoustic_path = os.path.join(CloudConfig.MFA_DIR, mfa_models["acoustic"][0])
225
+
226
+ # 配置流水线
227
+ whisper_model_name = CloudConfig.WHISPER_MODELS.get(whisper_model, "openai/whisper-small")
228
+
229
+ config = PipelineConfig(
230
+ source_name=source_name,
231
+ input_path=input_dir,
232
+ output_base_dir=bank_dir,
233
+ models_dir=CloudConfig.MODELS_DIR,
234
+ whisper_model=whisper_model_name,
235
+ mfa_dict_path=dict_path,
236
+ mfa_model_path=acoustic_path,
237
+ language=language
238
+ )
239
+
240
+ pipeline = VoiceBankPipeline(config, log)
241
+
242
+ # 步骤0: VAD切片 + Whisper转录
243
+ progress(0.1, desc="VAD切片 + Whisper转录...")
244
+ log("\n" + "=" * 50)
245
+ log("【步骤1】VAD切片 + Whisper转录")
246
+ success, msg, slices = pipeline.step0_preprocess()
247
+ if not success:
248
+ return f"❌ 预处理失败: {msg}", "\n".join(logs), None
249
+ log(f"✅ {msg}")
250
+
251
+ # 步骤1: MFA对齐
252
+ progress(0.6, desc="MFA语音对齐...")
253
+ log("\n" + "=" * 50)
254
+ log("【步骤2】MFA语音对齐")
255
+
256
+ if check_mfa_available():
257
+ success, msg = pipeline.step1_mfa_align()
258
+ if not success:
259
+ log(f"⚠️ MFA对齐失败: {msg}")
260
+ log("继续导出(无TextGrid)...")
261
+ else:
262
+ log(f"✅ {msg}")
263
+ else:
264
+ log("⚠️ MFA不可用,跳过对齐步骤")
265
+
266
+ # 打包结果
267
+ progress(0.9, desc="打包结果...")
268
+ log("\n" + "=" * 50)
269
+ log("【打包结果】")
270
+
271
+ source_dir = os.path.join(bank_dir, source_name)
272
+ zip_name = f"{source_name}_音源数据"
273
+ zip_path = create_zip(source_dir, zip_name)
274
+
275
+ if zip_path:
276
+ log(f"📦 已打包: {os.path.basename(zip_path)}")
277
+ progress(1.0, desc="完成")
278
+ return "✅ 音源制作完成", "\n".join(logs), zip_path
279
+ else:
280
+ return "❌ 打包失败", "\n".join(logs), None
281
+
282
+ except Exception as e:
283
+ logger.error(f"制作音源失败: {e}", exc_info=True)
284
+ return f"❌ 处理失败: {e}", "\n".join(logs), None
285
+
286
+ finally:
287
+ # 清理工作空间(保留zip文件)
288
+ cleanup_workspace(workspace)
289
+
290
+
291
+ # ==================== 导出音源功能 ====================
292
+
293
+ def validate_voicebank_zip(zip_file) -> Tuple[bool, str, Optional[str]]:
294
+ """
295
+ 验证上传的音源压缩包
296
+
297
+ 返回: (是否有效, 消息, 音源名称)
298
+ """
299
+ if not zip_file:
300
+ return False, "请上传音源压缩包", None
301
+
302
+ zip_path = zip_file.name if hasattr(zip_file, 'name') else str(zip_file)
303
+
304
+ if not zip_path.lower().endswith('.zip'):
305
+ return False, "请上传 .zip 格式的压缩包", None
306
+
307
+ # 检查压缩包内容
308
+ try:
309
+ with zipfile.ZipFile(zip_path, 'r') as zf:
310
+ names = zf.namelist()
311
+
312
+ # 查找 slices 目录
313
+ has_slices = any('slices/' in n for n in names)
314
+ has_textgrid = any('textgrid/' in n for n in names)
315
+ has_wav = any(n.endswith('.wav') for n in names)
316
+ has_lab = any(n.endswith('.lab') for n in names)
317
+
318
+ if not has_wav:
319
+ return False, "压缩包中未找到 .wav 音频文件", None
320
+
321
+ # 尝试从 meta.json 获取音源名称
322
+ source_name = None
323
+ if 'meta.json' in names:
324
+ try:
325
+ with zf.open('meta.json') as mf:
326
+ meta = json.load(mf)
327
+ source_name = meta.get('source_name')
328
+ except:
329
+ pass
330
+
331
+ # 如果没有 meta.json,从目录结构推断
332
+ if not source_name:
333
+ # 从 zip 文件名推断
334
+ source_name = Path(zip_path).stem.replace('_音源数据', '')
335
+
336
+ info_parts = []
337
+ if has_slices:
338
+ wav_count = len([n for n in names if 'slices/' in n and n.endswith('.wav')])
339
+ info_parts.append(f"切片: {wav_count} 个")
340
+ if has_textgrid:
341
+ tg_count = len([n for n in names if 'textgrid/' in n and n.endswith('.TextGrid')])
342
+ info_parts.append(f"TextGrid: {tg_count} 个")
343
+
344
+ info = " | ".join(info_parts) if info_parts else "有效的音源包"
345
+
346
+ return True, f"✅ {info}", source_name
347
+
348
+ except zipfile.BadZipFile:
349
+ return False, "无效的 zip 文件", None
350
+ except Exception as e:
351
+ return False, f"验证失败: {e}", None
352
+
353
+
354
+ def process_export_voicebank(
355
+ zip_file,
356
+ plugin_name: str,
357
+ max_samples: int,
358
+ naming_rule: str,
359
+ first_naming_rule: str,
360
+ progress=gr.Progress()
361
+ ) -> Tuple[str, str, Optional[str]]:
362
+ """
363
+ 导出音源:上传音源包 → 解压 → 导出 → 打包下载
364
+
365
+ 返回: (状态, 日志, 下载文件路径)
366
+ """
367
+ logs = []
368
+ def log(msg):
369
+ logs.append(msg)
370
+ logger.info(msg)
371
+
372
+ # 验证输入
373
+ valid, msg, source_name = validate_voicebank_zip(zip_file)
374
+ if not valid:
375
+ return f"❌ {msg}", "", None
376
+
377
+ log(f"📦 {msg}")
378
+ log(f"📝 音源名称: {source_name}")
379
+
380
+ # 创建临时工作空间
381
+ workspace = create_temp_workspace()
382
+ log(f"🔧 创建工作空间")
383
+
384
+ try:
385
+ zip_path = zip_file.name if hasattr(zip_file, 'name') else str(zip_file)
386
+
387
+ # 解压音源包
388
+ progress(0.1, desc="解压音源包...")
389
+ bank_dir = os.path.join(workspace, "bank")
390
+ source_dir = os.path.join(bank_dir, source_name)
391
+ os.makedirs(source_dir, exist_ok=True)
392
+
393
+ success, msg = extract_zip(zip_path, source_dir)
394
+ if not success:
395
+ return f"❌ {msg}", "\n".join(logs), None
396
+ log(f"📂 已解压到工作目录")
397
+
398
+ # 检查目录结构,处理可能的嵌套
399
+ slices_dir = os.path.join(source_dir, "slices")
400
+ if not os.path.exists(slices_dir):
401
+ # 可能解压后有额外的一层目录
402
+ subdirs = [d for d in os.listdir(source_dir) if os.path.isdir(os.path.join(source_dir, d))]
403
+ if len(subdirs) == 1:
404
+ nested_dir = os.path.join(source_dir, subdirs[0])
405
+ if os.path.exists(os.path.join(nested_dir, "slices")):
406
+ # 移动内容到上层
407
+ for item in os.listdir(nested_dir):
408
+ shutil.move(os.path.join(nested_dir, item), source_dir)
409
+ os.rmdir(nested_dir)
410
+
411
+ # 执行导出
412
+ progress(0.3, desc="执行导出...")
413
+ log("\n" + "=" * 50)
414
+ log(f"【{plugin_name}】")
415
+
416
+ from src.export_plugins import load_plugins
417
+ plugins = load_plugins()
418
+
419
+ if plugin_name not in plugins:
420
+ return f"❌ 未找到插件: {plugin_name}", "\n".join(logs), None
421
+
422
+ plugin = plugins[plugin_name]
423
+ plugin.set_progress_callback(log)
424
+
425
+ options = {
426
+ "max_samples": max_samples,
427
+ "naming_rule": naming_rule,
428
+ "first_naming_rule": first_naming_rule,
429
+ "clean_temp": True
430
+ }
431
+
432
+ success, msg = plugin.export(source_name, bank_dir, options)
433
+
434
+ if not success:
435
+ return f"❌ 导出失败: {msg}", "\n".join(logs), None
436
+
437
+ log(f"✅ {msg}")
438
+
439
+ # 打包导出结果
440
+ progress(0.9, desc="打包结果...")
441
+ log("\n" + "=" * 50)
442
+ log("【打包结果】")
443
+
444
+ export_dir = os.path.join(workspace, "export", source_name, "simple_export")
445
+
446
+ # 如果导出目录不存在,尝试其他位置
447
+ if not os.path.exists(export_dir):
448
+ alt_export = os.path.join(os.path.dirname(bank_dir), "export", source_name, "simple_export")
449
+ if os.path.exists(alt_export):
450
+ export_dir = alt_export
451
+
452
+ if not os.path.exists(export_dir):
453
+ return "❌ 未找到导出结果", "\n".join(logs), None
454
+
455
+ zip_name = f"{source_name}_导出结果"
456
+ result_zip = create_zip(export_dir, zip_name)
457
+
458
+ if result_zip:
459
+ # 统计导出文件数
460
+ file_count = len([f for f in os.listdir(export_dir) if f.endswith('.wav')])
461
+ log(f"📦 已打包: {file_count} 个音频文件")
462
+ progress(1.0, desc="完成")
463
+ return "✅ 导出完成", "\n".join(logs), result_zip
464
+ else:
465
+ return "❌ 打包失败", "\n".join(logs), None
466
+
467
+ except Exception as e:
468
+ logger.error(f"导出失败: {e}", exc_info=True)
469
+ return f"❌ 处理失败: {e}", "\n".join(logs), None
470
+
471
+ finally:
472
+ cleanup_workspace(workspace)
473
+
474
+
475
+ # ==================== 构建界面 ====================
476
+
477
+ def create_cloud_ui():
478
+ """创建云端 Gradio 界面"""
479
+
480
+ # 检查 MFA 状态
481
+ mfa_available = check_mfa_available()
482
+ mfa_status = "✅ MFA 已就绪" if mfa_available else "⚠️ MFA 不可用(将跳过对齐步骤)"
483
+
484
+ # 加载导出插件
485
+ from src.export_plugins import load_plugins
486
+ plugins = load_plugins()
487
+ plugin_names = list(plugins.keys()) if plugins else ["简单单字导出"]
488
+
489
+ with gr.Blocks(
490
+ title="人力V助手 (JinrikiHelper)",
491
+ theme=gr.themes.Soft()
492
+ ) as app:
493
+
494
+ gr.Markdown("# 🎤 人力V助手 (JinrikiHelper)")
495
+ gr.Markdown("语音数据集处理工具 - 自动化制作语音音源库")
496
+ gr.Markdown("> ☁️ 云端版:上传音频 → 自动处理 → 下载结果")
497
+
498
+ with gr.Tabs():
499
+ # ==================== 制作音源页 ====================
500
+ with gr.Tab("🎵 制作音源"):
501
+ gr.Markdown("### 上传音频文件")
502
+ gr.Markdown("支持格式: WAV, MP3, FLAC, OGG, M4A")
503
+
504
+ audio_upload = gr.File(
505
+ label="上传音频文件",
506
+ file_count="multiple",
507
+ file_types=["audio"]
508
+ )
509
+
510
+ with gr.Row():
511
+ make_source_name = gr.Textbox(
512
+ label="音源名称",
513
+ placeholder="my_voice",
514
+ info="用于标识输出的音源包"
515
+ )
516
+ make_language = gr.Dropdown(
517
+ choices=CloudConfig.LANGUAGES,
518
+ value="chinese",
519
+ label="语言"
520
+ )
521
+
522
+ with gr.Row():
523
+ make_whisper = gr.Dropdown(
524
+ choices=list(CloudConfig.WHISPER_MODELS.keys()),
525
+ value="whisper-small",
526
+ label="Whisper 模型",
527
+ info="small 更快,medium 更准"
528
+ )
529
+ make_mfa_status = gr.Textbox(
530
+ label="MFA 状态",
531
+ value=mfa_status,
532
+ interactive=False
533
+ )
534
+
535
+ make_btn = gr.Button("🚀 开始制作", variant="primary", size="lg")
536
+
537
+ make_status = gr.Textbox(label="状态", interactive=False)
538
+ make_log = gr.Textbox(label="处理日志", lines=12, interactive=False)
539
+
540
+ gr.Markdown("### 下载结果")
541
+ make_download = gr.File(label="音源包下载", interactive=False)
542
+
543
+ gr.Markdown("""
544
+ > 💡 处理流程:
545
+ > 1. VAD 语音活动检测,自动切分音频
546
+ > 2. Whisper 语音识别,生成文本标注
547
+ > 3. MFA 强制对齐,生成音素级时间标注
548
+ > 4. 打包为 zip 供下载
549
+ """)
550
+
551
+ make_btn.click(
552
+ fn=process_make_voicebank,
553
+ inputs=[audio_upload, make_source_name, make_language, make_whisper],
554
+ outputs=[make_status, make_log, make_download]
555
+ )
556
+
557
+ # ==================== 导出音源页 ====================
558
+ with gr.Tab("📤 导出音源"):
559
+ gr.Markdown("### 上传音源包")
560
+ gr.Markdown("上传之前制作的音源压缩包(包含 slices 和 textgrid 目录)")
561
+
562
+ export_upload = gr.File(
563
+ label="上传音源包 (.zip)",
564
+ file_types=[".zip"]
565
+ )
566
+
567
+ export_info = gr.Textbox(
568
+ label="音���信息",
569
+ interactive=False,
570
+ placeholder="上传后显示音源信息"
571
+ )
572
+
573
+ # 上传后自动验证
574
+ def on_upload(file):
575
+ if file:
576
+ valid, msg, name = validate_voicebank_zip(file)
577
+ return msg
578
+ return ""
579
+
580
+ export_upload.change(
581
+ fn=on_upload,
582
+ inputs=[export_upload],
583
+ outputs=[export_info]
584
+ )
585
+
586
+ gr.Markdown("---")
587
+ gr.Markdown("### 导出设置")
588
+
589
+ export_plugin = gr.Dropdown(
590
+ choices=plugin_names,
591
+ value=plugin_names[0] if plugin_names else None,
592
+ label="导出插件"
593
+ )
594
+
595
+ with gr.Row():
596
+ export_max_samples = gr.Number(
597
+ label="每个拼音最大样本数",
598
+ value=10,
599
+ minimum=1,
600
+ maximum=1000
601
+ )
602
+
603
+ with gr.Row():
604
+ export_naming = gr.Textbox(
605
+ label="命名规则",
606
+ value="%p%%n%",
607
+ info="%p%=拼音, %n%=序号"
608
+ )
609
+ export_first_naming = gr.Textbox(
610
+ label="首个样本命名",
611
+ value="%p%",
612
+ info="第0个样本的特殊规则"
613
+ )
614
+
615
+ export_btn = gr.Button("📤 开始导出", variant="primary", size="lg")
616
+
617
+ export_status = gr.Textbox(label="状态", interactive=False)
618
+ export_log = gr.Textbox(label="处理日志", lines=10, interactive=False)
619
+
620
+ gr.Markdown("### 下载结果")
621
+ export_download = gr.File(label="导出结果下载", interactive=False)
622
+
623
+ gr.Markdown("""
624
+ > 💡 导出说明:
625
+ > - 从 TextGrid 提取每个汉字/音节的时间边界
626
+ > - 按拼音/罗马音分类,选取最佳样本
627
+ > - 导出为适配其他软件的音源格式
628
+ """)
629
+
630
+ export_btn.click(
631
+ fn=process_export_voicebank,
632
+ inputs=[
633
+ export_upload, export_plugin,
634
+ export_max_samples, export_naming, export_first_naming
635
+ ],
636
+ outputs=[export_status, export_log, export_download]
637
+ )
638
+
639
+ # ==================== 关于页 ====================
640
+ with gr.Tab("ℹ️ 关于"):
641
+ gr.Markdown("""
642
+ ## 人力V助手 (JinrikiHelper)
643
+
644
+ 语音数据集处理工具,用于自动化制作语音音源库。
645
+
646
+ ### 功能特点
647
+
648
+ - **VAD 切片**: 使用 Silero VAD 自动检测语音片段
649
+ - **语音识别**: 使用 Whisper 模型转录文本
650
+ - **强制对齐**: 使用 MFA 生成音素级时间标注
651
+ - **智能导出**: 按拼音分类,选取最佳样本
652
+
653
+ ### 支持语言
654
+
655
+ - 中文(普通话)
656
+ - 日语
657
+
658
+ ### 使用流程
659
+
660
+ 1. **制作音源**: 上传原始音频 → 自动处理 → 下载音源包
661
+ 2. **导出音源**: 上传音源包 → 选择导出格式 → 下载导出结果
662
+
663
+ ---
664
+
665
+ **作者**: TNOT | **协议**: MIT
666
+
667
+ 本工具集成 Montreal Forced Aligner (MIT License)
668
+ """)
669
+
670
+ return app
671
+
672
+
673
+ def main():
674
+ """云端入口"""
675
+ app = create_cloud_ui()
676
+ app.launch(
677
+ server_name="0.0.0.0",
678
+ server_port=7860,
679
+ share=False,
680
+ show_error=True
681
+ )
682
+
683
+
684
+ if __name__ == "__main__":
685
+ main()