Localize UI status text
Browse files- i18n/en_US.json +77 -1
- i18n/zh_CN.json +77 -1
- tests/test_ui_language.py +87 -0
- ui/app.py +182 -92
i18n/en_US.json
CHANGED
|
@@ -24,7 +24,17 @@
|
|
| 24 |
"no_models": "(No models)",
|
| 25 |
"positive_pitch_info": "Positive raises pitch, negative lowers it",
|
| 26 |
"normal_volume_info": "100% keeps original volume",
|
| 27 |
-
"reverb_info": "Adds reverb to the vocals"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
},
|
| 29 |
"conversion": {
|
| 30 |
"model_settings": "Model Settings",
|
|
@@ -156,10 +166,76 @@
|
|
| 156 |
"conversion_failed": "Conversion failed",
|
| 157 |
"download_complete": "Download complete",
|
| 158 |
"download_failed": "Download failed",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
"cover_complete": "Cover complete",
|
|
|
|
| 160 |
"cover_failed": "Cover failed",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
"separating_vocals": "Separating vocals...",
|
| 162 |
"converting_vocals": "Converting vocals...",
|
| 163 |
"mixing_audio": "Mixing audio..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
}
|
| 165 |
}
|
|
|
|
| 24 |
"no_models": "(No models)",
|
| 25 |
"positive_pitch_info": "Positive raises pitch, negative lowers it",
|
| 26 |
"normal_volume_info": "100% keeps original volume",
|
| 27 |
+
"reverb_info": "Adds reverb to the vocals",
|
| 28 |
+
"all_series": "All",
|
| 29 |
+
"unknown": "Unknown",
|
| 30 |
+
"enabled": "On",
|
| 31 |
+
"disabled": "Off",
|
| 32 |
+
"language_korean": "Korean",
|
| 33 |
+
"language_japanese": "Japanese",
|
| 34 |
+
"language_chinese": "Chinese",
|
| 35 |
+
"language_english": "English",
|
| 36 |
+
"character_label_meta_separator": " | ",
|
| 37 |
+
"character_label_template": "[{language}] {display} | {meta} [{name}]"
|
| 38 |
},
|
| 39 |
"conversion": {
|
| 40 |
"model_settings": "Model Settings",
|
|
|
|
| 166 |
"conversion_failed": "Conversion failed",
|
| 167 |
"download_complete": "Download complete",
|
| 168 |
"download_failed": "Download failed",
|
| 169 |
+
"download_network_error": "An error occurred during download. Please check your network connection.",
|
| 170 |
+
"download_complete_status": "✅ Download complete",
|
| 171 |
+
"download_warning_status": "⚠️ Some downloads failed",
|
| 172 |
+
"download_error_status": "❌ Download failed: {error}",
|
| 173 |
+
"please_select_character_to_download": "Please select a character to download",
|
| 174 |
+
"character_download_complete": "✅ {name} model downloaded",
|
| 175 |
+
"character_download_failed": "❌ {name} model download failed",
|
| 176 |
+
"character_download_error": "❌ Download failed: {error}",
|
| 177 |
+
"bulk_download_complete": "✅ Complete: {count} succeeded",
|
| 178 |
+
"bulk_download_failed_items": ", {count} failed: {names}",
|
| 179 |
+
"bulk_download_error": "❌ Batch download failed: {error}",
|
| 180 |
+
"please_upload_song": "Please upload a song file",
|
| 181 |
+
"please_select_character": "Please select a character",
|
| 182 |
+
"character_model_missing": "Character model not found: {name}",
|
| 183 |
"cover_complete": "Cover complete",
|
| 184 |
+
"cover_complete_status": "✅ Cover complete!",
|
| 185 |
"cover_failed": "Cover failed",
|
| 186 |
+
"cover_process_failed": "❌ Processing failed: {error}",
|
| 187 |
+
"vc_pipeline_mode_status": "VC pipeline mode: {value}",
|
| 188 |
+
"singing_repair_status": "Singing repair: {value}",
|
| 189 |
+
"source_constraint_status": "Source constraint strategy: {value}",
|
| 190 |
+
"model_version_status": "Model version: {value}",
|
| 191 |
+
"character_continuity_status": "Character ownership: {value}",
|
| 192 |
+
"model_source_status": "Model source: {value}",
|
| 193 |
+
"all_files_dir_status": "All files directory: {value}",
|
| 194 |
"separating_vocals": "Separating vocals...",
|
| 195 |
"converting_vocals": "Converting vocals...",
|
| 196 |
"mixing_audio": "Mixing audio..."
|
| 197 |
+
},
|
| 198 |
+
"character_details": {
|
| 199 |
+
"downloaded_empty": "After you select a downloaded character, this panel shows version ownership, source repository, and local file paths.",
|
| 200 |
+
"available_empty": "After you select a character to download, this panel shows version ownership, source repository, and download source.",
|
| 201 |
+
"version_label": "Version tag",
|
| 202 |
+
"continuity": "Character ownership",
|
| 203 |
+
"role": "Model type",
|
| 204 |
+
"source": "Series source",
|
| 205 |
+
"distribution": "Distribution",
|
| 206 |
+
"repo": "Source repository",
|
| 207 |
+
"source_page_url": "Source page",
|
| 208 |
+
"download_url": "Download URL",
|
| 209 |
+
"local_weight": "Local weight",
|
| 210 |
+
"local_index": "Local index",
|
| 211 |
+
"internal_key": "Internal key",
|
| 212 |
+
"detail_code_line": "- {label}: `{value}`",
|
| 213 |
+
"detail_text_line": "- {label}: {value}"
|
| 214 |
+
},
|
| 215 |
+
"route_status": {
|
| 216 |
+
"mature_auto_preferred_suffix": "← preferred by current auto mode",
|
| 217 |
+
"mature_roformer_auto_download_note": " The RoFormer model is downloaded automatically by audio-separator on first run into assets/separator_models",
|
| 218 |
+
"mature_legacy_status_suffix": " ← status only; strict SOTA auto mode does not use this",
|
| 219 |
+
"mature_current_preferred": "Current learned DeEcho preference: {model}",
|
| 220 |
+
"mature_missing_strict": "Strict SOTA DeEcho runtime is currently missing; cover auto mode will stop instead of degrading",
|
| 221 |
+
"official_route_title": "Currently using the bundled official RVC implementation",
|
| 222 |
+
"official_route_flow": "Flow: lead vocal separation → official audio loader / official VC → mix",
|
| 223 |
+
"official_route_note": "Note: skips this project's custom VC preprocessing, source constraint, and silence-gate post-processing",
|
| 224 |
+
"strict_route_ready_title": "✅ Fixed to strict SOTA RoFormer De-Reverb",
|
| 225 |
+
"route_current_model": "Matched model: {model}",
|
| 226 |
+
"strict_route_flow": "Flow: lead vocal separation → RoFormer De-Reverb → RVC → mix",
|
| 227 |
+
"strict_route_unavailable_title": "⚠️ Strict SOTA RoFormer De-Reverb is selected, but the runtime is unavailable",
|
| 228 |
+
"strict_route_unavailable_flow": "Processing will stop and will not degrade to UVR or algorithmic dereverb",
|
| 229 |
+
"strict_route_unavailable_advice": "Suggestion: repair the audio-separator / RoFormer De-Reverb runtime",
|
| 230 |
+
"auto_route_ready_title": "✅ Auto mode currently uses strict SOTA RoFormer De-Reverb",
|
| 231 |
+
"auto_route_missing_title": "⚠️ Auto mode is missing the strict SOTA DeEcho runtime",
|
| 232 |
+
"auto_route_missing_reason": "Reason: no runnable RoFormer De-Reverb condition was detected"
|
| 233 |
+
},
|
| 234 |
+
"device_info": {
|
| 235 |
+
"pytorch_version": "PyTorch version: {version}",
|
| 236 |
+
"available_backends": "Available backends: {backends}",
|
| 237 |
+
"gpu_line": "GPU: {name} ({backend}) - VRAM: {memory}",
|
| 238 |
+
"backend_version": "{label} version: {version}",
|
| 239 |
+
"no_gpu_cpu": "No GPU detected; CPU will be used"
|
| 240 |
}
|
| 241 |
}
|
i18n/zh_CN.json
CHANGED
|
@@ -24,7 +24,17 @@
|
|
| 24 |
"no_models": "(无模型)",
|
| 25 |
"positive_pitch_info": "正数升调,负数降调",
|
| 26 |
"normal_volume_info": "100% 为原始音量",
|
| 27 |
-
"reverb_info": "为人声添加混响效果"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
},
|
| 29 |
"conversion": {
|
| 30 |
"model_settings": "模型设置",
|
|
@@ -156,10 +166,76 @@
|
|
| 156 |
"conversion_failed": "转换失败",
|
| 157 |
"download_complete": "下载完成",
|
| 158 |
"download_failed": "下载失败",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
"cover_complete": "翻唱完成",
|
|
|
|
| 160 |
"cover_failed": "翻唱失败",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
"separating_vocals": "正在分离人声...",
|
| 162 |
"converting_vocals": "正在转换人声...",
|
| 163 |
"mixing_audio": "正在混音..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
}
|
| 165 |
}
|
|
|
|
| 24 |
"no_models": "(无模型)",
|
| 25 |
"positive_pitch_info": "正数升调,负数降调",
|
| 26 |
"normal_volume_info": "100% 为原始音量",
|
| 27 |
+
"reverb_info": "为人声添加混响效果",
|
| 28 |
+
"all_series": "全部",
|
| 29 |
+
"unknown": "未知",
|
| 30 |
+
"enabled": "开启",
|
| 31 |
+
"disabled": "关闭",
|
| 32 |
+
"language_korean": "韩文",
|
| 33 |
+
"language_japanese": "日文",
|
| 34 |
+
"language_chinese": "中文",
|
| 35 |
+
"language_english": "英文",
|
| 36 |
+
"character_label_meta_separator": "|",
|
| 37 |
+
"character_label_template": "【{language}】{display}|{meta} [{name}]"
|
| 38 |
},
|
| 39 |
"conversion": {
|
| 40 |
"model_settings": "模型设置",
|
|
|
|
| 166 |
"conversion_failed": "转换失败",
|
| 167 |
"download_complete": "下载完成",
|
| 168 |
"download_failed": "下载失败",
|
| 169 |
+
"download_network_error": "下载过程中出现错误,请检查网络连接",
|
| 170 |
+
"download_complete_status": "✅ 下载完成",
|
| 171 |
+
"download_warning_status": "⚠️ 下载过程中存在失败项",
|
| 172 |
+
"download_error_status": "❌ 下载失败: {error}",
|
| 173 |
+
"please_select_character_to_download": "请选择要下载的角色",
|
| 174 |
+
"character_download_complete": "✅ {name} 模型下载完成",
|
| 175 |
+
"character_download_failed": "❌ {name} 模型下载失败",
|
| 176 |
+
"character_download_error": "❌ 下载失败: {error}",
|
| 177 |
+
"bulk_download_complete": "✅ 完成: 成功 {count} 个",
|
| 178 |
+
"bulk_download_failed_items": ",失败 {count} 个: {names}",
|
| 179 |
+
"bulk_download_error": "❌ 批量下载失败: {error}",
|
| 180 |
+
"please_upload_song": "请上传歌曲文件",
|
| 181 |
+
"please_select_character": "请选择角色",
|
| 182 |
+
"character_model_missing": "角色模型不存在: {name}",
|
| 183 |
"cover_complete": "翻唱完成",
|
| 184 |
+
"cover_complete_status": "✅ 翻唱完成!",
|
| 185 |
"cover_failed": "翻唱失败",
|
| 186 |
+
"cover_process_failed": "❌ 处理失败: {error}",
|
| 187 |
+
"vc_pipeline_mode_status": "VC管线模式: {value}",
|
| 188 |
+
"singing_repair_status": "唱歌修复: {value}",
|
| 189 |
+
"source_constraint_status": "源约束策略: {value}",
|
| 190 |
+
"model_version_status": "模型版本: {value}",
|
| 191 |
+
"character_continuity_status": "角色归属: {value}",
|
| 192 |
+
"model_source_status": "模型来源: {value}",
|
| 193 |
+
"all_files_dir_status": "全部文件目录: {value}",
|
| 194 |
"separating_vocals": "正在分离人声...",
|
| 195 |
"converting_vocals": "正在转换人声...",
|
| 196 |
"mixing_audio": "正在混音..."
|
| 197 |
+
},
|
| 198 |
+
"character_details": {
|
| 199 |
+
"downloaded_empty": "选择已下载角色后,这里会显示该模型的版本归属、来源仓库和本地文件路径。",
|
| 200 |
+
"available_empty": "选择待下载角色后,这里会显示该模型的版本归属、来源仓库和下载来源。",
|
| 201 |
+
"version_label": "版本标识",
|
| 202 |
+
"continuity": "角色归属",
|
| 203 |
+
"role": "模型类型",
|
| 204 |
+
"source": "作品来源",
|
| 205 |
+
"distribution": "分发方式",
|
| 206 |
+
"repo": "来源仓库",
|
| 207 |
+
"source_page_url": "来源页面",
|
| 208 |
+
"download_url": "下载链接",
|
| 209 |
+
"local_weight": "本地权重",
|
| 210 |
+
"local_index": "本地索引",
|
| 211 |
+
"internal_key": "内部键",
|
| 212 |
+
"detail_code_line": "- {label}:`{value}`",
|
| 213 |
+
"detail_text_line": "- {label}:{value}"
|
| 214 |
+
},
|
| 215 |
+
"route_status": {
|
| 216 |
+
"mature_auto_preferred_suffix": "← 当前自动模式优先使用",
|
| 217 |
+
"mature_roformer_auto_download_note": " RoFormer 模型由 audio-separator 首次运行时自动下载到 assets/separator_models",
|
| 218 |
+
"mature_legacy_status_suffix": " ← 仅保留状态显示,严格SOTA自动模式不使用",
|
| 219 |
+
"mature_current_preferred": "当前优先学习型 DeEcho: {model}",
|
| 220 |
+
"mature_missing_strict": "当前缺少严格SOTA DeEcho运行环境;翻唱自动模式将停止处理而不是降级",
|
| 221 |
+
"official_route_title": "当前使用内置官方 RVC 实现",
|
| 222 |
+
"official_route_flow": "流程:主唱分离 → 官方音频加载 / 官方 VC → 混音",
|
| 223 |
+
"official_route_note": "说明:跳过本项目自定义 VC 预处理、源约束与静音门限后处理",
|
| 224 |
+
"strict_route_ready_title": "✅ 当前固定使用严格 SOTA RoFormer De-Reverb",
|
| 225 |
+
"route_current_model": "当前命中模型: {model}",
|
| 226 |
+
"strict_route_flow": "流程: 主唱分离 → RoFormer De-Reverb → RVC → 混音",
|
| 227 |
+
"strict_route_unavailable_title": "⚠️ 当前设为严格 SOTA RoFormer De-Reverb,但运行环境不可用",
|
| 228 |
+
"strict_route_unavailable_flow": "流程会停止处理,不会降级到 UVR 或算法去混响",
|
| 229 |
+
"strict_route_unavailable_advice": "建议: 修复 audio-separator / RoFormer De-Reverb 运行环境",
|
| 230 |
+
"auto_route_ready_title": "✅ 自动模式当前使用严格 SOTA RoFormer De-Reverb",
|
| 231 |
+
"auto_route_missing_title": "⚠️ 自动模式缺少严格SOTA DeEcho运行环境",
|
| 232 |
+
"auto_route_missing_reason": "原因: 未检测到 RoFormer De-Reverb 可运行条件"
|
| 233 |
+
},
|
| 234 |
+
"device_info": {
|
| 235 |
+
"pytorch_version": "PyTorch 版本: {version}",
|
| 236 |
+
"available_backends": "可用后端: {backends}",
|
| 237 |
+
"gpu_line": "GPU: {name} ({backend}) - 显存: {memory}",
|
| 238 |
+
"backend_version": "{label} 版本: {version}",
|
| 239 |
+
"no_gpu_cpu": "未检测到 GPU,将使用 CPU"
|
| 240 |
}
|
| 241 |
}
|
tests/test_ui_language.py
CHANGED
|
@@ -62,6 +62,16 @@ class UiLanguageTests(unittest.TestCase):
|
|
| 62 |
"positive_pitch_info",
|
| 63 |
"normal_volume_info",
|
| 64 |
"reverb_info",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
required_settings_keys = {
|
| 67 |
"runtime_settings",
|
|
@@ -73,11 +83,88 @@ class UiLanguageTests(unittest.TestCase):
|
|
| 73 |
"about_body",
|
| 74 |
"model_sources",
|
| 75 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
for lang in ("zh_CN", "en_US"):
|
| 78 |
data = ui_app.load_i18n(lang)
|
| 79 |
self.assertTrue(required_ui_keys.issubset(data.get("ui", {})))
|
| 80 |
self.assertTrue(required_settings_keys.issubset(data.get("settings", {})))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
def test_ui_exposes_language_selector_and_save_handler(self):
|
| 83 |
source = Path("ui/app.py").read_text(encoding="utf-8")
|
|
|
|
| 62 |
"positive_pitch_info",
|
| 63 |
"normal_volume_info",
|
| 64 |
"reverb_info",
|
| 65 |
+
"all_series",
|
| 66 |
+
"unknown",
|
| 67 |
+
"enabled",
|
| 68 |
+
"disabled",
|
| 69 |
+
"language_korean",
|
| 70 |
+
"language_japanese",
|
| 71 |
+
"language_chinese",
|
| 72 |
+
"language_english",
|
| 73 |
+
"character_label_meta_separator",
|
| 74 |
+
"character_label_template",
|
| 75 |
}
|
| 76 |
required_settings_keys = {
|
| 77 |
"runtime_settings",
|
|
|
|
| 83 |
"about_body",
|
| 84 |
"model_sources",
|
| 85 |
}
|
| 86 |
+
required_message_keys = {
|
| 87 |
+
"download_network_error",
|
| 88 |
+
"please_select_character_to_download",
|
| 89 |
+
"character_download_complete",
|
| 90 |
+
"bulk_download_complete",
|
| 91 |
+
"please_upload_song",
|
| 92 |
+
"please_select_character",
|
| 93 |
+
"character_model_missing",
|
| 94 |
+
"cover_complete_status",
|
| 95 |
+
"cover_process_failed",
|
| 96 |
+
"vc_pipeline_mode_status",
|
| 97 |
+
"all_files_dir_status",
|
| 98 |
+
}
|
| 99 |
+
required_character_detail_keys = {
|
| 100 |
+
"downloaded_empty",
|
| 101 |
+
"available_empty",
|
| 102 |
+
"version_label",
|
| 103 |
+
"continuity",
|
| 104 |
+
"repo",
|
| 105 |
+
"local_weight",
|
| 106 |
+
"internal_key",
|
| 107 |
+
"detail_code_line",
|
| 108 |
+
"detail_text_line",
|
| 109 |
+
}
|
| 110 |
+
required_route_status_keys = {
|
| 111 |
+
"mature_auto_preferred_suffix",
|
| 112 |
+
"mature_current_preferred",
|
| 113 |
+
"official_route_title",
|
| 114 |
+
"strict_route_ready_title",
|
| 115 |
+
"route_current_model",
|
| 116 |
+
"strict_route_flow",
|
| 117 |
+
"auto_route_missing_title",
|
| 118 |
+
}
|
| 119 |
+
required_device_info_keys = {
|
| 120 |
+
"pytorch_version",
|
| 121 |
+
"available_backends",
|
| 122 |
+
"gpu_line",
|
| 123 |
+
"backend_version",
|
| 124 |
+
"no_gpu_cpu",
|
| 125 |
+
}
|
| 126 |
|
| 127 |
for lang in ("zh_CN", "en_US"):
|
| 128 |
data = ui_app.load_i18n(lang)
|
| 129 |
self.assertTrue(required_ui_keys.issubset(data.get("ui", {})))
|
| 130 |
self.assertTrue(required_settings_keys.issubset(data.get("settings", {})))
|
| 131 |
+
self.assertTrue(required_message_keys.issubset(data.get("messages", {})))
|
| 132 |
+
self.assertTrue(required_character_detail_keys.issubset(data.get("character_details", {})))
|
| 133 |
+
self.assertTrue(required_route_status_keys.issubset(data.get("route_status", {})))
|
| 134 |
+
self.assertTrue(required_device_info_keys.issubset(data.get("device_info", {})))
|
| 135 |
+
|
| 136 |
+
def test_character_metadata_values_are_not_localized(self):
|
| 137 |
+
original_i18n = ui_app.i18n
|
| 138 |
+
try:
|
| 139 |
+
ui_app.i18n = ui_app.load_i18n("en_US")
|
| 140 |
+
char_info = {
|
| 141 |
+
"name": "rin",
|
| 142 |
+
"display": "Rin Hoshizora",
|
| 143 |
+
"source": "Love Live!",
|
| 144 |
+
"continuity": "μ's",
|
| 145 |
+
"version_label": "500 epochs·40k",
|
| 146 |
+
"distribution": "HuggingFace",
|
| 147 |
+
"repo": "trioskosmos/rvc_models",
|
| 148 |
+
"source_page_url": "https://huggingface.co/trioskosmos/rvc_models",
|
| 149 |
+
"download_url": "https://huggingface.co/trioskosmos/rvc_models/resolve/main/rin.pth",
|
| 150 |
+
"model_path": "assets/weights/characters/rin/rin.pth",
|
| 151 |
+
"index_path": "assets/weights/characters/rin/rin.index",
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
label = ui_app.format_character_label(char_info)
|
| 155 |
+
details = ui_app.format_character_details(char_info, downloaded=True)
|
| 156 |
+
|
| 157 |
+
finally:
|
| 158 |
+
ui_app.i18n = original_i18n
|
| 159 |
+
|
| 160 |
+
self.assertIn("[Japanese]", label)
|
| 161 |
+
self.assertIn("trioskosmos/rvc_models", label)
|
| 162 |
+
self.assertIn("500 epochs·40k", label)
|
| 163 |
+
self.assertIn("- Source repository: `trioskosmos/rvc_models`", details)
|
| 164 |
+
self.assertIn("- Version tag: `500 epochs·40k`", details)
|
| 165 |
+
self.assertIn("assets/weights/characters/rin/rin.pth", details)
|
| 166 |
+
self.assertNotIn("版本标识", details)
|
| 167 |
+
self.assertNotIn("来源仓库", details)
|
| 168 |
|
| 169 |
def test_ui_exposes_language_selector_and_save_handler(self):
|
| 170 |
source = Path("ui/app.py").read_text(encoding="utf-8")
|
ui/app.py
CHANGED
|
@@ -77,6 +77,41 @@ def t(key: str, section: str = None) -> str:
|
|
| 77 |
return i18n.get(key, key)
|
| 78 |
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
def get_configured_language(config_data: Optional[dict] = None) -> str:
|
| 81 |
"""Return the configured UI language, failing on unsupported values."""
|
| 82 |
selected_config = config if config_data is None else config_data
|
|
@@ -262,7 +297,7 @@ def download_base_models() -> str:
|
|
| 262 |
if success:
|
| 263 |
return t("download_complete", "messages")
|
| 264 |
else:
|
| 265 |
-
return "
|
| 266 |
except Exception as e:
|
| 267 |
return f"{t('download_failed', 'messages')}: {str(e)}"
|
| 268 |
|
|
@@ -278,8 +313,8 @@ def get_downloaded_character_list() -> list:
|
|
| 278 |
def get_downloaded_character_series() -> list:
|
| 279 |
"""获取已下载角色的系列列表"""
|
| 280 |
characters = get_downloaded_character_list()
|
| 281 |
-
series = sorted({c.get("series"
|
| 282 |
-
return [
|
| 283 |
|
| 284 |
|
| 285 |
def get_available_character_list() -> list:
|
|
@@ -291,13 +326,29 @@ def get_available_character_list() -> list:
|
|
| 291 |
def get_available_character_series() -> list:
|
| 292 |
"""获取可用系列列表"""
|
| 293 |
from tools.character_models import list_available_series
|
| 294 |
-
return list_available_series()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
|
| 297 |
def format_character_label(char_info: dict) -> str:
|
| 298 |
"""格式化角色展示名称,明确显示版本、归属和来源。"""
|
| 299 |
display = char_info.get("base_display") or char_info.get("display") or char_info.get("description") or char_info.get("name", "")
|
| 300 |
-
source = char_info.get("source"
|
| 301 |
name = char_info.get("name", "")
|
| 302 |
lang_tag = get_character_language_tag(char_info)
|
| 303 |
parts: List[str] = []
|
|
@@ -318,39 +369,46 @@ def format_character_label(char_info: dict) -> str:
|
|
| 318 |
elif repo:
|
| 319 |
parts.append(repo)
|
| 320 |
|
| 321 |
-
meta = "
|
| 322 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
|
| 325 |
def get_character_language_tag(char_info: dict) -> str:
|
| 326 |
"""推断语言类型,用于下拉前缀标签"""
|
| 327 |
lang = char_info.get("lang")
|
| 328 |
if lang:
|
| 329 |
-
return lang
|
| 330 |
text = " ".join(
|
| 331 |
str(char_info.get(k, "")) for k in ("display", "description", "name")
|
| 332 |
).lower()
|
| 333 |
if "韩" in text or "kr" in text or "korean" in text:
|
| 334 |
-
return "
|
| 335 |
if "日" in text or "jp" in text or "japanese" in text:
|
| 336 |
-
return "
|
| 337 |
if "中" in text or "cn" in text or "chinese" in text:
|
| 338 |
-
return "
|
| 339 |
if "en" in text or "english" in text:
|
| 340 |
-
return "
|
| 341 |
|
| 342 |
source = char_info.get("source", "")
|
| 343 |
if source.startswith("Love Live!") or "ホロライブ" in source or "偶像大师" in source or "赛马娘" in source:
|
| 344 |
-
return "
|
| 345 |
if "原神" in source or "崩坏" in source or "明日方舟" in source or "碧蓝航线" in source:
|
| 346 |
-
return "
|
| 347 |
if "VOCALOID" in source or "Project SEKAI" in source:
|
| 348 |
-
return "
|
| 349 |
if "Hololive" in source:
|
| 350 |
-
return "
|
| 351 |
if "蔚蓝档案" in source or "绝区零" in source:
|
| 352 |
-
return "
|
| 353 |
-
return "
|
| 354 |
|
| 355 |
|
| 356 |
def _find_character_entry(selection: str, downloaded: bool) -> Optional[dict]:
|
|
@@ -366,11 +424,29 @@ def _find_character_entry(selection: str, downloaded: bool) -> Optional[dict]:
|
|
| 366 |
return None
|
| 367 |
|
| 368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
def format_character_details(char_info: Optional[dict], downloaded: bool = False) -> str:
|
| 370 |
if not char_info:
|
| 371 |
if downloaded:
|
| 372 |
-
return "
|
| 373 |
-
return "
|
| 374 |
|
| 375 |
title = char_info.get("base_display") or char_info.get("display") or char_info.get("name", "")
|
| 376 |
lines = [f"**{title}**"]
|
|
@@ -385,26 +461,26 @@ def format_character_details(char_info: Optional[dict], downloaded: bool = False
|
|
| 385 |
download_url = str(char_info.get("download_url") or "").strip()
|
| 386 |
|
| 387 |
if version_label:
|
| 388 |
-
lines.append(
|
| 389 |
if continuity:
|
| 390 |
-
lines.append(
|
| 391 |
if role:
|
| 392 |
-
lines.append(
|
| 393 |
if source:
|
| 394 |
-
lines.append(
|
| 395 |
if distribution:
|
| 396 |
-
lines.append(
|
| 397 |
if repo:
|
| 398 |
-
lines.append(
|
| 399 |
if source_page_url:
|
| 400 |
-
lines.append(
|
| 401 |
if download_url and download_url != source_page_url:
|
| 402 |
-
lines.append(
|
| 403 |
if downloaded and char_info.get("model_path"):
|
| 404 |
-
lines.append(
|
| 405 |
if downloaded and char_info.get("index_path"):
|
| 406 |
-
lines.append(
|
| 407 |
-
lines.append(
|
| 408 |
return "\n".join(lines)
|
| 409 |
|
| 410 |
|
|
@@ -419,8 +495,8 @@ def get_available_character_details(selection: str) -> str:
|
|
| 419 |
def get_downloaded_character_choices(series: str = "全部", keyword: str = "") -> list:
|
| 420 |
"""获取已下载角色的下拉选项"""
|
| 421 |
chars = get_downloaded_character_list()
|
| 422 |
-
if series and
|
| 423 |
-
chars = [c for c in chars if c.get("series")
|
| 424 |
if keyword:
|
| 425 |
kw = keyword.strip().lower()
|
| 426 |
if kw:
|
|
@@ -454,8 +530,8 @@ def resolve_character_name(selection: str) -> str:
|
|
| 454 |
def get_available_character_choices(series: str = "全部", keyword: str = "") -> list:
|
| 455 |
"""获取可下载角色的下拉选项"""
|
| 456 |
chars = get_available_character_list()
|
| 457 |
-
if series and
|
| 458 |
-
chars = [c for c in chars if c.get("series")
|
| 459 |
if keyword:
|
| 460 |
kw = keyword.strip().lower()
|
| 461 |
if kw:
|
|
@@ -474,8 +550,9 @@ def get_available_character_choices(series: str = "全部", keyword: str = "") -
|
|
| 474 |
|
| 475 |
def _refresh_downloaded_updates(series: str, keyword: str) -> Tuple[Dict, Dict]:
|
| 476 |
series_choices = get_downloaded_character_series()
|
|
|
|
| 477 |
if series not in series_choices:
|
| 478 |
-
series =
|
| 479 |
return (
|
| 480 |
gr.update(choices=series_choices, value=series),
|
| 481 |
gr.update(choices=get_downloaded_character_choices(series, keyword))
|
|
@@ -488,27 +565,27 @@ def download_character(name: str, selected_series: str = "全部", keyword: str
|
|
| 488 |
|
| 489 |
if not name:
|
| 490 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 491 |
-
return "
|
| 492 |
|
| 493 |
try:
|
| 494 |
success = download_character_model(name)
|
| 495 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 496 |
if success:
|
| 497 |
return (
|
| 498 |
-
|
| 499 |
choices_update,
|
| 500 |
series_update
|
| 501 |
)
|
| 502 |
else:
|
| 503 |
return (
|
| 504 |
-
|
| 505 |
choices_update,
|
| 506 |
series_update
|
| 507 |
)
|
| 508 |
except Exception as e:
|
| 509 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 510 |
return (
|
| 511 |
-
|
| 512 |
choices_update,
|
| 513 |
series_update
|
| 514 |
)
|
|
@@ -519,17 +596,18 @@ def download_all_characters(series: str = "全部", selected_series: str = "全
|
|
| 519 |
from tools.character_models import download_all_character_models
|
| 520 |
|
| 521 |
try:
|
| 522 |
-
|
|
|
|
| 523 |
ok = result.get("success", [])
|
| 524 |
failed = result.get("failed", [])
|
| 525 |
-
status =
|
| 526 |
if failed:
|
| 527 |
-
status +=
|
| 528 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 529 |
return status, choices_update, series_update
|
| 530 |
except Exception as e:
|
| 531 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 532 |
-
return
|
| 533 |
|
| 534 |
|
| 535 |
def update_download_choices(series: str, keyword: str) -> Dict:
|
|
@@ -574,10 +652,10 @@ def process_cover(
|
|
| 574 |
"""
|
| 575 |
_none6 = (None, None, None, None, None, None)
|
| 576 |
if audio_path is None:
|
| 577 |
-
return *_none6, "
|
| 578 |
|
| 579 |
if not character_name:
|
| 580 |
-
return *_none6, "
|
| 581 |
|
| 582 |
try:
|
| 583 |
from tools.character_models import get_character_model_path, get_character_info
|
|
@@ -588,7 +666,7 @@ def process_cover(
|
|
| 588 |
char_meta = get_character_info(resolved_name, downloaded_only=True) or {}
|
| 589 |
model_info = get_character_model_path(resolved_name)
|
| 590 |
if model_info is None:
|
| 591 |
-
return *_none6,
|
| 592 |
|
| 593 |
# 进度回调
|
| 594 |
def progress_callback(msg: str, step: int, total: int):
|
|
@@ -695,20 +773,32 @@ def process_cover(
|
|
| 695 |
progress_callback=progress_callback
|
| 696 |
)
|
| 697 |
|
| 698 |
-
status_msg = "
|
| 699 |
status_msg += f"\n{get_cover_vc_route_status(vc_preprocess_mode, vc_pipeline_mode).splitlines()[0]}"
|
| 700 |
-
status_msg +=
|
| 701 |
-
|
| 702 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
if char_meta.get("version_label"):
|
| 704 |
-
status_msg +=
|
| 705 |
if char_meta.get("continuity"):
|
| 706 |
-
status_msg +=
|
| 707 |
if char_meta.get("repo"):
|
| 708 |
-
status_msg +=
|
| 709 |
status_msg += f"\n{get_runtime_build_label()}"
|
| 710 |
if result.get("all_files_dir"):
|
| 711 |
-
status_msg +=
|
| 712 |
|
| 713 |
return (
|
| 714 |
result["cover"],
|
|
@@ -724,7 +814,7 @@ def process_cover(
|
|
| 724 |
import traceback
|
| 725 |
error_msg = str(e) if str(e) else traceback.format_exc()
|
| 726 |
log.error(f"处理失败: {error_msg}")
|
| 727 |
-
return None, None, None, None, None, None,
|
| 728 |
|
| 729 |
|
| 730 |
def check_mature_deecho_status() -> str:
|
|
@@ -736,23 +826,23 @@ def check_mature_deecho_status() -> str:
|
|
| 736 |
roformer_ready = check_roformer_available()
|
| 737 |
icon = "✅" if roformer_ready else "❌"
|
| 738 |
status_lines.append(
|
| 739 |
-
f"{icon} {ROFORMER_DEREVERB_DEFAULT_MODEL}
|
| 740 |
)
|
| 741 |
if roformer_ready:
|
| 742 |
-
status_lines.append("
|
| 743 |
|
| 744 |
for name in MATURE_DEECHO_MODELS:
|
| 745 |
exists = check_model(name)
|
| 746 |
icon = "✅" if exists else "❌"
|
| 747 |
-
suffix = "
|
| 748 |
status_lines.append(f"{icon} {name}{suffix}")
|
| 749 |
|
| 750 |
if roformer_ready:
|
| 751 |
status_lines.append("")
|
| 752 |
-
status_lines.append(
|
| 753 |
else:
|
| 754 |
status_lines.append("")
|
| 755 |
-
status_lines.append("
|
| 756 |
|
| 757 |
return "\n".join(status_lines)
|
| 758 |
|
|
@@ -764,10 +854,10 @@ def download_mature_deecho_models_ui() -> str:
|
|
| 764 |
try:
|
| 765 |
success = download_mature_deecho_models()
|
| 766 |
status = check_mature_deecho_status()
|
| 767 |
-
prefix = "
|
| 768 |
return f"{prefix}\n\n{status}"
|
| 769 |
except Exception as e:
|
| 770 |
-
return
|
| 771 |
|
| 772 |
|
| 773 |
def get_cover_vc_route_status(
|
|
@@ -790,38 +880,38 @@ def get_cover_vc_route_status(
|
|
| 790 |
|
| 791 |
if pipeline_mode == "official":
|
| 792 |
return newline.join([
|
| 793 |
-
"
|
| 794 |
-
"
|
| 795 |
-
"
|
| 796 |
build_label,
|
| 797 |
])
|
| 798 |
|
| 799 |
if mode == "uvr_deecho":
|
| 800 |
if preferred:
|
| 801 |
return newline.join([
|
| 802 |
-
"
|
| 803 |
-
|
| 804 |
-
"
|
| 805 |
build_label,
|
| 806 |
])
|
| 807 |
return newline.join([
|
| 808 |
-
"
|
| 809 |
-
"
|
| 810 |
-
"
|
| 811 |
build_label,
|
| 812 |
])
|
| 813 |
|
| 814 |
if preferred:
|
| 815 |
return newline.join([
|
| 816 |
-
"
|
| 817 |
-
|
| 818 |
-
"
|
| 819 |
build_label,
|
| 820 |
])
|
| 821 |
return newline.join([
|
| 822 |
-
"
|
| 823 |
-
"
|
| 824 |
-
"
|
| 825 |
build_label,
|
| 826 |
])
|
| 827 |
|
|
@@ -845,22 +935,22 @@ def get_device_info() -> str:
|
|
| 845 |
from lib.device import get_device_info as _get_info, _is_rocm, _has_xpu, _has_directml, _has_mps
|
| 846 |
|
| 847 |
lines = []
|
| 848 |
-
lines.append(
|
| 849 |
|
| 850 |
info = _get_info()
|
| 851 |
-
lines.append(
|
| 852 |
|
| 853 |
for dev in info["devices"]:
|
| 854 |
mem = f"{dev['total_memory_gb']} GB" if dev.get("total_memory_gb") else "N/A"
|
| 855 |
-
lines.append(
|
| 856 |
|
| 857 |
if torch.cuda.is_available():
|
| 858 |
ver = torch.version.hip if _is_rocm() else torch.version.cuda
|
| 859 |
label = "ROCm" if _is_rocm() else "CUDA"
|
| 860 |
-
lines.append(
|
| 861 |
|
| 862 |
if not info["devices"]:
|
| 863 |
-
lines.append("
|
| 864 |
|
| 865 |
return "\n".join(lines)
|
| 866 |
|
|
@@ -1385,7 +1475,7 @@ def create_ui() -> gr.Blocks:
|
|
| 1385 |
"""创建 Gradio 界面"""
|
| 1386 |
|
| 1387 |
with gr.Blocks(
|
| 1388 |
-
title=
|
| 1389 |
theme=gr.themes.Base(
|
| 1390 |
primary_hue="orange",
|
| 1391 |
secondary_hue="gray",
|
|
@@ -1454,11 +1544,11 @@ def create_ui() -> gr.Blocks:
|
|
| 1454 |
|
| 1455 |
# 标题
|
| 1456 |
gr.Markdown(
|
| 1457 |
-
f"# 🎤 {
|
| 1458 |
elem_classes=["main-title"]
|
| 1459 |
)
|
| 1460 |
gr.Markdown(
|
| 1461 |
-
f"<center>{
|
| 1462 |
)
|
| 1463 |
gr.Markdown(
|
| 1464 |
f"<div class='runtime-stamp'>{get_runtime_build_label()}</div>"
|
|
@@ -1590,7 +1680,7 @@ def create_ui() -> gr.Blocks:
|
|
| 1590 |
downloaded_series = gr.Dropdown(
|
| 1591 |
label=t("series_filter", "ui"),
|
| 1592 |
choices=get_downloaded_character_series(),
|
| 1593 |
-
value=
|
| 1594 |
interactive=True
|
| 1595 |
)
|
| 1596 |
|
|
@@ -1602,7 +1692,7 @@ def create_ui() -> gr.Blocks:
|
|
| 1602 |
|
| 1603 |
character_dropdown = gr.Dropdown(
|
| 1604 |
label=t("character", "cover"),
|
| 1605 |
-
choices=get_downloaded_character_choices(
|
| 1606 |
interactive=True,
|
| 1607 |
info=t("character_choice_info", "ui")
|
| 1608 |
)
|
|
@@ -1620,11 +1710,11 @@ def create_ui() -> gr.Blocks:
|
|
| 1620 |
|
| 1621 |
# 角色下载区域
|
| 1622 |
with gr.Accordion(t("download_character", "cover"), open=False):
|
| 1623 |
-
series_choices = [
|
| 1624 |
download_series = gr.Dropdown(
|
| 1625 |
label=t("series_filter", "ui"),
|
| 1626 |
choices=series_choices,
|
| 1627 |
-
value=
|
| 1628 |
interactive=True
|
| 1629 |
)
|
| 1630 |
|
|
@@ -1636,7 +1726,7 @@ def create_ui() -> gr.Blocks:
|
|
| 1636 |
|
| 1637 |
download_char_dropdown = gr.Dropdown(
|
| 1638 |
label=t("select_to_download", "cover"),
|
| 1639 |
-
choices=get_available_character_choices(
|
| 1640 |
interactive=True,
|
| 1641 |
info=t("download_character_info", "ui")
|
| 1642 |
)
|
|
@@ -1974,7 +2064,7 @@ def create_ui() -> gr.Blocks:
|
|
| 1974 |
)
|
| 1975 |
|
| 1976 |
download_all_btn.click(
|
| 1977 |
-
fn=lambda series, keyword: download_all_characters(
|
| 1978 |
inputs=[downloaded_series, downloaded_keyword],
|
| 1979 |
outputs=[download_char_status, character_dropdown, downloaded_series]
|
| 1980 |
)
|
|
|
|
| 77 |
return i18n.get(key, key)
|
| 78 |
|
| 79 |
|
| 80 |
+
def tf(key: str, section: str = None, **kwargs) -> str:
|
| 81 |
+
"""Format translated UI text while preserving supplied technical values."""
|
| 82 |
+
return t(key, section).format(**kwargs)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _all_series_label() -> str:
|
| 86 |
+
return t("all_series", "ui")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _unknown_label() -> str:
|
| 90 |
+
return t("unknown", "ui")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _is_all_series(series: Optional[str]) -> bool:
|
| 94 |
+
text = str(series or "").strip()
|
| 95 |
+
return text in {"", "全部", "All", _all_series_label()}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _normalize_series_choice(series: Optional[str]) -> str:
|
| 99 |
+
return _all_series_label() if _is_all_series(series) else str(series)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _display_series_label(series: Optional[str]) -> str:
|
| 103 |
+
text = str(series or "").strip()
|
| 104 |
+
return _unknown_label() if text in {"", "未知", "Unknown"} else text
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _series_matches(char_series: Optional[str], selected_series: str) -> bool:
|
| 108 |
+
return _display_series_label(char_series) == _normalize_series_choice(selected_series)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _bool_status_label(value: bool) -> str:
|
| 112 |
+
return t("enabled", "ui") if value else t("disabled", "ui")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
def get_configured_language(config_data: Optional[dict] = None) -> str:
|
| 116 |
"""Return the configured UI language, failing on unsupported values."""
|
| 117 |
selected_config = config if config_data is None else config_data
|
|
|
|
| 297 |
if success:
|
| 298 |
return t("download_complete", "messages")
|
| 299 |
else:
|
| 300 |
+
return t("download_network_error", "messages")
|
| 301 |
except Exception as e:
|
| 302 |
return f"{t('download_failed', 'messages')}: {str(e)}"
|
| 303 |
|
|
|
|
| 313 |
def get_downloaded_character_series() -> list:
|
| 314 |
"""获取已下载角色的系列列表"""
|
| 315 |
characters = get_downloaded_character_list()
|
| 316 |
+
series = sorted({_display_series_label(c.get("series")) for c in characters})
|
| 317 |
+
return [_all_series_label()] + series
|
| 318 |
|
| 319 |
|
| 320 |
def get_available_character_list() -> list:
|
|
|
|
| 326 |
def get_available_character_series() -> list:
|
| 327 |
"""获取可用系列列表"""
|
| 328 |
from tools.character_models import list_available_series
|
| 329 |
+
return sorted({_display_series_label(series) for series in list_available_series()})
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def _localized_language_tag(lang: str) -> str:
|
| 333 |
+
text = str(lang or "").strip()
|
| 334 |
+
if not text:
|
| 335 |
+
return text
|
| 336 |
+
lowered = text.lower()
|
| 337 |
+
if text in {"韩文", "韓文"} or "kr" in lowered or "korean" in lowered:
|
| 338 |
+
return t("language_korean", "ui")
|
| 339 |
+
if text in {"日文", "日本語"} or "jp" in lowered or "japanese" in lowered:
|
| 340 |
+
return t("language_japanese", "ui")
|
| 341 |
+
if text in {"中文", "汉语", "漢語"} or "cn" in lowered or "chinese" in lowered:
|
| 342 |
+
return t("language_chinese", "ui")
|
| 343 |
+
if text in {"英文", "英语", "英語"} or lowered in {"en", "english"}:
|
| 344 |
+
return t("language_english", "ui")
|
| 345 |
+
return text
|
| 346 |
|
| 347 |
|
| 348 |
def format_character_label(char_info: dict) -> str:
|
| 349 |
"""格式化角色展示名称,明确显示版本、归属和来源。"""
|
| 350 |
display = char_info.get("base_display") or char_info.get("display") or char_info.get("description") or char_info.get("name", "")
|
| 351 |
+
source = char_info.get("source") or _unknown_label()
|
| 352 |
name = char_info.get("name", "")
|
| 353 |
lang_tag = get_character_language_tag(char_info)
|
| 354 |
parts: List[str] = []
|
|
|
|
| 369 |
elif repo:
|
| 370 |
parts.append(repo)
|
| 371 |
|
| 372 |
+
meta = t("character_label_meta_separator", "ui").join(part for part in parts if part)
|
| 373 |
+
return tf(
|
| 374 |
+
"character_label_template",
|
| 375 |
+
"ui",
|
| 376 |
+
language=lang_tag,
|
| 377 |
+
display=display,
|
| 378 |
+
meta=meta,
|
| 379 |
+
name=name,
|
| 380 |
+
)
|
| 381 |
|
| 382 |
|
| 383 |
def get_character_language_tag(char_info: dict) -> str:
|
| 384 |
"""推断语言类型,用于下拉前缀标签"""
|
| 385 |
lang = char_info.get("lang")
|
| 386 |
if lang:
|
| 387 |
+
return _localized_language_tag(lang)
|
| 388 |
text = " ".join(
|
| 389 |
str(char_info.get(k, "")) for k in ("display", "description", "name")
|
| 390 |
).lower()
|
| 391 |
if "韩" in text or "kr" in text or "korean" in text:
|
| 392 |
+
return t("language_korean", "ui")
|
| 393 |
if "日" in text or "jp" in text or "japanese" in text:
|
| 394 |
+
return t("language_japanese", "ui")
|
| 395 |
if "中" in text or "cn" in text or "chinese" in text:
|
| 396 |
+
return t("language_chinese", "ui")
|
| 397 |
if "en" in text or "english" in text:
|
| 398 |
+
return t("language_english", "ui")
|
| 399 |
|
| 400 |
source = char_info.get("source", "")
|
| 401 |
if source.startswith("Love Live!") or "ホロライブ" in source or "偶像大师" in source or "赛马娘" in source:
|
| 402 |
+
return t("language_japanese", "ui")
|
| 403 |
if "原神" in source or "崩坏" in source or "明日方舟" in source or "碧蓝航线" in source:
|
| 404 |
+
return t("language_chinese", "ui")
|
| 405 |
if "VOCALOID" in source or "Project SEKAI" in source:
|
| 406 |
+
return t("language_japanese", "ui")
|
| 407 |
if "Hololive" in source:
|
| 408 |
+
return t("language_japanese", "ui")
|
| 409 |
if "蔚蓝档案" in source or "绝区零" in source:
|
| 410 |
+
return t("language_japanese", "ui")
|
| 411 |
+
return t("language_chinese", "ui")
|
| 412 |
|
| 413 |
|
| 414 |
def _find_character_entry(selection: str, downloaded: bool) -> Optional[dict]:
|
|
|
|
| 424 |
return None
|
| 425 |
|
| 426 |
|
| 427 |
+
def _character_detail_code(label_key: str, value: str) -> str:
|
| 428 |
+
return tf(
|
| 429 |
+
"detail_code_line",
|
| 430 |
+
"character_details",
|
| 431 |
+
label=t(label_key, "character_details"),
|
| 432 |
+
value=value,
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def _character_detail_text(label_key: str, value: str) -> str:
|
| 437 |
+
return tf(
|
| 438 |
+
"detail_text_line",
|
| 439 |
+
"character_details",
|
| 440 |
+
label=t(label_key, "character_details"),
|
| 441 |
+
value=value,
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
|
| 445 |
def format_character_details(char_info: Optional[dict], downloaded: bool = False) -> str:
|
| 446 |
if not char_info:
|
| 447 |
if downloaded:
|
| 448 |
+
return t("downloaded_empty", "character_details")
|
| 449 |
+
return t("available_empty", "character_details")
|
| 450 |
|
| 451 |
title = char_info.get("base_display") or char_info.get("display") or char_info.get("name", "")
|
| 452 |
lines = [f"**{title}**"]
|
|
|
|
| 461 |
download_url = str(char_info.get("download_url") or "").strip()
|
| 462 |
|
| 463 |
if version_label:
|
| 464 |
+
lines.append(_character_detail_code("version_label", version_label))
|
| 465 |
if continuity:
|
| 466 |
+
lines.append(_character_detail_code("continuity", continuity))
|
| 467 |
if role:
|
| 468 |
+
lines.append(_character_detail_code("role", role))
|
| 469 |
if source:
|
| 470 |
+
lines.append(_character_detail_code("source", source))
|
| 471 |
if distribution:
|
| 472 |
+
lines.append(_character_detail_code("distribution", distribution))
|
| 473 |
if repo:
|
| 474 |
+
lines.append(_character_detail_code("repo", repo))
|
| 475 |
if source_page_url:
|
| 476 |
+
lines.append(_character_detail_text("source_page_url", source_page_url))
|
| 477 |
if download_url and download_url != source_page_url:
|
| 478 |
+
lines.append(_character_detail_text("download_url", download_url))
|
| 479 |
if downloaded and char_info.get("model_path"):
|
| 480 |
+
lines.append(_character_detail_code("local_weight", char_info["model_path"]))
|
| 481 |
if downloaded and char_info.get("index_path"):
|
| 482 |
+
lines.append(_character_detail_code("local_index", char_info["index_path"]))
|
| 483 |
+
lines.append(_character_detail_code("internal_key", char_info.get("name", "")))
|
| 484 |
return "\n".join(lines)
|
| 485 |
|
| 486 |
|
|
|
|
| 495 |
def get_downloaded_character_choices(series: str = "全部", keyword: str = "") -> list:
|
| 496 |
"""获取已下载角色的下拉选项"""
|
| 497 |
chars = get_downloaded_character_list()
|
| 498 |
+
if series and not _is_all_series(series):
|
| 499 |
+
chars = [c for c in chars if _series_matches(c.get("series"), series)]
|
| 500 |
if keyword:
|
| 501 |
kw = keyword.strip().lower()
|
| 502 |
if kw:
|
|
|
|
| 530 |
def get_available_character_choices(series: str = "全部", keyword: str = "") -> list:
|
| 531 |
"""获取可下载角色的下拉选项"""
|
| 532 |
chars = get_available_character_list()
|
| 533 |
+
if series and not _is_all_series(series):
|
| 534 |
+
chars = [c for c in chars if _series_matches(c.get("series"), series)]
|
| 535 |
if keyword:
|
| 536 |
kw = keyword.strip().lower()
|
| 537 |
if kw:
|
|
|
|
| 550 |
|
| 551 |
def _refresh_downloaded_updates(series: str, keyword: str) -> Tuple[Dict, Dict]:
|
| 552 |
series_choices = get_downloaded_character_series()
|
| 553 |
+
series = _normalize_series_choice(series)
|
| 554 |
if series not in series_choices:
|
| 555 |
+
series = _all_series_label()
|
| 556 |
return (
|
| 557 |
gr.update(choices=series_choices, value=series),
|
| 558 |
gr.update(choices=get_downloaded_character_choices(series, keyword))
|
|
|
|
| 565 |
|
| 566 |
if not name:
|
| 567 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 568 |
+
return t("please_select_character_to_download", "messages"), choices_update, series_update
|
| 569 |
|
| 570 |
try:
|
| 571 |
success = download_character_model(name)
|
| 572 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 573 |
if success:
|
| 574 |
return (
|
| 575 |
+
tf("character_download_complete", "messages", name=name),
|
| 576 |
choices_update,
|
| 577 |
series_update
|
| 578 |
)
|
| 579 |
else:
|
| 580 |
return (
|
| 581 |
+
tf("character_download_failed", "messages", name=name),
|
| 582 |
choices_update,
|
| 583 |
series_update
|
| 584 |
)
|
| 585 |
except Exception as e:
|
| 586 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 587 |
return (
|
| 588 |
+
tf("character_download_error", "messages", error=str(e)),
|
| 589 |
choices_update,
|
| 590 |
series_update
|
| 591 |
)
|
|
|
|
| 596 |
from tools.character_models import download_all_character_models
|
| 597 |
|
| 598 |
try:
|
| 599 |
+
series_arg = None if _is_all_series(series) else series
|
| 600 |
+
result = download_all_character_models(series=series_arg)
|
| 601 |
ok = result.get("success", [])
|
| 602 |
failed = result.get("failed", [])
|
| 603 |
+
status = tf("bulk_download_complete", "messages", count=len(ok))
|
| 604 |
if failed:
|
| 605 |
+
status += tf("bulk_download_failed_items", "messages", count=len(failed), names=", ".join(failed))
|
| 606 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 607 |
return status, choices_update, series_update
|
| 608 |
except Exception as e:
|
| 609 |
series_update, choices_update = _refresh_downloaded_updates(selected_series, keyword)
|
| 610 |
+
return tf("bulk_download_error", "messages", error=str(e)), choices_update, series_update
|
| 611 |
|
| 612 |
|
| 613 |
def update_download_choices(series: str, keyword: str) -> Dict:
|
|
|
|
| 652 |
"""
|
| 653 |
_none6 = (None, None, None, None, None, None)
|
| 654 |
if audio_path is None:
|
| 655 |
+
return *_none6, t("please_upload_song", "messages")
|
| 656 |
|
| 657 |
if not character_name:
|
| 658 |
+
return *_none6, t("please_select_character", "messages")
|
| 659 |
|
| 660 |
try:
|
| 661 |
from tools.character_models import get_character_model_path, get_character_info
|
|
|
|
| 666 |
char_meta = get_character_info(resolved_name, downloaded_only=True) or {}
|
| 667 |
model_info = get_character_model_path(resolved_name)
|
| 668 |
if model_info is None:
|
| 669 |
+
return *_none6, tf("character_model_missing", "messages", name=resolved_name)
|
| 670 |
|
| 671 |
# 进度回调
|
| 672 |
def progress_callback(msg: str, step: int, total: int):
|
|
|
|
| 773 |
progress_callback=progress_callback
|
| 774 |
)
|
| 775 |
|
| 776 |
+
status_msg = t("cover_complete_status", "messages")
|
| 777 |
status_msg += f"\n{get_cover_vc_route_status(vc_preprocess_mode, vc_pipeline_mode).splitlines()[0]}"
|
| 778 |
+
status_msg += "\n" + tf(
|
| 779 |
+
"vc_pipeline_mode_status",
|
| 780 |
+
"messages",
|
| 781 |
+
value=pipeline_value_to_label.get(vc_pipeline_mode, vc_pipeline_mode),
|
| 782 |
+
)
|
| 783 |
+
status_msg += "\n" + tf(
|
| 784 |
+
"singing_repair_status",
|
| 785 |
+
"messages",
|
| 786 |
+
value=_bool_status_label(singing_repair),
|
| 787 |
+
)
|
| 788 |
+
status_msg += "\n" + tf(
|
| 789 |
+
"source_constraint_status",
|
| 790 |
+
"messages",
|
| 791 |
+
value=source_value_to_label.get(source_constraint_mode, source_constraint_mode),
|
| 792 |
+
)
|
| 793 |
if char_meta.get("version_label"):
|
| 794 |
+
status_msg += "\n" + tf("model_version_status", "messages", value=char_meta["version_label"])
|
| 795 |
if char_meta.get("continuity"):
|
| 796 |
+
status_msg += "\n" + tf("character_continuity_status", "messages", value=char_meta["continuity"])
|
| 797 |
if char_meta.get("repo"):
|
| 798 |
+
status_msg += "\n" + tf("model_source_status", "messages", value=char_meta["repo"])
|
| 799 |
status_msg += f"\n{get_runtime_build_label()}"
|
| 800 |
if result.get("all_files_dir"):
|
| 801 |
+
status_msg += "\n" + tf("all_files_dir_status", "messages", value=result["all_files_dir"])
|
| 802 |
|
| 803 |
return (
|
| 804 |
result["cover"],
|
|
|
|
| 814 |
import traceback
|
| 815 |
error_msg = str(e) if str(e) else traceback.format_exc()
|
| 816 |
log.error(f"处理失败: {error_msg}")
|
| 817 |
+
return None, None, None, None, None, None, tf("cover_process_failed", "messages", error=error_msg)
|
| 818 |
|
| 819 |
|
| 820 |
def check_mature_deecho_status() -> str:
|
|
|
|
| 826 |
roformer_ready = check_roformer_available()
|
| 827 |
icon = "✅" if roformer_ready else "❌"
|
| 828 |
status_lines.append(
|
| 829 |
+
f"{icon} {ROFORMER_DEREVERB_DEFAULT_MODEL} {t('mature_auto_preferred_suffix', 'route_status')}"
|
| 830 |
)
|
| 831 |
if roformer_ready:
|
| 832 |
+
status_lines.append(t("mature_roformer_auto_download_note", "route_status"))
|
| 833 |
|
| 834 |
for name in MATURE_DEECHO_MODELS:
|
| 835 |
exists = check_model(name)
|
| 836 |
icon = "✅" if exists else "❌"
|
| 837 |
+
suffix = t("mature_legacy_status_suffix", "route_status")
|
| 838 |
status_lines.append(f"{icon} {name}{suffix}")
|
| 839 |
|
| 840 |
if roformer_ready:
|
| 841 |
status_lines.append("")
|
| 842 |
+
status_lines.append(tf("mature_current_preferred", "route_status", model=f"RoFormer {ROFORMER_DEREVERB_DEFAULT_MODEL}"))
|
| 843 |
else:
|
| 844 |
status_lines.append("")
|
| 845 |
+
status_lines.append(t("mature_missing_strict", "route_status"))
|
| 846 |
|
| 847 |
return "\n".join(status_lines)
|
| 848 |
|
|
|
|
| 854 |
try:
|
| 855 |
success = download_mature_deecho_models()
|
| 856 |
status = check_mature_deecho_status()
|
| 857 |
+
prefix = t("download_complete_status", "messages") if success else t("download_warning_status", "messages")
|
| 858 |
return f"{prefix}\n\n{status}"
|
| 859 |
except Exception as e:
|
| 860 |
+
return tf("download_error_status", "messages", error=str(e))
|
| 861 |
|
| 862 |
|
| 863 |
def get_cover_vc_route_status(
|
|
|
|
| 880 |
|
| 881 |
if pipeline_mode == "official":
|
| 882 |
return newline.join([
|
| 883 |
+
t("official_route_title", "route_status"),
|
| 884 |
+
t("official_route_flow", "route_status"),
|
| 885 |
+
t("official_route_note", "route_status"),
|
| 886 |
build_label,
|
| 887 |
])
|
| 888 |
|
| 889 |
if mode == "uvr_deecho":
|
| 890 |
if preferred:
|
| 891 |
return newline.join([
|
| 892 |
+
t("strict_route_ready_title", "route_status"),
|
| 893 |
+
tf("route_current_model", "route_status", model=preferred),
|
| 894 |
+
t("strict_route_flow", "route_status"),
|
| 895 |
build_label,
|
| 896 |
])
|
| 897 |
return newline.join([
|
| 898 |
+
t("strict_route_unavailable_title", "route_status"),
|
| 899 |
+
t("strict_route_unavailable_flow", "route_status"),
|
| 900 |
+
t("strict_route_unavailable_advice", "route_status"),
|
| 901 |
build_label,
|
| 902 |
])
|
| 903 |
|
| 904 |
if preferred:
|
| 905 |
return newline.join([
|
| 906 |
+
t("auto_route_ready_title", "route_status"),
|
| 907 |
+
tf("route_current_model", "route_status", model=preferred),
|
| 908 |
+
t("strict_route_flow", "route_status"),
|
| 909 |
build_label,
|
| 910 |
])
|
| 911 |
return newline.join([
|
| 912 |
+
t("auto_route_missing_title", "route_status"),
|
| 913 |
+
t("auto_route_missing_reason", "route_status"),
|
| 914 |
+
t("strict_route_unavailable_flow", "route_status"),
|
| 915 |
build_label,
|
| 916 |
])
|
| 917 |
|
|
|
|
| 935 |
from lib.device import get_device_info as _get_info, _is_rocm, _has_xpu, _has_directml, _has_mps
|
| 936 |
|
| 937 |
lines = []
|
| 938 |
+
lines.append(tf("pytorch_version", "device_info", version=torch.__version__))
|
| 939 |
|
| 940 |
info = _get_info()
|
| 941 |
+
lines.append(tf("available_backends", "device_info", backends=", ".join(info["backends"])))
|
| 942 |
|
| 943 |
for dev in info["devices"]:
|
| 944 |
mem = f"{dev['total_memory_gb']} GB" if dev.get("total_memory_gb") else "N/A"
|
| 945 |
+
lines.append(tf("gpu_line", "device_info", name=dev["name"], backend=dev["backend"], memory=mem))
|
| 946 |
|
| 947 |
if torch.cuda.is_available():
|
| 948 |
ver = torch.version.hip if _is_rocm() else torch.version.cuda
|
| 949 |
label = "ROCm" if _is_rocm() else "CUDA"
|
| 950 |
+
lines.append(tf("backend_version", "device_info", label=label, version=ver))
|
| 951 |
|
| 952 |
if not info["devices"]:
|
| 953 |
+
lines.append(t("no_gpu_cpu", "device_info"))
|
| 954 |
|
| 955 |
return "\n".join(lines)
|
| 956 |
|
|
|
|
| 1475 |
"""创建 Gradio 界面"""
|
| 1476 |
|
| 1477 |
with gr.Blocks(
|
| 1478 |
+
title=t("app_title"),
|
| 1479 |
theme=gr.themes.Base(
|
| 1480 |
primary_hue="orange",
|
| 1481 |
secondary_hue="gray",
|
|
|
|
| 1544 |
|
| 1545 |
# 标题
|
| 1546 |
gr.Markdown(
|
| 1547 |
+
f"# 🎤 {t('app_title')}",
|
| 1548 |
elem_classes=["main-title"]
|
| 1549 |
)
|
| 1550 |
gr.Markdown(
|
| 1551 |
+
f"<center>{t('app_description')}</center>"
|
| 1552 |
)
|
| 1553 |
gr.Markdown(
|
| 1554 |
f"<div class='runtime-stamp'>{get_runtime_build_label()}</div>"
|
|
|
|
| 1680 |
downloaded_series = gr.Dropdown(
|
| 1681 |
label=t("series_filter", "ui"),
|
| 1682 |
choices=get_downloaded_character_series(),
|
| 1683 |
+
value=_all_series_label(),
|
| 1684 |
interactive=True
|
| 1685 |
)
|
| 1686 |
|
|
|
|
| 1692 |
|
| 1693 |
character_dropdown = gr.Dropdown(
|
| 1694 |
label=t("character", "cover"),
|
| 1695 |
+
choices=get_downloaded_character_choices(_all_series_label(), ""),
|
| 1696 |
interactive=True,
|
| 1697 |
info=t("character_choice_info", "ui")
|
| 1698 |
)
|
|
|
|
| 1710 |
|
| 1711 |
# 角色下载区域
|
| 1712 |
with gr.Accordion(t("download_character", "cover"), open=False):
|
| 1713 |
+
series_choices = [_all_series_label()] + get_available_character_series()
|
| 1714 |
download_series = gr.Dropdown(
|
| 1715 |
label=t("series_filter", "ui"),
|
| 1716 |
choices=series_choices,
|
| 1717 |
+
value=_all_series_label(),
|
| 1718 |
interactive=True
|
| 1719 |
)
|
| 1720 |
|
|
|
|
| 1726 |
|
| 1727 |
download_char_dropdown = gr.Dropdown(
|
| 1728 |
label=t("select_to_download", "cover"),
|
| 1729 |
+
choices=get_available_character_choices(_all_series_label(), ""),
|
| 1730 |
interactive=True,
|
| 1731 |
info=t("download_character_info", "ui")
|
| 1732 |
)
|
|
|
|
| 2064 |
)
|
| 2065 |
|
| 2066 |
download_all_btn.click(
|
| 2067 |
+
fn=lambda series, keyword: download_all_characters(_all_series_label(), series, keyword),
|
| 2068 |
inputs=[downloaded_series, downloaded_keyword],
|
| 2069 |
outputs=[download_char_status, character_dropdown, downloaded_series]
|
| 2070 |
)
|