TNOT commited on
Commit
13d5900
·
1 Parent(s): 891d653

feat: 添加魔搭创空间部署配置

Browse files
app.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 人力V助手 (JinrikiHelper) - 云端部署入口
4
+ 适用于 Hugging Face Spaces / 魔塔社区
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import subprocess
10
+ import platform
11
+ import logging
12
+
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
+ )
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def setup_environment():
21
+ """初始化云端环境"""
22
+
23
+ # 检测运行环境
24
+ is_cloud = any([
25
+ os.environ.get("SPACE_ID"), # Hugging Face Spaces
26
+ os.environ.get("MODELSCOPE_SPACE"), # 魔塔社区
27
+ os.environ.get("GRADIO_SERVER_NAME"), # 通用 Gradio 云端
28
+ ])
29
+
30
+ if is_cloud:
31
+ logger.info("检测到云端环境,正在初始化...")
32
+
33
+ # 设置临时目录
34
+ os.environ.setdefault("TMPDIR", "/tmp")
35
+
36
+ # 安装 MFA (如果未安装)
37
+ if platform.system() != "Windows":
38
+ setup_mfa_linux()
39
+ else:
40
+ logger.info("本地环境运行")
41
+
42
+
43
+ def setup_mfa_linux():
44
+ """Linux 环境下安装和配置 MFA"""
45
+ import shutil
46
+
47
+ # 检查 mfa 是否已安装
48
+ if shutil.which("mfa"):
49
+ logger.info("MFA 已安装")
50
+ return
51
+
52
+ logger.info("正在安装 MFA...")
53
+
54
+ try:
55
+ # 尝试 pip 安装
56
+ subprocess.run(
57
+ [sys.executable, "-m", "pip", "install",
58
+ "montreal-forced-aligner", "--quiet"],
59
+ check=True,
60
+ capture_output=True
61
+ )
62
+ logger.info("MFA 安装完成")
63
+
64
+ # 下载中文模型
65
+ download_mfa_models()
66
+
67
+ except subprocess.CalledProcessError as e:
68
+ logger.warning(f"MFA pip 安装失败: {e}")
69
+ logger.info("尝试使用 conda 安装...")
70
+ try:
71
+ subprocess.run(
72
+ ["conda", "install", "-c", "conda-forge",
73
+ "montreal-forced-aligner", "-y"],
74
+ check=True,
75
+ capture_output=True
76
+ )
77
+ download_mfa_models()
78
+ except Exception as e2:
79
+ logger.error(f"MFA 安装失败: {e2}")
80
+
81
+
82
+ def download_mfa_models():
83
+ """下载 MFA 预训练模型"""
84
+ models = [
85
+ ("acoustic", "mandarin_mfa"),
86
+ ("dictionary", "mandarin_china_mfa"),
87
+ ]
88
+
89
+ for model_type, model_name in models:
90
+ try:
91
+ logger.info(f"下载 MFA 模型: {model_type}/{model_name}")
92
+ subprocess.run(
93
+ ["mfa", "model", "download", model_type, model_name],
94
+ check=True,
95
+ capture_output=True,
96
+ timeout=300 # 5分钟超时
97
+ )
98
+ except Exception as e:
99
+ logger.warning(f"模型下载失败 {model_name}: {e}")
100
+
101
+
102
+ 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(
113
+ server_name="0.0.0.0",
114
+ server_port=7860,
115
+ share=False,
116
+ show_error=True,
117
+ )
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
deploy_schema.json ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://modelscope.cn/api/v1/studios/deploy_schema.json",
3
+ "type": "object",
4
+ "properties": {
5
+ "sdk_type": {
6
+ "type": "string",
7
+ "enum": ["gradio", "streamlit", "static", "docker"],
8
+ "description": "The application's frontend SDK or deployment mode. \n- **gradio**: Use this when the project uses the gradio framework. Requires an `app.py` in the repository root. Uses a base image and runs directly (no build step required). Requires specifying `sdk_version` and `base_image`. \n- **streamlit**: Use this when the project uses the streamlit framework. Requires an `app.py` in the repository root. Runs directly from a base image (no build step required). Requires specifying `base_image`. \n- **static**: Serves pre-built static files (e.g., HTML, CSS, JS) via Nginx, using the system's default environment. Cannot support projects that require a build step (e.g., Node.js projects). Requires an `index.html` in the repository root. **No build or compilation steps are performed**—files must already be built and committed to the repository. If your frontend requires building (e.g., via `npm run build`), use the `docker` mode instead. Requires specifying `base_image`. \n- **docker**: Builds and runs a custom container from a `Dockerfile` in the repository root. The resulting image must expose the service on `0.0.0.0:7860`. Does not use `sdk_version` or `base_image`; instead, `port` must be exactly `7860`. For projects where you’re unsure which other SDK types to use, `docker` is always recommended."
9
+ },
10
+ "sdk_version": {
11
+ "type": "string",
12
+ "enum": [
13
+ "3.29.0", "3.39.0", "3.47.1", "4.8.0", "4.14.0", "4.19.1", "4.31.3", "4.38.1",
14
+ "4.44.0", "5.3.0", "5.4.0", "5.12.0", "5.20.1", "5.29.0", "5.34.1", "5.42.0",
15
+ "5.49.1", "6.2.0"
16
+ ],
17
+ "default": "6.2.0",
18
+ "description": "Version of Gradio SDK. Required only when sdk_type is 'gradio'. Recommended to use the latest version."
19
+ },
20
+ "resource_configuration": {
21
+ "type": "string",
22
+ "enum": [
23
+ "platform/2v-cpu-16g-mem",
24
+ "xgpu/8v-cpu-32g-mem-16g",
25
+ "xgpu/8v-cpu-64g-mem-48g"
26
+ ],
27
+ "description": "Cloud instance type with predefined resources. The 'platform/2v-cpu-16g-mem' instance is available to all users at no cost. The 'xgpu' instances require eligibility and approval; see https://modelscope.cn/docs/studios/xGPU for details. Note: Resources from personal cloud accounts cannot be configured via this JSON schema. "
28
+ },
29
+ "base_image": {
30
+ "type": "string",
31
+ "enum": [
32
+ "ubuntu20.04-py37-torch1.11.0-tf1.15.5-modelscope1.6.1",
33
+ "ubuntu20.04-py38-torch1.11.0-tf1.15.5-modelscope1.6.1",
34
+ "ubuntu20.04-py38-torch2.0.1-tf1.15.5-modelscope1.8.1",
35
+ "ubuntu20.04-py38-torch2.0.1-tf2.13.0-modelscope1.9.5",
36
+ "ubuntu22.04-py310-torch2.1.0-tf2.14.0-modelscope1.10.0",
37
+ "ubuntu22.04-py310-torch2.1.2-tf2.14.0-modelscope1.13.1",
38
+ "ubuntu22.04-py310-torch2.1.2-tf2.14.0-modelscope1.13.3",
39
+ "ubuntu22.04-py310-torch2.1.2-tf2.14.0-modelscope1.14.0",
40
+ "ubuntu22.04-py310-torch2.3.0-modelscope1.15.0",
41
+ "ubuntu22.04-py310-torch2.3.0-modelscope1.16.1",
42
+ "ubuntu22.04-py310-torch2.3.0-modelscope1.17.1",
43
+ "ubuntu22.04-py310-torch2.3.0-modelscope1.18.0",
44
+ "ubuntu22.04-py310-torch2.3.0-modelscope1.19.2",
45
+ "ubuntu22.04-py310-torch2.3.1-modelscope1.20.0",
46
+ "ubuntu22.04-py310-torch2.3.1-modelscope1.21.0",
47
+ "ubuntu20.04-py310-paddle3.0.0-modelscope1.24.0",
48
+ "ubuntu22.04-py311-torch2.3.1-modelscope1.24.0",
49
+ "ubuntu22.04-py311-torch2.3.1-modelscope1.25.0",
50
+ "ubuntu22.04-cuda12.4.0-py311-torch2.6.0-modelscope1.25.0-LLM",
51
+ "ubuntu22.04-cuda12.4.0-py311-torch2.8.0-modelscope1.31.0-LLM",
52
+ "ubuntu22.04-py311-torch2.3.1-modelscope1.31.0"
53
+ ],
54
+ "default": "ubuntu22.04-py311-torch2.3.1-modelscope1.31.0",
55
+ "description": "Prebuilt runtime environment (Docker base image). Required for 'gradio', 'streamlit', and 'static' deployments. Not used for 'docker' type."
56
+ },
57
+ "environment_variables": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "object",
61
+ "properties": {
62
+ "name": { "type": "string" },
63
+ "value": { "type": "string" }
64
+ },
65
+ "required": ["name", "value"]
66
+ },
67
+ "description": "List of environment variables to inject at runtime. Please ensure the names and values of the environment variables required for runtime are provided directly here."
68
+ },
69
+ "port": {
70
+ "type": "integer",
71
+ "description": "The port the application listens on. Required only when sdk_type is 'docker', and must be exactly 7860."
72
+ }
73
+ },
74
+ "required": ["sdk_type", "resource_configuration"],
75
+ "allOf": [
76
+ {
77
+ "if": {
78
+ "properties": { "sdk_type": { "const": "gradio" } },
79
+ "required": ["sdk_type"]
80
+ },
81
+ "then": {
82
+ "required": ["sdk_version", "base_image"]
83
+ }
84
+ },
85
+ {
86
+ "if": {
87
+ "properties": { "sdk_type": { "enum": ["streamlit", "static"] } },
88
+ "required": ["sdk_type"]
89
+ },
90
+ "then": {
91
+ "required": ["base_image"]
92
+ }
93
+ },
94
+ {
95
+ "if": {
96
+ "properties": { "sdk_type": { "const": "docker" } },
97
+ "required": ["sdk_type"]
98
+ },
99
+ "then": {
100
+ "not": {
101
+ "required": ["sdk_version"]
102
+ },
103
+ "required": ["port"],
104
+ "properties": {
105
+ "port": {
106
+ "const": 7860
107
+ }
108
+ }
109
+ }
110
+ }
111
+ ]
112
+ }
docs/公开部署方案.md CHANGED
@@ -1,5 +1,38 @@
1
  # 人力V助手 在线部署方案
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ## 一、平台对比与选择
4
 
