| |
|
| | """
|
| | 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_path = ROOT_DIR / config.get("hubert_path", "assets/hubert/hubert_base.pt")
|
| | if hubert_path.exists():
|
| | pipeline.load_hubert(str(hubert_path))
|
| |
|
| |
|
| | 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,
|
| | 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)
|
| |
|
| |
|
| |
|
| | 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()
|
| |
|