mason369 commited on
Commit
22e90f1
·
verified ·
1 Parent(s): 762eecb

Upload folder using huggingface_hub

Browse files
tools/apply_preset.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 配置预设应用工具
5
+ 快速切换不同的音质优化配置
6
+ """
7
+ import json
8
+ import shutil
9
+ from pathlib import Path
10
+
11
+ PRESETS_DIR = Path("configs/presets")
12
+ CONFIG_FILE = Path("configs/config.json")
13
+ BACKUP_FILE = Path("configs/config.backup.json")
14
+
15
+ PRESETS = {
16
+ "1": "balanced.json",
17
+ "2": "clarity_priority.json",
18
+ "3": "timbre_priority.json"
19
+ }
20
+
21
+ def load_json(path: Path) -> dict:
22
+ """加载JSON文件"""
23
+ with open(path, "r", encoding="utf-8") as f:
24
+ return json.load(f)
25
+
26
+ def save_json(path: Path, data: dict):
27
+ """保存JSON文件"""
28
+ with open(path, "w", encoding="utf-8") as f:
29
+ json.dump(data, f, indent=2, ensure_ascii=False)
30
+
31
+ def backup_config():
32
+ """备份当前配置"""
33
+ if CONFIG_FILE.exists():
34
+ shutil.copy(CONFIG_FILE, BACKUP_FILE)
35
+ print(f"✓ 已备份当前配置到: {BACKUP_FILE}")
36
+
37
+ def restore_config():
38
+ """恢复备份配置"""
39
+ if BACKUP_FILE.exists():
40
+ shutil.copy(BACKUP_FILE, CONFIG_FILE)
41
+ print(f"✓ 已恢复配置从: {BACKUP_FILE}")
42
+ else:
43
+ print("✗ 未找到备份文件")
44
+
45
+ def apply_preset(preset_name: str):
46
+ """应用预设配置"""
47
+ preset_path = PRESETS_DIR / preset_name
48
+
49
+ if not preset_path.exists():
50
+ print(f"✗ 预设文件不存在: {preset_path}")
51
+ return
52
+
53
+ # 备份当前配置
54
+ backup_config()
55
+
56
+ # 加载预设和当前配置
57
+ preset = load_json(preset_path)
58
+ config = load_json(CONFIG_FILE)
59
+
60
+ # 合并配置 (只更新 cover 部分)
61
+ if "cover" in preset:
62
+ config["cover"].update(preset["cover"])
63
+
64
+ # 保存
65
+ save_json(CONFIG_FILE, config)
66
+
67
+ print(f"\n✓ 已应用预设: {preset.get('name', preset_name)}")
68
+ print(f" 描述: {preset.get('description', 'N/A')}")
69
+ print(f"\n主要参数:")
70
+ print(f" - index_rate: {config['cover']['index_rate']}")
71
+ print(f" - protect: {config['cover']['protect']}")
72
+ print(f" - rms_mix_rate: {config['cover']['rms_mix_rate']}")
73
+ print(f" - filter_radius: {config['cover']['filter_radius']}")
74
+ print(f" - f0_stabilize: {config['cover']['f0_stabilize']}")
75
+
76
+ def show_menu():
77
+ """显示菜单"""
78
+ print("\n" + "="*60)
79
+ print("RVC 音质优化配置预设")
80
+ print("="*60)
81
+ print("\n可用预设:")
82
+ print(" 1. 平衡型配置 (推荐)")
83
+ print(" - 适合大多数歌曲")
84
+ print(" - 在音色转换和清晰度之间平衡")
85
+ print()
86
+ print(" 2. 清晰度优先配置")
87
+ print(" - 减少伪影和失真")
88
+ print(" - 适合复杂歌曲和高音多的情况")
89
+ print(" - 保留更多源音频特征")
90
+ print()
91
+ print(" 3. 音色优先配置")
92
+ print(" - 彻底的音色转换")
93
+ print(" - 适合音色特征明显的角色")
94
+ print(" - 可能有轻微口齿模糊")
95
+ print()
96
+ print(" r. 恢复备份配置")
97
+ print(" q. 退出")
98
+ print("="*60)
99
+
100
+ def main():
101
+ """主函数"""
102
+ while True:
103
+ show_menu()
104
+ choice = input("\n请选择 (1-3/r/q): ").strip().lower()
105
+
106
+ if choice == "q":
107
+ print("\n再见!")
108
+ break
109
+ elif choice == "r":
110
+ restore_config()
111
+ elif choice in PRESETS:
112
+ apply_preset(PRESETS[choice])
113
+ else:
114
+ print("\n✗ 无效选择,请重试")
115
+
116
+ input("\n按回车继续...")
117
+
118
+ if __name__ == "__main__":
119
+ main()
tools/character_models.py ADDED
@@ -0,0 +1,1792 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 角色模型管理 - 从 HuggingFace 下载 RVC 角色模型
4
+ """
5
+ import os
6
+ import json
7
+ import re
8
+ import zipfile
9
+ import shutil
10
+ from pathlib import Path
11
+ from typing import Optional, List, Dict, Callable
12
+
13
+ try:
14
+ from huggingface_hub import hf_hub_download, list_repo_files
15
+ HF_AVAILABLE = True
16
+ except ImportError:
17
+ HF_AVAILABLE = False
18
+
19
+
20
+ # HuggingFace 仓库配置
21
+ HF_REPO_ID = "trioskosmos/rvc_models"
22
+ _VERSION_NOTE_CACHE: Dict[str, Optional[str]] = {}
23
+ _VERSION_NOTE_CACHE_LOADED = False
24
+
25
+
26
+ def _get_hf_token() -> Optional[str]:
27
+ """获取 HuggingFace Token(支持 HF_TOKEN / HUGGINGFACE_HUB_TOKEN / HUGGINGFACE_TOKEN)"""
28
+ return (
29
+ os.environ.get("HF_TOKEN")
30
+ or os.environ.get("HUGGINGFACE_HUB_TOKEN")
31
+ or os.environ.get("HUGGINGFACE_TOKEN")
32
+ )
33
+
34
+ # 作品归类(用于 UI 分类筛选)
35
+ SERIES_ALIASES = {
36
+ "Love Live!": "Love Live!",
37
+ "Love Live! Sunshine!!": "Love Live!",
38
+ "Love Live! Superstar!!": "Love Live!",
39
+ "Love Live! 虹咲学园": "Love Live!",
40
+ "Love Live! 虹咲学園": "Love Live!",
41
+ "Love Live! 莲之空女学院学园偶像俱乐部": "Love Live!",
42
+ "Love Live! Sunshine!! / 幻日夜羽": "Love Live!",
43
+ "Hololive Japan": "Hololive",
44
+ "Hololive English": "Hololive",
45
+ "Hololive Indonesia": "Hololive",
46
+ "崩坏:星穹铁道": "崩坏系列",
47
+ "崩坏3rd": "崩坏系列",
48
+ "偶像大师 灰姑娘女孩": "偶像大师",
49
+ }
50
+
51
+
52
+ def normalize_series(source: str) -> str:
53
+ """将来源归类到系列"""
54
+ if not source:
55
+ return "未知"
56
+ for key, series in SERIES_ALIASES.items():
57
+ if source.startswith(key):
58
+ return series
59
+ return source
60
+
61
+
62
+ def _get_display_name(info: Dict, fallback: str) -> str:
63
+ """拼接中文名 / 英文名 / 日文名用于展示"""
64
+ zh_name = info.get("zh_name") or info.get("description") or fallback
65
+ en_name = info.get("en_name")
66
+ jp_name = info.get("jp_name")
67
+ parts = [zh_name]
68
+ if en_name and en_name != zh_name:
69
+ parts.append(en_name)
70
+ if jp_name and jp_name != zh_name and jp_name != en_name:
71
+ parts.append(jp_name)
72
+ display = " / ".join(parts)
73
+ variant = info.get("variant")
74
+ if variant:
75
+ display = f"{display} - {variant}"
76
+ variant_note = _get_version_note(fallback, info)
77
+ if variant_note:
78
+ display = f"{display} ({variant_note})"
79
+ return display
80
+
81
+
82
+ def _find_index_file(pth_file: Path) -> Optional[Path]:
83
+ """尝试找到对应的索引文件"""
84
+ candidate = pth_file.with_suffix(".index")
85
+ if candidate.exists():
86
+ return candidate
87
+
88
+ index_files = list(pth_file.parent.glob("*.index"))
89
+ if not index_files:
90
+ return None
91
+
92
+ for idx in pth_file.parent.glob("*.index"):
93
+ if idx.stem.lower() == pth_file.stem.lower():
94
+ return idx
95
+
96
+ if len(index_files) == 1:
97
+ return index_files[0]
98
+
99
+ def _normalize_name(text: str) -> str:
100
+ return re.sub(r"[^a-z0-9]+", "", text.lower())
101
+
102
+ def _tokenize_name(text: str) -> List[str]:
103
+ return [token for token in re.split(r"[^a-z0-9]+", text.lower()) if len(token) >= 2]
104
+
105
+ model_norm = _normalize_name(pth_file.stem)
106
+ model_tokens = set(_tokenize_name(pth_file.stem))
107
+
108
+ best_match = None
109
+ best_score = -1
110
+ for idx in index_files:
111
+ idx_norm = _normalize_name(idx.stem)
112
+ idx_tokens = set(_tokenize_name(idx.stem))
113
+ score = 0
114
+ if idx_norm == model_norm:
115
+ score += 1000
116
+ if model_norm and (model_norm in idx_norm or idx_norm in model_norm):
117
+ score += 300
118
+ shared_tokens = len(model_tokens & idx_tokens)
119
+ score += shared_tokens * 40
120
+ if "added" in idx.stem.lower():
121
+ score += 10
122
+ if score > best_score:
123
+ best_score = score
124
+ best_match = idx
125
+
126
+ if best_match is not None and best_score > 0:
127
+ return best_match
128
+ return None
129
+
130
+
131
+ def _safe_print(message: str):
132
+ """避免控制台编码问题导致的输出异常"""
133
+ try:
134
+ print(message)
135
+ except UnicodeEncodeError:
136
+ print(message.encode("utf-8", "backslashreplace").decode("utf-8"))
137
+
138
+
139
+ def _load_version_note_cache():
140
+ global _VERSION_NOTE_CACHE_LOADED
141
+ if _VERSION_NOTE_CACHE_LOADED:
142
+ return
143
+ _VERSION_NOTE_CACHE_LOADED = True
144
+ try:
145
+ cache_path = get_character_models_dir() / "_version_notes.json"
146
+ if cache_path.exists():
147
+ with open(cache_path, "r", encoding="utf-8") as f:
148
+ data = json.load(f)
149
+ if isinstance(data, dict):
150
+ _VERSION_NOTE_CACHE.update(data)
151
+ except Exception:
152
+ pass
153
+
154
+
155
+ def _save_version_note_cache():
156
+ try:
157
+ cache_path = get_character_models_dir() / "_version_notes.json"
158
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
159
+ with open(cache_path, "w", encoding="utf-8") as f:
160
+ json.dump(_VERSION_NOTE_CACHE, f, ensure_ascii=False, indent=2)
161
+ except Exception:
162
+ pass
163
+
164
+
165
+ def _normalize_note(note: str) -> Optional[str]:
166
+ if not note:
167
+ return None
168
+ # 取首行,移除链接与多余空白
169
+ line = note.strip().splitlines()[0].strip()
170
+ if not line:
171
+ return None
172
+ line = re.sub(r"https?://\S+", "", line).strip()
173
+ line = re.sub(r"\s+", " ", line).strip()
174
+ if not line:
175
+ return None
176
+ if len(line) > 60:
177
+ return line[:57] + "..."
178
+ return line
179
+
180
+
181
+ def _note_from_metadata(path: Path) -> Optional[str]:
182
+ try:
183
+ with open(path, "r", encoding="utf-8") as f:
184
+ data = json.load(f)
185
+ except Exception:
186
+ return None
187
+
188
+ parts = []
189
+ title = str(data.get("title") or "")
190
+ desc = str(data.get("description") or "")
191
+ type_val = str(data.get("type") or data.get("version") or "")
192
+ text = f"{title}\n{desc}"
193
+
194
+ epoch_match = re.search(r"(\d+)\s*epoch", text, re.IGNORECASE)
195
+ if epoch_match:
196
+ parts.append(f"{epoch_match.group(1)} epochs")
197
+
198
+ if type_val:
199
+ t = type_val.lower()
200
+ if t.startswith("v"):
201
+ parts.append(f"RVC {type_val}")
202
+ elif "rvc" in t:
203
+ parts.append(type_val)
204
+
205
+ if parts:
206
+ return " · ".join(dict.fromkeys(parts))
207
+
208
+ # fallback to title/description first line
209
+ return _normalize_note(desc) or _normalize_note(title)
210
+
211
+
212
+ def _note_from_pth(path: Path) -> Optional[str]:
213
+ try:
214
+ import torch
215
+ except Exception:
216
+ return None
217
+
218
+ try:
219
+ obj = torch.load(path, map_location="cpu", weights_only=False)
220
+ except Exception:
221
+ return None
222
+
223
+ if not isinstance(obj, dict):
224
+ return None
225
+
226
+ parts = []
227
+ info = obj.get("info")
228
+ if isinstance(info, str):
229
+ match = re.search(r"(\d+)\s*epoch", info, re.IGNORECASE)
230
+ if match:
231
+ parts.append(f"{match.group(1)} epochs")
232
+
233
+ sr = obj.get("sr")
234
+ if sr:
235
+ if isinstance(sr, (int, float)):
236
+ sr_note = f"{int(sr/1000)}k" if sr >= 1000 else str(int(sr))
237
+ else:
238
+ sr_note = str(sr)
239
+ parts.append(sr_note)
240
+
241
+ if parts:
242
+ return " · ".join(dict.fromkeys(parts))
243
+ return None
244
+
245
+
246
+ def _note_from_filename(name: str) -> Optional[str]:
247
+ if not name:
248
+ return None
249
+ lower = name.lower()
250
+ parts = []
251
+
252
+ if "rmvpe" in lower:
253
+ parts.append("RMVPE")
254
+ if "ov2" in lower or "ov2super" in lower:
255
+ parts.append("OV2")
256
+ if "pre-anime" in lower or "preanime" in lower:
257
+ parts.append("预TV")
258
+ # 训练轮次与步数
259
+ epoch_match = re.search(r"(?:^|[_-])e(\d{2,5})(?:[_-]|$)", lower)
260
+ if epoch_match:
261
+ parts.append(f"e{epoch_match.group(1)}")
262
+ step_match = re.search(r"(?:^|[_-])s(\d{3,7})(?:[_-]|$)", lower)
263
+ if step_match:
264
+ parts.append(f"s{step_match.group(1)}")
265
+
266
+ if parts:
267
+ return " · ".join(dict.fromkeys(parts))
268
+ return None
269
+
270
+
271
+ def _get_version_note(name: str, info: Dict) -> Optional[str]:
272
+ _load_version_note_cache()
273
+ note = info.get("variant_note")
274
+ if note:
275
+ return _normalize_note(note)
276
+
277
+ cached = _VERSION_NOTE_CACHE.get(name)
278
+ if cached is not None:
279
+ return cached
280
+
281
+ # 1) 读取本地模型目录中的 metadata / info
282
+ char_dir = get_character_models_dir() / name
283
+ if char_dir.exists():
284
+ for candidate in ("metadata.json", "model_info.json", "info.json"):
285
+ path = char_dir / candidate
286
+ if path.exists():
287
+ note = _note_from_metadata(path)
288
+ if note:
289
+ _VERSION_NOTE_CACHE[name] = note
290
+ _save_version_note_cache()
291
+ return note
292
+
293
+ # 2) 尝试从本地权重读取 info/版本
294
+ pth_files = sorted(char_dir.glob("*.pth"))
295
+ if pth_files:
296
+ note = _note_from_pth(pth_files[0]) or _note_from_filename(pth_files[0].name)
297
+ if note:
298
+ _VERSION_NOTE_CACHE[name] = note
299
+ _save_version_note_cache()
300
+ return note
301
+ index_files = sorted(char_dir.glob("*.index"))
302
+ if index_files:
303
+ note = _note_from_filename(index_files[0].name)
304
+ if note:
305
+ _VERSION_NOTE_CACHE[name] = note
306
+ _save_version_note_cache()
307
+ return note
308
+
309
+ # 3) 未下载时,从配置文件名解析
310
+ file_name = info.get("file") or ""
311
+ note = _note_from_filename(Path(str(file_name)).name)
312
+ if not note:
313
+ files = info.get("files") or []
314
+ if files:
315
+ note = _note_from_filename(Path(str(files[0])).name)
316
+
317
+ if not note and info.get("variant"):
318
+ note = "未提供版本说明"
319
+
320
+ _VERSION_NOTE_CACHE[name] = note
321
+ _save_version_note_cache()
322
+ return note
323
+
324
+
325
+ def refresh_version_notes(force: bool = False) -> Dict[str, Optional[str]]:
326
+ """批量读取本地模型的版本说明,写入缓存文件"""
327
+ if force:
328
+ _VERSION_NOTE_CACHE.clear()
329
+ # 避免读取旧缓存文件
330
+ global _VERSION_NOTE_CACHE_LOADED
331
+ _VERSION_NOTE_CACHE_LOADED = True
332
+ notes = {}
333
+ for name in CHARACTER_MODELS.keys():
334
+ notes[name] = _get_version_note(name, CHARACTER_MODELS.get(name, {}))
335
+ return notes
336
+
337
+
338
+ def _get_confirm_token(response) -> Optional[str]:
339
+ """获取 Google Drive 下载确认 token"""
340
+ import re
341
+ for key, value in response.cookies.items():
342
+ if key.startswith("download_warning"):
343
+ return value
344
+ try:
345
+ # 兼容通过页面确认下载的场景
346
+ match = re.search(r"confirm=([0-9A-Za-z_]+)", response.text)
347
+ if match:
348
+ return match.group(1)
349
+ except Exception:
350
+ pass
351
+ return None
352
+
353
+
354
+ def _download_gdrive_file(file_id: str, dest_path: Path) -> bool:
355
+ """下载 Google Drive 文件(支持大文件确认)"""
356
+ import requests
357
+
358
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
359
+ url = "https://drive.google.com/uc?export=download"
360
+ session = requests.Session()
361
+
362
+ response = session.get(url, params={"id": file_id}, stream=True, timeout=30)
363
+ token = _get_confirm_token(response)
364
+ if token:
365
+ response = session.get(
366
+ url, params={"id": file_id, "confirm": token}, stream=True, timeout=30
367
+ )
368
+
369
+ if response.status_code != 200:
370
+ print(f" 下载失败: HTTP {response.status_code}")
371
+ return False
372
+ content_type = response.headers.get("content-type", "")
373
+ if "text/html" in content_type:
374
+ print(" 下载失败: 需要手动确认或无访问权限")
375
+ return False
376
+
377
+ total_size = int(response.headers.get("content-length", 0))
378
+ downloaded = 0
379
+ with open(dest_path, "wb") as f:
380
+ for chunk in response.iter_content(chunk_size=8192):
381
+ if chunk:
382
+ f.write(chunk)
383
+ downloaded += len(chunk)
384
+ # 简单进度输出
385
+ if total_size > 0:
386
+ percent = downloaded * 100 / total_size
387
+ if int(percent) % 10 == 0:
388
+ print(f" 下载进度: {percent:.0f}%")
389
+
390
+ return dest_path.exists() and dest_path.stat().st_size > 0
391
+
392
+
393
+ # 角色模型列表
394
+ CHARACTER_MODELS = {
395
+ "Aimi": {
396
+ "file": "Aimi.zip",
397
+ "zh_name": "爱美",
398
+ "en_name": "Aimi",
399
+ "jp_name": "愛美",
400
+ "source": "原创角色"
401
+ },
402
+ "hanayo": {
403
+ "file": "kubotayurika.zip",
404
+ "zh_name": "小泉花阳",
405
+ "en_name": "Hanayo Koizumi",
406
+ "jp_name": "小泉花陽",
407
+ "source": "Love Live!"
408
+ },
409
+ "rin": {
410
+ "file": "rin.zip",
411
+ "zh_name": "星空凛",
412
+ "en_name": "Rin Hoshizora",
413
+ "jp_name": "星空凛",
414
+ "source": "Love Live!"
415
+ },
416
+ "umi": {
417
+ "file": "umi.zip",
418
+ "zh_name": "园田海未",
419
+ "en_name": "Umi Sonoda",
420
+ "jp_name": "園田海未",
421
+ "source": "Love Live!"
422
+ },
423
+ "nozomi": {
424
+ "file": "nozomi.zip",
425
+ "zh_name": "东条希",
426
+ "en_name": "Nozomi Tojo",
427
+ "jp_name": "東條希",
428
+ "source": "Love Live!"
429
+ },
430
+ "dia": {
431
+ "file": "dia.zip",
432
+ "zh_name": "黑泽黛雅",
433
+ "en_name": "Dia Kurosawa",
434
+ "jp_name": "黒澤ダイヤ",
435
+ "source": "Love Live! Sunshine!!"
436
+ },
437
+ "ayumu": {
438
+ "file": "ayumu.zip",
439
+ "zh_name": "上原步梦",
440
+ "en_name": "Ayumu Uehara",
441
+ "jp_name": "上原歩夢",
442
+ "source": "Love Live! 虹咲学园"
443
+ },
444
+ "chika": {
445
+ "file": "TakamiChika.zip",
446
+ "zh_name": "高海千歌",
447
+ "en_name": "Chika Takami",
448
+ "jp_name": "高海千歌",
449
+ "source": "Love Live! Sunshine!!",
450
+ "repo": "Icchan/LoveLive"
451
+ },
452
+ "riko": {
453
+ "file": "SakurauchiRiko.zip",
454
+ "zh_name": "樱内梨子",
455
+ "en_name": "Riko Sakurauchi",
456
+ "jp_name": "桜内梨子",
457
+ "source": "Love Live! Sunshine!!",
458
+ "repo": "Icchan/LoveLive"
459
+ },
460
+ "mari": {
461
+ "file": "Mari Ohara (Love Live! Sunshine!!) - Weights.gg Model.zip",
462
+ "zh_name": "小原鞠莉",
463
+ "en_name": "Mari Ohara",
464
+ "jp_name": "小原鞠莉",
465
+ "source": "Love Live! Sunshine!!",
466
+ "repo": "ChocoKat/Mari_Ohara"
467
+ },
468
+ "yohane": {
469
+ "file": "Yoshiko Tsushima.zip",
470
+ "zh_name": "津岛善子(夜羽)",
471
+ "en_name": "Yoshiko Tsushima (Yohane)",
472
+ "jp_name": "ヨハネ",
473
+ "source": "Love Live! Sunshine!! / 幻日夜羽",
474
+ "repo": "HarunaKasuga/YoshikoTsushima"
475
+ },
476
+ "nico": {
477
+ "url": "https://huggingface.co/Zurakichi/RVC/resolve/main/Models/Love%20Live/V2/%C2%B5%27s/NicoYazawa.zip",
478
+ "filename": "NicoYazawa.zip",
479
+ "zh_name": "矢泽妮可",
480
+ "en_name": "Nico Yazawa",
481
+ "jp_name": "矢澤にこ",
482
+ "source": "Love Live!",
483
+ "repo": "Zurakichi/RVC"
484
+ },
485
+ "kanata": {
486
+ "file": "Models/Love Live/V2/Nijigasaki/KanataKonoe.zip",
487
+ "zh_name": "近江彼方",
488
+ "en_name": "Kanata Konoe",
489
+ "jp_name": "近江彼方",
490
+ "source": "Love Live! 虹咲学园",
491
+ "repo": "Zurakichi/RVC"
492
+ },
493
+ "setsuna_yuki_nana": {
494
+ "file": "Models/Love Live/V2/Nijigasaki/SetsunaYuki.zip",
495
+ "zh_name": "优木雪菜",
496
+ "en_name": "Setsuna Yuki",
497
+ "jp_name": "優木せつ菜",
498
+ "variant": "Zurakichi v2",
499
+ "source": "Love Live! 虹咲学园",
500
+ "repo": "Zurakichi/RVC"
501
+ },
502
+ "hanamaru_zurakichi": {
503
+ "file": "Models/Love Live/V2/Aqours/HanamaruKunikida.zip",
504
+ "zh_name": "国木田花丸",
505
+ "en_name": "Hanamaru Kunikida",
506
+ "jp_name": "国木田花丸",
507
+ "variant": "Zurakichi v2",
508
+ "source": "Love Live! Sunshine!!",
509
+ "repo": "Zurakichi/RVC"
510
+ },
511
+ "kanan": {
512
+ "gdrive_id": "16dPSDGb3ciLsy1HtEXG2OSPhU7BuCty_",
513
+ "filename": "Kanan_Matsuura.zip",
514
+ "zh_name": "松浦果南",
515
+ "en_name": "Kanan Matsuura",
516
+ "jp_name": "松浦果南",
517
+ "source": "Love Live! Sunshine!!"
518
+ },
519
+ "yu_takasaki": {
520
+ "gdrive_id": "1xjIG_bsBzOTOwghGaLSMnO_vL2GgMPNw",
521
+ "filename": "Yu_Takasaki.zip",
522
+ "zh_name": "高咲侑",
523
+ "en_name": "Yu Takasaki",
524
+ "jp_name": "高咲侑",
525
+ "source": "Love Live! 虹咲学园"
526
+ },
527
+ "shizuku_osaka": {
528
+ "url": "https://mega.nz/file/UbZDEaRY#YnxExpDIJzh-rEDfEo2khTPAH1p6GZ5FzaMCfWdUQ34",
529
+ "zh_name": "樱坂雫",
530
+ "en_name": "Shizuku Osaka",
531
+ "jp_name": "桜坂しずく",
532
+ "source": "Love Live! 虹咲学园"
533
+ },
534
+ "sarah_kazuno": {
535
+ "file": "SarahKazuno2.zip",
536
+ "zh_name": "鹿角圣良",
537
+ "en_name": "Sarah Kazuno",
538
+ "jp_name": "鹿角聖良",
539
+ "variant": "v2",
540
+ "source": "Love Live! Sunshine!!",
541
+ "repo": "thebuddyadrian/RVC_Models"
542
+ },
543
+ "leah_kazuno": {
544
+ "file": "LeahKazuno2.zip",
545
+ "zh_name": "鹿角理亚",
546
+ "en_name": "Leah Kazuno",
547
+ "jp_name": "鹿角理亞",
548
+ "variant": "v2",
549
+ "source": "Love Live! Sunshine!!",
550
+ "repo": "thebuddyadrian/RVC_Models"
551
+ },
552
+ "eli": {
553
+ "file": "AyaseEli.zip",
554
+ "zh_name": "绚濑绘里",
555
+ "en_name": "Eli Ayase",
556
+ "jp_name": "絢瀬絵里",
557
+ "source": "Love Live!",
558
+ "repo": "Icchan/LoveLive"
559
+ },
560
+ "you": {
561
+ "file": "WatanabeYou.zip",
562
+ "zh_name": "渡边曜",
563
+ "en_name": "You Watanabe",
564
+ "jp_name": "渡辺曜",
565
+ "source": "Love Live! Sunshine!!",
566
+ "repo": "Icchan/LoveLive"
567
+ },
568
+ "honoka": {
569
+ "files": ["weights/Honoka.pth", "weights/honoka.index"],
570
+ "zh_name": "高坂穗乃果",
571
+ "en_name": "Honoka Kosaka",
572
+ "jp_name": "高坂穂乃果",
573
+ "source": "Love Live!",
574
+ "repo": "trioskosmos/rvc_models"
575
+ },
576
+ "kotori": {
577
+ "files": ["weights/Kotori.pth", "weights/kotori.index"],
578
+ "zh_name": "南小鸟",
579
+ "en_name": "Kotori Minami",
580
+ "jp_name": "南ことり",
581
+ "source": "Love Live!",
582
+ "repo": "trioskosmos/rvc_models"
583
+ },
584
+ "maki": {
585
+ "files": ["weights2/Maki.pth", "weights2/maki.index"],
586
+ "zh_name": "西木野真姬",
587
+ "en_name": "Maki Nishikino",
588
+ "jp_name": "西木野真姫",
589
+ "source": "Love Live!",
590
+ "repo": "trioskosmos/rvc_models"
591
+ },
592
+ "ruby": {
593
+ "files": ["weights2/Ruby.pth", "weights2/ruby.index"],
594
+ "zh_name": "黑泽露比",
595
+ "en_name": "Ruby Kurosawa",
596
+ "jp_name": "黒澤ルビィ",
597
+ "source": "Love Live! Sunshine!!",
598
+ "repo": "trioskosmos/rvc_models"
599
+ },
600
+ "kasumi": {
601
+ "files": ["weights/Kasumi.pth", "weights/kasumi.index"],
602
+ "zh_name": "中须霞",
603
+ "en_name": "Kasumi Nakasu",
604
+ "jp_name": "中須かすみ",
605
+ "source": "Love Live! 虹咲学园",
606
+ "repo": "trioskosmos/rvc_models"
607
+ },
608
+ "karin": {
609
+ "files": ["weights/Karin.pth", "weights/karin.index"],
610
+ "zh_name": "朝香果林",
611
+ "en_name": "Karin Asaka",
612
+ "jp_name": "朝香果林",
613
+ "source": "Love Live! 虹咲学园",
614
+ "repo": "trioskosmos/rvc_models"
615
+ },
616
+ "rina": {
617
+ "files": ["weights2/Rina.pth", "weights2/rina.index"],
618
+ "zh_name": "天王寺璃奈",
619
+ "en_name": "Rina Tennoji",
620
+ "jp_name": "天王寺璃奈",
621
+ "source": "Love Live! 虹咲学园",
622
+ "repo": "trioskosmos/rvc_models"
623
+ },
624
+ "lanzhu": {
625
+ "files": ["weights/Lanzhu.pth", "weights/lanzhu.index"],
626
+ "zh_name": "钟岚珠",
627
+ "en_name": "Lanzhu Zhong",
628
+ "jp_name": "鐘嵐珠",
629
+ "source": "Love Live! 虹咲学园",
630
+ "repo": "trioskosmos/rvc_models"
631
+ },
632
+ "keke": {
633
+ "files": ["weights/Keke.pth", "weights/keke.index"],
634
+ "zh_name": "唐可可",
635
+ "en_name": "Keke Tang",
636
+ "jp_name": "唐可可",
637
+ "source": "Love Live! Superstar!!",
638
+ "repo": "trioskosmos/rvc_models"
639
+ },
640
+ "ai_miyashita": {
641
+ "file": "AiMiyashitaV2.zip",
642
+ "zh_name": "宫下爱",
643
+ "en_name": "Ai Miyashita",
644
+ "jp_name": "宮下愛",
645
+ "source": "Love Live! 虹咲学园",
646
+ "repo": "0xMifune/LoveLive"
647
+ },
648
+ "emma_verde": {
649
+ "file": "EmmaVerdeV2.zip",
650
+ "zh_name": "艾玛·维尔德",
651
+ "en_name": "Emma Verde",
652
+ "jp_name": "エマ・ヴェルデ",
653
+ "source": "Love Live! 虹咲学园",
654
+ "repo": "0xMifune/LoveLive"
655
+ },
656
+ "shioriko_mifune": {
657
+ "file": "ShiorikoMifuneV2.zip",
658
+ "zh_name": "三船栞子",
659
+ "en_name": "Shioriko Mifune",
660
+ "jp_name": "三船栞子",
661
+ "source": "Love Live! 虹咲学园",
662
+ "repo": "0xMifune/LoveLive"
663
+ },
664
+ "chisato_arashi": {
665
+ "file": "ChisatoArashiV2.zip",
666
+ "zh_name": "岚千砂都",
667
+ "en_name": "Chisato Arashi",
668
+ "jp_name": "嵐千砂都",
669
+ "source": "Love Live! Superstar!!",
670
+ "repo": "0xMifune/LoveLive"
671
+ },
672
+ "ren_hazuki": {
673
+ "file": "RenHazukiV2.zip",
674
+ "zh_name": "叶月恋",
675
+ "en_name": "Ren Hazuki",
676
+ "jp_name": "葉月恋",
677
+ "source": "Love Live! Superstar!!",
678
+ "repo": "0xMifune/LoveLive"
679
+ },
680
+ "sumire_heanna": {
681
+ "file": "SumireHeannaV2.zip",
682
+ "zh_name": "平安名堇",
683
+ "en_name": "Sumire Heanna",
684
+ "jp_name": "平安名すみれ",
685
+ "source": "Love Live! Superstar!!",
686
+ "repo": "0xMifune/LoveLive"
687
+ },
688
+ "kinako_sakurakoji": {
689
+ "file": "SakurakojiKinakoV2.zip",
690
+ "zh_name": "樱小路希奈子",
691
+ "en_name": "Kinako Sakurakoji",
692
+ "jp_name": "桜小路きな子",
693
+ "source": "Love Live! Superstar!!",
694
+ "repo": "0xMifune/LoveLive"
695
+ },
696
+ "mei_yoneme": {
697
+ "file": "MeiYonemeV2.zip",
698
+ "zh_name": "米女芽衣",
699
+ "en_name": "Mei Yoneme",
700
+ "jp_name": "米女芽衣",
701
+ "source": "Love Live! Superstar!!",
702
+ "repo": "0xMifune/LoveLive"
703
+ },
704
+ "shiki_wakana": {
705
+ "file": "ShikiWakanaV2.zip",
706
+ "zh_name": "若菜四季",
707
+ "en_name": "Shiki Wakana",
708
+ "jp_name": "若菜四季",
709
+ "source": "Love Live! Superstar!!",
710
+ "repo": "0xMifune/LoveLive"
711
+ },
712
+ "natsumi_onitsuka": {
713
+ "file": "NatsumiOnitsukaV2.zip",
714
+ "zh_name": "鬼塚夏美",
715
+ "en_name": "Natsumi Onitsuka",
716
+ "jp_name": "鬼塚夏美",
717
+ "source": "Love Live! Superstar!!",
718
+ "repo": "0xMifune/LoveLive"
719
+ },
720
+ "sayaka_murano": {
721
+ "file": "SayakaMuranoV2.zip",
722
+ "zh_name": "村野纱香",
723
+ "en_name": "Sayaka Murano",
724
+ "jp_name": "村野さやか",
725
+ "source": "Love Live! 莲之空女学院学园偶像俱乐部",
726
+ "repo": "0xMifune/LoveLive"
727
+ },
728
+ "tsuzuri_yugiri": {
729
+ "file": "TsuzuriYugiri.zip",
730
+ "zh_name": "夕雾缀理",
731
+ "en_name": "Tsuzuri Yugiri",
732
+ "jp_name": "夕霧綴理",
733
+ "source": "Love Live! 莲之空女学院学园偶像俱乐部",
734
+ "repo": "0xMifune/LoveLive"
735
+ },
736
+ "hanamaru": {
737
+ "file": "Aqours_Hanamaru-Kunikida-RMVPE-Ov2.zip",
738
+ "zh_name": "国木田花丸",
739
+ "en_name": "Hanamaru Kunikida",
740
+ "jp_name": "国木田花丸",
741
+ "source": "Love Live! Sunshine!!",
742
+ "repo": "Swordsmagus/Love-Live-RVC"
743
+ },
744
+ "setsuna_yuki_og": {
745
+ "file": "Nijigasaki_Setsuna-Yuki-OG-RMVPE-Ov2.zip",
746
+ "zh_name": "优木雪菜",
747
+ "en_name": "Setsuna Yuki",
748
+ "jp_name": "優木せつ菜",
749
+ "variant": "OG",
750
+ "source": "Love Live! 虹咲学园",
751
+ "repo": "Swordsmagus/Love-Live-RVC"
752
+ },
753
+ "setsuna_yuki_coco": {
754
+ "file": "setsuna-yuki-coco-hayashi-ver-555epochs.zip",
755
+ "zh_name": "优木雪菜",
756
+ "en_name": "Setsuna Yuki",
757
+ "jp_name": "優木せつ菜",
758
+ "variant": "林鼓子版",
759
+ "source": "Love Live! 虹咲学园",
760
+ "repo": "Swordsmagus/Love-Live-RVC"
761
+ },
762
+ "hanayo_pre_anime_v0": {
763
+ "file": "Hanayo-Pre-Anime-v0.zip",
764
+ "zh_name": "小泉花阳",
765
+ "en_name": "Hanayo Koizumi",
766
+ "jp_name": "小泉花陽",
767
+ "variant": "预TV v0",
768
+ "source": "Love Live!",
769
+ "repo": "Swordsmagus/Love-Live-RVC"
770
+ },
771
+ "hanayo_pre_anime_v1": {
772
+ "file": "Hanayo-Pre-Anime-v1.zip",
773
+ "zh_name": "小泉花阳",
774
+ "en_name": "Hanayo Koizumi",
775
+ "jp_name": "小泉花陽",
776
+ "variant": "预TV v1",
777
+ "source": "Love Live!",
778
+ "repo": "Swordsmagus/Love-Live-RVC"
779
+ },
780
+ "kotori_pre_anime": {
781
+ "file": "Kotori-Pre-Anime.zip",
782
+ "zh_name": "南小鸟",
783
+ "en_name": "Kotori Minami",
784
+ "jp_name": "南ことり",
785
+ "variant": "预TV",
786
+ "source": "Love Live!",
787
+ "repo": "Swordsmagus/Love-Live-RVC"
788
+ },
789
+ "kotori_pre_anime_v2": {
790
+ "file": "Kotori-Pre-Anime-v2.zip",
791
+ "zh_name": "南小鸟",
792
+ "en_name": "Kotori Minami",
793
+ "jp_name": "南ことり",
794
+ "variant": "预TV v2",
795
+ "source": "Love Live!",
796
+ "repo": "Swordsmagus/Love-Live-RVC"
797
+ },
798
+ "wien_margarete": {
799
+ "file": "Liella_Wien-Margarete-RMVPE-Ov2.zip",
800
+ "zh_name": "维恩·玛格丽特",
801
+ "en_name": "Wien Margarete",
802
+ "jp_name": "ウィーン・マルガレーテ",
803
+ "source": "Love Live! Superstar!!",
804
+ "repo": "Swordsmagus/Love-Live-RVC"
805
+ },
806
+ "mia_taylor": {
807
+ "file": "Mia-Taylor-SIFAS-Lines-Only.zip",
808
+ "zh_name": "米娅·泰勒",
809
+ "en_name": "Mia Taylor",
810
+ "jp_name": "ミア・テイラー",
811
+ "source": "Love Live! 虹咲学园",
812
+ "repo": "Swordsmagus/Love-Live-RVC"
813
+ },
814
+ "anju_yuki_arise": {
815
+ "file": "A-RISE_Anju-Yuki-RMVPE.zip",
816
+ "zh_name": "优木安朱",
817
+ "en_name": "Anju Yuki",
818
+ "jp_name": "優木あんじゅ",
819
+ "source": "Love Live!",
820
+ "repo": "Swordsmagus/Love-Live-RVC"
821
+ },
822
+ "erena_todo_arise": {
823
+ "file": "A-RISE_Erena-Todo-RMVPE.zip",
824
+ "zh_name": "统堂英玲奈",
825
+ "en_name": "Erena Todo",
826
+ "jp_name": "統堂英玲奈",
827
+ "source": "Love Live!",
828
+ "repo": "Swordsmagus/Love-Live-RVC"
829
+ },
830
+ "tsubasa_kira_arise": {
831
+ "file": "A-RISE_Tsubasa-Kira-RMVPE.zip",
832
+ "zh_name": "绮罗翼",
833
+ "en_name": "Tsubasa Kira",
834
+ "jp_name": "綺羅ツバサ",
835
+ "source": "Love Live!",
836
+ "repo": "Swordsmagus/Love-Live-RVC"
837
+ },
838
+ "kanon": {
839
+ "file": "ShibuyaKanonRVC.zip",
840
+ "zh_name": "涩谷香音",
841
+ "en_name": "Kanon Shibuya",
842
+ "jp_name": "渋谷かのん",
843
+ "source": "Love Live! Superstar!!",
844
+ "repo": "Phos252/RVCmodels"
845
+ },
846
+ "setsuna": {
847
+ "files": ["weights2/Setsuna.pth", "weights2/setsuna.index"],
848
+ "zh_name": "优木雪菜",
849
+ "en_name": "Setsuna Yuki",
850
+ "jp_name": "優木せつ菜",
851
+ "source": "Love Live! 虹咲学园",
852
+ "repo": "trioskosmos/rvc_models"
853
+ },
854
+ "setsuna_v2": {
855
+ "files": ["weights2/Setsuna2.pth", "weights2/setsuna2.index"],
856
+ "zh_name": "优木雪菜",
857
+ "en_name": "Setsuna Yuki",
858
+ "jp_name": "優木せつ菜",
859
+ "variant": "v2",
860
+ "source": "Love Live! 虹咲学园",
861
+ "repo": "trioskosmos/rvc_models"
862
+ },
863
+ "chika_v2": {
864
+ "files": ["weights/Chika2.pth", "weights/chika2.index"],
865
+ "zh_name": "高海千歌",
866
+ "en_name": "Chika Takami",
867
+ "jp_name": "高海千歌",
868
+ "variant": "v2",
869
+ "source": "Love Live! Sunshine!!",
870
+ "repo": "trioskosmos/rvc_models"
871
+ },
872
+ "dia_v2": {
873
+ "files": ["weights/Dia2.pth", "weights/dia2.index"],
874
+ "zh_name": "黑泽黛雅",
875
+ "en_name": "Dia Kurosawa",
876
+ "jp_name": "黒澤ダイヤ",
877
+ "variant": "v2",
878
+ "source": "Love Live! Sunshine!!",
879
+ "repo": "trioskosmos/rvc_models"
880
+ },
881
+ "hanamaru_v2": {
882
+ "files": ["weights/Hanamaru2.pth", "weights/hanamaru2.index"],
883
+ "zh_name": "国木田花丸",
884
+ "en_name": "Hanamaru Kunikida",
885
+ "jp_name": "国木田花丸",
886
+ "variant": "v2",
887
+ "source": "Love Live! Sunshine!!",
888
+ "repo": "trioskosmos/rvc_models"
889
+ },
890
+ "honoka_v2": {
891
+ "files": ["weights/Honoka2.pth", "weights/honoka2.index"],
892
+ "zh_name": "高坂穗乃果",
893
+ "en_name": "Honoka Kosaka",
894
+ "jp_name": "高坂穂乃果",
895
+ "variant": "v2",
896
+ "source": "Love Live!",
897
+ "repo": "trioskosmos/rvc_models"
898
+ },
899
+ "ayumu_v2": {
900
+ "files": ["weights/Ayumu2.pth", "weights/ayumu2.index"],
901
+ "zh_name": "上原步梦",
902
+ "en_name": "Ayumu Uehara",
903
+ "jp_name": "上原歩夢",
904
+ "variant": "v2",
905
+ "source": "Love Live! 虹咲学园",
906
+ "repo": "trioskosmos/rvc_models"
907
+ },
908
+ "ayumu_v3": {
909
+ "files": ["weights/Ayumu3.pth", "weights/ayumu3.index"],
910
+ "zh_name": "上原步梦",
911
+ "en_name": "Ayumu Uehara",
912
+ "jp_name": "上原歩夢",
913
+ "variant": "v3",
914
+ "source": "Love Live! 虹咲学园",
915
+ "repo": "trioskosmos/rvc_models"
916
+ },
917
+ "ayumu_v4": {
918
+ "files": ["weights/Ayumu4.pth", "weights/ayumu4.index"],
919
+ "zh_name": "上原步梦",
920
+ "en_name": "Ayumu Uehara",
921
+ "jp_name": "上原歩夢",
922
+ "variant": "v4",
923
+ "source": "Love Live! 虹咲学园",
924
+ "repo": "trioskosmos/rvc_models"
925
+ },
926
+ "karin_v2": {
927
+ "files": ["weights/Karin2.pth", "weights/karin2.index"],
928
+ "zh_name": "朝香果林",
929
+ "en_name": "Karin Asaka",
930
+ "jp_name": "朝香果林",
931
+ "variant": "v2",
932
+ "source": "Love Live! 虹咲学园",
933
+ "repo": "trioskosmos/rvc_models"
934
+ },
935
+ "keke_v2": {
936
+ "files": ["weights/Keke2.pth", "weights/keke2.index"],
937
+ "zh_name": "唐可可",
938
+ "en_name": "Keke Tang",
939
+ "jp_name": "唐可可",
940
+ "variant": "v2",
941
+ "source": "Love Live! Superstar!!",
942
+ "repo": "trioskosmos/rvc_models"
943
+ },
944
+ "riko_v2": {
945
+ "files": ["weights2/Riko2.pth", "weights2/riko2.index"],
946
+ "zh_name": "樱内梨子",
947
+ "en_name": "Riko Sakurauchi",
948
+ "jp_name": "桜内梨子",
949
+ "variant": "v2",
950
+ "source": "Love Live! Sunshine!!",
951
+ "repo": "trioskosmos/rvc_models"
952
+ },
953
+ "umi_v2": {
954
+ "files": ["weights2/Umi2.pth", "weights2/umi2.index"],
955
+ "zh_name": "园田海未",
956
+ "en_name": "Umi Sonoda",
957
+ "jp_name": "園田海未",
958
+ "variant": "v2",
959
+ "source": "Love Live!",
960
+ "repo": "trioskosmos/rvc_models"
961
+ },
962
+ "wien_v2": {
963
+ "files": ["weights2/Wien2.pth", "weights2/wien.index"],
964
+ "zh_name": "维恩·玛格丽特",
965
+ "en_name": "Wien Margarete",
966
+ "jp_name": "ウィーン・マルガレーテ",
967
+ "variant": "v2",
968
+ "source": "Love Live! Superstar!!",
969
+ "repo": "trioskosmos/rvc_models"
970
+ },
971
+ "yohane_trios": {
972
+ "files": ["weights2/Yohane.pth", "weights2/yohane.index"],
973
+ "zh_name": "津岛善子(夜羽)",
974
+ "en_name": "Yoshiko Tsushima (Yohane)",
975
+ "jp_name": "ヨハネ",
976
+ "variant": "trios",
977
+ "source": "Love Live! Sunshine!! / 幻日夜羽",
978
+ "repo": "trioskosmos/rvc_models"
979
+ },
980
+ "yoshiko_trios": {
981
+ "files": ["weights2/yoshiko.pth", "weights2/yoshiko.index"],
982
+ "zh_name": "津岛善子",
983
+ "en_name": "Yoshiko Tsushima",
984
+ "jp_name": "津島善子",
985
+ "variant": "trios",
986
+ "source": "Love Live! Sunshine!!",
987
+ "repo": "trioskosmos/rvc_models"
988
+ },
989
+ "yoshiko_v2": {
990
+ "files": ["weights2/Yoshiko2.pth", "weights2/yoshiko2.index"],
991
+ "zh_name": "津岛善子",
992
+ "en_name": "Yoshiko Tsushima",
993
+ "jp_name": "津島善子",
994
+ "variant": "v2",
995
+ "source": "Love Live! Sunshine!!",
996
+ "repo": "trioskosmos/rvc_models"
997
+ },
998
+ "tokai_teio": {
999
+ "file": "Tokai Teio (Uma Musume) - Weights Model.zip",
1000
+ "zh_name": "东海帝皇",
1001
+ "en_name": "Tokai Teio",
1002
+ "jp_name": "トウカイテイオー",
1003
+ "source": "赛马娘",
1004
+ "repo": "kohaku12/RVC-MODELS"
1005
+ },
1006
+ "focalors": {
1007
+ "file": "Focalors.zip",
1008
+ "zh_name": "芙卡洛斯",
1009
+ "en_name": "Focalors",
1010
+ "jp_name": "フォカロルス",
1011
+ "source": "原神",
1012
+ "repo": "makiligon/RVC-Models"
1013
+ },
1014
+ "essex": {
1015
+ "file": "Essex.zip",
1016
+ "zh_name": "埃塞克斯",
1017
+ "en_name": "Essex",
1018
+ "jp_name": "エセックス",
1019
+ "source": "碧蓝航线",
1020
+ "repo": "makiligon/RVC-Models"
1021
+ },
1022
+ "ellie": {
1023
+ "file": "ellie.zip",
1024
+ "zh_name": "艾莉",
1025
+ "en_name": "Ellie",
1026
+ "jp_name": "エリー",
1027
+ "source": "社区模型",
1028
+ "repo": "megaaziib/my-rvc-models-collection"
1029
+ },
1030
+ "furina": {
1031
+ "file": "Furina.zip",
1032
+ "zh_name": "芙宁娜",
1033
+ "en_name": "Furina",
1034
+ "jp_name": "フリーナ",
1035
+ "source": "原神",
1036
+ "repo": "makiligon/RVC-Models"
1037
+ },
1038
+ "ayaka": {
1039
+ "file": "Ayaka.zip",
1040
+ "zh_name": "神里绫华",
1041
+ "en_name": "Kamisato Ayaka",
1042
+ "jp_name": "神里綾華",
1043
+ "source": "原神",
1044
+ "repo": "makiligon/RVC-Models"
1045
+ },
1046
+ "takane_shijou": {
1047
+ "file": "TakaneShijou.zip",
1048
+ "zh_name": "四条贵音",
1049
+ "en_name": "Takane Shijou",
1050
+ "jp_name": "四条貴��",
1051
+ "source": "偶像大师",
1052
+ "repo": "makiligon/RVC-Models"
1053
+ },
1054
+ "kobo": {
1055
+ "file": "kobo.zip",
1056
+ "zh_name": "可波·卡娜埃露",
1057
+ "en_name": "Kobo Kanaeru",
1058
+ "jp_name": "こぼ・かなえる",
1059
+ "source": "Hololive Indonesia",
1060
+ "repo": "megaaziib/my-rvc-models-collection"
1061
+ },
1062
+ "kaela": {
1063
+ "file": "kaela.zip",
1064
+ "zh_name": "卡埃拉·科瓦尔斯基亚",
1065
+ "en_name": "Kaela Kovalskia",
1066
+ "jp_name": "カエラ・コヴァルスキア",
1067
+ "source": "Hololive Indonesia",
1068
+ "repo": "megaaziib/my-rvc-models-collection"
1069
+ },
1070
+ "pekora": {
1071
+ "file": "pekora.zip",
1072
+ "zh_name": "兔田佩克拉",
1073
+ "en_name": "Usada Pekora",
1074
+ "jp_name": "兎田ぺこら",
1075
+ "source": "Hololive Japan",
1076
+ "repo": "megaaziib/my-rvc-models-collection"
1077
+ },
1078
+ "kizuna_ai": {
1079
+ "file": "kizuna-ai.zip",
1080
+ "zh_name": "绊爱",
1081
+ "en_name": "Kizuna AI",
1082
+ "jp_name": "キズナアイ",
1083
+ "source": "虚拟主播",
1084
+ "repo": "megaaziib/my-rvc-models-collection"
1085
+ },
1086
+ "fuwawa": {
1087
+ "file": "fuwawa.zip",
1088
+ "zh_name": "软软·阿比斯加德",
1089
+ "en_name": "Fuwawa Abyssgard",
1090
+ "jp_name": "フワワ・アビスガード",
1091
+ "source": "Hololive English",
1092
+ "repo": "megaaziib/my-rvc-models-collection"
1093
+ },
1094
+ "mococo": {
1095
+ "file": "mococo.zip",
1096
+ "zh_name": "茸茸·阿比斯加德",
1097
+ "en_name": "Mococo Abyssgard",
1098
+ "jp_name": "モココ・アビスガード",
1099
+ "source": "Hololive English",
1100
+ "repo": "megaaziib/my-rvc-models-collection"
1101
+ },
1102
+ "vo_furina_kr": {
1103
+ "file": "vo_furina_kr.zip",
1104
+ "zh_name": "芙宁娜",
1105
+ "en_name": "Furina",
1106
+ "jp_name": "フリーナ",
1107
+ "variant": "韩语",
1108
+ "lang": "韩文",
1109
+ "source": "原神",
1110
+ "repo": "jarari/RVC-v2"
1111
+ },
1112
+ "silverwolf_kr": {
1113
+ "file": "silverwolf_kr.zip",
1114
+ "zh_name": "银狼",
1115
+ "en_name": "Silver Wolf",
1116
+ "jp_name": "銀狼",
1117
+ "variant": "韩语",
1118
+ "lang": "韩文",
1119
+ "source": "崩坏:星穹铁道",
1120
+ "repo": "jarari/RVC-v2"
1121
+ },
1122
+ "sparkle_e40": {
1123
+ "file": "sparkle_e40.zip",
1124
+ "zh_name": "花火",
1125
+ "en_name": "Sparkle",
1126
+ "jp_name": "花火",
1127
+ "variant": "日语",
1128
+ "source": "崩坏:星穹铁道",
1129
+ "repo": "kohaku12/RVC-MODELS"
1130
+ },
1131
+ "ranko": {
1132
+ "file": "ranko.zip",
1133
+ "zh_name": "神崎兰子",
1134
+ "en_name": "Ranko Kanzaki",
1135
+ "jp_name": "神崎蘭子",
1136
+ "source": "偶像大师 灰姑娘女孩"
1137
+ },
1138
+ "yumemiriamu": {
1139
+ "file": "yumemiriamu.zip",
1140
+ "zh_name": "梦见莉亚梦",
1141
+ "en_name": "Riamu Yumemi",
1142
+ "jp_name": "夢見りあむ",
1143
+ "source": "偶像大师 灰姑娘女孩"
1144
+ },
1145
+ # ===== VOCALOID =====
1146
+ "hatsune_miku": {
1147
+ "file": "infamous_miku_v2.zip",
1148
+ "zh_name": "初音未来",
1149
+ "en_name": "Hatsune Miku",
1150
+ "jp_name": "初音ミク",
1151
+ "source": "VOCALOID",
1152
+ "repo": "javinfamous/infamous_miku_v2"
1153
+ },
1154
+ # ===== 原神 (更多角色) =====
1155
+ "nahida": {
1156
+ "file": "Nahida JP (VA_ Yukari Tamura) - Weights.gg Model.zip",
1157
+ "zh_name": "纳西妲",
1158
+ "en_name": "Nahida",
1159
+ "jp_name": "ナヒーダ",
1160
+ "source": "原神",
1161
+ "repo": "kohaku12/RVC-MODELS"
1162
+ },
1163
+ "nilou": {
1164
+ "file": "Nilou%20JP%20(Kanemoto%20Hisako)%20-%20Weights.gg%20Model.zip",
1165
+ "zh_name": "妮露",
1166
+ "en_name": "Nilou",
1167
+ "jp_name": "ニィロウ",
1168
+ "source": "原神",
1169
+ "repo": "kohaku12/RVC-MODELS"
1170
+ },
1171
+ # ===== 崩坏:星穹铁道 (更多角色) =====
1172
+ "herta_hsr": {
1173
+ "file": "Herta JP (VA_ Haruka Yamazaki) (Honkai_ Star Rail) - Weights.gg Model.zip",
1174
+ "zh_name": "黑塔",
1175
+ "en_name": "Herta",
1176
+ "jp_name": "ヘルタ",
1177
+ "source": "崩坏:星穹铁道",
1178
+ "repo": "kohaku12/RVC-MODELS"
1179
+ },
1180
+ "the_herta": {
1181
+ "file": "The Herta -Honkai Star Rail - Weights Model.zip",
1182
+ "zh_name": "大黑塔",
1183
+ "en_name": "The Herta",
1184
+ "jp_name": "マダム・ヘルタ",
1185
+ "source": "崩坏:星穹铁道",
1186
+ "repo": "kohaku12/RVC-MODELS"
1187
+ },
1188
+ "firefly_hsr": {
1189
+ "file": "Firefly _ Honkai_ Star Rail - Weights.gg Model.zip",
1190
+ "zh_name": "流萤",
1191
+ "en_name": "Firefly",
1192
+ "jp_name": "ホタル",
1193
+ "source": "崩坏:星穹铁道",
1194
+ "repo": "kohaku12/RVC-MODELS"
1195
+ },
1196
+ "tingyun_hsr": {
1197
+ "file": "Tingyun (Honkai_ Star Rail) (VA_ Yuuki Takada) - Weights.gg Model.zip",
1198
+ "zh_name": "停云",
1199
+ "en_name": "Tingyun",
1200
+ "jp_name": "停雲",
1201
+ "source": "崩坏:星穹铁道",
1202
+ "repo": "kohaku12/RVC-MODELS"
1203
+ },
1204
+ "yunli_hsr": {
1205
+ "file": "Yunli (Japanese Voice) Honkai Star Rail - Weights.gg Model.zip",
1206
+ "zh_name": "云璃",
1207
+ "en_name": "Yunli",
1208
+ "jp_name": "雲璃",
1209
+ "source": "崩坏:星穹铁道",
1210
+ "repo": "kohaku12/RVC-MODELS"
1211
+ },
1212
+ "tribbie_hsr": {
1213
+ "file": "Tribbie _ Honkai Star Rail JP (CV_ Hikaru Tono) - Weights Model.zip",
1214
+ "zh_name": "缇宝",
1215
+ "en_name": "Tribbie",
1216
+ "jp_name": "トリビー",
1217
+ "source": "崩坏:星穹铁道",
1218
+ "repo": "kohaku12/RVC-MODELS"
1219
+ },
1220
+ "huohuo_hsr": {
1221
+ "file": "huohuo2.zip",
1222
+ "zh_name": "藿藿",
1223
+ "en_name": "Huohuo",
1224
+ "jp_name": "フォフォ",
1225
+ "source": "崩坏:星穹铁道",
1226
+ "repo": "kohaku12/RVC-MODELS"
1227
+ },
1228
+ "castorice_hsr": {
1229
+ "file": "Castorice (Honkai_ Star Rail, Japanese - CV_ Chiwa Saito) [Preliminary] - Weights Model.zip",
1230
+ "zh_name": "遐蝶",
1231
+ "en_name": "Castorice",
1232
+ "jp_name": "キャストリス",
1233
+ "source": "崩坏:星穹铁道",
1234
+ "repo": "kohaku12/RVC-MODELS"
1235
+ },
1236
+ # ===== 崩坏3rd =====
1237
+ "seele_hi3": {
1238
+ "file": "Seele (Honkai impact 3rd) - Weights.gg Model.zip",
1239
+ "zh_name": "希儿",
1240
+ "en_name": "Seele",
1241
+ "jp_name": "ゼーレ",
1242
+ "source": "崩坏3rd",
1243
+ "repo": "kohaku12/RVC-MODELS"
1244
+ },
1245
+ "herrscher_ego_hi3": {
1246
+ "file": "Herrscher of Human_ Ego (Honkai Impact 3rd) - Weights.gg Model.zip",
1247
+ "zh_name": "真我·人之律者",
1248
+ "en_name": "Herrscher of Human: Ego",
1249
+ "jp_name": "真我・人の律者",
1250
+ "source": "崩坏3rd",
1251
+ "repo": "kohaku12/RVC-MODELS"
1252
+ },
1253
+ # ===== 绝区零 =====
1254
+ "miyabi_zzz": {
1255
+ "file": "hoshimi miyabi (Zenless Zone Zero)JP - Weights Model.zip",
1256
+ "zh_name": "星见雅",
1257
+ "en_name": "Hoshimi Miyabi",
1258
+ "jp_name": "星見雅",
1259
+ "source": "绝区零",
1260
+ "repo": "kohaku12/RVC-MODELS"
1261
+ },
1262
+ # ===== Project SEKAI =====
1263
+ "nene_kusanagi": {
1264
+ "file": "Nene Kusanagi (Project Sekai) - Weights Model.zip",
1265
+ "zh_name": "草薙宁宁",
1266
+ "en_name": "Nene Kusanagi",
1267
+ "jp_name": "草薙寧々",
1268
+ "source": "Project SEKAI",
1269
+ "repo": "kohaku12/RVC-MODELS"
1270
+ },
1271
+ # ===== Hololive (更多角色) =====
1272
+ "miko_sakura": {
1273
+ "file": "miko.zip",
1274
+ "zh_name": "樱巫女",
1275
+ "en_name": "Sakura Miko",
1276
+ "jp_name": "さくらみこ",
1277
+ "source": "Hololive Japan",
1278
+ "repo": "megaaziib/my-rvc-models-collection"
1279
+ },
1280
+ "subaru_oozora": {
1281
+ "file": "subaru.zip",
1282
+ "zh_name": "大空昴",
1283
+ "en_name": "Oozora Subaru",
1284
+ "jp_name": "大空スバル",
1285
+ "source": "Hololive Japan",
1286
+ "repo": "megaaziib/my-rvc-models-collection"
1287
+ },
1288
+ "moona_hoshinova": {
1289
+ "file": "moona.zip",
1290
+ "zh_name": "穆娜・霍希诺瓦",
1291
+ "en_name": "Moona Hoshinova",
1292
+ "jp_name": "ムーナ・ホシノヴァ",
1293
+ "source": "Hololive Indonesia",
1294
+ "repo": "megaaziib/my-rvc-models-collection"
1295
+ },
1296
+ "risu_ayunda": {
1297
+ "file": "risu.zip",
1298
+ "zh_name": "阿云达·莉苏",
1299
+ "en_name": "Ayunda Risu",
1300
+ "jp_name": "アユンダ・リス",
1301
+ "source": "Hololive Indonesia",
1302
+ "repo": "megaaziib/my-rvc-models-collection"
1303
+ },
1304
+ "reine_pavolia": {
1305
+ "file": "reine.zip",
1306
+ "zh_name": "帕沃莉亚・蕾妮",
1307
+ "en_name": "Pavolia Reine",
1308
+ "jp_name": "パヴォリア・レイネ",
1309
+ "source": "Hololive Indonesia",
1310
+ "repo": "megaaziib/my-rvc-models-collection"
1311
+ },
1312
+ "zeta_vestia": {
1313
+ "file": "zeta.zip",
1314
+ "zh_name": "贝斯蒂亚・泽塔",
1315
+ "en_name": "Vestia Zeta",
1316
+ "jp_name": "ヴェスティア・ゼータ",
1317
+ "source": "Hololive Indonesia",
1318
+ "repo": "megaaziib/my-rvc-models-collection"
1319
+ },
1320
+ "anya_melfissa": {
1321
+ "file": "anya.zip",
1322
+ "zh_name": "安亚・梅尔菲莎",
1323
+ "en_name": "Anya Melfissa",
1324
+ "jp_name": "アーニャ・メルフィッサ",
1325
+ "source": "Hololive Indonesia",
1326
+ "repo": "megaaziib/my-rvc-models-collection"
1327
+ },
1328
+ "luna_himemori": {
1329
+ "file": "luna.zip",
1330
+ "zh_name": "姬森露娜",
1331
+ "en_name": "Himemori Luna",
1332
+ "jp_name": "姫森ルーナ",
1333
+ "source": "Hololive Japan",
1334
+ "repo": "megaaziib/my-rvc-models-collection"
1335
+ },
1336
+ # ===== 碧蓝航线 =====
1337
+ "boothill": {
1338
+ "file": "Boothill.zip",
1339
+ "zh_name": "波提欧",
1340
+ "en_name": "Boothill",
1341
+ "jp_name": "ブートヒル",
1342
+ "source": "崩坏:星穹铁道",
1343
+ "repo": "makiligon/RVC-Models"
1344
+ },
1345
+ # ===== 明日方舟 =====
1346
+ "shiroko_rosmontis": {
1347
+ "file": "ShirokoRosmontis.zip",
1348
+ "zh_name": "白子/迷迭香",
1349
+ "en_name": "Shiroko / Rosmontis",
1350
+ "jp_name": "シロコ / ロスモンティス",
1351
+ "source": "蔚蓝档案 / 明日方舟",
1352
+ "repo": "makiligon/RVC-Models"
1353
+ },
1354
+ # ===== 赛马娘 (更多角色) =====
1355
+ "rice_shower": {
1356
+ "file": "RiceShowerSinging.zip",
1357
+ "zh_name": "米浴",
1358
+ "en_name": "Rice Shower",
1359
+ "jp_name": "ライスシャワー",
1360
+ "source": "赛马娘",
1361
+ "repo": "makiligon/RVC-Models"
1362
+ },
1363
+ }
1364
+
1365
+
1366
+ def get_project_root() -> Path:
1367
+ """获取项目根目录"""
1368
+ return Path(__file__).parent.parent
1369
+
1370
+
1371
+ def get_character_models_dir() -> Path:
1372
+ """获取角色模型目录"""
1373
+ return get_project_root() / "assets" / "weights" / "characters"
1374
+
1375
+
1376
+ def list_available_characters() -> List[Dict]:
1377
+ """
1378
+ 列出可用的角色模型
1379
+
1380
+ Returns:
1381
+ list: 角色信息列表
1382
+ """
1383
+ result = []
1384
+ for name, info in CHARACTER_MODELS.items():
1385
+ source = info.get("source", "未知")
1386
+ display = _get_display_name(info, name)
1387
+ result.append({
1388
+ "name": name,
1389
+ "description": info.get("description", display),
1390
+ "display": display,
1391
+ "source": source,
1392
+ "series": normalize_series(source),
1393
+ "file": info.get("file"),
1394
+ "files": info.get("files"),
1395
+ "url": info.get("url"),
1396
+ "repo": info.get("repo", HF_REPO_ID)
1397
+ })
1398
+ return result
1399
+
1400
+
1401
+ def list_downloaded_characters() -> List[Dict]:
1402
+ """
1403
+ 列出已下载的角色模型
1404
+
1405
+ Returns:
1406
+ list: 已下载的角色信息
1407
+ """
1408
+ models_dir = get_character_models_dir()
1409
+ if not models_dir.exists():
1410
+ return []
1411
+
1412
+ downloaded = []
1413
+ seen = set()
1414
+
1415
+ # 递归搜索 .pth,优先使用顶层目录名作为角色名
1416
+ for pth_file in models_dir.rglob("*.pth"):
1417
+ rel_path = pth_file.relative_to(models_dir)
1418
+ parts = rel_path.parts
1419
+ if len(parts) == 1:
1420
+ char_name = pth_file.stem
1421
+ else:
1422
+ char_name = parts[0]
1423
+
1424
+ if char_name in seen:
1425
+ continue
1426
+ seen.add(char_name)
1427
+
1428
+ info = CHARACTER_MODELS.get(char_name, {})
1429
+ source = info.get("source", "未知")
1430
+ display = _get_display_name(info, char_name)
1431
+ index_file = _find_index_file(pth_file)
1432
+ downloaded.append({
1433
+ "name": char_name,
1434
+ "description": info.get("description", display),
1435
+ "display": display,
1436
+ "source": source,
1437
+ "series": normalize_series(source),
1438
+ "model_path": str(pth_file),
1439
+ "index_path": str(index_file) if index_file else None
1440
+ })
1441
+
1442
+ return downloaded
1443
+
1444
+
1445
+ def get_character_model_path(name: str) -> Optional[Dict]:
1446
+ """
1447
+ 获取角色模型路径
1448
+
1449
+ Args:
1450
+ name: 角色名称
1451
+
1452
+ Returns:
1453
+ dict: 包含 model_path 和 index_path 的字典,或 None
1454
+ """
1455
+ models_dir = get_character_models_dir()
1456
+ char_dir = models_dir / name
1457
+
1458
+ # 1) 标准目录结构: characters/<name>/*.pth
1459
+ if char_dir.exists():
1460
+ pth_files = list(char_dir.glob("*.pth"))
1461
+ if pth_files:
1462
+ index_file = _find_index_file(pth_files[0])
1463
+ return {
1464
+ "model_path": str(pth_files[0]),
1465
+ "index_path": str(index_file) if index_file else None
1466
+ }
1467
+
1468
+ # 2) 允许直接放在 characters 目录
1469
+ direct_pth = models_dir / f"{name}.pth"
1470
+ if direct_pth.exists():
1471
+ index_file = _find_index_file(direct_pth)
1472
+ return {
1473
+ "model_path": str(direct_pth),
1474
+ "index_path": str(index_file) if index_file else None
1475
+ }
1476
+
1477
+ # 3) 兜底:在 characters 目录递归查找同名模型
1478
+ for pth_file in models_dir.rglob("*.pth"):
1479
+ if pth_file.stem.lower() == name.lower():
1480
+ index_file = _find_index_file(pth_file)
1481
+ return {
1482
+ "model_path": str(pth_file),
1483
+ "index_path": str(index_file) if index_file else None
1484
+ }
1485
+
1486
+ return None
1487
+
1488
+
1489
+ def download_character_model(
1490
+ name: str,
1491
+ progress_callback: Optional[Callable[[str, float], None]] = None
1492
+ ) -> bool:
1493
+ """
1494
+ 下载角色模型
1495
+
1496
+ Args:
1497
+ name: 角色名称
1498
+ progress_callback: 进度回调 (message, progress)
1499
+
1500
+ Returns:
1501
+ bool: 是否成功
1502
+ """
1503
+ if name not in CHARACTER_MODELS:
1504
+ _safe_print(f"未知角色: {name}")
1505
+ return False
1506
+
1507
+ char_info = CHARACTER_MODELS[name]
1508
+ repo_id = char_info.get("repo", HF_REPO_ID)
1509
+ zip_file = char_info.get("file")
1510
+ file_list = char_info.get("files")
1511
+ direct_url = char_info.get("url")
1512
+ gdrive_id = char_info.get("gdrive_id")
1513
+ pattern = char_info.get("pattern")
1514
+
1515
+ # 检查是否已下载
1516
+ char_dir = get_character_models_dir() / name
1517
+ if char_dir.exists() and list(char_dir.glob("*.pth")):
1518
+ _safe_print(f"角色模型已存在: {name}")
1519
+ return True
1520
+
1521
+ if progress_callback:
1522
+ progress_callback(f"正在下载 {name} 模型...", 0.1)
1523
+
1524
+ hf_token = _get_hf_token()
1525
+
1526
+ try:
1527
+ # Google Drive 下载
1528
+ if gdrive_id:
1529
+ filename = char_info.get("filename") or f"{name}.zip"
1530
+ temp_dir = get_project_root() / "temp" / "downloads"
1531
+ temp_dir.mkdir(parents=True, exist_ok=True)
1532
+ temp_path = temp_dir / filename
1533
+
1534
+ _safe_print(f"正在从 Google Drive 下载: {filename}")
1535
+ if not _download_gdrive_file(gdrive_id, temp_path):
1536
+ return False
1537
+
1538
+ char_dir.mkdir(parents=True, exist_ok=True)
1539
+ if temp_path.suffix.lower() == ".zip":
1540
+ if progress_callback:
1541
+ progress_callback(f"正在解压 {name} 模型...", 0.6)
1542
+ with zipfile.ZipFile(temp_path, 'r') as zip_ref:
1543
+ zip_ref.extractall(char_dir)
1544
+ _flatten_extracted_dir(char_dir)
1545
+ else:
1546
+ shutil.copy(str(temp_path), str(char_dir / temp_path.name))
1547
+
1548
+ # 直链下载(可用于非 HuggingFace 源)
1549
+ elif direct_url:
1550
+ if "mega.nz" in direct_url:
1551
+ _safe_print("Mega 下载暂不支持,请手动下载并放入角色目录")
1552
+ return False
1553
+ from tools.download_models import download_file
1554
+
1555
+ filename = char_info.get("filename") or Path(direct_url).name
1556
+ temp_dir = get_project_root() / "temp" / "downloads"
1557
+ temp_dir.mkdir(parents=True, exist_ok=True)
1558
+ temp_path = temp_dir / filename
1559
+
1560
+ _safe_print(f"正在下载: {direct_url}")
1561
+ if not download_file(direct_url, temp_path, name):
1562
+ return False
1563
+
1564
+ char_dir.mkdir(parents=True, exist_ok=True)
1565
+ if temp_path.suffix.lower() == ".zip":
1566
+ if progress_callback:
1567
+ progress_callback(f"正在解压 {name} 模型...", 0.6)
1568
+ with zipfile.ZipFile(temp_path, 'r') as zip_ref:
1569
+ zip_ref.extractall(char_dir)
1570
+ _flatten_extracted_dir(char_dir)
1571
+ else:
1572
+ shutil.copy(str(temp_path), str(char_dir / temp_path.name))
1573
+
1574
+ # HuggingFace 多文件下载
1575
+ elif file_list:
1576
+ if not HF_AVAILABLE:
1577
+ raise ImportError("请安装 huggingface_hub: pip install huggingface_hub")
1578
+
1579
+ _safe_print(f"正在从 HuggingFace 下载: {repo_id} (files)")
1580
+ char_dir.mkdir(parents=True, exist_ok=True)
1581
+ total = len(file_list)
1582
+ for idx, hf_file in enumerate(file_list, start=1):
1583
+ if progress_callback and total > 0:
1584
+ progress_callback(
1585
+ f"正在下载 {name} 文件 {idx}/{total}...",
1586
+ 0.1 + 0.8 * (idx / total)
1587
+ )
1588
+ downloaded_path = hf_hub_download(
1589
+ repo_id=repo_id,
1590
+ filename=hf_file,
1591
+ cache_dir=str(get_project_root() / "temp" / "hf_cache"),
1592
+ token=hf_token
1593
+ )
1594
+ target_name = Path(hf_file).name
1595
+ shutil.copy(str(downloaded_path), str(char_dir / target_name))
1596
+
1597
+ # HuggingFace: 根据 pattern 自动选择文件
1598
+ elif pattern:
1599
+ if not HF_AVAILABLE:
1600
+ raise ImportError("请安装 huggingface_hub: pip install huggingface_hub")
1601
+
1602
+ _safe_print(f"正在从 HuggingFace 下载: {repo_id} (auto)")
1603
+ files = list_repo_files(repo_id, token=hf_token)
1604
+ if isinstance(pattern, str):
1605
+ patterns = [pattern]
1606
+ else:
1607
+ patterns = list(pattern)
1608
+
1609
+ candidates = []
1610
+ for f in files:
1611
+ for p in patterns:
1612
+ if p in f or (p == ".zip" and f.lower().endswith(".zip")):
1613
+ candidates.append(f)
1614
+ break
1615
+
1616
+ if not candidates:
1617
+ _safe_print(f"未找到匹配文件: {pattern}")
1618
+ return False
1619
+
1620
+ # 优先 zip
1621
+ zip_candidates = [c for c in candidates if c.lower().endswith(".zip")]
1622
+ selected = zip_candidates[0] if zip_candidates else candidates[0]
1623
+
1624
+ if selected.lower().endswith(".zip"):
1625
+ zip_file = selected
1626
+ downloaded_path = hf_hub_download(
1627
+ repo_id=repo_id,
1628
+ filename=zip_file,
1629
+ cache_dir=str(get_project_root() / "temp" / "hf_cache"),
1630
+ token=hf_token
1631
+ )
1632
+ if progress_callback:
1633
+ progress_callback(f"正在解压 {name} 模型...", 0.6)
1634
+ char_dir.mkdir(parents=True, exist_ok=True)
1635
+ with zipfile.ZipFile(downloaded_path, 'r') as zip_ref:
1636
+ zip_ref.extractall(char_dir)
1637
+ _flatten_extracted_dir(char_dir)
1638
+ else:
1639
+ # 多文件下载
1640
+ file_list = candidates
1641
+ char_dir.mkdir(parents=True, exist_ok=True)
1642
+ total = len(file_list)
1643
+ for idx, hf_file in enumerate(file_list, start=1):
1644
+ if progress_callback and total > 0:
1645
+ progress_callback(
1646
+ f"正在下载 {name} 文件 {idx}/{total}...",
1647
+ 0.1 + 0.8 * (idx / total)
1648
+ )
1649
+ downloaded_path = hf_hub_download(
1650
+ repo_id=repo_id,
1651
+ filename=hf_file,
1652
+ cache_dir=str(get_project_root() / "temp" / "hf_cache"),
1653
+ token=hf_token
1654
+ )
1655
+ target_name = Path(hf_file).name
1656
+ shutil.copy(str(downloaded_path), str(char_dir / target_name))
1657
+
1658
+ # HuggingFace zip 下载
1659
+ else:
1660
+ if not HF_AVAILABLE:
1661
+ raise ImportError("请安装 huggingface_hub: pip install huggingface_hub")
1662
+ _safe_print(f"正在从 HuggingFace 下载: {repo_id}/{zip_file}")
1663
+
1664
+ downloaded_path = hf_hub_download(
1665
+ repo_id=repo_id,
1666
+ filename=zip_file,
1667
+ cache_dir=str(get_project_root() / "temp" / "hf_cache"),
1668
+ token=hf_token
1669
+ )
1670
+
1671
+ if progress_callback:
1672
+ progress_callback(f"正在解压 {name} 模型...", 0.6)
1673
+
1674
+ char_dir.mkdir(parents=True, exist_ok=True)
1675
+
1676
+ with zipfile.ZipFile(downloaded_path, 'r') as zip_ref:
1677
+ zip_ref.extractall(char_dir)
1678
+
1679
+ _flatten_extracted_dir(char_dir)
1680
+
1681
+ if progress_callback:
1682
+ progress_callback(f"{name} 模型下载完成", 1.0)
1683
+
1684
+ # 下载完成后更新版本说明缓存
1685
+ _get_version_note(name, char_info)
1686
+ _safe_print(f"角色模型已下载: {name}")
1687
+ return True
1688
+
1689
+ except Exception as e:
1690
+ _safe_print(f"下载失败: {e}")
1691
+ if progress_callback:
1692
+ progress_callback(f"下载失败: {e}", 0)
1693
+ return False
1694
+
1695
+
1696
+ def _flatten_extracted_dir(char_dir: Path):
1697
+ """
1698
+ 处理解压后可能的嵌套目录结构
1699
+ """
1700
+ # 检查是否有嵌套的单一目录
1701
+ items = list(char_dir.iterdir())
1702
+ if len(items) == 1 and items[0].is_dir():
1703
+ nested_dir = items[0]
1704
+ # 移动内容到上级目录
1705
+ for item in nested_dir.iterdir():
1706
+ shutil.move(str(item), str(char_dir / item.name))
1707
+ nested_dir.rmdir()
1708
+
1709
+
1710
+ def delete_character_model(name: str) -> bool:
1711
+ """
1712
+ 删除角色模型
1713
+
1714
+ Args:
1715
+ name: 角色名称
1716
+
1717
+ Returns:
1718
+ bool: 是否成功
1719
+ """
1720
+ char_dir = get_character_models_dir() / name
1721
+ if char_dir.exists():
1722
+ shutil.rmtree(char_dir)
1723
+ print(f"已删除角色模型: {name}")
1724
+ return True
1725
+ return False
1726
+
1727
+
1728
+ def check_hf_available() -> bool:
1729
+ """检查 huggingface_hub 是否可用"""
1730
+ return HF_AVAILABLE
1731
+
1732
+
1733
+ def list_available_series() -> List[str]:
1734
+ """
1735
+ 获取可用的作品/系列列表(去重)
1736
+ """
1737
+ series_set = set()
1738
+ for info in CHARACTER_MODELS.values():
1739
+ source = info.get("source", "未知")
1740
+ series_set.add(normalize_series(source))
1741
+ return sorted(series_set)
1742
+
1743
+
1744
+ def download_all_character_models(
1745
+ series: Optional[str] = None,
1746
+ progress_callback: Optional[Callable[[str, float], None]] = None
1747
+ ) -> Dict[str, List[str]]:
1748
+ """
1749
+ 批量下载角色模型
1750
+
1751
+ Args:
1752
+ series: 仅下载指定系列(如 "Love Live!"),None 表示全部
1753
+ progress_callback: 进度回调 (message, progress)
1754
+
1755
+ Returns:
1756
+ dict: { "success": [...], "failed": [...] }
1757
+ """
1758
+ targets = list_available_characters()
1759
+ if series and series != "全部":
1760
+ targets = [c for c in targets if c.get("series") == series]
1761
+
1762
+ success = []
1763
+ failed = []
1764
+ total = max(len(targets), 1)
1765
+
1766
+ for idx, char in enumerate(targets, start=1):
1767
+ if progress_callback:
1768
+ progress_callback(
1769
+ f"正在下载 {char['name']} ({idx}/{total})...",
1770
+ idx / total
1771
+ )
1772
+ ok = download_character_model(char["name"])
1773
+ if ok:
1774
+ success.append(char["name"])
1775
+ else:
1776
+ failed.append(char["name"])
1777
+
1778
+ return {
1779
+ "success": success,
1780
+ "failed": failed
1781
+ }
1782
+
1783
+
1784
+ def get_character_choices() -> List[str]:
1785
+ """
1786
+ 获取角色选择列表 (用于 UI 下拉框)
1787
+
1788
+ Returns:
1789
+ list: 角色名称列表
1790
+ """
1791
+ downloaded = list_downloaded_characters()
1792
+ return [c["name"] for c in downloaded]
tools/download_models.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 模型下载工具 - 自动从 Hugging Face 下载所需模型
4
+ """
5
+ import os
6
+ import hashlib
7
+ import requests
8
+ from pathlib import Path
9
+ from tqdm import tqdm
10
+ from typing import Optional, Dict, List
11
+
12
+ # 模型下载配置
13
+ MODELS = {
14
+ "HP2_all_vocals.pth": {
15
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/uvr5_weights/HP2_all_vocals.pth",
16
+ "path": "assets/uvr5_weights/HP2_all_vocals.pth",
17
+ "size_mb": 140,
18
+ "description": "UVR5 HP2 vocal model"
19
+ },
20
+ "HP3_all_vocals.pth": {
21
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/uvr5_weights/HP3_all_vocals.pth",
22
+ "path": "assets/uvr5_weights/HP3_all_vocals.pth",
23
+ "size_mb": 140,
24
+ "description": "UVR5 HP3 vocal model"
25
+ },
26
+ "HP5_only_main_vocal.pth": {
27
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/uvr5_weights/HP5_only_main_vocal.pth",
28
+ "path": "assets/uvr5_weights/HP5_only_main_vocal.pth",
29
+ "size_mb": 140,
30
+ "description": "UVR5 HP5 main vocal model"
31
+ },
32
+ "VR-DeEchoAggressive.pth": {
33
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/uvr5_weights/VR-DeEchoAggressive.pth",
34
+ "path": "assets/uvr5_weights/VR-DeEchoAggressive.pth",
35
+ "size_mb": 130,
36
+ "description": "UVR5 de-echo aggressive"
37
+ },
38
+ "VR-DeEchoDeReverb.pth": {
39
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/uvr5_weights/VR-DeEchoDeReverb.pth",
40
+ "path": "assets/uvr5_weights/VR-DeEchoDeReverb.pth",
41
+ "size_mb": 130,
42
+ "description": "UVR5 de-echo + de-reverb"
43
+ },
44
+ "VR-DeEchoNormal.pth": {
45
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/uvr5_weights/VR-DeEchoNormal.pth",
46
+ "path": "assets/uvr5_weights/VR-DeEchoNormal.pth",
47
+ "size_mb": 130,
48
+ "description": "UVR5 de-echo normal"
49
+ },
50
+ "onnx_dereverb_By_FoxJoy/vocals.onnx": {
51
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/uvr5_weights/onnx_dereverb_By_FoxJoy/vocals.onnx",
52
+ "path": "assets/uvr5_weights/onnx_dereverb_By_FoxJoy/vocals.onnx",
53
+ "size_mb": 50,
54
+ "description": "UVR5 ONNX dereverb"
55
+ },
56
+ "hubert_base.pt": {
57
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/hubert_base.pt",
58
+ "path": "assets/hubert/hubert_base.pt",
59
+ "size_mb": 189,
60
+ "description": "HuBERT 特征提取模型"
61
+ },
62
+ "rmvpe.pt": {
63
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/rmvpe.pt",
64
+ "path": "assets/rmvpe/rmvpe.pt",
65
+ "size_mb": 181,
66
+ "description": "RMVPE 音高提取模型"
67
+ },
68
+ "f0G48k.pth": {
69
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/pretrained_v2/f0G48k.pth",
70
+ "path": "assets/pretrained_v2/f0G48k.pth",
71
+ "size_mb": 55,
72
+ "description": "48kHz 生成器预训练权重"
73
+ },
74
+ "f0D48k.pth": {
75
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/pretrained_v2/f0D48k.pth",
76
+ "path": "assets/pretrained_v2/f0D48k.pth",
77
+ "size_mb": 55,
78
+ "description": "48kHz 判别器预训练权重"
79
+ },
80
+ "f0G40k.pth": {
81
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/pretrained_v2/f0G40k.pth",
82
+ "path": "assets/pretrained_v2/f0G40k.pth",
83
+ "size_mb": 55,
84
+ "description": "40kHz 生成器预训练权重"
85
+ },
86
+ "f0D40k.pth": {
87
+ "url": "https://huggingface.co/lj1995/VoiceConversionWebUI/resolve/main/pretrained_v2/f0D40k.pth",
88
+ "path": "assets/pretrained_v2/f0D40k.pth",
89
+ "size_mb": 55,
90
+ "description": "40kHz 判别器预训练权重"
91
+ }
92
+ }
93
+
94
+ # 必需模型列表
95
+ REQUIRED_MODELS = ["hubert_base.pt", "rmvpe.pt", "HP2_all_vocals.pth"]
96
+
97
+ # Mature DeEcho / DeReverb models downloaded separately
98
+ MATURE_DEECHO_MODELS = [
99
+ "VR-DeEchoDeReverb.pth",
100
+ "onnx_dereverb_By_FoxJoy/vocals.onnx",
101
+ "VR-DeEchoNormal.pth",
102
+ "VR-DeEchoAggressive.pth",
103
+ ]
104
+
105
+
106
+ def get_project_root() -> Path:
107
+ """获取项目根目录"""
108
+ return Path(__file__).parent.parent
109
+
110
+
111
+ def download_file(url: str, dest_path: Path, desc: str = None) -> bool:
112
+ """
113
+ 下载文件,支持断点续传和进度显示
114
+
115
+ Args:
116
+ url: 下载链接
117
+ dest_path: 目标路径
118
+ desc: 进度条描述
119
+
120
+ Returns:
121
+ bool: 下载是否成功
122
+ """
123
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
124
+
125
+ # 检查已下载的部分
126
+ resume_pos = 0
127
+ if dest_path.exists():
128
+ resume_pos = dest_path.stat().st_size
129
+
130
+ headers = {}
131
+ if resume_pos > 0:
132
+ headers["Range"] = f"bytes={resume_pos}-"
133
+ if "huggingface.co" in url:
134
+ hf_token = (
135
+ os.environ.get("HF_TOKEN")
136
+ or os.environ.get("HUGGINGFACE_HUB_TOKEN")
137
+ or os.environ.get("HUGGINGFACE_TOKEN")
138
+ )
139
+ if hf_token:
140
+ headers["Authorization"] = f"Bearer {hf_token}"
141
+
142
+ try:
143
+ response = requests.get(url, headers=headers, stream=True, timeout=30)
144
+
145
+ # 检查是否支持断点续传
146
+ if response.status_code == 416: # Range not satisfiable
147
+ print(f" 文件已完整下载: {dest_path.name}")
148
+ return True
149
+
150
+ if response.status_code not in [200, 206]:
151
+ print(f" 下载失败: HTTP {response.status_code}")
152
+ return False
153
+
154
+ # 获取文件总大小
155
+ total_size = int(response.headers.get("content-length", 0))
156
+ if response.status_code == 206:
157
+ total_size += resume_pos
158
+
159
+ # 下载模式
160
+ mode = "ab" if resume_pos > 0 else "wb"
161
+
162
+ with open(dest_path, mode) as f:
163
+ with tqdm(
164
+ total=total_size,
165
+ initial=resume_pos,
166
+ unit="B",
167
+ unit_scale=True,
168
+ desc=desc or dest_path.name
169
+ ) as pbar:
170
+ for chunk in response.iter_content(chunk_size=8192):
171
+ if chunk:
172
+ f.write(chunk)
173
+ pbar.update(len(chunk))
174
+
175
+ return True
176
+
177
+ except requests.exceptions.RequestException as e:
178
+ print(f" 下载错误: {e}")
179
+ return False
180
+
181
+
182
+ def check_model(name: str) -> bool:
183
+ """
184
+ 检查模型是否已下载
185
+
186
+ Args:
187
+ name: 模型名称
188
+
189
+ Returns:
190
+ bool: 模型是否存在
191
+ """
192
+ if name not in MODELS:
193
+ return False
194
+
195
+ model_path = get_project_root() / MODELS[name]["path"]
196
+ return model_path.exists()
197
+
198
+
199
+ def download_model(name: str) -> bool:
200
+ """
201
+ 下载指定模型
202
+
203
+ Args:
204
+ name: 模型名称
205
+
206
+ Returns:
207
+ bool: 下载是否成功
208
+ """
209
+ if name not in MODELS:
210
+ print(f"未知模型: {name}")
211
+ return False
212
+
213
+ model_info = MODELS[name]
214
+ model_path = get_project_root() / model_info["path"]
215
+
216
+ if model_path.exists():
217
+ print(f"模型已存在: {name}")
218
+ return True
219
+
220
+ print(f"正在下载: {model_info['description']} ({model_info['size_mb']}MB)")
221
+ return download_file(model_info["url"], model_path, name)
222
+
223
+
224
+ def download_required_models() -> bool:
225
+ """
226
+ 下载所有必需模型
227
+
228
+ Returns:
229
+ bool: 是否全部下载成功
230
+ """
231
+ print("=" * 50)
232
+ print("检查必需模型...")
233
+ print("=" * 50)
234
+
235
+ success = True
236
+ for name in REQUIRED_MODELS:
237
+ if not check_model(name):
238
+ if not download_model(name):
239
+ success = False
240
+ else:
241
+ print(f"[OK] {name} 已存在")
242
+
243
+ return success
244
+
245
+
246
+ def download_all_models() -> bool:
247
+ """
248
+ 下载所有模型
249
+
250
+ Returns:
251
+ bool: 是否全部下载成功
252
+ """
253
+ print("=" * 50)
254
+ print("下载所有模型...")
255
+ print("=" * 50)
256
+
257
+ success = True
258
+ for name in MODELS:
259
+ if not check_model(name):
260
+ if not download_model(name):
261
+ success = False
262
+ else:
263
+ print(f"[OK] {name} 已存在")
264
+
265
+ return success
266
+
267
+
268
+ def check_all_models() -> Dict[str, bool]:
269
+ """
270
+ 检查所有模型状态
271
+
272
+ Returns:
273
+ dict: 模型名称 -> 是否存在
274
+ """
275
+ return {name: check_model(name) for name in MODELS}
276
+
277
+
278
+ def get_available_mature_deecho_models() -> List[str]:
279
+ """Return locally available mature DeEcho / DeReverb models."""
280
+ return [name for name in MATURE_DEECHO_MODELS if check_model(name)]
281
+
282
+
283
+ def get_preferred_mature_deecho_model() -> Optional[str]:
284
+ """Return the preferred learned DeEcho model by priority."""
285
+ available = set(get_available_mature_deecho_models())
286
+ for name in MATURE_DEECHO_MODELS:
287
+ if name in available:
288
+ return name
289
+ return None
290
+
291
+
292
+ def download_mature_deecho_models() -> bool:
293
+ """Download mature DeEcho / DeReverb recommended models."""
294
+ print("=" * 50)
295
+ print("Downloading mature DeEcho / DeReverb models...")
296
+ print("=" * 50)
297
+
298
+ success = True
299
+ for name in MATURE_DEECHO_MODELS:
300
+ if not check_model(name):
301
+ if not download_model(name):
302
+ success = False
303
+ else:
304
+ print(f"[OK] {name} already exists")
305
+
306
+ return success
307
+
308
+
309
+ def print_model_status():
310
+ """打印模型状态"""
311
+ print("=" * 50)
312
+ print("模型状态")
313
+ print("=" * 50)
314
+
315
+ status = check_all_models()
316
+ for name, exists in status.items():
317
+ info = MODELS[name]
318
+ mark = "OK" if exists else "MISSING"
319
+ print(f" {mark} {name}")
320
+ print(f" {info['description']}")
321
+ print(f" 大小: {info['size_mb']}MB")
322
+ if name in REQUIRED_MODELS:
323
+ print(f" [必需]")
324
+ print()
325
+
326
+
327
+ if __name__ == "__main__":
328
+ import argparse
329
+
330
+ parser = argparse.ArgumentParser(description="RVC 模型下载工具")
331
+ parser.add_argument("--check", action="store_true", help="检查模型状态")
332
+ parser.add_argument("--all", action="store_true", help="下载所有模型")
333
+ parser.add_argument("--model", type=str, help="下载指定模型")
334
+
335
+ args = parser.parse_args()
336
+
337
+ if args.check:
338
+ print_model_status()
339
+ elif args.model:
340
+ download_model(args.model)
341
+ elif args.all:
342
+ download_all_models()
343
+ else:
344
+ download_required_models()