mason369 commited on
Commit
e103b22
·
verified ·
1 Parent(s): c0c8ed0

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. ui/__init__.py +7 -0
  2. ui/app.py +2038 -0
ui/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ UI 模块
4
+ """
5
+ from .app import create_ui, launch
6
+
7
+ __all__ = ["create_ui", "launch"]
ui/app.py ADDED
@@ -0,0 +1,2038 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Gradio 界面 - RVC AI 翻唱
4
+ """
5
+ import os
6
+ import json
7
+ import re
8
+ import tempfile
9
+ import gradio as gr
10
+ from pathlib import Path
11
+ from typing import Optional, Tuple, Dict
12
+
13
+ from lib.logger import log
14
+
15
+ # 项目根目录
16
+ ROOT_DIR = Path(__file__).parent.parent
17
+
18
+ # 加载语言包
19
+ def load_i18n(lang: str = "zh_CN") -> dict:
20
+ """加载语言包"""
21
+ i18n_path = ROOT_DIR / "i18n" / f"{lang}.json"
22
+ if i18n_path.exists():
23
+ with open(i18n_path, "r", encoding="utf-8") as f:
24
+ return json.load(f)
25
+ return {}
26
+
27
+ # 加载配置
28
+ def load_config() -> dict:
29
+ """加载配置"""
30
+ config_path = ROOT_DIR / "configs" / "config.json"
31
+ if config_path.exists():
32
+ with open(config_path, "r", encoding="utf-8") as f:
33
+ return json.load(f)
34
+ return {}
35
+
36
+
37
+ def normalize_config(config: dict) -> dict:
38
+ """Normalize legacy path keys to top-level entries."""
39
+ if not config:
40
+ return {}
41
+
42
+ paths = config.get("paths", {})
43
+ if "hubert_path" not in config and "hubert" in paths:
44
+ config["hubert_path"] = paths["hubert"]
45
+ if "rmvpe_path" not in config and "rmvpe" in paths:
46
+ config["rmvpe_path"] = paths["rmvpe"]
47
+ if "weights_dir" not in config and "weights" in paths:
48
+ config["weights_dir"] = paths["weights"]
49
+ if "output_dir" not in config and "outputs" in paths:
50
+ config["output_dir"] = paths["outputs"]
51
+ elif config.get("output_dir") == "output" and "outputs" in paths:
52
+ config["output_dir"] = paths["outputs"]
53
+ if "temp_dir" not in config and "temp" in paths:
54
+ config["temp_dir"] = paths["temp"]
55
+
56
+ return config
57
+
58
+ # 全局变量
59
+ i18n = load_i18n()
60
+ config = normalize_config(load_config())
61
+ pipeline = None
62
+
63
+
64
+ def t(key: str, section: str = None) -> str:
65
+ """获取翻译文本"""
66
+ if section:
67
+ return i18n.get(section, {}).get(key, key)
68
+ return i18n.get(key, key)
69
+
70
+
71
+ def _to_int(value, fallback: int) -> int:
72
+ try:
73
+ return int(value)
74
+ except (TypeError, ValueError):
75
+ return fallback
76
+
77
+
78
+ def _to_float(value, fallback: float) -> float:
79
+ try:
80
+ return float(value)
81
+ except (TypeError, ValueError):
82
+ return fallback
83
+
84
+
85
+ def get_cover_mix_defaults() -> Dict[str, int]:
86
+ """获取翻唱混音默认值"""
87
+ cover_cfg = config.get("cover", {})
88
+ return {
89
+ "vocals_volume": _to_int(cover_cfg.get("default_vocals_volume", 100), 100),
90
+ "accompaniment_volume": _to_int(cover_cfg.get("default_accompaniment_volume", 100), 100),
91
+ "reverb": _to_int(cover_cfg.get("default_reverb", 10), 10),
92
+ }
93
+
94
+
95
+ def get_cover_mix_presets() -> Tuple[Dict[str, Dict[str, int]], str]:
96
+ """获取混音预设与默认预设名称"""
97
+ defaults = get_cover_mix_defaults()
98
+
99
+ presets = {
100
+ t("mix_preset_universal", "cover"): defaults.copy(),
101
+ t("mix_preset_vocal", "cover"): {
102
+ "vocals_volume": min(200, defaults["vocals_volume"] + 15),
103
+ "accompaniment_volume": max(0, defaults["accompaniment_volume"] - 10),
104
+ "reverb": max(0, defaults["reverb"] - 5),
105
+ },
106
+ t("mix_preset_accompaniment", "cover"): {
107
+ "vocals_volume": max(0, defaults["vocals_volume"] - 10),
108
+ "accompaniment_volume": min(200, defaults["accompaniment_volume"] + 15),
109
+ "reverb": max(0, defaults["reverb"] - 5),
110
+ },
111
+ t("mix_preset_live", "cover"): {
112
+ "vocals_volume": defaults["vocals_volume"],
113
+ "accompaniment_volume": defaults["accompaniment_volume"],
114
+ "reverb": min(100, defaults["reverb"] + 10),
115
+ },
116
+ }
117
+
118
+ default_name = t("mix_preset_universal", "cover")
119
+ return presets, default_name
120
+
121
+
122
+ def apply_cover_mix_preset(preset_name: str) -> Tuple[int, int, int]:
123
+ """根据预设名称返回混音参数"""
124
+ presets, default_name = get_cover_mix_presets()
125
+ preset = presets.get(preset_name) or presets[default_name]
126
+ return preset["vocals_volume"], preset["accompaniment_volume"], preset["reverb"]
127
+
128
+
129
+
130
+ def get_vc_preprocess_option_maps() -> Tuple[Dict[str, str], Dict[str, str]]:
131
+ """Build VC preprocess dropdown option maps."""
132
+ label_to_value = {
133
+ t("vc_preprocess_auto", "cover"): "auto",
134
+ t("vc_preprocess_direct", "cover"): "direct",
135
+ t("vc_preprocess_uvr_deecho", "cover"): "uvr_deecho",
136
+ t("vc_preprocess_legacy", "cover"): "legacy",
137
+ }
138
+ value_to_label = {value: label for label, value in label_to_value.items()}
139
+ return label_to_value, value_to_label
140
+
141
+
142
+ def get_source_constraint_option_maps() -> Tuple[Dict[str, str], Dict[str, str]]:
143
+ """Build source constraint dropdown option maps."""
144
+ label_to_value = {
145
+ t("source_constraint_auto", "cover"): "auto",
146
+ t("source_constraint_off", "cover"): "off",
147
+ t("source_constraint_on", "cover"): "on",
148
+ }
149
+ value_to_label = {value: label for label, value in label_to_value.items()}
150
+ return label_to_value, value_to_label
151
+
152
+
153
+ def get_vc_pipeline_mode_option_maps() -> Tuple[Dict[str, str], Dict[str, str]]:
154
+ """Build VC pipeline mode dropdown option maps."""
155
+ label_to_value = {
156
+ t("vc_pipeline_mode_current", "cover"): "current",
157
+ t("vc_pipeline_mode_official", "cover"): "official",
158
+ }
159
+ value_to_label = {value: label for label, value in label_to_value.items()}
160
+ return label_to_value, value_to_label
161
+
162
+
163
+ def update_singing_repair_visibility(vc_pipeline_mode: str):
164
+ """Only show singing repair option for official mode."""
165
+ pipeline_label_to_value, _ = get_vc_pipeline_mode_option_maps()
166
+ normalized = pipeline_label_to_value.get(
167
+ str(vc_pipeline_mode),
168
+ str(vc_pipeline_mode or "").strip().lower(),
169
+ )
170
+ return gr.update(visible=(normalized == "official"))
171
+
172
+
173
+ def init_pipeline():
174
+ """初始化推理管道"""
175
+ global pipeline
176
+
177
+ if pipeline is not None:
178
+ return pipeline
179
+
180
+ from infer.pipeline import VoiceConversionPipeline
181
+
182
+ device = config.get("device", "cuda")
183
+ pipeline = VoiceConversionPipeline(device=device)
184
+ pipeline.hubert_layer = config.get("hubert_layer", 12)
185
+
186
+ # 加载 HuBERT
187
+ hubert_path = ROOT_DIR / config.get("hubert_path", "assets/hubert/hubert_base.pt")
188
+ if hubert_path.exists():
189
+ pipeline.load_hubert(str(hubert_path))
190
+
191
+ # 加载 F0 提取器
192
+ rmvpe_path = ROOT_DIR / config.get("rmvpe_path", "assets/rmvpe/rmvpe.pt")
193
+ if rmvpe_path.exists():
194
+ pipeline.load_f0_extractor("rmvpe", str(rmvpe_path))
195
+
196
+ return pipeline
197
+
198
+
199
+ def download_base_models() -> str:
200
+ """下载基础模型"""
201
+ from tools.download_models import download_required_models
202
+
203
+ try:
204
+ success = download_required_models()
205
+ if success:
206
+ return t("download_complete", "messages")
207
+ else:
208
+ return "下载过程中出现错误,请检查网络连接"
209
+ except Exception as e:
210
+ return f"{t('download_failed', 'messages')}: {str(e)}"
211
+
212
+
213
+ # ===== 翻唱功能相关函数 =====
214
+
215
+ def get_downloaded_character_list() -> list:
216
+ """获取已下载的角色列表"""
217
+ from tools.character_models import list_downloaded_characters
218
+ return list_downloaded_characters()
219
+
220
+
221
+ def get_downloaded_character_series() -> list:
222
+ """获取已下载角色的系列列表"""
223
+ characters = get_downloaded_character_list()
224
+ series = sorted({c.get("series", "未知") for c in characters})
225
+ return ["全部"] + series
226
+
227
+
228
+ def get_available_character_list() -> list:
229
+ """获取可下载的角色列表"""
230
+ from tools.character_models import list_available_characters
231
+ return list_available_characters()
232
+
233
+
234
+ def get_available_character_series() -> list:
235
+ """获取可用系列列表"""
236
+ from tools.character_models import list_available_series
237
+ return list_available_series()
238
+
239
+
240
+ def format_character_label(char_info: dict) -> str:
241
+ """格式化角色展示名称:【语言】角色名(中/英/日) · 出处 · 内部名"""
242
+ display = char_info.get("display") or char_info.get("description") or char_info.get("name", "")
243
+ source = char_info.get("source", "未知")
244
+ name = char_info.get("name", "")
245
+ lang_tag = get_character_language_tag(char_info)
246
+ return f"【{lang_tag}】{display}(出自:{source})[{name}]"
247
+
248
+
249
+ def get_character_language_tag(char_info: dict) -> str:
250
+ """推断语言类型,用于下拉前缀标签"""
251
+ lang = char_info.get("lang")
252
+ if lang:
253
+ return lang
254
+ text = " ".join(
255
+ str(char_info.get(k, "")) for k in ("display", "description", "name")
256
+ ).lower()
257
+ if "韩" in text or "kr" in text or "korean" in text:
258
+ return "韩文"
259
+ if "日" in text or "jp" in text or "japanese" in text:
260
+ return "日文"
261
+ if "中" in text or "cn" in text or "chinese" in text:
262
+ return "中文"
263
+ if "en" in text or "english" in text:
264
+ return "英文"
265
+
266
+ source = char_info.get("source", "")
267
+ if source.startswith("Love Live!") or "ホロライブ" in source or "偶像大师" in source or "赛马娘" in source:
268
+ return "日文"
269
+ if "原神" in source or "崩坏" in source or "明日方舟" in source or "碧蓝航线" in source:
270
+ return "中文"
271
+ if "VOCALOID" in source or "Project SEKAI" in source:
272
+ return "日文"
273
+ if "Hololive" in source:
274
+ return "日文"
275
+ if "蔚蓝档案" in source or "绝区零" in source:
276
+ return "日文"
277
+ return "中文"
278
+
279
+
280
+ def get_downloaded_character_choices(series: str = "全部", keyword: str = "") -> list:
281
+ """获取已下载角色的下拉选项"""
282
+ chars = get_downloaded_character_list()
283
+ if series and series != "全部":
284
+ chars = [c for c in chars if c.get("series") == series]
285
+ if keyword:
286
+ kw = keyword.strip().lower()
287
+ if kw:
288
+ chars = [
289
+ c for c in chars
290
+ if kw in c.get("name", "").lower()
291
+ or kw in c.get("display", "").lower()
292
+ or kw in c.get("source", "").lower()
293
+ ]
294
+ return [(format_character_label(c), c["name"]) for c in chars]
295
+
296
+
297
+ def resolve_character_name(selection: str) -> str:
298
+ """将下拉显示文本解析为实际角色名"""
299
+ if not selection:
300
+ return selection
301
+ from tools.character_models import list_downloaded_characters
302
+ for c in list_downloaded_characters():
303
+ if selection == c.get("name") or selection == format_character_label(c):
304
+ return c.get("name")
305
+ if " · " in selection:
306
+ return selection.split(" · ")[-1].strip()
307
+ parts = selection.strip().split()
308
+ return parts[-1] if parts else selection
309
+
310
+
311
+ def get_available_character_choices(series: str = "全部", keyword: str = "") -> list:
312
+ """获取可下载角色的下拉选项"""
313
+ chars = get_available_character_list()
314
+ if series and series != "全部":
315
+ chars = [c for c in chars if c.get("series") == series]
316
+ if keyword:
317
+ kw = keyword.strip().lower()
318
+ if kw:
319
+ chars = [
320
+ c for c in chars
321
+ if kw in c.get("name", "").lower()
322
+ or kw in c.get("display", "").lower()
323
+ or kw in c.get("source", "").lower()
324
+ ]
325
+ return [(format_character_label(c), c["name"]) for c in chars]
326
+
327
+
328
+ def _refresh_downloaded_updates(series: str, keyword: str) -> Tuple[Dict, Dict]:
329
+ series_choices = get_downloaded_character_series()
330
+ if series not in series_choices:
331
+ series = "全部"
332
+ return (
333
+ gr.update(choices=series_choices, value=series),
334
+ gr.update(choices=get_downloaded_character_choices(series, keyword))
335
+ )
336
+
337
+
338
+ def download_character(name: str, selected_series: str = "全部", keyword: str = "") -> Tuple[str, Dict, Dict]:
339
+ """下载角色模型"""
340
+ from tools.character_models import download_character_model
341
+
342
+ if not name:
343
+ series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
344
+ return "请选择要下载的角色", choices_update, series_update
345
+
346
+ try:
347
+ success = download_character_model(name)
348
+ series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
349
+ if success:
350
+ return (
351
+ f"✅ {name} 模型下载完成",
352
+ choices_update,
353
+ series_update
354
+ )
355
+ else:
356
+ return (
357
+ f"❌ {name} 模型下载失败",
358
+ choices_update,
359
+ series_update
360
+ )
361
+ except Exception as e:
362
+ series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
363
+ return (
364
+ f"❌ 下载失败: {str(e)}",
365
+ choices_update,
366
+ series_update
367
+ )
368
+
369
+
370
+ def download_all_characters(series: str = "全部", selected_series: str = "全部", keyword: str = "") -> Tuple[str, Dict, Dict]:
371
+ """批量下载角色模型"""
372
+ from tools.character_models import download_all_character_models
373
+
374
+ try:
375
+ result = download_all_character_models(series=series)
376
+ ok = result.get("success", [])
377
+ failed = result.get("failed", [])
378
+ status = f"✅ 完成: 成功 {len(ok)} 个"
379
+ if failed:
380
+ status += f",失败 {len(failed)} 个: {', '.join(failed)}"
381
+ series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
382
+ return status, choices_update, series_update
383
+ except Exception as e:
384
+ series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
385
+ return f"❌ 批量下载失败: {str(e)}", choices_update, series_update
386
+
387
+
388
+ def update_download_choices(series: str, keyword: str) -> Dict:
389
+ """更新下载下拉列表"""
390
+ return gr.update(choices=get_available_character_choices(series, keyword))
391
+
392
+
393
+ def update_downloaded_choices(series: str, keyword: str) -> Dict:
394
+ """更新已下载角色下拉列表"""
395
+ return gr.update(choices=get_downloaded_character_choices(series, keyword))
396
+
397
+
398
+ def refresh_downloaded_controls(series: str, keyword: str) -> Tuple[Dict, Dict]:
399
+ """刷新已下载角色的筛选和列表"""
400
+ return _refresh_downloaded_updates(series, keyword)
401
+
402
+
403
+ def process_cover(
404
+ audio_path: str,
405
+ character_name: str,
406
+ pitch_shift: int,
407
+ index_ratio: float,
408
+ speaker_id: float,
409
+ karaoke_separation: bool,
410
+ karaoke_merge_backing_into_accompaniment: bool,
411
+ vc_preprocess_mode: str,
412
+ source_constraint_mode: str,
413
+ vc_pipeline_mode: str,
414
+ singing_repair: bool,
415
+ vocals_volume: float,
416
+ accompaniment_volume: float,
417
+ reverb_amount: float,
418
+ rms_mix_rate: float,
419
+ backing_mix: float,
420
+ progress=gr.Progress()
421
+ ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], Optional[str], Optional[str], str]:
422
+ """
423
+ 处理翻唱
424
+
425
+ Returns:
426
+ Tuple[cover, converted_vocals, original_vocals, lead_vocals, backing_vocals, accompaniment, status]
427
+ """
428
+ _none6 = (None, None, None, None, None, None)
429
+ if audio_path is None:
430
+ return *_none6, "请上传歌曲文件"
431
+
432
+ if not character_name:
433
+ return *_none6, "请选择角色"
434
+
435
+ try:
436
+ from tools.character_models import get_character_model_path
437
+ from infer.cover_pipeline import get_cover_pipeline
438
+
439
+ # 获取角色模型路径
440
+ resolved_name = resolve_character_name(character_name)
441
+ model_info = get_character_model_path(resolved_name)
442
+ if model_info is None:
443
+ return *_none6, f"角色模型不存在: {resolved_name}"
444
+
445
+ # 进度回调
446
+ def progress_callback(msg: str, step: int, total: int):
447
+ if total > 0:
448
+ progress(step / total, desc=msg)
449
+
450
+ # 获取流水线
451
+ device = config.get("device", "cuda")
452
+ pipeline = get_cover_pipeline(device)
453
+
454
+ cover_cfg = config.get("cover", {})
455
+ demucs_model = cover_cfg.get("demucs_model", "htdemucs")
456
+ demucs_shifts = int(cover_cfg.get("demucs_shifts", 2))
457
+ demucs_overlap = float(cover_cfg.get("demucs_overlap", 0.25))
458
+ demucs_split = bool(cover_cfg.get("demucs_split", True))
459
+ separator = cover_cfg.get("separator", "roformer")
460
+ uvr5_model = cover_cfg.get("uvr5_model")
461
+ uvr5_agg = int(cover_cfg.get("uvr5_agg", 10))
462
+ uvr5_format = cover_cfg.get("uvr5_format", "wav")
463
+ use_official = bool(cover_cfg.get("use_official", True))
464
+ f0_method = cover_cfg.get("f0_method", config.get("f0_method", "rmvpe"))
465
+ filter_radius = cover_cfg.get("filter_radius", config.get("filter_radius", 3))
466
+ protect = cover_cfg.get("protect", config.get("protect", 0.33))
467
+ silence_gate = cover_cfg.get("silence_gate", True)
468
+ silence_threshold_db = cover_cfg.get("silence_threshold_db", -40.0)
469
+ silence_smoothing_ms = cover_cfg.get("silence_smoothing_ms", 50.0)
470
+ silence_min_duration_ms = cover_cfg.get("silence_min_duration_ms", 200.0)
471
+ hubert_layer = cover_cfg.get("hubert_layer", config.get("hubert_layer", 12))
472
+ karaoke_model = cover_cfg.get("karaoke_model", "mel_band_roformer_karaoke_gabox.ckpt")
473
+ default_vc_preprocess_mode = str(cover_cfg.get("vc_preprocess_mode", "auto"))
474
+ default_source_constraint_mode = str(cover_cfg.get("source_constraint_mode", "auto"))
475
+ default_vc_pipeline_mode = str(cover_cfg.get("vc_pipeline_mode", "current"))
476
+ default_singing_repair = bool(cover_cfg.get("singing_repair", False))
477
+ vc_label_to_value, vc_value_to_label = get_vc_preprocess_option_maps()
478
+ source_label_to_value, source_value_to_label = get_source_constraint_option_maps()
479
+ pipeline_label_to_value, pipeline_value_to_label = get_vc_pipeline_mode_option_maps()
480
+
481
+ vc_preprocess_mode = vc_label_to_value.get(str(vc_preprocess_mode), str(vc_preprocess_mode or default_vc_preprocess_mode).strip().lower())
482
+ if vc_preprocess_mode not in {"auto", "direct", "uvr_deecho", "legacy"}:
483
+ vc_preprocess_mode = default_vc_preprocess_mode
484
+ source_constraint_mode = source_label_to_value.get(str(source_constraint_mode), str(source_constraint_mode or default_source_constraint_mode).strip().lower())
485
+ if source_constraint_mode not in {"auto", "off", "on"}:
486
+ source_constraint_mode = default_source_constraint_mode
487
+ vc_pipeline_mode = pipeline_label_to_value.get(str(vc_pipeline_mode), str(vc_pipeline_mode or default_vc_pipeline_mode).strip().lower())
488
+ if vc_pipeline_mode not in {"current", "official"}:
489
+ vc_pipeline_mode = default_vc_pipeline_mode
490
+ singing_repair = bool(singing_repair if singing_repair is not None else default_singing_repair)
491
+
492
+ index_ratio = max(0.0, min(1.0, float(index_ratio) / 100.0))
493
+ speaker_id = int(max(0, round(float(speaker_id))))
494
+ rms_mix_rate = max(0.0, min(1.0, float(rms_mix_rate) / 100.0))
495
+ backing_mix = max(0.0, min(1.0, float(backing_mix) / 100.0))
496
+
497
+ # 输出目录
498
+ output_dir = ROOT_DIR / config.get("paths", {}).get(
499
+ "outputs",
500
+ config.get("output_dir", "outputs")
501
+ )
502
+
503
+ # 执行翻唱
504
+ result = pipeline.process(
505
+ input_audio=audio_path,
506
+ model_path=model_info["model_path"],
507
+ index_path=model_info.get("index_path"),
508
+ pitch_shift=pitch_shift,
509
+ index_ratio=index_ratio,
510
+ filter_radius=filter_radius,
511
+ rms_mix_rate=rms_mix_rate,
512
+ protect=protect,
513
+ speaker_id=speaker_id,
514
+ f0_method=f0_method,
515
+ demucs_model=demucs_model,
516
+ demucs_shifts=demucs_shifts,
517
+ demucs_overlap=demucs_overlap,
518
+ demucs_split=demucs_split,
519
+ separator=separator,
520
+ uvr5_model=uvr5_model,
521
+ uvr5_agg=uvr5_agg,
522
+ uvr5_format=uvr5_format,
523
+ use_official=use_official,
524
+ hubert_layer=hubert_layer,
525
+ silence_gate=silence_gate,
526
+ silence_threshold_db=silence_threshold_db,
527
+ silence_smoothing_ms=silence_smoothing_ms,
528
+ silence_min_duration_ms=silence_min_duration_ms,
529
+ vocals_volume=vocals_volume / 100, # 转换为 0-2 范围
530
+ accompaniment_volume=accompaniment_volume / 100,
531
+ reverb_amount=reverb_amount / 100,
532
+ backing_mix=backing_mix,
533
+ karaoke_separation=bool(karaoke_separation),
534
+ karaoke_model=karaoke_model,
535
+ karaoke_merge_backing_into_accompaniment=bool(karaoke_merge_backing_into_accompaniment),
536
+ vc_preprocess_mode=vc_preprocess_mode,
537
+ source_constraint_mode=source_constraint_mode,
538
+ vc_pipeline_mode=vc_pipeline_mode,
539
+ singing_repair=singing_repair,
540
+ output_dir=str(output_dir),
541
+ model_display_name=resolved_name,
542
+ progress_callback=progress_callback
543
+ )
544
+
545
+ status_msg = "\u2705 \u7ffb\u5531\u5b8c\u6210!"
546
+ status_msg += f"\n{get_cover_vc_route_status(vc_preprocess_mode, vc_pipeline_mode).splitlines()[0]}"
547
+ status_msg += f"\nVC\u7ba1\u7ebf\u6a21\u5f0f: {pipeline_value_to_label.get(vc_pipeline_mode, vc_pipeline_mode)}"
548
+ status_msg += f"\n唱歌修复: {'开启' if singing_repair else '关闭'}"
549
+ status_msg += f"\n\u6e90\u7ea6\u675f\u7b56\u7565: {source_value_to_label.get(source_constraint_mode, source_constraint_mode)}"
550
+ if result.get("all_files_dir"):
551
+ status_msg += f"\n\u5168\u90e8\u6587\u4ef6\u76ee\u5f55: {result['all_files_dir']}"
552
+
553
+ return (
554
+ result["cover"],
555
+ result["converted_vocals"],
556
+ result.get("vocals"),
557
+ result.get("lead_vocals"),
558
+ result.get("backing_vocals"),
559
+ result["accompaniment"],
560
+ status_msg
561
+ )
562
+
563
+ except Exception as e:
564
+ import traceback
565
+ error_msg = str(e) if str(e) else traceback.format_exc()
566
+ log.error(f"处理失败: {error_msg}")
567
+ return None, None, None, None, None, None, f"❌ 处理失败: {error_msg}"
568
+
569
+
570
+ def check_mature_deecho_status() -> str:
571
+ """Check mature DeEcho model availability."""
572
+ from tools.download_models import MATURE_DEECHO_MODELS, check_model, get_preferred_mature_deecho_model
573
+
574
+ status_lines = []
575
+ preferred = get_preferred_mature_deecho_model()
576
+ for name in MATURE_DEECHO_MODELS:
577
+ exists = check_model(name)
578
+ icon = "✅" if exists else "❌"
579
+ suffix = " ← 当前自动模式优先使用" if preferred == name else ""
580
+ status_lines.append(f"{icon} {name}{suffix}")
581
+
582
+ if preferred:
583
+ status_lines.append("")
584
+ status_lines.append(f"当前可用学习型 DeEcho: {preferred}")
585
+ else:
586
+ status_lines.append("")
587
+ status_lines.append("当前未检测到学习型 DeEcho 模型;翻唱自动模式将回退为主唱直通 RVC")
588
+
589
+ return "\n".join(status_lines)
590
+
591
+
592
+ def download_mature_deecho_models_ui() -> str:
593
+ """Download mature DeEcho models."""
594
+ from tools.download_models import download_mature_deecho_models
595
+
596
+ try:
597
+ success = download_mature_deecho_models()
598
+ status = check_mature_deecho_status()
599
+ prefix = "✅ 下载完成" if success else "⚠️ 下载过程中存在失败项"
600
+ return f"{prefix}\n\n{status}"
601
+ except Exception as e:
602
+ return f"❌ 下载失败: {str(e)}"
603
+
604
+
605
+ def get_cover_vc_route_status(
606
+ vc_preprocess_mode: Optional[str] = None,
607
+ vc_pipeline_mode: Optional[str] = None,
608
+ ) -> str:
609
+ """Return the active VC route shown in the cover UI."""
610
+ from tools.download_models import get_preferred_mature_deecho_model
611
+
612
+ mode = str(vc_preprocess_mode or config.get("cover", {}).get("vc_preprocess_mode", "auto")).strip().lower()
613
+ pipeline_mode = str(vc_pipeline_mode or config.get("cover", {}).get("vc_pipeline_mode", "current")).strip().lower()
614
+ vc_label_to_value, _ = get_vc_preprocess_option_maps()
615
+ pipeline_label_to_value, _ = get_vc_pipeline_mode_option_maps()
616
+ mode = vc_label_to_value.get(mode, mode)
617
+ pipeline_mode = pipeline_label_to_value.get(pipeline_mode, pipeline_mode)
618
+ preferred = get_preferred_mature_deecho_model()
619
+ newline = chr(10)
620
+
621
+ if pipeline_mode == "official":
622
+ return newline.join([
623
+ "当前使用内置官方 RVC 实现",
624
+ "流程:主唱分离 → 官方音频加载 / 官方 VC → 混音",
625
+ "说明:跳过本项目自定义 VC 预处理、源约束与静音门限后处理",
626
+ ])
627
+
628
+ if mode == "direct":
629
+ return newline.join([
630
+ "ℹ️ 当前固定为主唱直通 RVC",
631
+ "流程: 主唱分离 → 直接进入 RVC → 混音",
632
+ "说明: 不使用学习型 DeEcho,也不走旧版手工链",
633
+ ])
634
+ if mode == "legacy":
635
+ return newline.join([
636
+ "⚠️ 当前固定为旧版手工链",
637
+ "流程: 主唱分离 → 手工去回声链 → RVC → 混音",
638
+ "说明: 仅用于对比,不是默认推荐路径",
639
+ ])
640
+ if mode == "uvr_deecho":
641
+ if preferred:
642
+ return newline.join([
643
+ "✅ 当前固定优先使用学习型 DeEcho / DeReverb",
644
+ f"当前命中模型: {preferred}",
645
+ "流程: 主唱分离 → UVR DeEcho/DeReverb → RVC → 混音",
646
+ ])
647
+ return newline.join([
648
+ "⚠️ 当前设为官方 DeEcho 优先,但本地缺少模型",
649
+ "当前将回退流程: 主唱分离 → 直接进入 RVC → 混音",
650
+ "建议: 先在模型管理页下载成熟 DeEcho 模型",
651
+ ])
652
+
653
+ if preferred:
654
+ return newline.join([
655
+ "✅ 自动模式当前会优先使用学习型 DeEcho / DeReverb",
656
+ f"当前命中模型: {preferred}",
657
+ "流程: 主唱分离 → UVR DeEcho/DeReverb → RVC → 混音",
658
+ ])
659
+ return newline.join([
660
+ "ℹ️ 自动模式当前会回退为主唱直通 RVC",
661
+ "原因: 本地未检测到成熟 DeEcho / DeReverb 模型",
662
+ "流程: 主唱分离 → 直接进入 RVC → 混音",
663
+ ])
664
+
665
+
666
+ def check_models_status() -> str:
667
+ """检查模型状态"""
668
+ from tools.download_models import check_model, REQUIRED_MODELS
669
+
670
+ status_lines = []
671
+ for name in REQUIRED_MODELS:
672
+ exists = check_model(name)
673
+ icon = "✅" if exists else "❌"
674
+ status_lines.append(f"{icon} {name}")
675
+
676
+ return "\n".join(status_lines)
677
+
678
+
679
+ def get_device_info() -> str:
680
+ """获取设备信息"""
681
+ import torch
682
+ from lib.device import get_device_info as _get_info, _is_rocm, _has_xpu, _has_directml, _has_mps
683
+
684
+ lines = []
685
+ lines.append(f"PyTorch 版本: {torch.__version__}")
686
+
687
+ info = _get_info()
688
+ lines.append(f"可用后端: {', '.join(info['backends'])}")
689
+
690
+ for dev in info["devices"]:
691
+ mem = f"{dev['total_memory_gb']} GB" if dev.get("total_memory_gb") else "N/A"
692
+ lines.append(f"GPU: {dev['name']} ({dev['backend']}) - 显存: {mem}")
693
+
694
+ if torch.cuda.is_available():
695
+ ver = torch.version.hip if _is_rocm() else torch.version.cuda
696
+ label = "ROCm" if _is_rocm() else "CUDA"
697
+ lines.append(f"{label} 版本: {ver}")
698
+
699
+ if not info["devices"]:
700
+ lines.append("未检测到 GPU,将使用 CPU")
701
+
702
+ return "\n".join(lines)
703
+
704
+
705
+ # 自定义 CSS - 深灰 + 橙色强调配色
706
+ CUSTOM_CSS = """
707
+ /* 深色主题基础 - 纯色背景 */
708
+ .gradio-container {
709
+ background: #121212 !important;
710
+ min-height: 100vh;
711
+ }
712
+
713
+ .main-title {
714
+ text-align: center;
715
+ margin-bottom: 1rem;
716
+ color: #e0e0e0 !important;
717
+ }
718
+
719
+ /* 状态框样式 */
720
+ .status-box {
721
+ font-family: 'Consolas', 'Monaco', monospace;
722
+ white-space: pre-wrap;
723
+ background: #1e1e1e !important;
724
+ border: 1px solid #404040 !important;
725
+ color: #9e9e9e !important;
726
+ }
727
+
728
+ /* 提示框 */
729
+ .model-hint {
730
+ padding: 1rem;
731
+ background: #1e1e1e !important;
732
+ border: 1px solid #404040 !important;
733
+ border-radius: 8px;
734
+ margin: 1rem 0;
735
+ color: #e0e0e0 !important;
736
+ }
737
+
738
+ /* 成功/错误消息 */
739
+ .success-msg {
740
+ color: #4caf50 !important;
741
+ font-weight: bold;
742
+ }
743
+ .error-msg {
744
+ color: #f44336 !important;
745
+ font-weight: bold;
746
+ }
747
+
748
+ /* 标签页样式 */
749
+ .tabs > .tab-nav {
750
+ background: #1e1e1e !important;
751
+ border-bottom: 1px solid #404040 !important;
752
+ }
753
+ .tabs > .tab-nav > button {
754
+ color: #9e9e9e !important;
755
+ background: transparent !important;
756
+ border: none !important;
757
+ padding: 12px 24px !important;
758
+ transition: color 0.2s ease !important;
759
+ }
760
+ .tabs > .tab-nav > button:hover {
761
+ color: #e0e0e0 !important;
762
+ }
763
+ .tabs > .tab-nav > button.selected {
764
+ color: #ff9800 !important;
765
+ border-bottom: 2px solid #ff9800 !important;
766
+ background: transparent !important;
767
+ }
768
+
769
+ /* 输入框和下拉框 */
770
+ .gr-input, .gr-dropdown, textarea, input[type="text"] {
771
+ background: #2d2d2d !important;
772
+ border: 1px solid #404040 !important;
773
+ color: #e0e0e0 !important;
774
+ }
775
+ .gr-input:focus, .gr-dropdown:focus, textarea:focus, input[type="text"]:focus {
776
+ border-color: #ff9800 !important;
777
+ outline: none !important;
778
+ }
779
+
780
+ /* 滑块 */
781
+ .gr-slider input[type="range"] {
782
+ background: #404040 !important;
783
+ }
784
+ .gr-slider input[type="range"]::-webkit-slider-thumb {
785
+ background: #ff9800 !important;
786
+ }
787
+ .gr-slider input[type="range"]::-moz-range-thumb {
788
+ background: #ff9800 !important;
789
+ }
790
+ input[type="range"]::-webkit-slider-runnable-track {
791
+ background: #404040 !important;
792
+ }
793
+ input[type="range"]::-moz-range-track {
794
+ background: #404040 !important;
795
+ }
796
+
797
+ /* 按钮样式 - 主按钮橙色 */
798
+ .gr-button-primary, button.primary {
799
+ background: #ff9800 !important;
800
+ border: none !important;
801
+ color: #121212 !important;
802
+ font-weight: 600 !important;
803
+ transition: all 0.2s ease !important;
804
+ }
805
+ .gr-button-primary:hover, button.primary:hover {
806
+ background: #ffa726 !important;
807
+ transform: translateY(-1px) !important;
808
+ box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3) !important;
809
+ }
810
+ .gr-button-primary:active, button.primary:active {
811
+ background: #f57c00 !important;
812
+ transform: translateY(0) !important;
813
+ }
814
+
815
+ /* 次要按钮 */
816
+ .gr-button-secondary, button.secondary {
817
+ background: #404040 !important;
818
+ border: none !important;
819
+ color: #e0e0e0 !important;
820
+ transition: all 0.2s ease !important;
821
+ }
822
+ .gr-button-secondary:hover, button.secondary:hover {
823
+ background: #4a4a4a !important;
824
+ }
825
+
826
+ /* 音频播放器 */
827
+ .gr-audio {
828
+ background: #1e1e1e !important;
829
+ border: 1px solid #404040 !important;
830
+ border-radius: 8px !important;
831
+ }
832
+ .gr-audio audio {
833
+ background: #1e1e1e !important;
834
+ color: #e0e0e0 !important;
835
+ accent-color: #ff9800 !important;
836
+ }
837
+ .gr-audio audio::-webkit-media-controls-panel {
838
+ background: #1e1e1e !important;
839
+ }
840
+ .gr-audio audio::-webkit-media-controls-enclosure {
841
+ background: #1e1e1e !important;
842
+ }
843
+ .gr-audio audio::-webkit-media-controls-timeline {
844
+ background: #404040 !important;
845
+ }
846
+ .gr-audio audio::-webkit-media-controls-current-time-display,
847
+ .gr-audio audio::-webkit-media-controls-time-remaining-display {
848
+ color: #e0e0e0 !important;
849
+ }
850
+ .gr-audio audio::-webkit-media-controls-play-button,
851
+ .gr-audio audio::-webkit-media-controls-mute-button,
852
+ .gr-audio audio::-webkit-media-controls-volume-slider {
853
+ filter: invert(1) sepia(1) saturate(5) hue-rotate(10deg) !important;
854
+ }
855
+
856
+ /* 折叠面板 */
857
+ .gr-accordion {
858
+ background: #1e1e1e !important;
859
+ border: 1px solid #404040 !important;
860
+ border-radius: 8px !important;
861
+ }
862
+ .gr-accordion > .label-wrap {
863
+ background: #1e1e1e !important;
864
+ }
865
+
866
+ /* 表格 */
867
+ .gr-dataframe {
868
+ background: #1e1e1e !important;
869
+ }
870
+ .gr-dataframe table {
871
+ color: #e0e0e0 !important;
872
+ }
873
+ .gr-dataframe th {
874
+ background: #2d2d2d !important;
875
+ color: #9e9e9e !important;
876
+ }
877
+ .gr-dataframe td {
878
+ background: #1e1e1e !important;
879
+ border-color: #404040 !important;
880
+ }
881
+ .gr-dataframe tr:hover td {
882
+ background: #333333 !important;
883
+ }
884
+ /* Gradio v4 Dataframe */
885
+ div[data-testid="dataframe"] {
886
+ background: #1e1e1e !important;
887
+ color: #e0e0e0 !important;
888
+ border: 1px solid #404040 !important;
889
+ }
890
+ div[data-testid="dataframe"] table {
891
+ color: #e0e0e0 !important;
892
+ }
893
+ div[data-testid="dataframe"] thead th {
894
+ background: #2d2d2d !important;
895
+ color: #9e9e9e !important;
896
+ border-color: #404040 !important;
897
+ }
898
+ div[data-testid="dataframe"] tbody td {
899
+ background: #1e1e1e !important;
900
+ color: #e0e0e0 !important;
901
+ border-color: #404040 !important;
902
+ }
903
+ div[data-testid="dataframe"] tbody tr:hover td {
904
+ background: #333333 !important;
905
+ }
906
+ div[data-testid="dataframe"] input,
907
+ div[data-testid="dataframe"] textarea {
908
+ background: #1e1e1e !important;
909
+ color: #e0e0e0 !important;
910
+ border: 1px solid #404040 !important;
911
+ }
912
+
913
+ /* Markdown 文本 */
914
+ .prose {
915
+ color: #e0e0e0 !important;
916
+ }
917
+ .prose h1, .prose h2, .prose h3, .prose h4 {
918
+ color: #e0e0e0 !important;
919
+ }
920
+ .prose a {
921
+ color: #ff9800 !important;
922
+ }
923
+ .prose a:hover {
924
+ color: #ffa726 !important;
925
+ }
926
+ .prose code {
927
+ background: #2d2d2d !important;
928
+ color: #ff9800 !important;
929
+ padding: 2px 6px !important;
930
+ border-radius: 4px !important;
931
+ }
932
+ .prose blockquote {
933
+ border-left: 3px solid #ff9800 !important;
934
+ background: #1e1e1e !important;
935
+ padding: 8px 16px !important;
936
+ color: #9e9e9e !important;
937
+ }
938
+
939
+ /* 单选按钮和复选框 */
940
+ .gr-radio label, .gr-checkbox label {
941
+ color: #e0e0e0 !important;
942
+ }
943
+ input[type="radio"]:checked + label, input[type="checkbox"]:checked + label {
944
+ color: #ff9800 !important;
945
+ }
946
+
947
+ /* 进度条 */
948
+ .progress-bar {
949
+ background: #404040 !important;
950
+ }
951
+ .progress-bar > div {
952
+ background: #ff9800 !important;
953
+ }
954
+
955
+ /* 分隔线 */
956
+ hr {
957
+ border-color: #404040 !important;
958
+ }
959
+
960
+ /* 标签 */
961
+ label {
962
+ color: #9e9e9e !important;
963
+ }
964
+
965
+ /* 信息文本 */
966
+ .gr-info {
967
+ color: #9e9e9e !important;
968
+ }
969
+
970
+ /* 块/面板背景 */
971
+ .gr-block, .gr-box, .gr-panel {
972
+ background: #1e1e1e !important;
973
+ border-color: #404040 !important;
974
+ }
975
+
976
+ /* 下拉菜单选项 */
977
+ .gr-dropdown option, select option {
978
+ background: #2d2d2d !important;
979
+ color: #e0e0e0 !important;
980
+ }
981
+
982
+ /* Gradio 下拉选择器完整样式 */
983
+ .gr-dropdown, .gr-dropdown select,
984
+ div[data-testid="dropdown"],
985
+ .dropdown-container,
986
+ .svelte-select,
987
+ .wrap-inner,
988
+ .secondary-wrap {
989
+ background: #2d2d2d !important;
990
+ border: 1px solid #404040 !important;
991
+ color: #e0e0e0 !important;
992
+ }
993
+
994
+ /* 下拉选择器输入框 */
995
+ .gr-dropdown input,
996
+ div[data-testid="dropdown"] input,
997
+ .svelte-select input {
998
+ background: #2d2d2d !important;
999
+ color: #e0e0e0 !important;
1000
+ border: none !important;
1001
+ }
1002
+
1003
+ /* 下拉菜单列表 */
1004
+ .gr-dropdown ul,
1005
+ .gr-dropdown .options,
1006
+ div[data-testid="dropdown"] ul,
1007
+ .svelte-select .listContainer,
1008
+ .dropdown-menu,
1009
+ ul[role="listbox"] {
1010
+ background: #2d2d2d !important;
1011
+ border: 1px solid #404040 !important;
1012
+ color: #e0e0e0 !important;
1013
+ }
1014
+
1015
+ /* 下拉菜单选项 */
1016
+ .gr-dropdown li,
1017
+ .gr-dropdown .option,
1018
+ div[data-testid="dropdown"] li,
1019
+ .svelte-select .listItem,
1020
+ li[role="option"] {
1021
+ background: #2d2d2d !important;
1022
+ color: #e0e0e0 !important;
1023
+ }
1024
+
1025
+ /* 下拉菜单选项悬停 */
1026
+ .gr-dropdown li:hover,
1027
+ .gr-dropdown .option:hover,
1028
+ div[data-testid="dropdown"] li:hover,
1029
+ .svelte-select .listItem:hover,
1030
+ .svelte-select .listItem.hover,
1031
+ li[role="option"]:hover {
1032
+ background: #404040 !important;
1033
+ color: #ff9800 !important;
1034
+ }
1035
+
1036
+ /* 下拉菜单选中项 */
1037
+ .gr-dropdown li.selected,
1038
+ .gr-dropdown .option.selected,
1039
+ .svelte-select .listItem.active,
1040
+ li[role="option"][aria-selected="true"] {
1041
+ background: #333333 !important;
1042
+ color: #ff9800 !important;
1043
+ }
1044
+
1045
+ /* 下拉箭头图标 */
1046
+ .gr-dropdown svg,
1047
+ div[data-testid="dropdown"] svg,
1048
+ .svelte-select .indicator svg {
1049
+ fill: #9e9e9e !important;
1050
+ color: #9e9e9e !important;
1051
+ }
1052
+
1053
+ /* Gradio 3.x 特定选择器样式 */
1054
+ .wrap.svelte-1m1zvyj,
1055
+ .wrap-inner.svelte-1m1zvyj,
1056
+ .secondary-wrap.svelte-1m1zvyj {
1057
+ background: #2d2d2d !important;
1058
+ border-color: #404040 !important;
1059
+ }
1060
+
1061
+ .dropdown.svelte-1m1zvyj,
1062
+ .options.svelte-1m1zvyj {
1063
+ background: #2d2d2d !important;
1064
+ border: 1px solid #404040 !important;
1065
+ }
1066
+
1067
+ .item.svelte-1m1zvyj {
1068
+ background: #2d2d2d !important;
1069
+ color: #e0e0e0 !important;
1070
+ }
1071
+
1072
+ .item.svelte-1m1zvyj:hover,
1073
+ .item.svelte-1m1zvyj.active {
1074
+ background: #404040 !important;
1075
+ color: #ff9800 !important;
1076
+ }
1077
+
1078
+ /* 单选按钮组样式 */
1079
+ .gr-radio,
1080
+ .gr-radio-group,
1081
+ div[data-testid="radio"] {
1082
+ background: transparent !important;
1083
+ }
1084
+
1085
+ .gr-radio label span,
1086
+ div[data-testid="radio"] label span {
1087
+ color: #e0e0e0 !important;
1088
+ }
1089
+
1090
+ .gr-radio input[type="radio"],
1091
+ div[data-testid="radio"] input[type="radio"] {
1092
+ accent-color: #ff9800 !important;
1093
+ }
1094
+
1095
+ /* Radio 按钮容器 */
1096
+ .radio-group,
1097
+ .gr-radio-row {
1098
+ background: #1e1e1e !important;
1099
+ }
1100
+
1101
+ .radio-group label,
1102
+ .gr-radio-row label {
1103
+ background: #2d2d2d !important;
1104
+ border: 1px solid #404040 !important;
1105
+ color: #e0e0e0 !important;
1106
+ }
1107
+
1108
+ .radio-group label:hover,
1109
+ .gr-radio-row label:hover {
1110
+ background: #333333 !important;
1111
+ }
1112
+
1113
+ .radio-group label.selected,
1114
+ .gr-radio-row label.selected,
1115
+ .radio-group input:checked + label,
1116
+ .gr-radio-row input:checked + label {
1117
+ background: #333333 !important;
1118
+ border-color: #ff9800 !important;
1119
+ color: #ff9800 !important;
1120
+ }
1121
+
1122
+ /* 滚动条样式 */
1123
+ ::-webkit-scrollbar {
1124
+ width: 8px;
1125
+ height: 8px;
1126
+ }
1127
+ ::-webkit-scrollbar-track {
1128
+ background: #1e1e1e;
1129
+ }
1130
+ ::-webkit-scrollbar-thumb {
1131
+ background: #404040;
1132
+ border-radius: 4px;
1133
+ }
1134
+ ::-webkit-scrollbar-thumb:hover {
1135
+ background: #4a4a4a;
1136
+ }
1137
+
1138
+ /* Dataframe 表头修复 - 强制深色主题 */
1139
+ table thead th,
1140
+ table thead td,
1141
+ .table-wrap thead th,
1142
+ .table-wrap thead td,
1143
+ [data-testid="table"] thead th,
1144
+ [data-testid="table"] thead td {
1145
+ background: #2d2d2d !important;
1146
+ color: #ff9800 !important;
1147
+ border-color: #404040 !important;
1148
+ }
1149
+
1150
+ /* Gradio 4.x Dataframe 表头 */
1151
+ .svelte-1kcgrqr thead th,
1152
+ .svelte-1kcgrqr thead td,
1153
+ .cell-wrap span,
1154
+ th .cell-wrap,
1155
+ th span.svelte-1kcgrqr {
1156
+ background: #2d2d2d !important;
1157
+ color: #ff9800 !important;
1158
+ }
1159
+
1160
+ /* 音频播放器进度条修复 */
1161
+ audio::-webkit-media-controls-timeline {
1162
+ background: linear-gradient(to right, #ff9800 var(--buffered-width, 0%), #404040 var(--buffered-width, 0%)) !important;
1163
+ border-radius: 4px !important;
1164
+ height: 4px !important;
1165
+ }
1166
+
1167
+ /* 音频播放器 - Gradio 组件内部 */
1168
+ .audio-container input[type="range"],
1169
+ .waveform-container input[type="range"],
1170
+ div[data-testid="audio"] input[type="range"],
1171
+ div[data-testid="waveform"] input[type="range"] {
1172
+ accent-color: #ff9800 !important;
1173
+ }
1174
+
1175
+ /* WaveSurfer 波形进度条 */
1176
+ .wavesurfer-region,
1177
+ .wavesurfer-handle,
1178
+ wave > wave {
1179
+ background: #ff9800 !important;
1180
+ }
1181
+
1182
+ /* Gradio Audio 组件进度条 */
1183
+ .audio-player input[type="range"]::-webkit-slider-runnable-track {
1184
+ background: linear-gradient(to right, #ff9800 0%, #ff9800 var(--value, 0%), #404040 var(--value, 0%), #404040 100%) !important;
1185
+ }
1186
+
1187
+ .audio-player input[type="range"]::-moz-range-track {
1188
+ background: linear-gradient(to right, #ff9800 0%, #ff9800 var(--value, 0%), #404040 var(--value, 0%), #404040 100%) !important;
1189
+ }
1190
+
1191
+ .audio-player input[type="range"]::-webkit-slider-thumb {
1192
+ background: #ff9800 !important;
1193
+ }
1194
+
1195
+ .audio-player input[type="range"]::-moz-range-thumb {
1196
+ background: #ff9800 !important;
1197
+ }
1198
+
1199
+ /* 通用 range input 进度样式 */
1200
+ input[type="range"] {
1201
+ accent-color: #ff9800 !important;
1202
+ }
1203
+
1204
+ /* Gradio 4.x 音频波形 */
1205
+ .waveform-container,
1206
+ .audio-container {
1207
+ --waveform-color: #ff9800 !important;
1208
+ --progress-color: #ff9800 !important;
1209
+ }
1210
+ """
1211
+
1212
+
1213
+ def create_ui() -> gr.Blocks:
1214
+ """创建 Gradio 界面"""
1215
+
1216
+ with gr.Blocks(
1217
+ title=i18n.get("app_title", "RVC AI 翻唱"),
1218
+ theme=gr.themes.Base(
1219
+ primary_hue="orange",
1220
+ secondary_hue="gray",
1221
+ neutral_hue="gray",
1222
+ ).set(
1223
+ # 背景色
1224
+ body_background_fill="#121212",
1225
+ body_background_fill_dark="#121212",
1226
+ # 面板/卡片背景
1227
+ block_background_fill="#1e1e1e",
1228
+ block_background_fill_dark="#1e1e1e",
1229
+ # 边框
1230
+ block_border_color="#404040",
1231
+ block_border_color_dark="#404040",
1232
+ # 标签背景
1233
+ block_label_background_fill="#2d2d2d",
1234
+ block_label_background_fill_dark="#2d2d2d",
1235
+ # 标签文字
1236
+ block_label_text_color="#9e9e9e",
1237
+ block_label_text_color_dark="#9e9e9e",
1238
+ # 标题文字
1239
+ block_title_text_color="#e0e0e0",
1240
+ block_title_text_color_dark="#e0e0e0",
1241
+ # 输入框
1242
+ input_background_fill="#2d2d2d",
1243
+ input_background_fill_dark="#2d2d2d",
1244
+ input_border_color="#404040",
1245
+ input_border_color_dark="#404040",
1246
+ # 主按钮 - 橙色
1247
+ button_primary_background_fill="#ff9800",
1248
+ button_primary_background_fill_dark="#ff9800",
1249
+ button_primary_background_fill_hover="#ffa726",
1250
+ button_primary_background_fill_hover_dark="#ffa726",
1251
+ button_primary_text_color="#121212",
1252
+ button_primary_text_color_dark="#121212",
1253
+ # 次要按钮 - 深灰
1254
+ button_secondary_background_fill="#404040",
1255
+ button_secondary_background_fill_dark="#404040",
1256
+ button_secondary_background_fill_hover="#4a4a4a",
1257
+ button_secondary_background_fill_hover_dark="#4a4a4a",
1258
+ button_secondary_text_color="#e0e0e0",
1259
+ button_secondary_text_color_dark="#e0e0e0",
1260
+ # 文字颜色
1261
+ body_text_color="#e0e0e0",
1262
+ body_text_color_dark="#e0e0e0",
1263
+ body_text_color_subdued="#9e9e9e",
1264
+ body_text_color_subdued_dark="#9e9e9e",
1265
+ # 链接颜色 - 橙色
1266
+ link_text_color="#ff9800",
1267
+ link_text_color_dark="#ff9800",
1268
+ link_text_color_hover="#ffa726",
1269
+ link_text_color_hover_dark="#ffa726",
1270
+ # 滑块颜色
1271
+ slider_color="#ff9800",
1272
+ slider_color_dark="#ff9800",
1273
+ # 复选框/单选框
1274
+ checkbox_background_color="#2d2d2d",
1275
+ checkbox_background_color_dark="#2d2d2d",
1276
+ checkbox_border_color="#404040",
1277
+ checkbox_border_color_dark="#404040",
1278
+ checkbox_label_text_color="#e0e0e0",
1279
+ checkbox_label_text_color_dark="#e0e0e0",
1280
+ ),
1281
+ css=CUSTOM_CSS
1282
+ ) as app:
1283
+
1284
+ # 标题
1285
+ gr.Markdown(
1286
+ f"# 🎤 {i18n.get('app_title', 'RVC AI 翻唱')}",
1287
+ elem_classes=["main-title"]
1288
+ )
1289
+ gr.Markdown(
1290
+ f"<center>{i18n.get('app_description', '基于 RVC v2 的 AI 翻唱系统')}</center>"
1291
+ )
1292
+
1293
+ with gr.Tabs():
1294
+ # ===== 模型管理标签页 =====
1295
+ with gr.Tab(t("models", "tabs")):
1296
+ gr.Markdown(f"### 📦 {t('base_models', 'models')}")
1297
+ gr.Markdown(t("base_models_desc", "models"))
1298
+
1299
+ with gr.Row():
1300
+ check_btn = gr.Button(
1301
+ f"🔍 {t('check_status', 'models')}",
1302
+ variant="secondary"
1303
+ )
1304
+ download_btn = gr.Button(
1305
+ f"⬇️ {t('download_required', 'models')}",
1306
+ variant="primary"
1307
+ )
1308
+
1309
+ model_status = gr.Textbox(
1310
+ label=t("model_status", "models"),
1311
+ interactive=False,
1312
+ lines=6,
1313
+ elem_classes=["status-box"]
1314
+ )
1315
+
1316
+ check_btn.click(
1317
+ fn=check_models_status,
1318
+ outputs=[model_status]
1319
+ )
1320
+
1321
+ download_btn.click(
1322
+ fn=download_base_models,
1323
+ outputs=[model_status]
1324
+ )
1325
+
1326
+ gr.Markdown("---")
1327
+ gr.Markdown(f"### 🎛️ {t('mature_deecho_models', 'models')}")
1328
+ gr.Markdown(t("mature_deecho_models_desc", "models"))
1329
+
1330
+ with gr.Row():
1331
+ mature_deecho_check_btn = gr.Button(
1332
+ f"🔍 {t('mature_deecho_check', 'models')}",
1333
+ variant="secondary"
1334
+ )
1335
+ mature_deecho_download_btn = gr.Button(
1336
+ f"⬇️ {t('download_mature_deecho', 'models')}",
1337
+ variant="primary"
1338
+ )
1339
+
1340
+ mature_deecho_status = gr.Textbox(
1341
+ label=t("mature_deecho_status", "models"),
1342
+ interactive=False,
1343
+ lines=7,
1344
+ value=check_mature_deecho_status(),
1345
+ elem_classes=["status-box"]
1346
+ )
1347
+
1348
+ gr.Markdown("---")
1349
+
1350
+ gr.Markdown(f"### 🎤 {t('voice_models', 'models')}")
1351
+ gr.Markdown(t("voice_models_desc", "models"))
1352
+
1353
+ def get_model_table():
1354
+ from infer.pipeline import list_voice_models
1355
+ weights_dir = ROOT_DIR / config.get("weights_dir", "assets/weights")
1356
+ models = list_voice_models(str(weights_dir))
1357
+ if not models:
1358
+ return [["(无模型)", "", ""]]
1359
+ return [[m["name"], m["model_path"], m.get("index_path", "无")] for m in models]
1360
+
1361
+ model_table = gr.Dataframe(
1362
+ headers=["模型名称", "模型路径", "索引路径"],
1363
+ value=get_model_table(),
1364
+ interactive=False
1365
+ )
1366
+
1367
+ refresh_table_btn = gr.Button(
1368
+ f"🔄 刷新模型列表",
1369
+ variant="secondary"
1370
+ )
1371
+
1372
+ refresh_table_btn.click(
1373
+ fn=get_model_table,
1374
+ outputs=[model_table]
1375
+ )
1376
+
1377
+ # ===== 歌曲翻唱标签页 =====
1378
+ with gr.Tab(t("cover", "tabs")):
1379
+ gr.Markdown(f"### 🎵 {t('song_cover', 'cover')}")
1380
+ gr.Markdown(
1381
+ """
1382
+ **一键 AI 翻唱**:上传歌曲 → 自动分离人声 → 转换音色 → 混合伴奏 → 输出翻唱
1383
+
1384
+ **使用步骤:**
1385
+ 1. 先下载角色模型(展开下方「下载角色模型」)
1386
+ 2. 上传歌曲文件(支持 MP3/WAV/FLAC)
1387
+ 3. 选择已下载的角色
1388
+ 4. 调整参数后点击「开始翻唱」
1389
+
1390
+ > ⚠️ 首次运行会自动下载 Mel-Band Roformer 人声分离模型(约 200MB),请耐心等待
1391
+ """
1392
+ )
1393
+
1394
+ with gr.Row():
1395
+ # 左侧:输入和角色选择
1396
+ with gr.Column(scale=1):
1397
+ gr.Markdown(f"#### 📁 {t('upload_song', 'cover')}")
1398
+ cover_input_audio = gr.Audio(
1399
+ label=t("input_song", "cover"),
1400
+ type="filepath"
1401
+ )
1402
+
1403
+ gr.Markdown(f"#### 🎭 {t('select_character', 'cover')}")
1404
+
1405
+ downloaded_series = gr.Dropdown(
1406
+ label="作品/分类",
1407
+ choices=get_downloaded_character_series(),
1408
+ value="全部",
1409
+ interactive=True
1410
+ )
1411
+
1412
+ downloaded_keyword = gr.Textbox(
1413
+ label="关键词搜索",
1414
+ placeholder="输入角色名/作品名",
1415
+ interactive=True
1416
+ )
1417
+
1418
+ character_dropdown = gr.Dropdown(
1419
+ label="选择角色",
1420
+ choices=get_downloaded_character_choices("全部", ""),
1421
+ interactive=True,
1422
+ info="括号中的信息为模型训练参数:epochs=训练轮数(越大通常越成熟),数字+k=训练采样率(如40k=40000Hz)"
1423
+ )
1424
+
1425
+ with gr.Row():
1426
+ refresh_char_btn = gr.Button(
1427
+ "🔄 刷新",
1428
+ size="sm",
1429
+ variant="secondary"
1430
+ )
1431
+
1432
+ # 角色下载区域
1433
+ with gr.Accordion("下载角色模型", open=False):
1434
+ series_choices = ["全部"] + get_available_character_series()
1435
+ download_series = gr.Dropdown(
1436
+ label="作品/分类",
1437
+ choices=series_choices,
1438
+ value="全部",
1439
+ interactive=True
1440
+ )
1441
+
1442
+ download_keyword = gr.Textbox(
1443
+ label="关键词搜索",
1444
+ placeholder="输入角色名/作品名",
1445
+ interactive=True
1446
+ )
1447
+
1448
+ download_char_dropdown = gr.Dropdown(
1449
+ label="选择角色",
1450
+ choices=get_available_character_choices("全部", ""),
1451
+ interactive=True
1452
+ )
1453
+
1454
+ download_char_btn = gr.Button(
1455
+ "⬇️ 下载选中角色",
1456
+ variant="primary"
1457
+ )
1458
+
1459
+ download_all_series_btn = gr.Button(
1460
+ "⬇️ 下载该分类全部",
1461
+ variant="secondary"
1462
+ )
1463
+
1464
+ download_all_btn = gr.Button(
1465
+ "⬇️ 下载全部角色模型",
1466
+ variant="secondary"
1467
+ )
1468
+
1469
+ download_char_status = gr.Textbox(
1470
+ label="下载状态",
1471
+ interactive=False
1472
+ )
1473
+
1474
+ # 右侧:参数设置
1475
+ with gr.Column(scale=1):
1476
+ gr.Markdown(f"#### ⚙️ {t('conversion_settings', 'cover')}")
1477
+ cover_cfg = config.get("cover", {})
1478
+
1479
+ cover_pitch_shift = gr.Slider(
1480
+ label=t("pitch_shift", "cover"),
1481
+ minimum=-12,
1482
+ maximum=12,
1483
+ value=0,
1484
+ step=1,
1485
+ info="正数升调,负数降调"
1486
+ )
1487
+
1488
+ cover_index_rate = gr.Slider(
1489
+ label=t("index_rate", "cover"),
1490
+ minimum=0,
1491
+ maximum=100,
1492
+ value=_to_int(
1493
+ round(
1494
+ _to_float(
1495
+ cover_cfg.get("index_rate", config.get("index_rate", 0.35)),
1496
+ 0.35,
1497
+ ) * 100
1498
+ ),
1499
+ 35,
1500
+ ),
1501
+ step=5,
1502
+ info=t("index_rate_info", "cover"),
1503
+ )
1504
+
1505
+ cover_speaker_id = gr.Slider(
1506
+ label=t("speaker_id", "cover"),
1507
+ minimum=0,
1508
+ maximum=255,
1509
+ value=_to_int(cover_cfg.get("speaker_id", 0), 0),
1510
+ step=1,
1511
+ info=t("speaker_id_info", "cover"),
1512
+ )
1513
+
1514
+ gr.Markdown(f"#### 🎚️ {t('mix_settings', 'cover')}")
1515
+ cover_karaoke = gr.Checkbox(
1516
+ label=t("karaoke_separation", "cover"),
1517
+ value=bool(cover_cfg.get("karaoke_separation", True)),
1518
+ info=t("karaoke_separation_info", "cover")
1519
+ )
1520
+ cover_karaoke_merge_backing = gr.Checkbox(
1521
+ label=t("karaoke_merge_backing", "cover"),
1522
+ value=bool(
1523
+ cover_cfg.get(
1524
+ "karaoke_merge_backing_into_accompaniment",
1525
+ True
1526
+ )
1527
+ ),
1528
+ info=t("karaoke_merge_backing_info", "cover")
1529
+ )
1530
+
1531
+ vc_label_to_value, vc_value_to_label = get_vc_preprocess_option_maps()
1532
+ source_label_to_value, source_value_to_label = get_source_constraint_option_maps()
1533
+ pipeline_label_to_value, pipeline_value_to_label = get_vc_pipeline_mode_option_maps()
1534
+
1535
+ cover_vc_preprocess_mode = gr.Dropdown(
1536
+ label=t("vc_preprocess_mode", "cover"),
1537
+ choices=list(vc_label_to_value.keys()),
1538
+ value=vc_value_to_label.get(str(cover_cfg.get("vc_preprocess_mode", "auto")), list(vc_label_to_value.keys())[0]),
1539
+ info=t("vc_preprocess_mode_info", "cover"),
1540
+ )
1541
+
1542
+ cover_source_constraint_mode = gr.Dropdown(
1543
+ label=t("source_constraint_mode", "cover"),
1544
+ choices=list(source_label_to_value.keys()),
1545
+ value=source_value_to_label.get(str(cover_cfg.get("source_constraint_mode", "auto")), list(source_label_to_value.keys())[0]),
1546
+ info=t("source_constraint_mode_info", "cover"),
1547
+ )
1548
+ cover_vc_pipeline_mode = gr.Dropdown(
1549
+ label=t("vc_pipeline_mode", "cover"),
1550
+ choices=list(pipeline_label_to_value.keys()),
1551
+ value=pipeline_value_to_label.get(str(cover_cfg.get("vc_pipeline_mode", "current")), list(pipeline_label_to_value.keys())[0]),
1552
+ info=t("vc_pipeline_mode_info", "cover"),
1553
+ )
1554
+ cover_singing_repair = gr.Checkbox(
1555
+ label=t("singing_repair", "cover"),
1556
+ value=bool(cover_cfg.get("singing_repair", False)),
1557
+ info=t("singing_repair_info", "cover"),
1558
+ visible=str(cover_cfg.get("vc_pipeline_mode", "current")).strip().lower() == "official",
1559
+ )
1560
+
1561
+ cover_vc_route_status = gr.Textbox(
1562
+ label=t("vc_preprocess_status", "cover"),
1563
+ value=get_cover_vc_route_status(
1564
+ cover_cfg.get("vc_preprocess_mode", "auto"),
1565
+ cover_cfg.get("vc_pipeline_mode", "current"),
1566
+ ),
1567
+ info=t("vc_preprocess_status_info", "cover"),
1568
+ interactive=False,
1569
+ lines=3,
1570
+ elem_classes=["status-box"]
1571
+ )
1572
+
1573
+ mix_presets, default_mix_preset = get_cover_mix_presets()
1574
+ default_mix = mix_presets[default_mix_preset]
1575
+
1576
+ cover_mix_preset = gr.Dropdown(
1577
+ label=t("mix_preset", "cover"),
1578
+ choices=list(mix_presets.keys()),
1579
+ value=default_mix_preset,
1580
+ info=t("mix_preset_info", "cover"),
1581
+ interactive=True
1582
+ )
1583
+
1584
+ cover_vocals_volume = gr.Slider(
1585
+ label=t("vocals_volume", "cover"),
1586
+ minimum=0,
1587
+ maximum=200,
1588
+ value=default_mix["vocals_volume"],
1589
+ step=5,
1590
+ info="100% 为原始音量"
1591
+ )
1592
+
1593
+ cover_accompaniment_volume = gr.Slider(
1594
+ label=t("accompaniment_volume", "cover"),
1595
+ minimum=0,
1596
+ maximum=200,
1597
+ value=default_mix["accompaniment_volume"],
1598
+ step=5,
1599
+ info="100% 为原始音量"
1600
+ )
1601
+
1602
+ cover_reverb = gr.Slider(
1603
+ label=t("vocals_reverb", "cover"),
1604
+ minimum=0,
1605
+ maximum=100,
1606
+ value=default_mix["reverb"],
1607
+ step=5,
1608
+ info="为人声添加混响效果"
1609
+ )
1610
+
1611
+ cover_rms_mix_rate = gr.Slider(
1612
+ label=t("rms_mix_rate", "cover"),
1613
+ minimum=0,
1614
+ maximum=100,
1615
+ value=_to_int(
1616
+ round(
1617
+ _to_float(
1618
+ cover_cfg.get(
1619
+ "rms_mix_rate",
1620
+ config.get("rms_mix_rate", 0.15),
1621
+ ),
1622
+ 0.15,
1623
+ ) * 100
1624
+ ),
1625
+ 15,
1626
+ ),
1627
+ step=5,
1628
+ info=t("rms_mix_rate_info", "cover"),
1629
+ )
1630
+
1631
+ cover_backing_mix = gr.Slider(
1632
+ label=t("backing_mix", "cover"),
1633
+ minimum=0,
1634
+ maximum=100,
1635
+ value=_to_int(
1636
+ round(_to_float(cover_cfg.get("backing_mix", 0.0), 0.0) * 100),
1637
+ 0,
1638
+ ),
1639
+ step=5,
1640
+ info=t("backing_mix_info", "cover"),
1641
+ )
1642
+
1643
+ # 开始按钮
1644
+ cover_btn = gr.Button(
1645
+ f"🚀 {t('start_cover', 'cover')}",
1646
+ variant="primary",
1647
+ size="lg"
1648
+ )
1649
+
1650
+ # 状态显示
1651
+ cover_status = gr.Textbox(
1652
+ label=t("progress", "cover"),
1653
+ interactive=False,
1654
+ elem_classes=["status-box"]
1655
+ )
1656
+
1657
+ # 输出区域
1658
+ gr.Markdown(f"#### 🎵 {t('results', 'cover')}")
1659
+
1660
+ with gr.Row():
1661
+ cover_output = gr.Audio(
1662
+ label=t("final_cover", "cover"),
1663
+ type="filepath",
1664
+ interactive=False
1665
+ )
1666
+
1667
+ with gr.Row():
1668
+ cover_converted_vocals_output = gr.Audio(
1669
+ label=t("converted_vocals", "cover"),
1670
+ type="filepath",
1671
+ interactive=False
1672
+ )
1673
+ cover_original_vocals_output = gr.Audio(
1674
+ label=t("original_vocals", "cover"),
1675
+ type="filepath",
1676
+ interactive=False
1677
+ )
1678
+
1679
+ with gr.Row():
1680
+ cover_lead_vocals_output = gr.Audio(
1681
+ label=t("lead_vocals", "cover"),
1682
+ type="filepath",
1683
+ interactive=False
1684
+ )
1685
+ cover_backing_vocals_output = gr.Audio(
1686
+ label=t("backing_vocals", "cover"),
1687
+ type="filepath",
1688
+ interactive=False
1689
+ )
1690
+
1691
+ with gr.Row():
1692
+ cover_accompaniment_output = gr.Audio(
1693
+ label=t("accompaniment", "cover"),
1694
+ type="filepath",
1695
+ interactive=False
1696
+ )
1697
+
1698
+ # 事件绑定
1699
+ refresh_char_btn.click(
1700
+ fn=refresh_downloaded_controls,
1701
+ inputs=[downloaded_series, downloaded_keyword],
1702
+ outputs=[downloaded_series, character_dropdown]
1703
+ )
1704
+
1705
+ downloaded_series.change(
1706
+ fn=update_downloaded_choices,
1707
+ inputs=[downloaded_series, downloaded_keyword],
1708
+ outputs=[character_dropdown]
1709
+ )
1710
+
1711
+ downloaded_keyword.change(
1712
+ fn=update_downloaded_choices,
1713
+ inputs=[downloaded_series, downloaded_keyword],
1714
+ outputs=[character_dropdown]
1715
+ )
1716
+
1717
+ download_series.change(
1718
+ fn=update_download_choices,
1719
+ inputs=[download_series, download_keyword],
1720
+ outputs=[download_char_dropdown]
1721
+ )
1722
+
1723
+ download_keyword.change(
1724
+ fn=update_download_choices,
1725
+ inputs=[download_series, download_keyword],
1726
+ outputs=[download_char_dropdown]
1727
+ )
1728
+
1729
+ download_char_btn.click(
1730
+ fn=download_character,
1731
+ inputs=[download_char_dropdown, downloaded_series, downloaded_keyword],
1732
+ outputs=[download_char_status, character_dropdown, downloaded_series]
1733
+ )
1734
+
1735
+ download_all_series_btn.click(
1736
+ fn=download_all_characters,
1737
+ inputs=[download_series, downloaded_series, downloaded_keyword],
1738
+ outputs=[download_char_status, character_dropdown, downloaded_series]
1739
+ )
1740
+
1741
+ download_all_btn.click(
1742
+ fn=lambda series, keyword: download_all_characters("全部", series, keyword),
1743
+ inputs=[downloaded_series, downloaded_keyword],
1744
+ outputs=[download_char_status, character_dropdown, downloaded_series]
1745
+ )
1746
+
1747
+ cover_mix_preset.change(
1748
+ fn=apply_cover_mix_preset,
1749
+ inputs=[cover_mix_preset],
1750
+ outputs=[
1751
+ cover_vocals_volume,
1752
+ cover_accompaniment_volume,
1753
+ cover_reverb
1754
+ ]
1755
+ )
1756
+
1757
+ mature_deecho_check_btn.click(
1758
+ fn=check_mature_deecho_status,
1759
+ outputs=[mature_deecho_status]
1760
+ )
1761
+ mature_deecho_check_btn.click(
1762
+ fn=get_cover_vc_route_status,
1763
+ inputs=[cover_vc_preprocess_mode, cover_vc_pipeline_mode],
1764
+ outputs=[cover_vc_route_status]
1765
+ )
1766
+
1767
+ mature_deecho_download_btn.click(
1768
+ fn=download_mature_deecho_models_ui,
1769
+ outputs=[mature_deecho_status]
1770
+ )
1771
+ mature_deecho_download_btn.click(
1772
+ fn=get_cover_vc_route_status,
1773
+ inputs=[cover_vc_preprocess_mode, cover_vc_pipeline_mode],
1774
+ outputs=[cover_vc_route_status]
1775
+ )
1776
+
1777
+ cover_vc_preprocess_mode.change(
1778
+ fn=get_cover_vc_route_status,
1779
+ inputs=[cover_vc_preprocess_mode, cover_vc_pipeline_mode],
1780
+ outputs=[cover_vc_route_status]
1781
+ )
1782
+
1783
+ cover_vc_pipeline_mode.change(
1784
+ fn=get_cover_vc_route_status,
1785
+ inputs=[cover_vc_preprocess_mode, cover_vc_pipeline_mode],
1786
+ outputs=[cover_vc_route_status]
1787
+ )
1788
+ cover_vc_pipeline_mode.change(
1789
+ fn=update_singing_repair_visibility,
1790
+ inputs=[cover_vc_pipeline_mode],
1791
+ outputs=[cover_singing_repair]
1792
+ )
1793
+
1794
+ cover_btn.click(
1795
+ fn=process_cover,
1796
+ inputs=[
1797
+ cover_input_audio,
1798
+ character_dropdown,
1799
+ cover_pitch_shift,
1800
+ cover_index_rate,
1801
+ cover_speaker_id,
1802
+ cover_karaoke,
1803
+ cover_karaoke_merge_backing,
1804
+ cover_vc_preprocess_mode,
1805
+ cover_source_constraint_mode,
1806
+ cover_vc_pipeline_mode,
1807
+ cover_singing_repair,
1808
+ cover_vocals_volume,
1809
+ cover_accompaniment_volume,
1810
+ cover_reverb,
1811
+ cover_rms_mix_rate,
1812
+ cover_backing_mix,
1813
+ ],
1814
+ outputs=[
1815
+ cover_output,
1816
+ cover_converted_vocals_output,
1817
+ cover_original_vocals_output,
1818
+ cover_lead_vocals_output,
1819
+ cover_backing_vocals_output,
1820
+ cover_accompaniment_output,
1821
+ cover_status
1822
+ ]
1823
+ )
1824
+
1825
+ # ===== 设置标签页 =====
1826
+ with gr.Tab(t("settings", "tabs")):
1827
+ gr.Markdown(f"### 💻 {t('device_info', 'settings')}")
1828
+
1829
+ device_info = gr.Textbox(
1830
+ label=t("current_device", "settings"),
1831
+ value=get_device_info(),
1832
+ interactive=False,
1833
+ lines=5,
1834
+ elem_classes=["status-box"]
1835
+ )
1836
+
1837
+ refresh_device_btn = gr.Button(
1838
+ f"🔄 {t('refresh_device', 'settings')}",
1839
+ variant="secondary"
1840
+ )
1841
+
1842
+ refresh_device_btn.click(
1843
+ fn=get_device_info,
1844
+ outputs=[device_info]
1845
+ )
1846
+
1847
+ gr.Markdown("---")
1848
+
1849
+ gr.Markdown(f"### ⚙️ 运行设置")
1850
+
1851
+ def _build_device_choices():
1852
+ from lib.device import _has_xpu, _has_directml, _has_mps, _is_rocm
1853
+ import torch
1854
+ choices = []
1855
+ if torch.cuda.is_available():
1856
+ label = "ROCm (AMD GPU)" if _is_rocm() else "CUDA (NVIDIA GPU)"
1857
+ choices.append((label, "cuda"))
1858
+ if _has_xpu():
1859
+ choices.append(("XPU (Intel GPU)", "xpu"))
1860
+ if _has_directml():
1861
+ choices.append(("DirectML (AMD/Intel GPU)", "directml"))
1862
+ if _has_mps():
1863
+ choices.append(("MPS (Apple GPU)", "mps"))
1864
+ choices.append(("CPU (较慢)", "cpu"))
1865
+ return choices
1866
+
1867
+ device_radio = gr.Radio(
1868
+ label="计算设备",
1869
+ choices=_build_device_choices(),
1870
+ value=config.get("device", "cuda")
1871
+ )
1872
+
1873
+ save_settings_btn = gr.Button(
1874
+ "💾 保存设置",
1875
+ variant="primary"
1876
+ )
1877
+
1878
+ settings_status = gr.Textbox(
1879
+ label="状态",
1880
+ interactive=False
1881
+ )
1882
+
1883
+ def save_settings(device):
1884
+ global config
1885
+ config["device"] = device
1886
+
1887
+ config_path = ROOT_DIR / "configs" / "config.json"
1888
+ with open(config_path, "w", encoding="utf-8") as f:
1889
+ json.dump(config, f, indent=4, ensure_ascii=False)
1890
+
1891
+ return "✅ 设置已保存,重启后生效"
1892
+
1893
+ save_settings_btn.click(
1894
+ fn=save_settings,
1895
+ inputs=[device_radio],
1896
+ outputs=[settings_status]
1897
+ )
1898
+
1899
+ gr.Markdown("---")
1900
+
1901
+ gr.Markdown(f"### ℹ️ {t('about', 'settings')}")
1902
+ gr.Markdown(
1903
+ """
1904
+ **RVC AI 翻唱系统**
1905
+
1906
+ - 基于 RVC v2 + Mel-Band Roformer
1907
+ - 使用 RMVPE 进行高质量 F0 提取
1908
+ - 支持 CUDA GPU 加速
1909
+
1910
+ [GitHub](https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI)
1911
+ """
1912
+ )
1913
+
1914
+ gr.Markdown("---")
1915
+
1916
+ gr.Markdown(
1917
+ """
1918
+ ### 📥 角色模型来源
1919
+
1920
+ 以下是本项目角色模型的 HuggingFace 仓库来源,你也可以手动下载模型后放入 `assets/weights/characters/<角色名>/` 目录使用:
1921
+
1922
+ **Love Live! 系列**
1923
+ - [trioskosmos/rvc_models](https://huggingface.co/trioskosmos/rvc_models) — μ's / Aqours / 虹咲 / Liella! 多角色
1924
+ - [Icchan/LoveLive](https://huggingface.co/Icchan/LoveLive) — 千歌、梨子、绘里、曜
1925
+ - [0xMifune/LoveLive](https://huggingface.co/0xMifune/LoveLive) — 虹咲 / Liella! / 莲之空
1926
+ - [Swordsmagus/Love-Live-RVC](https://huggingface.co/Swordsmagus/Love-Live-RVC) — 花丸、雪菜、小鸟、A-RISE 等
1927
+ - [Zurakichi/RVC](https://huggingface.co/Zurakichi/RVC) — 妮可、彼方、雪菜、花丸
1928
+ - [Phos252/RVCmodels](https://huggingface.co/Phos252/RVCmodels) — 涩谷香音
1929
+ - [ChocoKat/Mari_Ohara](https://huggingface.co/ChocoKat/Mari_Ohara) — 小原鞠莉
1930
+ - [HarunaKasuga/YoshikoTsushima](https://huggingface.co/HarunaKasuga/YoshikoTsushima) — 津岛善子
1931
+ - [thebuddyadrian/RVC_Models](https://huggingface.co/thebuddyadrian/RVC_Models) — 鹿角姐妹
1932
+
1933
+ **原神 / 崩坏 / 绝区零 (米哈游)**
1934
+ - [makiligon/RVC-Models](https://huggingface.co/makiligon/RVC-Models) — 芙宁娜、绫华、芙卡洛斯
1935
+ - [kohaku12/RVC-MODELS](https://huggingface.co/kohaku12/RVC-MODELS) — 纳西妲、黑塔、流萤、停云、星见雅 等
1936
+ - [jarari/RVC-v2](https://huggingface.co/jarari/RVC-v2) — 芙宁娜(韩语)、银狼(韩语)
1937
+ - [mrmocciai/genshin-impact](https://huggingface.co/mrmocciai/genshin-impact) — 原神 50+ 角色(需手动下载)
1938
+
1939
+ **VOCALOID**
1940
+ - [javinfamous/infamous_miku_v2](https://huggingface.co/javinfamous/infamous_miku_v2) — 初音未来 (1000 epochs)
1941
+
1942
+ **Hololive / VTuber**
1943
+ - [megaaziib/my-rvc-models-collection](https://huggingface.co/megaaziib/my-rvc-models-collection) — 佩克拉、樱巫女、大空昴、Kobo、Kaela 等
1944
+ - [Kit-Lemonfoot/kitlemonfoot_rvc_models](https://huggingface.co/Kit-Lemonfoot/kitlemonfoot_rvc_models) — Hololive JP/EN 多角色
1945
+
1946
+ **偶像大师 / 赛马娘**
1947
+ - [trioskosmos/rvc_models](https://huggingface.co/trioskosmos/rvc_models) — 神崎兰子、梦见莉亚梦
1948
+ - [makiligon/RVC-Models](https://huggingface.co/makiligon/RVC-Models) — 四条贵音、米浴
1949
+
1950
+ **Project SEKAI**
1951
+ - [kohaku12/RVC-MODELS](https://huggingface.co/kohaku12/RVC-MODELS) — 草薙宁宁
1952
+
1953
+ > 💡 手动下载后,将 `.pth` 和 `.index` 文件放入 `assets/weights/characters/<角色名>/` 目录,刷新即可使用。
1954
+ """
1955
+ )
1956
+
1957
+ return app
1958
+
1959
+
1960
+ def _patch_gradio_file_download(blocks):
1961
+ """
1962
+ Patch Gradio v3 的 /file= 路由,为文件添加 Content-Disposition header,
1963
+ 使浏览器下载时使用干净的文件名而非完整路径。
1964
+ """
1965
+ try:
1966
+ from starlette.responses import FileResponse
1967
+ from urllib.parse import quote
1968
+ import fastapi
1969
+
1970
+ def _clean_download_name(response: FileResponse, path_or_url: str) -> str:
1971
+ candidates = [
1972
+ getattr(response, "filename", None),
1973
+ getattr(response, "path", None),
1974
+ path_or_url,
1975
+ ]
1976
+ for candidate in candidates:
1977
+ if not candidate:
1978
+ continue
1979
+ name = Path(str(candidate)).name
1980
+ if not name:
1981
+ continue
1982
+ name = re.sub(
1983
+ r"^[A-Za-z]__.*?_gradio_[0-9a-f]{8,}_",
1984
+ "",
1985
+ name,
1986
+ flags=re.IGNORECASE,
1987
+ )
1988
+ if name:
1989
+ return name
1990
+ return "download"
1991
+
1992
+ fastapi_app = getattr(blocks, "server_app", None)
1993
+ if fastapi_app is None:
1994
+ return
1995
+
1996
+ for route in fastapi_app.routes:
1997
+ if hasattr(route, "path") and route.path == "/file={path_or_url:path}":
1998
+ original_endpoint = route.endpoint
1999
+
2000
+ async def patched_file(
2001
+ path_or_url: str,
2002
+ request: fastapi.Request,
2003
+ _orig=original_endpoint,
2004
+ ):
2005
+ response = await _orig(path_or_url, request=request)
2006
+ if isinstance(response, FileResponse) and "content-disposition" not in response.headers:
2007
+ basename = _clean_download_name(response, path_or_url)
2008
+ encoded = quote(basename)
2009
+ if encoded != basename:
2010
+ cd = f"inline; filename*=utf-8''{encoded}"
2011
+ else:
2012
+ cd = f'inline; filename="{basename}"'
2013
+ response.headers["content-disposition"] = cd
2014
+ return response
2015
+
2016
+ route.endpoint = patched_file
2017
+ break
2018
+ except Exception as e:
2019
+ log.warning(f"Patch Gradio file download failed: {e}")
2020
+
2021
+
2022
+ def launch(host: str = "127.0.0.1", port: int = 7860, share: bool = False):
2023
+ """启动 Gradio 界面"""
2024
+ app = create_ui()
2025
+ app.queue() # 启用队列以支持进度跟踪
2026
+ app.launch(
2027
+ server_name=host,
2028
+ server_port=port,
2029
+ share=share,
2030
+ inbrowser=True,
2031
+ prevent_thread_lock=True
2032
+ )
2033
+ _patch_gradio_file_download(app)
2034
+ app.block_thread()
2035
+
2036
+
2037
+ if __name__ == "__main__":
2038
+ launch()