AI-RVC / ui /app.py
mason369's picture
Upload folder using huggingface_hub
e103b22 verified
# -*- coding: utf-8 -*-
"""
Gradio 界面 - RVC AI 翻唱
"""
import os
import json
import re
import tempfile
import gradio as gr
from pathlib import Path
from typing import Optional, Tuple, Dict
from lib.logger import log
# 项目根目录
ROOT_DIR = Path(__file__).parent.parent
# 加载语言包
def load_i18n(lang: str = "zh_CN") -> dict:
"""加载语言包"""
i18n_path = ROOT_DIR / "i18n" / f"{lang}.json"
if i18n_path.exists():
with open(i18n_path, "r", encoding="utf-8") as f:
return json.load(f)
return {}
# 加载配置
def load_config() -> dict:
"""加载配置"""
config_path = ROOT_DIR / "configs" / "config.json"
if config_path.exists():
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def normalize_config(config: dict) -> dict:
"""Normalize legacy path keys to top-level entries."""
if not config:
return {}
paths = config.get("paths", {})
if "hubert_path" not in config and "hubert" in paths:
config["hubert_path"] = paths["hubert"]
if "rmvpe_path" not in config and "rmvpe" in paths:
config["rmvpe_path"] = paths["rmvpe"]
if "weights_dir" not in config and "weights" in paths:
config["weights_dir"] = paths["weights"]
if "output_dir" not in config and "outputs" in paths:
config["output_dir"] = paths["outputs"]
elif config.get("output_dir") == "output" and "outputs" in paths:
config["output_dir"] = paths["outputs"]
if "temp_dir" not in config and "temp" in paths:
config["temp_dir"] = paths["temp"]
return config
# 全局变量
i18n = load_i18n()
config = normalize_config(load_config())
pipeline = None
def t(key: str, section: str = None) -> str:
"""获取翻译文本"""
if section:
return i18n.get(section, {}).get(key, key)
return i18n.get(key, key)
def _to_int(value, fallback: int) -> int:
try:
return int(value)
except (TypeError, ValueError):
return fallback
def _to_float(value, fallback: float) -> float:
try:
return float(value)
except (TypeError, ValueError):
return fallback
def get_cover_mix_defaults() -> Dict[str, int]:
"""获取翻唱混音默认值"""
cover_cfg = config.get("cover", {})
return {
"vocals_volume": _to_int(cover_cfg.get("default_vocals_volume", 100), 100),
"accompaniment_volume": _to_int(cover_cfg.get("default_accompaniment_volume", 100), 100),
"reverb": _to_int(cover_cfg.get("default_reverb", 10), 10),
}
def get_cover_mix_presets() -> Tuple[Dict[str, Dict[str, int]], str]:
"""获取混音预设与默认预设名称"""
defaults = get_cover_mix_defaults()
presets = {
t("mix_preset_universal", "cover"): defaults.copy(),
t("mix_preset_vocal", "cover"): {
"vocals_volume": min(200, defaults["vocals_volume"] + 15),
"accompaniment_volume": max(0, defaults["accompaniment_volume"] - 10),
"reverb": max(0, defaults["reverb"] - 5),
},
t("mix_preset_accompaniment", "cover"): {
"vocals_volume": max(0, defaults["vocals_volume"] - 10),
"accompaniment_volume": min(200, defaults["accompaniment_volume"] + 15),
"reverb": max(0, defaults["reverb"] - 5),
},
t("mix_preset_live", "cover"): {
"vocals_volume": defaults["vocals_volume"],
"accompaniment_volume": defaults["accompaniment_volume"],
"reverb": min(100, defaults["reverb"] + 10),
},
}
default_name = t("mix_preset_universal", "cover")
return presets, default_name
def apply_cover_mix_preset(preset_name: str) -> Tuple[int, int, int]:
"""根据预设名称返回混音参数"""
presets, default_name = get_cover_mix_presets()
preset = presets.get(preset_name) or presets[default_name]
return preset["vocals_volume"], preset["accompaniment_volume"], preset["reverb"]
def get_vc_preprocess_option_maps() -> Tuple[Dict[str, str], Dict[str, str]]:
"""Build VC preprocess dropdown option maps."""
label_to_value = {
t("vc_preprocess_auto", "cover"): "auto",
t("vc_preprocess_direct", "cover"): "direct",
t("vc_preprocess_uvr_deecho", "cover"): "uvr_deecho",
t("vc_preprocess_legacy", "cover"): "legacy",
}
value_to_label = {value: label for label, value in label_to_value.items()}
return label_to_value, value_to_label
def get_source_constraint_option_maps() -> Tuple[Dict[str, str], Dict[str, str]]:
"""Build source constraint dropdown option maps."""
label_to_value = {
t("source_constraint_auto", "cover"): "auto",
t("source_constraint_off", "cover"): "off",
t("source_constraint_on", "cover"): "on",
}
value_to_label = {value: label for label, value in label_to_value.items()}
return label_to_value, value_to_label
def get_vc_pipeline_mode_option_maps() -> Tuple[Dict[str, str], Dict[str, str]]:
"""Build VC pipeline mode dropdown option maps."""
label_to_value = {
t("vc_pipeline_mode_current", "cover"): "current",
t("vc_pipeline_mode_official", "cover"): "official",
}
value_to_label = {value: label for label, value in label_to_value.items()}
return label_to_value, value_to_label
def update_singing_repair_visibility(vc_pipeline_mode: str):
"""Only show singing repair option for official mode."""
pipeline_label_to_value, _ = get_vc_pipeline_mode_option_maps()
normalized = pipeline_label_to_value.get(
str(vc_pipeline_mode),
str(vc_pipeline_mode or "").strip().lower(),
)
return gr.update(visible=(normalized == "official"))
def init_pipeline():
"""初始化推理管道"""
global pipeline
if pipeline is not None:
return pipeline
from infer.pipeline import VoiceConversionPipeline
device = config.get("device", "cuda")
pipeline = VoiceConversionPipeline(device=device)
pipeline.hubert_layer = config.get("hubert_layer", 12)
# 加载 HuBERT
hubert_path = ROOT_DIR / config.get("hubert_path", "assets/hubert/hubert_base.pt")
if hubert_path.exists():
pipeline.load_hubert(str(hubert_path))
# 加载 F0 提取器
rmvpe_path = ROOT_DIR / config.get("rmvpe_path", "assets/rmvpe/rmvpe.pt")
if rmvpe_path.exists():
pipeline.load_f0_extractor("rmvpe", str(rmvpe_path))
return pipeline
def download_base_models() -> str:
"""下载基础模型"""
from tools.download_models import download_required_models
try:
success = download_required_models()
if success:
return t("download_complete", "messages")
else:
return "下载过程中出现错误,请检查网络连接"
except Exception as e:
return f"{t('download_failed', 'messages')}: {str(e)}"
# ===== 翻唱功能相关函数 =====
def get_downloaded_character_list() -> list:
"""获取已下载的角色列表"""
from tools.character_models import list_downloaded_characters
return list_downloaded_characters()
def get_downloaded_character_series() -> list:
"""获取已下载角色的系列列表"""
characters = get_downloaded_character_list()
series = sorted({c.get("series", "未知") for c in characters})
return ["全部"] + series
def get_available_character_list() -> list:
"""获取可下载的角色列表"""
from tools.character_models import list_available_characters
return list_available_characters()
def get_available_character_series() -> list:
"""获取可用系列列表"""
from tools.character_models import list_available_series
return list_available_series()
def format_character_label(char_info: dict) -> str:
"""格式化角色展示名称:【语言】角色名(中/英/日) · 出处 · 内部名"""
display = char_info.get("display") or char_info.get("description") or char_info.get("name", "")
source = char_info.get("source", "未知")
name = char_info.get("name", "")
lang_tag = get_character_language_tag(char_info)
return f"【{lang_tag}{display}(出自:{source})[{name}]"
def get_character_language_tag(char_info: dict) -> str:
"""推断语言类型,用于下拉前缀标签"""
lang = char_info.get("lang")
if lang:
return lang
text = " ".join(
str(char_info.get(k, "")) for k in ("display", "description", "name")
).lower()
if "韩" in text or "kr" in text or "korean" in text:
return "韩文"
if "日" in text or "jp" in text or "japanese" in text:
return "日文"
if "中" in text or "cn" in text or "chinese" in text:
return "中文"
if "en" in text or "english" in text:
return "英文"
source = char_info.get("source", "")
if source.startswith("Love Live!") or "ホロライブ" in source or "偶像大师" in source or "赛马娘" in source:
return "日文"
if "原神" in source or "崩坏" in source or "明日方舟" in source or "碧蓝航线" in source:
return "中文"
if "VOCALOID" in source or "Project SEKAI" in source:
return "日文"
if "Hololive" in source:
return "日文"
if "蔚蓝档案" in source or "绝区零" in source:
return "日文"
return "中文"
def get_downloaded_character_choices(series: str = "全部", keyword: str = "") -> list:
"""获取已下载角色的下拉选项"""
chars = get_downloaded_character_list()
if series and series != "全部":
chars = [c for c in chars if c.get("series") == series]
if keyword:
kw = keyword.strip().lower()
if kw:
chars = [
c for c in chars
if kw in c.get("name", "").lower()
or kw in c.get("display", "").lower()
or kw in c.get("source", "").lower()
]
return [(format_character_label(c), c["name"]) for c in chars]
def resolve_character_name(selection: str) -> str:
"""将下拉显示文本解析为实际角色名"""
if not selection:
return selection
from tools.character_models import list_downloaded_characters
for c in list_downloaded_characters():
if selection == c.get("name") or selection == format_character_label(c):
return c.get("name")
if " · " in selection:
return selection.split(" · ")[-1].strip()
parts = selection.strip().split()
return parts[-1] if parts else selection
def get_available_character_choices(series: str = "全部", keyword: str = "") -> list:
"""获取可下载角色的下拉选项"""
chars = get_available_character_list()
if series and series != "全部":
chars = [c for c in chars if c.get("series") == series]
if keyword:
kw = keyword.strip().lower()
if kw:
chars = [
c for c in chars
if kw in c.get("name", "").lower()
or kw in c.get("display", "").lower()
or kw in c.get("source", "").lower()
]
return [(format_character_label(c), c["name"]) for c in chars]
def _refresh_downloaded_updates(series: str, keyword: str) -> Tuple[Dict, Dict]:
series_choices = get_downloaded_character_series()
if series not in series_choices:
series = "全部"
return (
gr.update(choices=series_choices, value=series),
gr.update(choices=get_downloaded_character_choices(series, keyword))
)
def download_character(name: str, selected_series: str = "全部", keyword: str = "") -> Tuple[str, Dict, Dict]:
"""下载角色模型"""
from tools.character_models import download_character_model
if not name:
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
return "请选择要下载的角色", choices_update, series_update
try:
success = download_character_model(name)
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
if success:
return (
f"✅ {name} 模型下载完成",
choices_update,
series_update
)
else:
return (
f"❌ {name} 模型下载失败",
choices_update,
series_update
)
except Exception as e:
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
return (
f"❌ 下载失败: {str(e)}",
choices_update,
series_update
)
def download_all_characters(series: str = "全部", selected_series: str = "全部", keyword: str = "") -> Tuple[str, Dict, Dict]:
"""批量下载角色模型"""
from tools.character_models import download_all_character_models
try:
result = download_all_character_models(series=series)
ok = result.get("success", [])
failed = result.get("failed", [])
status = f"✅ 完成: 成功 {len(ok)} 个"
if failed:
status += f",失败 {len(failed)} 个: {', '.join(failed)}"
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
return status, choices_update, series_update
except Exception as e:
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
return f"❌ 批量下载失败: {str(e)}", choices_update, series_update
def update_download_choices(series: str, keyword: str) -> Dict:
"""更新下载下拉列表"""
return gr.update(choices=get_available_character_choices(series, keyword))
def update_downloaded_choices(series: str, keyword: str) -> Dict:
"""更新已下载角色下拉列表"""
return gr.update(choices=get_downloaded_character_choices(series, keyword))
def refresh_downloaded_controls(series: str, keyword: str) -> Tuple[Dict, Dict]:
"""刷新已下载角色的筛选和列表"""
return _refresh_downloaded_updates(series, keyword)
def process_cover(
audio_path: str,
character_name: str,
pitch_shift: int,
index_ratio: float,
speaker_id: float,
karaoke_separation: bool,
karaoke_merge_backing_into_accompaniment: bool,
vc_preprocess_mode: str,
source_constraint_mode: str,
vc_pipeline_mode: str,
singing_repair: bool,
vocals_volume: float,
accompaniment_volume: float,
reverb_amount: float,
rms_mix_rate: float,
backing_mix: float,
progress=gr.Progress()
) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], Optional[str], Optional[str], str]:
"""
处理翻唱
Returns:
Tuple[cover, converted_vocals, original_vocals, lead_vocals, backing_vocals, accompaniment, status]
"""
_none6 = (None, None, None, None, None, None)
if audio_path is None:
return *_none6, "请上传歌曲文件"
if not character_name:
return *_none6, "请选择角色"
try:
from tools.character_models import get_character_model_path
from infer.cover_pipeline import get_cover_pipeline
# 获取角色模型路径
resolved_name = resolve_character_name(character_name)
model_info = get_character_model_path(resolved_name)
if model_info is None:
return *_none6, f"角色模型不存在: {resolved_name}"
# 进度回调
def progress_callback(msg: str, step: int, total: int):
if total > 0:
progress(step / total, desc=msg)
# 获取流水线
device = config.get("device", "cuda")
pipeline = get_cover_pipeline(device)
cover_cfg = config.get("cover", {})
demucs_model = cover_cfg.get("demucs_model", "htdemucs")
demucs_shifts = int(cover_cfg.get("demucs_shifts", 2))
demucs_overlap = float(cover_cfg.get("demucs_overlap", 0.25))
demucs_split = bool(cover_cfg.get("demucs_split", True))
separator = cover_cfg.get("separator", "roformer")
uvr5_model = cover_cfg.get("uvr5_model")
uvr5_agg = int(cover_cfg.get("uvr5_agg", 10))
uvr5_format = cover_cfg.get("uvr5_format", "wav")
use_official = bool(cover_cfg.get("use_official", True))
f0_method = cover_cfg.get("f0_method", config.get("f0_method", "rmvpe"))
filter_radius = cover_cfg.get("filter_radius", config.get("filter_radius", 3))
protect = cover_cfg.get("protect", config.get("protect", 0.33))
silence_gate = cover_cfg.get("silence_gate", True)
silence_threshold_db = cover_cfg.get("silence_threshold_db", -40.0)
silence_smoothing_ms = cover_cfg.get("silence_smoothing_ms", 50.0)
silence_min_duration_ms = cover_cfg.get("silence_min_duration_ms", 200.0)
hubert_layer = cover_cfg.get("hubert_layer", config.get("hubert_layer", 12))
karaoke_model = cover_cfg.get("karaoke_model", "mel_band_roformer_karaoke_gabox.ckpt")
default_vc_preprocess_mode = str(cover_cfg.get("vc_preprocess_mode", "auto"))
default_source_constraint_mode = str(cover_cfg.get("source_constraint_mode", "auto"))
default_vc_pipeline_mode = str(cover_cfg.get("vc_pipeline_mode", "current"))
default_singing_repair = bool(cover_cfg.get("singing_repair", False))
vc_label_to_value, vc_value_to_label = get_vc_preprocess_option_maps()
source_label_to_value, source_value_to_label = get_source_constraint_option_maps()
pipeline_label_to_value, pipeline_value_to_label = get_vc_pipeline_mode_option_maps()
vc_preprocess_mode = vc_label_to_value.get(str(vc_preprocess_mode), str(vc_preprocess_mode or default_vc_preprocess_mode).strip().lower())
if vc_preprocess_mode not in {"auto", "direct", "uvr_deecho", "legacy"}:
vc_preprocess_mode = default_vc_preprocess_mode
source_constraint_mode = source_label_to_value.get(str(source_constraint_mode), str(source_constraint_mode or default_source_constraint_mode).strip().lower())
if source_constraint_mode not in {"auto", "off", "on"}:
source_constraint_mode = default_source_constraint_mode
vc_pipeline_mode = pipeline_label_to_value.get(str(vc_pipeline_mode), str(vc_pipeline_mode or default_vc_pipeline_mode).strip().lower())
if vc_pipeline_mode not in {"current", "official"}:
vc_pipeline_mode = default_vc_pipeline_mode
singing_repair = bool(singing_repair if singing_repair is not None else default_singing_repair)
index_ratio = max(0.0, min(1.0, float(index_ratio) / 100.0))
speaker_id = int(max(0, round(float(speaker_id))))
rms_mix_rate = max(0.0, min(1.0, float(rms_mix_rate) / 100.0))
backing_mix = max(0.0, min(1.0, float(backing_mix) / 100.0))
# 输出目录
output_dir = ROOT_DIR / config.get("paths", {}).get(
"outputs",
config.get("output_dir", "outputs")
)
# 执行翻唱
result = pipeline.process(
input_audio=audio_path,
model_path=model_info["model_path"],
index_path=model_info.get("index_path"),
pitch_shift=pitch_shift,
index_ratio=index_ratio,
filter_radius=filter_radius,
rms_mix_rate=rms_mix_rate,
protect=protect,
speaker_id=speaker_id,
f0_method=f0_method,
demucs_model=demucs_model,
demucs_shifts=demucs_shifts,
demucs_overlap=demucs_overlap,
demucs_split=demucs_split,
separator=separator,
uvr5_model=uvr5_model,
uvr5_agg=uvr5_agg,
uvr5_format=uvr5_format,
use_official=use_official,
hubert_layer=hubert_layer,
silence_gate=silence_gate,
silence_threshold_db=silence_threshold_db,
silence_smoothing_ms=silence_smoothing_ms,
silence_min_duration_ms=silence_min_duration_ms,
vocals_volume=vocals_volume / 100, # 转换为 0-2 范围
accompaniment_volume=accompaniment_volume / 100,
reverb_amount=reverb_amount / 100,
backing_mix=backing_mix,
karaoke_separation=bool(karaoke_separation),
karaoke_model=karaoke_model,
karaoke_merge_backing_into_accompaniment=bool(karaoke_merge_backing_into_accompaniment),
vc_preprocess_mode=vc_preprocess_mode,
source_constraint_mode=source_constraint_mode,
vc_pipeline_mode=vc_pipeline_mode,
singing_repair=singing_repair,
output_dir=str(output_dir),
model_display_name=resolved_name,
progress_callback=progress_callback
)
status_msg = "\u2705 \u7ffb\u5531\u5b8c\u6210!"
status_msg += f"\n{get_cover_vc_route_status(vc_preprocess_mode, vc_pipeline_mode).splitlines()[0]}"
status_msg += f"\nVC\u7ba1\u7ebf\u6a21\u5f0f: {pipeline_value_to_label.get(vc_pipeline_mode, vc_pipeline_mode)}"
status_msg += f"\n唱歌修复: {'开启' if singing_repair else '关闭'}"
status_msg += f"\n\u6e90\u7ea6\u675f\u7b56\u7565: {source_value_to_label.get(source_constraint_mode, source_constraint_mode)}"
if result.get("all_files_dir"):
status_msg += f"\n\u5168\u90e8\u6587\u4ef6\u76ee\u5f55: {result['all_files_dir']}"
return (
result["cover"],
result["converted_vocals"],
result.get("vocals"),
result.get("lead_vocals"),
result.get("backing_vocals"),
result["accompaniment"],
status_msg
)
except Exception as e:
import traceback
error_msg = str(e) if str(e) else traceback.format_exc()
log.error(f"处理失败: {error_msg}")
return None, None, None, None, None, None, f"❌ 处理失败: {error_msg}"
def check_mature_deecho_status() -> str:
"""Check mature DeEcho model availability."""
from tools.download_models import MATURE_DEECHO_MODELS, check_model, get_preferred_mature_deecho_model
status_lines = []
preferred = get_preferred_mature_deecho_model()
for name in MATURE_DEECHO_MODELS:
exists = check_model(name)
icon = "✅" if exists else "❌"
suffix = " ← 当前自动模式优先使用" if preferred == name else ""
status_lines.append(f"{icon} {name}{suffix}")
if preferred:
status_lines.append("")
status_lines.append(f"当前可用学习型 DeEcho: {preferred}")
else:
status_lines.append("")
status_lines.append("当前未检测到学习型 DeEcho 模型;翻唱自动模式将回退为主唱直通 RVC")
return "\n".join(status_lines)
def download_mature_deecho_models_ui() -> str:
"""Download mature DeEcho models."""
from tools.download_models import download_mature_deecho_models
try:
success = download_mature_deecho_models()
status = check_mature_deecho_status()
prefix = "✅ 下载完成" if success else "⚠️ 下载过程中存在失败项"
return f"{prefix}\n\n{status}"
except Exception as e:
return f"❌ 下载失败: {str(e)}"
def get_cover_vc_route_status(
vc_preprocess_mode: Optional[str] = None,
vc_pipeline_mode: Optional[str] = None,
) -> str:
"""Return the active VC route shown in the cover UI."""
from tools.download_models import get_preferred_mature_deecho_model
mode = str(vc_preprocess_mode or config.get("cover", {}).get("vc_preprocess_mode", "auto")).strip().lower()
pipeline_mode = str(vc_pipeline_mode or config.get("cover", {}).get("vc_pipeline_mode", "current")).strip().lower()
vc_label_to_value, _ = get_vc_preprocess_option_maps()
pipeline_label_to_value, _ = get_vc_pipeline_mode_option_maps()
mode = vc_label_to_value.get(mode, mode)
pipeline_mode = pipeline_label_to_value.get(pipeline_mode, pipeline_mode)
preferred = get_preferred_mature_deecho_model()
newline = chr(10)
if pipeline_mode == "official":
return newline.join([
"当前使用内置官方 RVC 实现",
"流程:主唱分离 → 官方音频加载 / 官方 VC → 混音",
"说明:跳过本项目自定义 VC 预处理、源约束与静音门限后处理",
])
if mode == "direct":
return newline.join([
"ℹ️ 当前固定为主唱直通 RVC",
"流程: 主唱分离 → 直接进入 RVC → 混音",
"说明: 不使用学习型 DeEcho,也不走旧版手工链",
])
if mode == "legacy":
return newline.join([
"⚠️ 当前固定为旧版手工链",
"流程: 主唱分离 → 手工去回声链 → RVC → 混音",
"说明: 仅用于对比,不是默认推荐路径",
])
if mode == "uvr_deecho":
if preferred:
return newline.join([
"✅ 当前固定优先使用学习型 DeEcho / DeReverb",
f"当前命中模型: {preferred}",
"流程: 主唱分离 → UVR DeEcho/DeReverb → RVC → 混音",
])
return newline.join([
"⚠️ 当前设为官方 DeEcho 优先,但本地缺少模型",
"当前将回退流程: 主唱分离 → 直接进入 RVC → 混音",
"建议: 先在模型管理页下载成熟 DeEcho 模型",
])
if preferred:
return newline.join([
"✅ 自动模式当前会优先使用学习型 DeEcho / DeReverb",
f"当前命中模型: {preferred}",
"流程: 主唱分离 → UVR DeEcho/DeReverb → RVC → 混音",
])
return newline.join([
"ℹ️ 自动模式当前会回退为主唱直通 RVC",
"原因: 本地未检测到成熟 DeEcho / DeReverb 模型",
"流程: 主唱分离 → 直接进入 RVC → 混音",
])
def check_models_status() -> str:
"""检查模型状态"""
from tools.download_models import check_model, REQUIRED_MODELS
status_lines = []
for name in REQUIRED_MODELS:
exists = check_model(name)
icon = "✅" if exists else "❌"
status_lines.append(f"{icon} {name}")
return "\n".join(status_lines)
def get_device_info() -> str:
"""获取设备信息"""
import torch
from lib.device import get_device_info as _get_info, _is_rocm, _has_xpu, _has_directml, _has_mps
lines = []
lines.append(f"PyTorch 版本: {torch.__version__}")
info = _get_info()
lines.append(f"可用后端: {', '.join(info['backends'])}")
for dev in info["devices"]:
mem = f"{dev['total_memory_gb']} GB" if dev.get("total_memory_gb") else "N/A"
lines.append(f"GPU: {dev['name']} ({dev['backend']}) - 显存: {mem}")
if torch.cuda.is_available():
ver = torch.version.hip if _is_rocm() else torch.version.cuda
label = "ROCm" if _is_rocm() else "CUDA"
lines.append(f"{label} 版本: {ver}")
if not info["devices"]:
lines.append("未检测到 GPU,将使用 CPU")
return "\n".join(lines)
# 自定义 CSS - 深灰 + 橙色强调配色
CUSTOM_CSS = """
/* 深色主题基础 - 纯色背景 */
.gradio-container {
background: #121212 !important;
min-height: 100vh;
}
.main-title {
text-align: center;
margin-bottom: 1rem;
color: #e0e0e0 !important;
}
/* 状态框样式 */
.status-box {
font-family: 'Consolas', 'Monaco', monospace;
white-space: pre-wrap;
background: #1e1e1e !important;
border: 1px solid #404040 !important;
color: #9e9e9e !important;
}
/* 提示框 */
.model-hint {
padding: 1rem;
background: #1e1e1e !important;
border: 1px solid #404040 !important;
border-radius: 8px;
margin: 1rem 0;
color: #e0e0e0 !important;
}
/* 成功/错误消息 */
.success-msg {
color: #4caf50 !important;
font-weight: bold;
}
.error-msg {
color: #f44336 !important;
font-weight: bold;
}
/* 标签页样式 */
.tabs > .tab-nav {
background: #1e1e1e !important;
border-bottom: 1px solid #404040 !important;
}
.tabs > .tab-nav > button {
color: #9e9e9e !important;
background: transparent !important;
border: none !important;
padding: 12px 24px !important;
transition: color 0.2s ease !important;
}
.tabs > .tab-nav > button:hover {
color: #e0e0e0 !important;
}
.tabs > .tab-nav > button.selected {
color: #ff9800 !important;
border-bottom: 2px solid #ff9800 !important;
background: transparent !important;
}
/* 输入框和下拉框 */
.gr-input, .gr-dropdown, textarea, input[type="text"] {
background: #2d2d2d !important;
border: 1px solid #404040 !important;
color: #e0e0e0 !important;
}
.gr-input:focus, .gr-dropdown:focus, textarea:focus, input[type="text"]:focus {
border-color: #ff9800 !important;
outline: none !important;
}
/* 滑块 */
.gr-slider input[type="range"] {
background: #404040 !important;
}
.gr-slider input[type="range"]::-webkit-slider-thumb {
background: #ff9800 !important;
}
.gr-slider input[type="range"]::-moz-range-thumb {
background: #ff9800 !important;
}
input[type="range"]::-webkit-slider-runnable-track {
background: #404040 !important;
}
input[type="range"]::-moz-range-track {
background: #404040 !important;
}
/* 按钮样式 - 主按钮橙色 */
.gr-button-primary, button.primary {
background: #ff9800 !important;
border: none !important;
color: #121212 !important;
font-weight: 600 !important;
transition: all 0.2s ease !important;
}
.gr-button-primary:hover, button.primary:hover {
background: #ffa726 !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3) !important;
}
.gr-button-primary:active, button.primary:active {
background: #f57c00 !important;
transform: translateY(0) !important;
}
/* 次要按钮 */
.gr-button-secondary, button.secondary {
background: #404040 !important;
border: none !important;
color: #e0e0e0 !important;
transition: all 0.2s ease !important;
}
.gr-button-secondary:hover, button.secondary:hover {
background: #4a4a4a !important;
}
/* 音频播放器 */
.gr-audio {
background: #1e1e1e !important;
border: 1px solid #404040 !important;
border-radius: 8px !important;
}
.gr-audio audio {
background: #1e1e1e !important;
color: #e0e0e0 !important;
accent-color: #ff9800 !important;
}
.gr-audio audio::-webkit-media-controls-panel {
background: #1e1e1e !important;
}
.gr-audio audio::-webkit-media-controls-enclosure {
background: #1e1e1e !important;
}
.gr-audio audio::-webkit-media-controls-timeline {
background: #404040 !important;
}
.gr-audio audio::-webkit-media-controls-current-time-display,
.gr-audio audio::-webkit-media-controls-time-remaining-display {
color: #e0e0e0 !important;
}
.gr-audio audio::-webkit-media-controls-play-button,
.gr-audio audio::-webkit-media-controls-mute-button,
.gr-audio audio::-webkit-media-controls-volume-slider {
filter: invert(1) sepia(1) saturate(5) hue-rotate(10deg) !important;
}
/* 折叠面板 */
.gr-accordion {
background: #1e1e1e !important;
border: 1px solid #404040 !important;
border-radius: 8px !important;
}
.gr-accordion > .label-wrap {
background: #1e1e1e !important;
}
/* 表格 */
.gr-dataframe {
background: #1e1e1e !important;
}
.gr-dataframe table {
color: #e0e0e0 !important;
}
.gr-dataframe th {
background: #2d2d2d !important;
color: #9e9e9e !important;
}
.gr-dataframe td {
background: #1e1e1e !important;
border-color: #404040 !important;
}
.gr-dataframe tr:hover td {
background: #333333 !important;
}
/* Gradio v4 Dataframe */
div[data-testid="dataframe"] {
background: #1e1e1e !important;
color: #e0e0e0 !important;
border: 1px solid #404040 !important;
}
div[data-testid="dataframe"] table {
color: #e0e0e0 !important;
}
div[data-testid="dataframe"] thead th {
background: #2d2d2d !important;
color: #9e9e9e !important;
border-color: #404040 !important;
}
div[data-testid="dataframe"] tbody td {
background: #1e1e1e !important;
color: #e0e0e0 !important;
border-color: #404040 !important;
}
div[data-testid="dataframe"] tbody tr:hover td {
background: #333333 !important;
}
div[data-testid="dataframe"] input,
div[data-testid="dataframe"] textarea {
background: #1e1e1e !important;
color: #e0e0e0 !important;
border: 1px solid #404040 !important;
}
/* Markdown 文本 */
.prose {
color: #e0e0e0 !important;
}
.prose h1, .prose h2, .prose h3, .prose h4 {
color: #e0e0e0 !important;
}
.prose a {
color: #ff9800 !important;
}
.prose a:hover {
color: #ffa726 !important;
}
.prose code {
background: #2d2d2d !important;
color: #ff9800 !important;
padding: 2px 6px !important;
border-radius: 4px !important;
}
.prose blockquote {
border-left: 3px solid #ff9800 !important;
background: #1e1e1e !important;
padding: 8px 16px !important;
color: #9e9e9e !important;
}
/* 单选按钮和复选框 */
.gr-radio label, .gr-checkbox label {
color: #e0e0e0 !important;
}
input[type="radio"]:checked + label, input[type="checkbox"]:checked + label {
color: #ff9800 !important;
}
/* 进度条 */
.progress-bar {
background: #404040 !important;
}
.progress-bar > div {
background: #ff9800 !important;
}
/* 分隔线 */
hr {
border-color: #404040 !important;
}
/* 标签 */
label {
color: #9e9e9e !important;
}
/* 信息文本 */
.gr-info {
color: #9e9e9e !important;
}
/* 块/面板背景 */
.gr-block, .gr-box, .gr-panel {
background: #1e1e1e !important;
border-color: #404040 !important;
}
/* 下拉菜单选项 */
.gr-dropdown option, select option {
background: #2d2d2d !important;
color: #e0e0e0 !important;
}
/* Gradio 下拉选择器完整样式 */
.gr-dropdown, .gr-dropdown select,
div[data-testid="dropdown"],
.dropdown-container,
.svelte-select,
.wrap-inner,
.secondary-wrap {
background: #2d2d2d !important;
border: 1px solid #404040 !important;
color: #e0e0e0 !important;
}
/* 下拉选择器输入框 */
.gr-dropdown input,
div[data-testid="dropdown"] input,
.svelte-select input {
background: #2d2d2d !important;
color: #e0e0e0 !important;
border: none !important;
}
/* 下拉菜单列表 */
.gr-dropdown ul,
.gr-dropdown .options,
div[data-testid="dropdown"] ul,
.svelte-select .listContainer,
.dropdown-menu,
ul[role="listbox"] {
background: #2d2d2d !important;
border: 1px solid #404040 !important;
color: #e0e0e0 !important;
}
/* 下拉菜单选项 */
.gr-dropdown li,
.gr-dropdown .option,
div[data-testid="dropdown"] li,
.svelte-select .listItem,
li[role="option"] {
background: #2d2d2d !important;
color: #e0e0e0 !important;
}
/* 下拉菜单选项悬停 */
.gr-dropdown li:hover,
.gr-dropdown .option:hover,
div[data-testid="dropdown"] li:hover,
.svelte-select .listItem:hover,
.svelte-select .listItem.hover,
li[role="option"]:hover {
background: #404040 !important;
color: #ff9800 !important;
}
/* 下拉菜单选中项 */
.gr-dropdown li.selected,
.gr-dropdown .option.selected,
.svelte-select .listItem.active,
li[role="option"][aria-selected="true"] {
background: #333333 !important;
color: #ff9800 !important;
}
/* 下拉箭头图标 */
.gr-dropdown svg,
div[data-testid="dropdown"] svg,
.svelte-select .indicator svg {
fill: #9e9e9e !important;
color: #9e9e9e !important;
}
/* Gradio 3.x 特定选择器样式 */
.wrap.svelte-1m1zvyj,
.wrap-inner.svelte-1m1zvyj,
.secondary-wrap.svelte-1m1zvyj {
background: #2d2d2d !important;
border-color: #404040 !important;
}
.dropdown.svelte-1m1zvyj,
.options.svelte-1m1zvyj {
background: #2d2d2d !important;
border: 1px solid #404040 !important;
}
.item.svelte-1m1zvyj {
background: #2d2d2d !important;
color: #e0e0e0 !important;
}
.item.svelte-1m1zvyj:hover,
.item.svelte-1m1zvyj.active {
background: #404040 !important;
color: #ff9800 !important;
}
/* 单选按钮组样式 */
.gr-radio,
.gr-radio-group,
div[data-testid="radio"] {
background: transparent !important;
}
.gr-radio label span,
div[data-testid="radio"] label span {
color: #e0e0e0 !important;
}
.gr-radio input[type="radio"],
div[data-testid="radio"] input[type="radio"] {
accent-color: #ff9800 !important;
}
/* Radio 按钮容器 */
.radio-group,
.gr-radio-row {
background: #1e1e1e !important;
}
.radio-group label,
.gr-radio-row label {
background: #2d2d2d !important;
border: 1px solid #404040 !important;
color: #e0e0e0 !important;
}
.radio-group label:hover,
.gr-radio-row label:hover {
background: #333333 !important;
}
.radio-group label.selected,
.gr-radio-row label.selected,
.radio-group input:checked + label,
.gr-radio-row input:checked + label {
background: #333333 !important;
border-color: #ff9800 !important;
color: #ff9800 !important;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1e1e1e;
}
::-webkit-scrollbar-thumb {
background: #404040;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #4a4a4a;
}
/* Dataframe 表头修复 - 强制深色主题 */
table thead th,
table thead td,
.table-wrap thead th,
.table-wrap thead td,
[data-testid="table"] thead th,
[data-testid="table"] thead td {
background: #2d2d2d !important;
color: #ff9800 !important;
border-color: #404040 !important;
}
/* Gradio 4.x Dataframe 表头 */
.svelte-1kcgrqr thead th,
.svelte-1kcgrqr thead td,
.cell-wrap span,
th .cell-wrap,
th span.svelte-1kcgrqr {
background: #2d2d2d !important;
color: #ff9800 !important;
}
/* 音频播放器进度条修复 */
audio::-webkit-media-controls-timeline {
background: linear-gradient(to right, #ff9800 var(--buffered-width, 0%), #404040 var(--buffered-width, 0%)) !important;
border-radius: 4px !important;
height: 4px !important;
}
/* 音频播放器 - Gradio 组件内部 */
.audio-container input[type="range"],
.waveform-container input[type="range"],
div[data-testid="audio"] input[type="range"],
div[data-testid="waveform"] input[type="range"] {
accent-color: #ff9800 !important;
}
/* WaveSurfer 波形进度条 */
.wavesurfer-region,
.wavesurfer-handle,
wave > wave {
background: #ff9800 !important;
}
/* Gradio Audio 组件进度条 */
.audio-player input[type="range"]::-webkit-slider-runnable-track {
background: linear-gradient(to right, #ff9800 0%, #ff9800 var(--value, 0%), #404040 var(--value, 0%), #404040 100%) !important;
}
.audio-player input[type="range"]::-moz-range-track {
background: linear-gradient(to right, #ff9800 0%, #ff9800 var(--value, 0%), #404040 var(--value, 0%), #404040 100%) !important;
}
.audio-player input[type="range"]::-webkit-slider-thumb {
background: #ff9800 !important;
}
.audio-player input[type="range"]::-moz-range-thumb {
background: #ff9800 !important;
}
/* 通用 range input 进度样式 */
input[type="range"] {
accent-color: #ff9800 !important;
}
/* Gradio 4.x 音频波形 */
.waveform-container,
.audio-container {
--waveform-color: #ff9800 !important;
--progress-color: #ff9800 !important;
}
"""
def create_ui() -> gr.Blocks:
"""创建 Gradio 界面"""
with gr.Blocks(
title=i18n.get("app_title", "RVC AI 翻唱"),
theme=gr.themes.Base(
primary_hue="orange",
secondary_hue="gray",
neutral_hue="gray",
).set(
# 背景色
body_background_fill="#121212",
body_background_fill_dark="#121212",
# 面板/卡片背景
block_background_fill="#1e1e1e",
block_background_fill_dark="#1e1e1e",
# 边框
block_border_color="#404040",
block_border_color_dark="#404040",
# 标签背景
block_label_background_fill="#2d2d2d",
block_label_background_fill_dark="#2d2d2d",
# 标签文字
block_label_text_color="#9e9e9e",
block_label_text_color_dark="#9e9e9e",
# 标题文字
block_title_text_color="#e0e0e0",
block_title_text_color_dark="#e0e0e0",
# 输入框
input_background_fill="#2d2d2d",
input_background_fill_dark="#2d2d2d",
input_border_color="#404040",
input_border_color_dark="#404040",
# 主按钮 - 橙色
button_primary_background_fill="#ff9800",
button_primary_background_fill_dark="#ff9800",
button_primary_background_fill_hover="#ffa726",
button_primary_background_fill_hover_dark="#ffa726",
button_primary_text_color="#121212",
button_primary_text_color_dark="#121212",
# 次要按钮 - 深灰
button_secondary_background_fill="#404040",
button_secondary_background_fill_dark="#404040",
button_secondary_background_fill_hover="#4a4a4a",
button_secondary_background_fill_hover_dark="#4a4a4a",
button_secondary_text_color="#e0e0e0",
button_secondary_text_color_dark="#e0e0e0",
# 文字颜色
body_text_color="#e0e0e0",
body_text_color_dark="#e0e0e0",
body_text_color_subdued="#9e9e9e",
body_text_color_subdued_dark="#9e9e9e",
# 链接颜色 - 橙色
link_text_color="#ff9800",
link_text_color_dark="#ff9800",
link_text_color_hover="#ffa726",
link_text_color_hover_dark="#ffa726",
# 滑块颜色
slider_color="#ff9800",
slider_color_dark="#ff9800",
# 复选框/单选框
checkbox_background_color="#2d2d2d",
checkbox_background_color_dark="#2d2d2d",
checkbox_border_color="#404040",
checkbox_border_color_dark="#404040",
checkbox_label_text_color="#e0e0e0",
checkbox_label_text_color_dark="#e0e0e0",
),
css=CUSTOM_CSS
) as app:
# 标题
gr.Markdown(
f"# 🎤 {i18n.get('app_title', 'RVC AI 翻唱')}",
elem_classes=["main-title"]
)
gr.Markdown(
f"<center>{i18n.get('app_description', '基于 RVC v2 的 AI 翻唱系统')}</center>"
)
with gr.Tabs():
# ===== 模型管理标签页 =====
with gr.Tab(t("models", "tabs")):
gr.Markdown(f"### 📦 {t('base_models', 'models')}")
gr.Markdown(t("base_models_desc", "models"))
with gr.Row():
check_btn = gr.Button(
f"🔍 {t('check_status', 'models')}",
variant="secondary"
)
download_btn = gr.Button(
f"⬇️ {t('download_required', 'models')}",
variant="primary"
)
model_status = gr.Textbox(
label=t("model_status", "models"),
interactive=False,
lines=6,
elem_classes=["status-box"]
)
check_btn.click(
fn=check_models_status,
outputs=[model_status]
)
download_btn.click(
fn=download_base_models,
outputs=[model_status]
)
gr.Markdown("---")
gr.Markdown(f"### 🎛️ {t('mature_deecho_models', 'models')}")
gr.Markdown(t("mature_deecho_models_desc", "models"))
with gr.Row():
mature_deecho_check_btn = gr.Button(
f"🔍 {t('mature_deecho_check', 'models')}",
variant="secondary"
)
mature_deecho_download_btn = gr.Button(
f"⬇️ {t('download_mature_deecho', 'models')}",
variant="primary"
)
mature_deecho_status = gr.Textbox(
label=t("mature_deecho_status", "models"),
interactive=False,
lines=7,
value=check_mature_deecho_status(),
elem_classes=["status-box"]
)
gr.Markdown("---")
gr.Markdown(f"### 🎤 {t('voice_models', 'models')}")
gr.Markdown(t("voice_models_desc", "models"))
def get_model_table():
from infer.pipeline import list_voice_models
weights_dir = ROOT_DIR / config.get("weights_dir", "assets/weights")
models = list_voice_models(str(weights_dir))
if not models:
return [["(无模型)", "", ""]]
return [[m["name"], m["model_path"], m.get("index_path", "无")] for m in models]
model_table = gr.Dataframe(
headers=["模型名称", "模型路径", "索引路径"],
value=get_model_table(),
interactive=False
)
refresh_table_btn = gr.Button(
f"🔄 刷新模型列表",
variant="secondary"
)
refresh_table_btn.click(
fn=get_model_table,
outputs=[model_table]
)
# ===== 歌曲翻唱标签页 =====
with gr.Tab(t("cover", "tabs")):
gr.Markdown(f"### 🎵 {t('song_cover', 'cover')}")
gr.Markdown(
"""
**一键 AI 翻唱**:上传歌曲 → 自动分离人声 → 转换音色 → 混合伴奏 → 输出翻唱
**使用步骤:**
1. 先下载角色模型(展开下方「下载角色模型」)
2. 上传歌曲文件(支持 MP3/WAV/FLAC)
3. 选择已下载的角色
4. 调整参数后点击「开始翻唱」
> ⚠️ 首次运行会自动下载 Mel-Band Roformer 人声分离模型(约 200MB),请耐心等待
"""
)
with gr.Row():
# 左侧:输入和角色选择
with gr.Column(scale=1):
gr.Markdown(f"#### 📁 {t('upload_song', 'cover')}")
cover_input_audio = gr.Audio(
label=t("input_song", "cover"),
type="filepath"
)
gr.Markdown(f"#### 🎭 {t('select_character', 'cover')}")
downloaded_series = gr.Dropdown(
label="作品/分类",
choices=get_downloaded_character_series(),
value="全部",
interactive=True
)
downloaded_keyword = gr.Textbox(
label="关键词搜索",
placeholder="输入角色名/作品名",
interactive=True
)
character_dropdown = gr.Dropdown(
label="选择角色",
choices=get_downloaded_character_choices("全部", ""),
interactive=True,
info="括号中的信息为模型训练参数:epochs=训练轮数(越大通常越成熟),数字+k=训练采样率(如40k=40000Hz)"
)
with gr.Row():
refresh_char_btn = gr.Button(
"🔄 刷新",
size="sm",
variant="secondary"
)
# 角色下载区域
with gr.Accordion("下载角色模型", open=False):
series_choices = ["全部"] + get_available_character_series()
download_series = gr.Dropdown(
label="作品/分类",
choices=series_choices,
value="全部",
interactive=True
)
download_keyword = gr.Textbox(
label="关键词搜索",
placeholder="输入角色名/作品名",
interactive=True
)
download_char_dropdown = gr.Dropdown(
label="选择角色",
choices=get_available_character_choices("全部", ""),
interactive=True
)
download_char_btn = gr.Button(
"⬇️ 下载选中角色",
variant="primary"
)
download_all_series_btn = gr.Button(
"⬇️ 下载该分类全部",
variant="secondary"
)
download_all_btn = gr.Button(
"⬇️ 下载全部角色模型",
variant="secondary"
)
download_char_status = gr.Textbox(
label="下载状态",
interactive=False
)
# 右侧:参数设置
with gr.Column(scale=1):
gr.Markdown(f"#### ⚙️ {t('conversion_settings', 'cover')}")
cover_cfg = config.get("cover", {})
cover_pitch_shift = gr.Slider(
label=t("pitch_shift", "cover"),
minimum=-12,
maximum=12,
value=0,
step=1,
info="正数升调,负数降调"
)
cover_index_rate = gr.Slider(
label=t("index_rate", "cover"),
minimum=0,
maximum=100,
value=_to_int(
round(
_to_float(
cover_cfg.get("index_rate", config.get("index_rate", 0.35)),
0.35,
) * 100
),
35,
),
step=5,
info=t("index_rate_info", "cover"),
)
cover_speaker_id = gr.Slider(
label=t("speaker_id", "cover"),
minimum=0,
maximum=255,
value=_to_int(cover_cfg.get("speaker_id", 0), 0),
step=1,
info=t("speaker_id_info", "cover"),
)
gr.Markdown(f"#### 🎚️ {t('mix_settings', 'cover')}")
cover_karaoke = gr.Checkbox(
label=t("karaoke_separation", "cover"),
value=bool(cover_cfg.get("karaoke_separation", True)),
info=t("karaoke_separation_info", "cover")
)
cover_karaoke_merge_backing = gr.Checkbox(
label=t("karaoke_merge_backing", "cover"),
value=bool(
cover_cfg.get(
"karaoke_merge_backing_into_accompaniment",
True
)
),
info=t("karaoke_merge_backing_info", "cover")
)
vc_label_to_value, vc_value_to_label = get_vc_preprocess_option_maps()
source_label_to_value, source_value_to_label = get_source_constraint_option_maps()
pipeline_label_to_value, pipeline_value_to_label = get_vc_pipeline_mode_option_maps()
cover_vc_preprocess_mode = gr.Dropdown(
label=t("vc_preprocess_mode", "cover"),
choices=list(vc_label_to_value.keys()),
value=vc_value_to_label.get(str(cover_cfg.get("vc_preprocess_mode", "auto")), list(vc_label_to_value.keys())[0]),
info=t("vc_preprocess_mode_info", "cover"),
)
cover_source_constraint_mode = gr.Dropdown(
label=t("source_constraint_mode", "cover"),
choices=list(source_label_to_value.keys()),
value=source_value_to_label.get(str(cover_cfg.get("source_constraint_mode", "auto")), list(source_label_to_value.keys())[0]),
info=t("source_constraint_mode_info", "cover"),
)
cover_vc_pipeline_mode = gr.Dropdown(
label=t("vc_pipeline_mode", "cover"),
choices=list(pipeline_label_to_value.keys()),
value=pipeline_value_to_label.get(str(cover_cfg.get("vc_pipeline_mode", "current")), list(pipeline_label_to_value.keys())[0]),
info=t("vc_pipeline_mode_info", "cover"),
)
cover_singing_repair = gr.Checkbox(
label=t("singing_repair", "cover"),
value=bool(cover_cfg.get("singing_repair", False)),
info=t("singing_repair_info", "cover"),
visible=str(cover_cfg.get("vc_pipeline_mode", "current")).strip().lower() == "official",
)
cover_vc_route_status = gr.Textbox(
label=t("vc_preprocess_status", "cover"),
value=get_cover_vc_route_status(
cover_cfg.get("vc_preprocess_mode", "auto"),
cover_cfg.get("vc_pipeline_mode", "current"),
),
info=t("vc_preprocess_status_info", "cover"),
interactive=False,
lines=3,
elem_classes=["status-box"]
)
mix_presets, default_mix_preset = get_cover_mix_presets()
default_mix = mix_presets[default_mix_preset]
cover_mix_preset = gr.Dropdown(
label=t("mix_preset", "cover"),
choices=list(mix_presets.keys()),
value=default_mix_preset,
info=t("mix_preset_info", "cover"),
interactive=True
)
cover_vocals_volume = gr.Slider(
label=t("vocals_volume", "cover"),
minimum=0,
maximum=200,
value=default_mix["vocals_volume"],
step=5,
info="100% 为原始音量"
)
cover_accompaniment_volume = gr.Slider(
label=t("accompaniment_volume", "cover"),
minimum=0,
maximum=200,
value=default_mix["accompaniment_volume"],
step=5,
info="100% 为原始音量"
)
cover_reverb = gr.Slider(
label=t("vocals_reverb", "cover"),
minimum=0,
maximum=100,
value=default_mix["reverb"],
step=5,
info="为人声添加混响效果"
)
cover_rms_mix_rate = gr.Slider(
label=t("rms_mix_rate", "cover"),
minimum=0,
maximum=100,
value=_to_int(
round(
_to_float(
cover_cfg.get(
"rms_mix_rate",
config.get("rms_mix_rate", 0.15),
),
0.15,
) * 100
),
15,
),
step=5,
info=t("rms_mix_rate_info", "cover"),
)
cover_backing_mix = gr.Slider(
label=t("backing_mix", "cover"),
minimum=0,
maximum=100,
value=_to_int(
round(_to_float(cover_cfg.get("backing_mix", 0.0), 0.0) * 100),
0,
),
step=5,
info=t("backing_mix_info", "cover"),
)
# 开始按钮
cover_btn = gr.Button(
f"🚀 {t('start_cover', 'cover')}",
variant="primary",
size="lg"
)
# 状态显示
cover_status = gr.Textbox(
label=t("progress", "cover"),
interactive=False,
elem_classes=["status-box"]
)
# 输出区域
gr.Markdown(f"#### 🎵 {t('results', 'cover')}")
with gr.Row():
cover_output = gr.Audio(
label=t("final_cover", "cover"),
type="filepath",
interactive=False
)
with gr.Row():
cover_converted_vocals_output = gr.Audio(
label=t("converted_vocals", "cover"),
type="filepath",
interactive=False
)
cover_original_vocals_output = gr.Audio(
label=t("original_vocals", "cover"),
type="filepath",
interactive=False
)
with gr.Row():
cover_lead_vocals_output = gr.Audio(
label=t("lead_vocals", "cover"),
type="filepath",
interactive=False
)
cover_backing_vocals_output = gr.Audio(
label=t("backing_vocals", "cover"),
type="filepath",
interactive=False
)
with gr.Row():
cover_accompaniment_output = gr.Audio(
label=t("accompaniment", "cover"),
type="filepath",
interactive=False
)
# 事件绑定
refresh_char_btn.click(
fn=refresh_downloaded_controls,
inputs=[downloaded_series, downloaded_keyword],
outputs=[downloaded_series, character_dropdown]
)
downloaded_series.change(
fn=update_downloaded_choices,
inputs=[downloaded_series, downloaded_keyword],
outputs=[character_dropdown]
)
downloaded_keyword.change(
fn=update_downloaded_choices,
inputs=[downloaded_series, downloaded_keyword],
outputs=[character_dropdown]
)
download_series.change(
fn=update_download_choices,
inputs=[download_series, download_keyword],
outputs=[download_char_dropdown]
)
download_keyword.change(
fn=update_download_choices,
inputs=[download_series, download_keyword],
outputs=[download_char_dropdown]
)
download_char_btn.click(
fn=download_character,
inputs=[download_char_dropdown, downloaded_series, downloaded_keyword],
outputs=[download_char_status, character_dropdown, downloaded_series]
)
download_all_series_btn.click(
fn=download_all_characters,
inputs=[download_series, downloaded_series, downloaded_keyword],
outputs=[download_char_status, character_dropdown, downloaded_series]
)
download_all_btn.click(
fn=lambda series, keyword: download_all_characters("全部", series, keyword),
inputs=[downloaded_series, downloaded_keyword],
outputs=[download_char_status, character_dropdown, downloaded_series]
)
cover_mix_preset.change(
fn=apply_cover_mix_preset,
inputs=[cover_mix_preset],
outputs=[
cover_vocals_volume,
cover_accompaniment_volume,
cover_reverb
]
)
mature_deecho_check_btn.click(
fn=check_mature_deecho_status,
outputs=[mature_deecho_status]
)
mature_deecho_check_btn.click(
fn=get_cover_vc_route_status,
inputs=[cover_vc_preprocess_mode, cover_vc_pipeline_mode],
outputs=[cover_vc_route_status]
)
mature_deecho_download_btn.click(
fn=download_mature_deecho_models_ui,
outputs=[mature_deecho_status]
)
mature_deecho_download_btn.click(
fn=get_cover_vc_route_status,
inputs=[cover_vc_preprocess_mode, cover_vc_pipeline_mode],
outputs=[cover_vc_route_status]
)
cover_vc_preprocess_mode.change(
fn=get_cover_vc_route_status,
inputs=[cover_vc_preprocess_mode, cover_vc_pipeline_mode],
outputs=[cover_vc_route_status]
)
cover_vc_pipeline_mode.change(
fn=get_cover_vc_route_status,
inputs=[cover_vc_preprocess_mode, cover_vc_pipeline_mode],
outputs=[cover_vc_route_status]
)
cover_vc_pipeline_mode.change(
fn=update_singing_repair_visibility,
inputs=[cover_vc_pipeline_mode],
outputs=[cover_singing_repair]
)
cover_btn.click(
fn=process_cover,
inputs=[
cover_input_audio,
character_dropdown,
cover_pitch_shift,
cover_index_rate,
cover_speaker_id,
cover_karaoke,
cover_karaoke_merge_backing,
cover_vc_preprocess_mode,
cover_source_constraint_mode,
cover_vc_pipeline_mode,
cover_singing_repair,
cover_vocals_volume,
cover_accompaniment_volume,
cover_reverb,
cover_rms_mix_rate,
cover_backing_mix,
],
outputs=[
cover_output,
cover_converted_vocals_output,
cover_original_vocals_output,
cover_lead_vocals_output,
cover_backing_vocals_output,
cover_accompaniment_output,
cover_status
]
)
# ===== 设置标签页 =====
with gr.Tab(t("settings", "tabs")):
gr.Markdown(f"### 💻 {t('device_info', 'settings')}")
device_info = gr.Textbox(
label=t("current_device", "settings"),
value=get_device_info(),
interactive=False,
lines=5,
elem_classes=["status-box"]
)
refresh_device_btn = gr.Button(
f"🔄 {t('refresh_device', 'settings')}",
variant="secondary"
)
refresh_device_btn.click(
fn=get_device_info,
outputs=[device_info]
)
gr.Markdown("---")
gr.Markdown(f"### ⚙️ 运行设置")
def _build_device_choices():
from lib.device import _has_xpu, _has_directml, _has_mps, _is_rocm
import torch
choices = []
if torch.cuda.is_available():
label = "ROCm (AMD GPU)" if _is_rocm() else "CUDA (NVIDIA GPU)"
choices.append((label, "cuda"))
if _has_xpu():
choices.append(("XPU (Intel GPU)", "xpu"))
if _has_directml():
choices.append(("DirectML (AMD/Intel GPU)", "directml"))
if _has_mps():
choices.append(("MPS (Apple GPU)", "mps"))
choices.append(("CPU (较慢)", "cpu"))
return choices
device_radio = gr.Radio(
label="计算设备",
choices=_build_device_choices(),
value=config.get("device", "cuda")
)
save_settings_btn = gr.Button(
"💾 保存设置",
variant="primary"
)
settings_status = gr.Textbox(
label="状态",
interactive=False
)
def save_settings(device):
global config
config["device"] = device
config_path = ROOT_DIR / "configs" / "config.json"
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=4, ensure_ascii=False)
return "✅ 设置已保存,重启后生效"
save_settings_btn.click(
fn=save_settings,
inputs=[device_radio],
outputs=[settings_status]
)
gr.Markdown("---")
gr.Markdown(f"### ℹ️ {t('about', 'settings')}")
gr.Markdown(
"""
**RVC AI 翻唱系统**
- 基于 RVC v2 + Mel-Band Roformer
- 使用 RMVPE 进行高质量 F0 提取
- 支持 CUDA GPU 加速
[GitHub](https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI)
"""
)
gr.Markdown("---")
gr.Markdown(
"""
### 📥 角色模型来源
以下是本项目角色模型的 HuggingFace 仓库来源,你也可以手动下载模型后放入 `assets/weights/characters/<角色名>/` 目录使用:
**Love Live! 系列**
- [trioskosmos/rvc_models](https://huggingface.co/trioskosmos/rvc_models) — μ's / Aqours / 虹咲 / Liella! 多角色
- [Icchan/LoveLive](https://huggingface.co/Icchan/LoveLive) — 千歌、梨子、绘里、曜
- [0xMifune/LoveLive](https://huggingface.co/0xMifune/LoveLive) — 虹咲 / Liella! / 莲之空
- [Swordsmagus/Love-Live-RVC](https://huggingface.co/Swordsmagus/Love-Live-RVC) — 花丸、雪菜、小鸟、A-RISE 等
- [Zurakichi/RVC](https://huggingface.co/Zurakichi/RVC) — 妮可、彼方、雪菜、花丸
- [Phos252/RVCmodels](https://huggingface.co/Phos252/RVCmodels) — 涩谷香音
- [ChocoKat/Mari_Ohara](https://huggingface.co/ChocoKat/Mari_Ohara) — 小原鞠莉
- [HarunaKasuga/YoshikoTsushima](https://huggingface.co/HarunaKasuga/YoshikoTsushima) — 津岛善子
- [thebuddyadrian/RVC_Models](https://huggingface.co/thebuddyadrian/RVC_Models) — 鹿角姐妹
**原神 / 崩坏 / 绝区零 (米哈游)**
- [makiligon/RVC-Models](https://huggingface.co/makiligon/RVC-Models) — 芙宁娜、绫华、芙卡洛斯
- [kohaku12/RVC-MODELS](https://huggingface.co/kohaku12/RVC-MODELS) — 纳西妲、黑塔、流萤、停云、星见雅 等
- [jarari/RVC-v2](https://huggingface.co/jarari/RVC-v2) — 芙宁娜(韩语)、银狼(韩语)
- [mrmocciai/genshin-impact](https://huggingface.co/mrmocciai/genshin-impact) — 原神 50+ 角色(需手动下载)
**VOCALOID**
- [javinfamous/infamous_miku_v2](https://huggingface.co/javinfamous/infamous_miku_v2) — 初音未来 (1000 epochs)
**Hololive / VTuber**
- [megaaziib/my-rvc-models-collection](https://huggingface.co/megaaziib/my-rvc-models-collection) — 佩克拉、樱巫女、大空昴、Kobo、Kaela 等
- [Kit-Lemonfoot/kitlemonfoot_rvc_models](https://huggingface.co/Kit-Lemonfoot/kitlemonfoot_rvc_models) — Hololive JP/EN 多角色
**偶像大师 / 赛马娘**
- [trioskosmos/rvc_models](https://huggingface.co/trioskosmos/rvc_models) — 神崎兰子、梦见莉亚梦
- [makiligon/RVC-Models](https://huggingface.co/makiligon/RVC-Models) — 四条贵音、米浴
**Project SEKAI**
- [kohaku12/RVC-MODELS](https://huggingface.co/kohaku12/RVC-MODELS) — 草薙宁宁
> 💡 手动下载后,将 `.pth` 和 `.index` 文件放入 `assets/weights/characters/<角色名>/` 目录,刷新即可使用。
"""
)
return app
def _patch_gradio_file_download(blocks):
"""
Patch Gradio v3 的 /file= 路由,为文件添加 Content-Disposition header,
使浏览器下载时使用干净的文件名而非完整路径。
"""
try:
from starlette.responses import FileResponse
from urllib.parse import quote
import fastapi
def _clean_download_name(response: FileResponse, path_or_url: str) -> str:
candidates = [
getattr(response, "filename", None),
getattr(response, "path", None),
path_or_url,
]
for candidate in candidates:
if not candidate:
continue
name = Path(str(candidate)).name
if not name:
continue
name = re.sub(
r"^[A-Za-z]__.*?_gradio_[0-9a-f]{8,}_",
"",
name,
flags=re.IGNORECASE,
)
if name:
return name
return "download"
fastapi_app = getattr(blocks, "server_app", None)
if fastapi_app is None:
return
for route in fastapi_app.routes:
if hasattr(route, "path") and route.path == "/file={path_or_url:path}":
original_endpoint = route.endpoint
async def patched_file(
path_or_url: str,
request: fastapi.Request,
_orig=original_endpoint,
):
response = await _orig(path_or_url, request=request)
if isinstance(response, FileResponse) and "content-disposition" not in response.headers:
basename = _clean_download_name(response, path_or_url)
encoded = quote(basename)
if encoded != basename:
cd = f"inline; filename*=utf-8''{encoded}"
else:
cd = f'inline; filename="{basename}"'
response.headers["content-disposition"] = cd
return response
route.endpoint = patched_file
break
except Exception as e:
log.warning(f"Patch Gradio file download failed: {e}")
def launch(host: str = "127.0.0.1", port: int = 7860, share: bool = False):
"""启动 Gradio 界面"""
app = create_ui()
app.queue() # 启用队列以支持进度跟踪
app.launch(
server_name=host,
server_port=port,
share=share,
inbrowser=True,
prevent_thread_lock=True
)
_patch_gradio_file_download(app)
app.block_thread()
if __name__ == "__main__":
launch()