5
  | 平台 | 免费配额 | GPU | 存储 | 国内访问 | 推荐度 |
 
1
  # 人力V助手 在线部署方案
2
 
3
+ ## 快速部署指南 (魔搭创空间)
4
+
5
+ ### 部署文件清单
6
+
7
+ | 文件 | 说明 |
8
+ |------|------|
9
+ | `ms_deploy.json` | 魔搭创空间部署配置 |
10
+ | `requirements_cloud.txt` | 云端依赖文件 |
11
+ | `app.py` | 云端入口 (已就绪) |
12
+
13
+ ### 部署步骤
14
+
15
+ 1. **注册魔搭账号**: https://modelscope.cn
16
+ 2. **创建创空间**:
17
+ - 进入「创空间」→「创建创空间」
18
+ - 选择「Gradio」类型
19
+ - 填写名称和描述
20
+ 3. **上传代码**:
21
+ - 方式一: 直接上传 zip 包
22
+ - 方式二: 关联 GitHub/Gitee 仓库
23
+ 4. **配置部署**:
24
+ - 上传 `ms_deploy.json` 或在界面配置
25
+ - 将 `requirements_cloud.txt` 重命名为 `requirements.txt` (或在部署时指定)
26
+ 5. **启动应用**: 点击「部署」等待构建完成
27
+
28
+ ### 注意事项
29
+
30
+ - 首次启动需要下载模型,可能需要 5-10 分钟
31
+ - 云端数据不持久,处理完成后请及时下载结果
32
+ - 免费配额为 2vCPU/16GB 内存,适合小规模处理
33
+
34
+ ---
35
+
36
  ## 一、平台对比与选择
37
 
38
  | 平台 | 免费配额 | GPU | 存储 | 国内访问 | 推荐度 |
docs/流程文档_AI用.md CHANGED
@@ -164,9 +164,11 @@
164
 
165
  | 模块 | 文件 | 功能 |
166
  |------|------|------|
167
- | MFA 运行器 | `mfa_runner.py` | 外挂模式调用 MFA 引擎 |
168
 
169
- MFA 采用 Sidecar Pattern,通过 subprocess 调用独立的 Python 环境 (`tools/mfa_engine`),避免依赖冲突。
 
 
170
 
171
  ### 4. 导出插件系统
172
 
@@ -218,7 +220,7 @@ MFA 采用 Sidecar Pattern,通过 subprocess 调用独立的 Python 环境 (`t
218
 
219
  ## 使用流程
220
 
221
- ### 方式一: Web UI 界面
222
 
223
  1. 运行 `python main.py` 启动 Web UI
224
  2. 浏览器自动打开 http://127.0.0.1:7860
@@ -235,10 +237,21 @@ MFA 采用 Sidecar Pattern,通过 subprocess 调用独立的 Python 环境 (`t
235
  - 选择已制作的音源
236
  - 选择导出插件
237
  - 配置导出选项并执行
 
238
 
239
  > 注: 旧版 CustomTkinter 桌面 GUI 已移至 `src/gui_old.py`
240
 
241
- ### 方式二: 命令行/脚本
 
 
 
 
 
 
 
 
 
 
242
 
243
  ```python
244
  from src.pipeline import PipelineConfig, VoiceBankPipeline
@@ -273,5 +286,47 @@ success, msg = pipeline.run_make_pipeline()
273
  > 注: 旧版桌面 GUI 使用 `customtkinter`,代码保留在 `src/gui_old.py`
274
 
275
  MFA 环境:
276
- - 独立打包在 `tools/mfa_engine/`
277
- - 包含 Python 3.11 + montreal-forced-aligner
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  | 模块 | 文件 | 功能 |
166
  |------|------|------|
167
+ | MFA 运行器 | `mfa_runner.py` | 跨平台调用 MFA 引擎 |
168
 
169
+ MFA 支持两种运行模式:
170
+ - **Windows**: Sidecar Pattern,通过 subprocess 调用独立的 Python 环境 (`tools/mfa_engine`)
171
+ - **Linux**: 直接调用系统安装的 `mfa` 命令 (pip install montreal-forced-aligner)
172
 
173
  ### 4. 导出插件系统
174
 
 
220
 
221
  ## 使用流程
222
 
223
+ ### 方式一: 本地 Web UI
224
 
225
  1. 运行 `python main.py` 启动 Web UI
226
  2. 浏览器自动打开 http://127.0.0.1:7860
 
237
  - 选择已制作的音源
238
  - 选择导出插件
239
  - 配置导出选项并执行
240
+ - 点击下载按钮获取结果
241
 
242
  > 注: 旧版 CustomTkinter 桌面 GUI 已移至 `src/gui_old.py`
243
 
244
+ ### 方式二: 云端部署 (HF Spaces / 魔塔社区)
245
+
246
+ 1. 使用 `app.py` 作为入口文件
247
+ 2. 云端环境自动安装 MFA 和下载模型
248
+ 3. 处理完成后通过下载按钮获取结果 (云端数据不持久)
249
+
250
+ 支持的云平台:
251
+ - Hugging Face Spaces (Gradio SDK)
252
+ - 魔塔社区 ModelScope (推荐,国内访问快)
253
+
254
+ ### 方式三: 命令行/脚本
255
 
256
  ```python
257
  from src.pipeline import PipelineConfig, VoiceBankPipeline
 
286
  > 注: 旧版桌面 GUI 使用 `customtkinter`,代码保留在 `src/gui_old.py`
287
 
288
  MFA 环境:
289
+ - **Windows**: 独立打包在 `tools/mfa_engine/`,包含 Python 3.11 + montreal-forced-aligner
290
+ - **Linux**: 通过 pip 安装 `montreal-forced-aligner`
291
+
292
+ ## 云端部署说明
293
+
294
+ ### 目录结构 (云端)
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) |
311
+ |------|----------------|--------------|
312
+ | MFA 调用 | tools/mfa_engine 外挂 | 系统 mfa 命令 |
313
+ | 数据存储 | 本地持久化 | 临时目录,需下载 |
314
+ | GPU 加速 | 本地显卡 | 取决于平台配置 |
315
+ | 模型缓存 | models/ 目录 | 首次运行自动下载 |
316
+
317
+ ### 魔搭创空间部署配置
318
+
319
+ 部署配置文件 `ms_deploy.json`:
320
+ ```json
321
+ {
322
+ "sdk_type": "gradio",
323
+ "sdk_version": "6.2.0",
324
+ "resource_configuration": "platform/2v-cpu-16g-mem",
325
+ "base_image": "ubuntu22.04-py311-torch2.3.1-modelscope1.31.0"
326
+ }
327
+ ```
328
+
329
+ 云端依赖文件 `requirements_cloud.txt`:
330
+ - 移除 Windows 专用依赖 (customtkinter)
331
+ - 添加 `montreal-forced-aligner` (Linux pip 安装)
332
+ - 保持 gradio 版本与 sdk_version 一致
ms_deploy.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "sdk_type": "gradio",
3
+ "sdk_version": "6.2.0",
4
+ "resource_configuration": "platform/2v-cpu-16g-mem",
5
+ "base_image": "ubuntu22.04-py311-torch2.3.1-modelscope1.31.0",
6
+ "environment_variables": [
7
+ {
8
+ "name": "MODELSCOPE_SPACE",
9
+ "value": "true"
10
+ },
11
+ {
12
+ "name": "GRADIO_SERVER_NAME",
13
+ "value": "0.0.0.0"
14
+ },
15
+ {
16
+ "name": "GRADIO_SERVER_PORT",
17
+ "value": "7860"
18
+ }
19
+ ]
20
+ }
requirements_cloud.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 魔搭创空间云端依赖
2
+ # 基于 requirements.in,移除本地专用依赖,添加云端必需依赖
3
+
4
+ # GUI框架 (与 ms_deploy.json 中 sdk_version 保持一致)
5
+ gradio==6.2.0
6
+
7
+ # Whisper 语音识别
8
+ transformers>=4.25.0
9
+ torch
10
+ torchaudio
11
+ accelerate
12
+
13
+ # Silero VAD 语音活动检测
14
+ silero-vad>=5.1
15
+ onnxruntime
16
+
17
+ # 音频处理
18
+ textgrid
19
+ audiofile
20
+ soundfile
21
+
22
+ # 文本处理
23
+ pypinyin
24
+ pykakasi
25
+
26
+ # 工具
27
+ tqdm
28
+
29
+ # MFA 强制对齐 (Linux 环境通过 pip 安装)
30
+ montreal-forced-aligner
src/gui.py CHANGED
@@ -2,6 +2,7 @@
2
  """
3
  人力V助手 (JinrikiHelper) Web UI
4
  基于 Gradio 6.2.0 构建
 
5
  作者:TNOT
6
  """
7
 
@@ -11,9 +12,19 @@ import logging
11
  import os
12
  import sys
13
  import json
 
 
 
 
14
  from pathlib import Path
15
  from typing import Optional, List, Dict, Callable
16
 
 
 
 
 
 
 
17
  # 配置日志
18
  logging.basicConfig(
19
  level=logging.INFO,
@@ -382,14 +393,42 @@ def run_full_pipeline(source_name: str, input_path: str, output_dir: str,
382
 
383
  # ==================== 导出音源功能 ====================
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  def run_export(voice_bank: str, plugin_name: str, options: dict, progress=gr.Progress()):
386
  """执行导出"""
387
  if not voice_bank or voice_bank.startswith("("):
388
- return "❌ 请选择有效的音源", ""
389
 
390
  plugins = load_export_plugins()
391
  if plugin_name not in plugins:
392
- return f"❌ 未找到插件: {plugin_name}", ""
393
 
394
  plugin = plugins[plugin_name]
395
  bank_dir = config_manager.get("bank_dir")
@@ -406,14 +445,38 @@ def run_export(voice_bank: str, plugin_name: str, options: dict, progress=gr.Pro
406
  success, msg = plugin.export(voice_bank, bank_dir, options)
407
  progress(1, desc="完成")
408
 
 
409
  if success:
410
  log_callback(f"✅ {msg}")
 
 
 
 
 
 
 
 
 
411
  else:
412
  log_callback(f"❌ {msg}")
413
  log_callback("=" * 50)
414
 
415
  status = "✅ 导出完成" if success else f"❌ {msg}"
416
- return status, "\n".join(logs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
 
419
  # ==================== 构建界面 ====================
@@ -427,11 +490,25 @@ def create_ui():
427
  dict_files = mfa_models["dictionary"] if mfa_models["dictionary"] else ["(未找到字典文件)"]
428
  acoustic_files = mfa_models["acoustic"] if mfa_models["acoustic"] else ["(未找到声学模型)"]
429
  voice_banks = scan_voice_banks()
430
- mfa_status = "✅ MFA 环境已就绪" if check_mfa_available() else "❌ MFA 环境不可用,请检查 tools/mfa_engine"
 
 
 
 
 
 
 
 
 
 
 
 
431
 
432
  with gr.Blocks(title="人力V助手 (JinrikiHelper)") as app:
433
  gr.Markdown("# 🎤 人力V助手 (JinrikiHelper)")
434
  gr.Markdown("语音数据集处理工具 - 自动化制作语音音源库")
 
 
435
 
436
  with gr.Tabs():
437
  # ==================== 模型下载页 ====================
@@ -661,10 +738,27 @@ def create_ui():
661
  export_status = gr.Textbox(label="状态", interactive=False)
662
  export_log = gr.Textbox(label="日志输出", lines=8, interactive=False)
663
 
 
 
 
 
 
 
 
 
 
 
 
664
  export_btn.click(
665
  fn=run_export,
666
  inputs=[voice_bank_select, plugin_select, plugin_options],
667
- outputs=[export_status, export_log]
 
 
 
 
 
 
668
  )
669
 
670
  # ==================== 设置页 ====================
 
2
  """
3
  人力V助手 (JinrikiHelper) Web UI
4
  基于 Gradio 6.2.0 构建
5
+ 支持本地运行和云端部署 (HF Spaces / 魔塔社区)
6
  作者:TNOT
7
  """
8
 
 
12
  import os
13
  import sys
14
  import json
15
+ import platform
16
+ import tempfile
17
+ import zipfile
18
+ import shutil
19
  from pathlib import Path
20
  from typing import Optional, List, Dict, Callable
21
 
22
+ # 环境检测
23
+ IS_CLOUD = any([
24
+ os.environ.get("SPACE_ID"), # Hugging Face Spaces
25
+ os.environ.get("MODELSCOPE_SPACE"), # 魔塔社区
26
+ ])
27
+
28
  # 配置日志
29
  logging.basicConfig(
30
  level=logging.INFO,
 
393
 
394
  # ==================== 导出音源功能 ====================
395
 
396
+ def create_download_zip(source_dir: str, zip_name: str) -> Optional[str]:
397
+ """
398
+ 打包目录为 zip 文件供下载
399
+
400
+ 参数:
401
+ source_dir: 要打包的目录
402
+ zip_name: zip 文件名 (不含扩展名)
403
+
404
+ 返回:
405
+ zip 文件路径,失败返回 None
406
+ """
407
+ if not os.path.isdir(source_dir):
408
+ return None
409
+
410
+ try:
411
+ zip_path = os.path.join(tempfile.gettempdir(), f"{zip_name}.zip")
412
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
413
+ for root, dirs, files in os.walk(source_dir):
414
+ for file in files:
415
+ file_path = os.path.join(root, file)
416
+ arcname = os.path.relpath(file_path, source_dir)
417
+ zf.write(file_path, arcname)
418
+ return zip_path
419
+ except Exception as e:
420
+ logger.error(f"打包失败: {e}")
421
+ return None
422
+
423
+
424
  def run_export(voice_bank: str, plugin_name: str, options: dict, progress=gr.Progress()):
425
  """执行导出"""
426
  if not voice_bank or voice_bank.startswith("("):
427
+ return "❌ 请选择有效的音源", "", None
428
 
429
  plugins = load_export_plugins()
430
  if plugin_name not in plugins:
431
+ return f"❌ 未找到插件: {plugin_name}", "", None
432
 
433
  plugin = plugins[plugin_name]
434
  bank_dir = config_manager.get("bank_dir")
 
445
  success, msg = plugin.export(voice_bank, bank_dir, options)
446
  progress(1, desc="完成")
447
 
448
+ download_file = None
449
  if success:
450
  log_callback(f"✅ {msg}")
451
+ # 打包导出结果供下载
452
+ export_dir = os.path.join(
453
+ os.path.dirname(bank_dir), "export", voice_bank, plugin_name.replace(" ", "_")
454
+ )
455
+ if os.path.isdir(export_dir):
456
+ zip_name = f"{voice_bank}_{plugin_name.replace(' ', '_')}"
457
+ download_file = create_download_zip(export_dir, zip_name)
458
+ if download_file:
459
+ log_callback(f"📦 已打包: {os.path.basename(download_file)}")
460
  else:
461
  log_callback(f"❌ {msg}")
462
  log_callback("=" * 50)
463
 
464
  status = "✅ 导出完成" if success else f"❌ {msg}"
465
+ return status, "\n".join(logs), download_file
466
+
467
+
468
+ def download_voice_bank(voice_bank: str) -> Optional[str]:
469
+ """打包音源数据供下载"""
470
+ if not voice_bank or voice_bank.startswith("("):
471
+ return None
472
+
473
+ bank_dir = config_manager.get("bank_dir")
474
+ source_dir = os.path.join(bank_dir, voice_bank)
475
+
476
+ if not os.path.isdir(source_dir):
477
+ return None
478
+
479
+ return create_download_zip(source_dir, f"{voice_bank}_音源数据")
480
 
481
 
482
  # ==================== 构建界面 ====================
 
490
  dict_files = mfa_models["dictionary"] if mfa_models["dictionary"] else ["(未找到字典文件)"]
491
  acoustic_files = mfa_models["acoustic"] if mfa_models["acoustic"] else ["(未找到声学模型)"]
492
  voice_banks = scan_voice_banks()
493
+
494
+ # MFA 状态检测 (区分平台)
495
+ if check_mfa_available():
496
+ mfa_status = "✅ MFA 环境已就绪"
497
+ elif platform.system() == "Windows":
498
+ mfa_status = "❌ MFA 环境不可用,请检查 tools/mfa_engine"
499
+ else:
500
+ mfa_status = "❌ MFA 未安装,请运行: pip install montreal-forced-aligner"
501
+
502
+ # 云端环境提示
503
+ env_notice = ""
504
+ if IS_CLOUD:
505
+ env_notice = "> ☁️ 当前为云端环境,处理完成后请及时下载结果"
506
 
507
  with gr.Blocks(title="人力V助手 (JinrikiHelper)") as app:
508
  gr.Markdown("# 🎤 人力V助手 (JinrikiHelper)")
509
  gr.Markdown("语音数据集处理工具 - 自动化制作语音音源库")
510
+ if env_notice:
511
+ gr.Markdown(env_notice)
512
 
513
  with gr.Tabs():
514
  # ==================== 模型下载页 ====================
 
738
  export_status = gr.Textbox(label="状态", interactive=False)
739
  export_log = gr.Textbox(label="日志输出", lines=8, interactive=False)
740
 
741
+ # 下载区域
742
+ gr.Markdown("---")
743
+ gr.Markdown("### 下载结果")
744
+ with gr.Row():
745
+ export_download = gr.File(label="导出结果下载", interactive=False)
746
+ bank_download_btn = gr.Button("📥 下载音源数据", variant="secondary")
747
+ bank_download = gr.File(label="音源数据下载", interactive=False)
748
+
749
+ if IS_CLOUD:
750
+ gr.Markdown("> 💡 云端环境数据不会持久保存,请及时下载处理结果")
751
+
752
  export_btn.click(
753
  fn=run_export,
754
  inputs=[voice_bank_select, plugin_select, plugin_options],
755
+ outputs=[export_status, export_log, export_download]
756
+ )
757
+
758
+ bank_download_btn.click(
759
+ fn=download_voice_bank,
760
+ inputs=[voice_bank_select],
761
+ outputs=[bank_download]
762
  )
763
 
764
  # ==================== 设置页 ====================
src/mfa_runner.py CHANGED
@@ -1,10 +1,12 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- MFA 外挂调用模块
4
- 采用 Sidecar Pattern,通过 subprocess 调用独立的 MFA 环境
5
  """
6
 
7
  import os
 
 
8
  import subprocess
9
  import logging
10
  from pathlib import Path
@@ -22,30 +24,75 @@ DEFAULT_DICT_PATH = BASE_DIR / "models" / "mandarin.dict"
22
  DEFAULT_MODEL_PATH = BASE_DIR / "models" / "mandarin.zip"
23
  DEFAULT_TEMP_DIR = BASE_DIR / "mfa_temp"
24
 
 
 
 
25
 
26
  def check_mfa_available() -> bool:
27
- """检查 MFA 外挂环境是否可用"""
28
- if not MFA_ENGINE_DIR.exists():
29
- logger.warning(f"MFA 引擎目录不存在: {MFA_ENGINE_DIR}")
30
- return False
31
- if not MFA_PYTHON.exists():
32
- logger.warning(f"MFA Python 不存在: {MFA_PYTHON}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  return False
34
- return True
 
 
 
 
 
 
 
 
 
 
 
35
 
36
 
37
  def _build_mfa_env() -> dict:
38
  """构造 MFA 专用环境变量"""
39
  env = os.environ.copy()
40
 
41
- # 必须把 Library\bin 加入 PATH,否则 Kaldi DLL 找不到
42
- mfa_paths = [
43
- str(MFA_ENGINE_DIR),
44
- str(MFA_ENGINE_DIR / "Library" / "bin"),
45
- str(MFA_ENGINE_DIR / "Scripts"),
46
- str(MFA_ENGINE_DIR / "bin"),
47
- ]
48
- env["PATH"] = ";".join(mfa_paths) + ";" + env.get("PATH", "")
 
 
 
 
 
49
 
50
  return env
51
 
@@ -83,7 +130,8 @@ def run_mfa_alignment(
83
 
84
  # 检查环境
85
  if not check_mfa_available():
86
- return False, "MFA 外挂环境不可用,请检查 tools/mfa_engine 目录"
 
87
 
88
  # 设置默认路径
89
  dict_path = dict_path or str(DEFAULT_DICT_PATH)
@@ -103,24 +151,26 @@ def run_mfa_alignment(
103
  os.makedirs(temp_dir, exist_ok=True)
104
 
105
  # 构造命令
106
- cmd = [
107
- str(MFA_PYTHON),
108
- "-m", "montreal_forced_aligner",
109
  "align",
110
  str(corpus_dir),
111
  str(dict_path),
112
  str(model_path),
113
  str(output_dir),
114
  "--temp_directory", str(temp_dir),
115
- "--use_mp", "false", # 禁用多进程,避免Windows问题
116
  ]
117
 
 
 
 
 
118
  if clean:
119
  cmd.append("--clean")
120
  if single_speaker:
121
  cmd.append("--single_speaker")
122
 
123
  log(f"正在启动 MFA 对齐引擎...")
 
124
  log(f"输入目录: {corpus_dir}")
125
  log(f"输出目录: {output_dir}")
126
 
@@ -145,7 +195,7 @@ def run_mfa_alignment(
145
  return False, error_msg
146
 
147
  except FileNotFoundError as e:
148
- msg = f"找不到 MFA Python: {e}"
149
  log(msg)
150
  return False, msg
151
  except Exception as e:
@@ -176,13 +226,11 @@ def run_mfa_validate(
176
  progress_callback(msg)
177
 
178
  if not check_mfa_available():
179
- return False, "MFA 外挂环境不可用"
180
 
181
  dict_path = dict_path or str(DEFAULT_DICT_PATH)
182
 
183
- cmd = [
184
- str(MFA_PYTHON),
185
- "-m", "montreal_forced_aligner",
186
  "validate",
187
  str(corpus_dir),
188
  str(dict_path),
@@ -207,3 +255,83 @@ def run_mfa_validate(
207
 
208
  except Exception as e:
209
  return False, str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ MFA 调用模块
4
+ 支持 Windows (外挂模式) Linux (系统安装) 双平台
5
  """
6
 
7
  import os
8
+ import platform
9
+ import shutil
10
  import subprocess
11
  import logging
12
  from pathlib import Path
 
24
  DEFAULT_MODEL_PATH = BASE_DIR / "models" / "mandarin.zip"
25
  DEFAULT_TEMP_DIR = BASE_DIR / "mfa_temp"
26
 
27
+ # 平台检测
28
+ IS_WINDOWS = platform.system() == "Windows"
29
+
30
 
31
  def check_mfa_available() -> bool:
32
+ """
33
+ 检查 MFA 是否可用
34
+ Windows: 检查外挂 Python 环境
35
+ Linux: 检查系统 mfa 命令
36
+ """
37
+ if IS_WINDOWS:
38
+ if not MFA_ENGINE_DIR.exists():
39
+ logger.warning(f"MFA 引擎目录不存在: {MFA_ENGINE_DIR}")
40
+ return False
41
+ if not MFA_PYTHON.exists():
42
+ logger.warning(f"MFA Python 不存在: {MFA_PYTHON}")
43
+ return False
44
+ return True
45
+ else:
46
+ # Linux/macOS: 检查系统命令
47
+ mfa_path = shutil.which("mfa")
48
+ if mfa_path:
49
+ logger.info(f"找到系统 MFA: {mfa_path}")
50
+ return True
51
+ # 尝试检查 conda 环境中的 mfa
52
+ try:
53
+ result = subprocess.run(
54
+ ["mfa", "version"],
55
+ capture_output=True,
56
+ text=True,
57
+ timeout=10
58
+ )
59
+ if result.returncode == 0:
60
+ logger.info(f"MFA 版本: {result.stdout.strip()}")
61
+ return True
62
+ except Exception as e:
63
+ logger.warning(f"MFA 检查失败: {e}")
64
  return False
65
+
66
+
67
+ def _get_mfa_command() -> list:
68
+ """
69
+ 获取 MFA 命令前缀
70
+ Windows: 使用外挂 Python 调用
71
+ Linux: 直接使用 mfa 命令
72
+ """
73
+ if IS_WINDOWS:
74
+ return [str(MFA_PYTHON), "-m", "montreal_forced_aligner"]
75
+ else:
76
+ return ["mfa"]
77
 
78
 
79
  def _build_mfa_env() -> dict:
80
  """构造 MFA 专用环境变量"""
81
  env = os.environ.copy()
82
 
83
+ if IS_WINDOWS:
84
+ # Windows: 必须把 Library\bin 加入 PATH,否则 Kaldi DLL 找不到
85
+ mfa_paths = [
86
+ str(MFA_ENGINE_DIR),
87
+ str(MFA_ENGINE_DIR / "Library" / "bin"),
88
+ str(MFA_ENGINE_DIR / "Scripts"),
89
+ str(MFA_ENGINE_DIR / "bin"),
90
+ ]
91
+ env["PATH"] = ";".join(mfa_paths) + ";" + env.get("PATH", "")
92
+ else:
93
+ # Linux: 确保 conda 环境变量正确
94
+ # 通常不需要额外设置,但保留扩展点
95
+ pass
96
 
97
  return env
98
 
 
130
 
131
  # 检查环境
132
  if not check_mfa_available():
133
+ platform_hint = "tools/mfa_engine 目录" if IS_WINDOWS else "pip install montreal-forced-aligner"
134
+ return False, f"MFA 环境不可用,请检查 {platform_hint}"
135
 
136
  # 设置默认路径
137
  dict_path = dict_path or str(DEFAULT_DICT_PATH)
 
151
  os.makedirs(temp_dir, exist_ok=True)
152
 
153
  # 构造命令
154
+ cmd = _get_mfa_command() + [
 
 
155
  "align",
156
  str(corpus_dir),
157
  str(dict_path),
158
  str(model_path),
159
  str(output_dir),
160
  "--temp_directory", str(temp_dir),
 
161
  ]
162
 
163
+ # Windows 禁用多进程避免问题,Linux 可以启用
164
+ if IS_WINDOWS:
165
+ cmd.extend(["--use_mp", "false"])
166
+
167
  if clean:
168
  cmd.append("--clean")
169
  if single_speaker:
170
  cmd.append("--single_speaker")
171
 
172
  log(f"正在启动 MFA 对齐引擎...")
173
+ log(f"运行平台: {'Windows (外挂模式)' if IS_WINDOWS else 'Linux (系统安装)'}")
174
  log(f"输入目录: {corpus_dir}")
175
  log(f"输出目录: {output_dir}")
176
 
 
195
  return False, error_msg
196
 
197
  except FileNotFoundError as e:
198
+ msg = f"找不到 MFA 命令: {e}"
199
  log(msg)
200
  return False, msg
201
  except Exception as e:
 
226
  progress_callback(msg)
227
 
228
  if not check_mfa_available():
229
+ return False, "MFA 环境不可用"
230
 
231
  dict_path = dict_path or str(DEFAULT_DICT_PATH)
232
 
233
+ cmd = _get_mfa_command() + [
 
 
234
  "validate",
235
  str(corpus_dir),
236
  str(dict_path),
 
255
 
256
  except Exception as e:
257
  return False, str(e)
258
+
259
+
260
+ def install_mfa_model(
261
+ model_type: str,
262
+ model_name: str,
263
+ progress_callback: Optional[Callable[[str], None]] = None
264
+ ) -> tuple[bool, str]:
265
+ """
266
+ 下载 MFA 预训练模型 (仅 Linux 支持)
267
+
268
+ 参数:
269
+ model_type: 模型类型 ("acoustic" 或 "dictionary")
270
+ model_name: 模型名称 (如 "mandarin_mfa", "mandarin_china_mfa")
271
+ progress_callback: 进度回调函数
272
+
273
+ 返回:
274
+ (成功标志, 输出信息)
275
+ """
276
+ def log(msg: str):
277
+ logger.info(msg)
278
+ if progress_callback:
279
+ progress_callback(msg)
280
+
281
+ if IS_WINDOWS:
282
+ return False, "Windows 平台请手动下载模型文件"
283
+
284
+ if not check_mfa_available():
285
+ return False, "MFA 环境不可用"
286
+
287
+ cmd = _get_mfa_command() + [
288
+ "model", "download", model_type, model_name
289
+ ]
290
+
291
+ log(f"正在下载 MFA 模型: {model_type}/{model_name}")
292
+
293
+ try:
294
+ result = subprocess.run(
295
+ cmd,
296
+ capture_output=True,
297
+ text=True,
298
+ encoding='utf-8',
299
+ errors='replace'
300
+ )
301
+
302
+ if result.returncode == 0:
303
+ log(f"模型下载完成: {model_name}")
304
+ return True, result.stdout
305
+ else:
306
+ error_msg = result.stderr or result.stdout or "未知错误"
307
+ log(f"模型下载失败: {error_msg}")
308
+ return False, error_msg
309
+
310
+ except Exception as e:
311
+ return False, str(e)
312
+
313
+
314
+ def get_mfa_model_path(model_type: str, model_name: str) -> Optional[str]:
315
+ """
316
+ 获取 MFA 模型路径
317
+ Linux: 返回 MFA 内置模型名称 (mfa 会自动查找)
318
+ Windows: 返回本地文件路径
319
+
320
+ 参数:
321
+ model_type: 模型类型 ("acoustic" 或 "dictionary")
322
+ model_name: 模型名称
323
+
324
+ 返回:
325
+ 模型路径或名称,不存在返回 None
326
+ """
327
+ if IS_WINDOWS:
328
+ # Windows: 使用本地文件
329
+ mfa_dir = BASE_DIR / "models" / "mfa"
330
+ if model_type == "acoustic":
331
+ path = mfa_dir / f"{model_name}.zip"
332
+ else:
333
+ path = mfa_dir / f"{model_name}.dict"
334
+ return str(path) if path.exists() else None
335
+ else:
336
+ # Linux: 直接返回模型名称,mfa 会从缓存中查找
337
+ return model_name