Spaces:
Running
Running
feat: 简单导出插件的头尾拓展和质量评估集成
Browse files- docs/流程文档_AI用.md +21 -5
- src/export_plugins/base.py +57 -0
- src/export_plugins/simple_export.py +73 -73
- src/export_plugins/utau_oto_export.py +59 -64
- src/gui_cloud.py +14 -3
docs/流程文档_AI用.md
CHANGED
|
@@ -142,6 +142,8 @@
|
|
| 142 |
│ │ • Overlap: 交叉淡化区域 │ │
|
| 143 |
│ │ 3. IPA 音素转换为拼音/罗马音别名 │ │
|
| 144 |
│ │ 4. 生成 oto.ini 配置文件 │ │
|
|
|
|
|
|
|
| 145 |
│ └─────────────────────────────────────────────────────────────────────┘ │
|
| 146 |
│ │
|
| 147 |
│ 输出: export/[音源名称]/simple_export/ │
|
|
@@ -201,9 +203,9 @@ MFA 支持两种运行模式:
|
|
| 201 |
|
| 202 |
| 模块 | 文件 | 功能 |
|
| 203 |
|------|------|------|
|
| 204 |
-
| 插件基类 | `export_plugins/base.py` | 定义插件接口
|
| 205 |
| 插件加载器 | `export_plugins/loader.py` | 扫描和加载插件 |
|
| 206 |
-
| 简单导出 | `export_plugins/simple_export.py` | 按拼音分类导出单字音频 |
|
| 207 |
| UTAU 导出 | `export_plugins/utau_oto_export.py` | 生成 UTAU 音源配置文件 (oto.ini) |
|
| 208 |
| 质量评分 | `quality_scorer.py` | 音频质量多维度评估 |
|
| 209 |
|
|
@@ -215,6 +217,14 @@ MFA 支持两种运行模式:
|
|
| 215 |
- `MULTI_SELECT`: 多选框
|
| 216 |
- `FILE`/`FOLDER`: 文件/文件夹选择
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
### 5. 音源质量评分模块
|
| 219 |
|
| 220 |
`src/quality_scorer.py` 提供多维度音频质量评估:
|
|
@@ -234,9 +244,15 @@ scores = scorer.score_from_file("audio.wav")
|
|
| 234 |
# 返回: {"duration": 0.85, "f0": 0.91, "combined": 0.88}
|
| 235 |
```
|
| 236 |
|
| 237 |
-
导出插件
|
| 238 |
-
- `
|
| 239 |
-
- `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
### 5. MFA 跨平台支持
|
| 242 |
|
|
|
|
| 142 |
│ │ • Overlap: 交叉淡化区域 │ │
|
| 143 |
│ │ 3. IPA 音素转换为拼音/罗马音别名 │ │
|
| 144 |
│ │ 4. 生成 oto.ini 配置文件 │ │
|
| 145 |
+
│ │ 5. 生成 character.txt(支持自定义角色名) │ │
|
| 146 |
+
│ │ 6. 自动检测文件名编码兼容性,不合法时转拼音 │ │
|
| 147 |
│ └─────────────────────────────────────────────────────────────────────┘ │
|
| 148 |
│ │
|
| 149 |
│ 输出: export/[音源名称]/simple_export/ │
|
|
|
|
| 203 |
|
| 204 |
| 模块 | 文件 | 功能 |
|
| 205 |
|------|------|------|
|
| 206 |
+
| 插件基类 | `export_plugins/base.py` | 定义插件接口、配置选项和公共方法 |
|
| 207 |
| 插件加载器 | `export_plugins/loader.py` | 扫描和加载插件 |
|
| 208 |
+
| 简单导出 | `export_plugins/simple_export.py` | 按拼音分类导出单字音频,支持质量评估 |
|
| 209 |
| UTAU 导出 | `export_plugins/utau_oto_export.py` | 生成 UTAU 音源配置文件 (oto.ini) |
|
| 210 |
| 质量评分 | `quality_scorer.py` | 音频质量多维度评估 |
|
| 211 |
|
|
|
|
| 217 |
- `MULTI_SELECT`: 多选框
|
| 218 |
- `FILE`/`FOLDER`: 文件/文件夹选择
|
| 219 |
|
| 220 |
+
基类公共方法 (`ExportPlugin`):
|
| 221 |
+
- `load_language_from_meta()`: 从 meta.json 加载语言设置
|
| 222 |
+
- `parse_quality_metrics()`: 解析质量评估维度选项
|
| 223 |
+
- `apply_naming_rule()`: 应用命名规则生成文件名/别名
|
| 224 |
+
- `get_source_paths()`: 获取音源相关路径
|
| 225 |
+
- `get_export_dir()`: 获取导出目录路径
|
| 226 |
+
- `get_quality_scorer()`: 获取质量评分器实例
|
| 227 |
+
|
| 228 |
### 5. 音源质量评分模块
|
| 229 |
|
| 230 |
`src/quality_scorer.py` 提供多维度音频质量评估:
|
|
|
|
| 244 |
# 返回: {"duration": 0.85, "f0": 0.91, "combined": 0.88}
|
| 245 |
```
|
| 246 |
|
| 247 |
+
导出插件质量评估选项:
|
| 248 |
+
- `duration`: 仅时长评估(默认,最快)
|
| 249 |
+
- `duration+rms`: 时长 + 音量稳定性
|
| 250 |
+
- `duration+f0`: 时长 + 音高稳定性
|
| 251 |
+
- `all`: 全部维度(耗时较长)
|
| 252 |
+
|
| 253 |
+
已集成质量评估的插件:
|
| 254 |
+
- **简单单字导出**: 默认仅评估时长,可选启用 RMS/F0 评估
|
| 255 |
+
- **UTAU oto.ini 导出**: 默认评估时长+RMS,可选启用 F0 评估
|
| 256 |
|
| 257 |
### 5. MFA 跨平台支持
|
| 258 |
|
src/export_plugins/base.py
CHANGED
|
@@ -146,6 +146,63 @@ class ExportPlugin(ABC):
|
|
| 146 |
"textgrid_dir": os.path.join(source_dir, "textgrid")
|
| 147 |
}
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
def get_quality_scorer(
|
| 150 |
self,
|
| 151 |
enabled_metrics: Optional[List[str]] = None,
|
|
|
|
| 146 |
"textgrid_dir": os.path.join(source_dir, "textgrid")
|
| 147 |
}
|
| 148 |
|
| 149 |
+
def load_language_from_meta(self, bank_dir: str, source_name: str) -> str:
|
| 150 |
+
"""
|
| 151 |
+
从 meta.json 加载语言设置
|
| 152 |
+
|
| 153 |
+
参数:
|
| 154 |
+
bank_dir: bank 目录路径
|
| 155 |
+
source_name: 音源名称
|
| 156 |
+
|
| 157 |
+
返回:
|
| 158 |
+
语言代码,默认 "chinese"
|
| 159 |
+
"""
|
| 160 |
+
import json
|
| 161 |
+
meta_path = os.path.join(bank_dir, source_name, "meta.json")
|
| 162 |
+
try:
|
| 163 |
+
if os.path.exists(meta_path):
|
| 164 |
+
with open(meta_path, 'r', encoding='utf-8') as f:
|
| 165 |
+
meta = json.load(f)
|
| 166 |
+
language = meta.get("language", "chinese")
|
| 167 |
+
self._log(f"语言设置: {language}")
|
| 168 |
+
return language
|
| 169 |
+
except Exception as e:
|
| 170 |
+
logger.warning(f"读取 meta.json 失败: {e}")
|
| 171 |
+
return "chinese"
|
| 172 |
+
|
| 173 |
+
def parse_quality_metrics(self, metrics_str: str) -> List[str]:
|
| 174 |
+
"""
|
| 175 |
+
解析质量评估维度选项
|
| 176 |
+
|
| 177 |
+
参数:
|
| 178 |
+
metrics_str: 选项字符串,如 "duration", "duration+rms", "all"
|
| 179 |
+
|
| 180 |
+
返回:
|
| 181 |
+
启用的维度列表
|
| 182 |
+
"""
|
| 183 |
+
if metrics_str == "all":
|
| 184 |
+
return ["duration", "rms", "f0"]
|
| 185 |
+
elif metrics_str == "duration+rms":
|
| 186 |
+
return ["duration", "rms"]
|
| 187 |
+
elif metrics_str == "duration+f0":
|
| 188 |
+
return ["duration", "f0"]
|
| 189 |
+
else:
|
| 190 |
+
return ["duration"]
|
| 191 |
+
|
| 192 |
+
def apply_naming_rule(self, rule: str, base_name: str, index: int) -> str:
|
| 193 |
+
"""
|
| 194 |
+
应用命名规则生成文件名/别名
|
| 195 |
+
|
| 196 |
+
参数:
|
| 197 |
+
rule: 命名规则,支持 %p%(拼音)和 %n%(序号)
|
| 198 |
+
base_name: 基础名称(拼音/罗马音)
|
| 199 |
+
index: 序号
|
| 200 |
+
|
| 201 |
+
返回:
|
| 202 |
+
生成的名称
|
| 203 |
+
"""
|
| 204 |
+
return rule.replace("%p%", base_name).replace("%n%", str(index))
|
| 205 |
+
|
| 206 |
def get_quality_scorer(
|
| 207 |
self,
|
| 208 |
enabled_metrics: Optional[List[str]] = None,
|
src/export_plugins/simple_export.py
CHANGED
|
@@ -21,17 +21,12 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 21 |
"""简单单字导出插件"""
|
| 22 |
|
| 23 |
name = "简单单字导出"
|
| 24 |
-
description = "从TextGrid提取分词片段,按
|
| 25 |
version = "1.1.0"
|
| 26 |
author = "内置"
|
| 27 |
|
| 28 |
def get_options(self) -> List[PluginOption]:
|
| 29 |
return [
|
| 30 |
-
PluginOption(
|
| 31 |
-
key="info",
|
| 32 |
-
label="将每个汉字按拼音分类,选取最佳样本导出",
|
| 33 |
-
option_type=OptionType.LABEL
|
| 34 |
-
),
|
| 35 |
PluginOption(
|
| 36 |
key="max_samples",
|
| 37 |
label="每个拼音最大样本数",
|
|
@@ -39,14 +34,22 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 39 |
default=10,
|
| 40 |
min_value=1,
|
| 41 |
max_value=1000,
|
| 42 |
-
description="按
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
),
|
| 44 |
PluginOption(
|
| 45 |
key="extend_duration",
|
| 46 |
label="头尾拓展(秒)",
|
| 47 |
option_type=OptionType.TEXT,
|
| 48 |
default="0",
|
| 49 |
-
description="裁剪时头尾各拓展的时长,最大
|
| 50 |
),
|
| 51 |
PluginOption(
|
| 52 |
key="naming_rule",
|
|
@@ -71,25 +74,6 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 71 |
)
|
| 72 |
]
|
| 73 |
|
| 74 |
-
def _load_language_from_meta(self, bank_dir: str, source_name: str) -> str:
|
| 75 |
-
"""从meta.json加载语言设置"""
|
| 76 |
-
meta_path = os.path.join(bank_dir, source_name, "meta.json")
|
| 77 |
-
try:
|
| 78 |
-
if os.path.exists(meta_path):
|
| 79 |
-
with open(meta_path, 'r', encoding='utf-8') as f:
|
| 80 |
-
meta = json.load(f)
|
| 81 |
-
language = meta.get("language", "chinese")
|
| 82 |
-
self._log(f"从meta.json读取语言设置: {language}")
|
| 83 |
-
return language
|
| 84 |
-
except Exception as e:
|
| 85 |
-
logger.warning(f"读取meta.json失败: {e}")
|
| 86 |
-
return "chinese"
|
| 87 |
-
|
| 88 |
-
def _apply_naming_rule(self, rule: str, pinyin: str, index: int) -> str:
|
| 89 |
-
"""应用命名规则生成文件名"""
|
| 90 |
-
name = rule.replace("%p%", pinyin).replace("%n%", str(index))
|
| 91 |
-
return name
|
| 92 |
-
|
| 93 |
def _apply_extend(
|
| 94 |
self,
|
| 95 |
start_time: float,
|
|
@@ -101,45 +85,29 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 101 |
应用头尾拓展
|
| 102 |
|
| 103 |
头尾各拓展 extend_duration 秒,若一边到达边界则另一边继续拓展
|
| 104 |
-
|
| 105 |
-
参数:
|
| 106 |
-
start_time: 原始开始时间
|
| 107 |
-
end_time: 原始结束时间
|
| 108 |
-
extend_duration: 单边拓展时长
|
| 109 |
-
audio_duration: 音频总时长
|
| 110 |
-
|
| 111 |
-
返回:
|
| 112 |
-
(实际开始时间, 实际结束时间)
|
| 113 |
"""
|
| 114 |
if extend_duration <= 0:
|
| 115 |
return start_time, end_time
|
| 116 |
|
| 117 |
-
total_extend = extend_duration * 2
|
| 118 |
-
|
| 119 |
-
# 先尝试头尾各拓展
|
| 120 |
-
head_extend = extend_duration
|
| 121 |
-
tail_extend = extend_duration
|
| 122 |
|
| 123 |
-
#
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
head_remaining = head_extend - head_actual # 剩余量转给尾部
|
| 127 |
-
tail_extend += head_remaining
|
| 128 |
-
head_extend = head_actual
|
| 129 |
|
| 130 |
-
#
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
tail_remaining = tail_extend - tail_actual # 剩余量转给头部
|
| 134 |
-
# 头部再次尝试拓展(如果还有空间)
|
| 135 |
-
additional_head = min(tail_remaining, start_time - (start_time - head_extend))
|
| 136 |
-
head_extend = min(start_time, head_extend + tail_remaining)
|
| 137 |
-
tail_extend = tail_actual
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
-
return
|
| 143 |
|
| 144 |
def export(
|
| 145 |
self,
|
|
@@ -149,12 +117,16 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 149 |
) -> Tuple[bool, str]:
|
| 150 |
"""执行简单单字导出"""
|
| 151 |
try:
|
| 152 |
-
#
|
| 153 |
-
language = self.
|
| 154 |
max_samples = int(options.get("max_samples", 10))
|
| 155 |
naming_rule = options.get("naming_rule", "%p%_%n%")
|
| 156 |
first_naming_rule = options.get("first_naming_rule", "")
|
| 157 |
clean_temp = options.get("clean_temp", True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
paths = self.get_source_paths(bank_dir, source_name)
|
| 160 |
export_dir = self.get_export_dir(bank_dir, source_name, "simple_export")
|
|
@@ -164,7 +136,7 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 164 |
segments_dir = os.path.join(temp_base, source_name)
|
| 165 |
|
| 166 |
# 获取头尾拓展参数
|
| 167 |
-
extend_duration = min(float(options.get("extend_duration", 0)),
|
| 168 |
|
| 169 |
# 步骤1: 提取分词片段
|
| 170 |
self._log("【提取分词片段】")
|
|
@@ -181,13 +153,14 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 181 |
return False, msg
|
| 182 |
|
| 183 |
# 步骤2: 排序导出
|
| 184 |
-
self._log("\n【排序导出】")
|
| 185 |
success, msg = self._sort_and_export(
|
| 186 |
segments_dir,
|
| 187 |
export_dir,
|
| 188 |
max_samples,
|
| 189 |
naming_rule,
|
| 190 |
-
first_naming_rule
|
|
|
|
| 191 |
)
|
| 192 |
if not success:
|
| 193 |
return False, msg
|
|
@@ -517,11 +490,13 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 517 |
export_dir: str,
|
| 518 |
max_samples: int,
|
| 519 |
naming_rule: str,
|
| 520 |
-
first_naming_rule: str
|
|
|
|
| 521 |
) -> Tuple[bool, str]:
|
| 522 |
"""排序并导出"""
|
| 523 |
try:
|
| 524 |
import soundfile as sf
|
|
|
|
| 525 |
|
| 526 |
os.makedirs(export_dir, exist_ok=True)
|
| 527 |
|
|
@@ -541,8 +516,15 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 541 |
|
| 542 |
self._log(f"扫描到 {len(wav_files)} 个片段")
|
| 543 |
|
|
|
|
|
|
|
|
|
|
| 544 |
# 按拼音分组
|
| 545 |
-
stats: Dict[str, List[Tuple[str, float]]] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
for path in wav_files:
|
| 547 |
rel_path = os.path.relpath(path, segments_dir)
|
| 548 |
parts = rel_path.split(os.sep)
|
|
@@ -550,24 +532,42 @@ class SimpleExportPlugin(ExportPlugin):
|
|
| 550 |
pinyin = parts[0]
|
| 551 |
if pinyin not in stats:
|
| 552 |
stats[pinyin] = []
|
| 553 |
-
|
| 554 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
|
| 556 |
self._log(f"统计到 {len(stats)} 个拼音")
|
| 557 |
self._log(f"命名规则: {naming_rule}")
|
| 558 |
if first_naming_rule:
|
| 559 |
self._log(f"首个样本规则: {first_naming_rule}")
|
| 560 |
|
| 561 |
-
# 按
|
| 562 |
exported = 0
|
| 563 |
for pinyin, files in stats.items():
|
| 564 |
-
sorted_files = sorted(files, key=lambda x: -x[
|
| 565 |
-
for idx, (src_path, _) in enumerate(sorted_files[:max_samples]):
|
| 566 |
-
#
|
| 567 |
if idx == 0 and first_naming_rule:
|
| 568 |
-
filename = self.
|
| 569 |
else:
|
| 570 |
-
filename = self.
|
| 571 |
|
| 572 |
dst_path = os.path.join(export_dir, f'{filename}.wav')
|
| 573 |
shutil.copyfile(src_path, dst_path)
|
|
|
|
| 21 |
"""简单单字导出插件"""
|
| 22 |
|
| 23 |
name = "简单单字导出"
|
| 24 |
+
description = "从TextGrid提取分词片段,按时长排序导出"
|
| 25 |
version = "1.1.0"
|
| 26 |
author = "内置"
|
| 27 |
|
| 28 |
def get_options(self) -> List[PluginOption]:
|
| 29 |
return [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
PluginOption(
|
| 31 |
key="max_samples",
|
| 32 |
label="每个拼音最大样本数",
|
|
|
|
| 34 |
default=10,
|
| 35 |
min_value=1,
|
| 36 |
max_value=1000,
|
| 37 |
+
description="按质量评分排序,保留最佳的N个"
|
| 38 |
+
),
|
| 39 |
+
PluginOption(
|
| 40 |
+
key="quality_metrics",
|
| 41 |
+
label="质量评估维度",
|
| 42 |
+
option_type=OptionType.COMBO,
|
| 43 |
+
default="duration",
|
| 44 |
+
choices=["duration", "duration+rms", "duration+f0", "all"],
|
| 45 |
+
description="duration=仅时长, +rms=音量稳定性, +f0=音高稳定性。选择 all 可能耗时较长"
|
| 46 |
),
|
| 47 |
PluginOption(
|
| 48 |
key="extend_duration",
|
| 49 |
label="头尾拓展(秒)",
|
| 50 |
option_type=OptionType.TEXT,
|
| 51 |
default="0",
|
| 52 |
+
description="裁剪时头尾各拓展的时长,最大0.5秒。若一边到达边界,另一边继续拓展"
|
| 53 |
),
|
| 54 |
PluginOption(
|
| 55 |
key="naming_rule",
|
|
|
|
| 74 |
)
|
| 75 |
]
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
def _apply_extend(
|
| 78 |
self,
|
| 79 |
start_time: float,
|
|
|
|
| 85 |
应用头尾拓展
|
| 86 |
|
| 87 |
头尾各拓展 extend_duration 秒,若一边到达边界则另一边继续拓展
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
"""
|
| 89 |
if extend_duration <= 0:
|
| 90 |
return start_time, end_time
|
| 91 |
|
| 92 |
+
total_extend = extend_duration * 2
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
+
# 先尝试两边各拓展
|
| 95 |
+
new_start = max(0, start_time - extend_duration)
|
| 96 |
+
new_end = min(audio_duration, end_time + extend_duration)
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
+
# 计算实际拓展量,剩余量补偿到另一边
|
| 99 |
+
used = (start_time - new_start) + (new_end - end_time)
|
| 100 |
+
remaining = total_extend - used
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
if remaining > 0:
|
| 103 |
+
# 优先补偿到尾部,再补偿到头部
|
| 104 |
+
extra_end = min(remaining, audio_duration - new_end)
|
| 105 |
+
new_end += extra_end
|
| 106 |
+
remaining -= extra_end
|
| 107 |
+
if remaining > 0:
|
| 108 |
+
new_start = max(0, new_start - remaining)
|
| 109 |
|
| 110 |
+
return new_start, new_end
|
| 111 |
|
| 112 |
def export(
|
| 113 |
self,
|
|
|
|
| 117 |
) -> Tuple[bool, str]:
|
| 118 |
"""执行简单单字导出"""
|
| 119 |
try:
|
| 120 |
+
# 使用基类方法获取语言设置
|
| 121 |
+
language = self.load_language_from_meta(bank_dir, source_name)
|
| 122 |
max_samples = int(options.get("max_samples", 10))
|
| 123 |
naming_rule = options.get("naming_rule", "%p%_%n%")
|
| 124 |
first_naming_rule = options.get("first_naming_rule", "")
|
| 125 |
clean_temp = options.get("clean_temp", True)
|
| 126 |
+
quality_metrics = options.get("quality_metrics", "duration")
|
| 127 |
+
|
| 128 |
+
# 使用基类方法解析质量评估维度
|
| 129 |
+
enabled_metrics = self.parse_quality_metrics(quality_metrics)
|
| 130 |
|
| 131 |
paths = self.get_source_paths(bank_dir, source_name)
|
| 132 |
export_dir = self.get_export_dir(bank_dir, source_name, "simple_export")
|
|
|
|
| 136 |
segments_dir = os.path.join(temp_base, source_name)
|
| 137 |
|
| 138 |
# 获取头尾拓展参数
|
| 139 |
+
extend_duration = min(float(options.get("extend_duration", 0)), 0.5)
|
| 140 |
|
| 141 |
# 步骤1: 提取分词片段
|
| 142 |
self._log("【提取分词片段】")
|
|
|
|
| 153 |
return False, msg
|
| 154 |
|
| 155 |
# 步骤2: 排序导出
|
| 156 |
+
self._log(f"\n【排序导出】评估维度: {enabled_metrics}")
|
| 157 |
success, msg = self._sort_and_export(
|
| 158 |
segments_dir,
|
| 159 |
export_dir,
|
| 160 |
max_samples,
|
| 161 |
naming_rule,
|
| 162 |
+
first_naming_rule,
|
| 163 |
+
enabled_metrics
|
| 164 |
)
|
| 165 |
if not success:
|
| 166 |
return False, msg
|
|
|
|
| 490 |
export_dir: str,
|
| 491 |
max_samples: int,
|
| 492 |
naming_rule: str,
|
| 493 |
+
first_naming_rule: str,
|
| 494 |
+
enabled_metrics: List[str]
|
| 495 |
) -> Tuple[bool, str]:
|
| 496 |
"""排序并导出"""
|
| 497 |
try:
|
| 498 |
import soundfile as sf
|
| 499 |
+
from src.quality_scorer import QualityScorer, duration_score
|
| 500 |
|
| 501 |
os.makedirs(export_dir, exist_ok=True)
|
| 502 |
|
|
|
|
| 516 |
|
| 517 |
self._log(f"扫描到 {len(wav_files)} 个片段")
|
| 518 |
|
| 519 |
+
# 判断是否需要加载音频计算质量分数
|
| 520 |
+
need_audio_scoring = any(m in enabled_metrics for m in ["rms", "f0"])
|
| 521 |
+
|
| 522 |
# 按拼音分组
|
| 523 |
+
stats: Dict[str, List[Tuple[str, float, float]]] = {} # pinyin -> [(path, duration, score)]
|
| 524 |
+
|
| 525 |
+
if need_audio_scoring:
|
| 526 |
+
scorer = QualityScorer(enabled_metrics=enabled_metrics)
|
| 527 |
+
|
| 528 |
for path in wav_files:
|
| 529 |
rel_path = os.path.relpath(path, segments_dir)
|
| 530 |
parts = rel_path.split(os.sep)
|
|
|
|
| 532 |
pinyin = parts[0]
|
| 533 |
if pinyin not in stats:
|
| 534 |
stats[pinyin] = []
|
| 535 |
+
|
| 536 |
+
try:
|
| 537 |
+
info = sf.info(path)
|
| 538 |
+
duration = info.duration
|
| 539 |
+
|
| 540 |
+
if need_audio_scoring:
|
| 541 |
+
# 加载音频计算质量分数
|
| 542 |
+
audio, sr = sf.read(path)
|
| 543 |
+
if len(audio.shape) > 1:
|
| 544 |
+
audio = audio.mean(axis=1)
|
| 545 |
+
scores = scorer.score(audio, sr, duration)
|
| 546 |
+
quality_score = scores.get("combined", 0.5)
|
| 547 |
+
else:
|
| 548 |
+
# 仅使用时长评分
|
| 549 |
+
quality_score = duration_score(duration)
|
| 550 |
+
|
| 551 |
+
stats[pinyin].append((path, duration, quality_score))
|
| 552 |
+
except Exception as e:
|
| 553 |
+
logger.warning(f"处理文件失败 {path}: {e}")
|
| 554 |
+
continue
|
| 555 |
|
| 556 |
self._log(f"统计到 {len(stats)} 个拼音")
|
| 557 |
self._log(f"命名规则: {naming_rule}")
|
| 558 |
if first_naming_rule:
|
| 559 |
self._log(f"首个样本规则: {first_naming_rule}")
|
| 560 |
|
| 561 |
+
# 按质量分数排序并导出
|
| 562 |
exported = 0
|
| 563 |
for pinyin, files in stats.items():
|
| 564 |
+
sorted_files = sorted(files, key=lambda x: -x[2]) # 按质量分数降序
|
| 565 |
+
for idx, (src_path, _, _) in enumerate(sorted_files[:max_samples]):
|
| 566 |
+
# 使用基类方法应用命名规则
|
| 567 |
if idx == 0 and first_naming_rule:
|
| 568 |
+
filename = self.apply_naming_rule(first_naming_rule, pinyin, idx)
|
| 569 |
else:
|
| 570 |
+
filename = self.apply_naming_rule(naming_rule, pinyin, idx)
|
| 571 |
|
| 572 |
dst_path = os.path.join(export_dir, f'{filename}.wav')
|
| 573 |
shutil.copyfile(src_path, dst_path)
|
src/export_plugins/utau_oto_export.py
CHANGED
|
@@ -233,11 +233,6 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 233 |
|
| 234 |
def get_options(self) -> List[PluginOption]:
|
| 235 |
return [
|
| 236 |
-
PluginOption(
|
| 237 |
-
key="info",
|
| 238 |
-
label="从 TextGrid phones 层提取音素,生成 oto.ini(音频不裁剪)",
|
| 239 |
-
option_type=OptionType.LABEL
|
| 240 |
-
),
|
| 241 |
PluginOption(
|
| 242 |
key="cross_language",
|
| 243 |
label="跨语种导出",
|
|
@@ -309,11 +304,11 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 309 |
description="oto.ini 和 character.txt 编码(UTAU 标准为 Shift_JIS)"
|
| 310 |
),
|
| 311 |
PluginOption(
|
| 312 |
-
key="
|
| 313 |
-
label="
|
| 314 |
-
option_type=OptionType.
|
| 315 |
-
default=
|
| 316 |
-
description="
|
| 317 |
),
|
| 318 |
]
|
| 319 |
|
|
@@ -325,8 +320,8 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 325 |
) -> Tuple[bool, str]:
|
| 326 |
"""执行 UTAU oto.ini 导出"""
|
| 327 |
try:
|
| 328 |
-
# 加载语言设置
|
| 329 |
-
language = self.
|
| 330 |
|
| 331 |
# 获取选项
|
| 332 |
max_samples = int(options.get("max_samples", 5))
|
|
@@ -336,11 +331,11 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 336 |
alias_style = options.get("alias_style", "romaji")
|
| 337 |
overlap_ratio = float(options.get("overlap_ratio", 0.3))
|
| 338 |
encoding = options.get("encoding", "utf-8")
|
| 339 |
-
|
| 340 |
use_hiragana = (alias_style == "hiragana") and language in ('japanese', 'ja', 'jp')
|
| 341 |
|
| 342 |
-
# 解析质量评估维度
|
| 343 |
-
enabled_metrics = self.
|
| 344 |
|
| 345 |
paths = self.get_source_paths(bank_dir, source_name)
|
| 346 |
export_dir = self.get_export_dir(bank_dir, source_name, "utau_oto")
|
|
@@ -370,12 +365,10 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 370 |
)
|
| 371 |
self._log(f"筛选后保留 {len(filtered_entries)} 条配置,涉及 {len(used_wavs)} 个音频文件")
|
| 372 |
|
| 373 |
-
# 步骤3: 复制音频文件(
|
| 374 |
self._log("\n【复制音频文件】")
|
| 375 |
-
if sanitize_filename:
|
| 376 |
-
self._log("已启用文件名转拼音")
|
| 377 |
copied, filename_map = self._copy_wav_files(
|
| 378 |
-
used_wavs, paths["slices_dir"], export_dir,
|
| 379 |
)
|
| 380 |
self._log(f"复制了 {copied} 个音频文件")
|
| 381 |
|
|
@@ -388,7 +381,9 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 388 |
# 步骤5: 写入 character.txt
|
| 389 |
self._log("\n【生成 character.txt】")
|
| 390 |
char_path = os.path.join(export_dir, "character.txt")
|
| 391 |
-
|
|
|
|
|
|
|
| 392 |
self._log(f"写入: {char_path}")
|
| 393 |
|
| 394 |
# 统计别名数量
|
|
@@ -399,31 +394,6 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 399 |
logger.error(f"UTAU oto.ini 导出失败: {e}", exc_info=True)
|
| 400 |
return False, str(e)
|
| 401 |
|
| 402 |
-
def _parse_quality_metrics(self, metrics_str: str) -> List[str]:
|
| 403 |
-
"""解析质量评估维度选项"""
|
| 404 |
-
if metrics_str == "all":
|
| 405 |
-
return ["duration", "rms", "f0"]
|
| 406 |
-
elif metrics_str == "duration+rms":
|
| 407 |
-
return ["duration", "rms"]
|
| 408 |
-
elif metrics_str == "duration+f0":
|
| 409 |
-
return ["duration", "f0"]
|
| 410 |
-
else:
|
| 411 |
-
return ["duration"]
|
| 412 |
-
|
| 413 |
-
def _load_language_from_meta(self, bank_dir: str, source_name: str) -> str:
|
| 414 |
-
"""从 meta.json 加载语言设置"""
|
| 415 |
-
meta_path = os.path.join(bank_dir, source_name, "meta.json")
|
| 416 |
-
try:
|
| 417 |
-
if os.path.exists(meta_path):
|
| 418 |
-
with open(meta_path, 'r', encoding='utf-8') as f:
|
| 419 |
-
meta = json.load(f)
|
| 420 |
-
language = meta.get("language", "chinese")
|
| 421 |
-
self._log(f"语言设置: {language}")
|
| 422 |
-
return language
|
| 423 |
-
except Exception as e:
|
| 424 |
-
logger.warning(f"读取 meta.json 失败: {e}")
|
| 425 |
-
return "chinese"
|
| 426 |
-
|
| 427 |
def _parse_textgrids(
|
| 428 |
self,
|
| 429 |
slices_dir: str,
|
|
@@ -689,11 +659,11 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 689 |
|
| 690 |
# 保留前 N 个,并应用命名规则
|
| 691 |
for idx, entry in enumerate(sorted_group[:max_samples]):
|
| 692 |
-
#
|
| 693 |
if idx == 0 and first_naming_rule:
|
| 694 |
-
final_alias = self.
|
| 695 |
else:
|
| 696 |
-
final_alias = self.
|
| 697 |
|
| 698 |
entry["alias"] = final_alias
|
| 699 |
filtered.append(entry)
|
|
@@ -747,16 +717,12 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 747 |
|
| 748 |
return entries
|
| 749 |
|
| 750 |
-
def _apply_naming_rule(self, rule: str, base_alias: str, index: int) -> str:
|
| 751 |
-
"""应用命名规则生成别名"""
|
| 752 |
-
return rule.replace("%p%", base_alias).replace("%n%", str(index))
|
| 753 |
-
|
| 754 |
def _copy_wav_files(
|
| 755 |
self,
|
| 756 |
wav_files: set,
|
| 757 |
slices_dir: str,
|
| 758 |
export_dir: str,
|
| 759 |
-
|
| 760 |
) -> Tuple[int, Dict[str, str]]:
|
| 761 |
"""
|
| 762 |
复制音频文件到导出目录
|
|
@@ -765,7 +731,7 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 765 |
wav_files: 需要复制的文件名集合
|
| 766 |
slices_dir: 源目录
|
| 767 |
export_dir: 目标目录
|
| 768 |
-
|
| 769 |
|
| 770 |
返回:
|
| 771 |
(复制数量, 文件名映射表 {原文件名: 新文件名})
|
|
@@ -773,25 +739,48 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 773 |
copied = 0
|
| 774 |
filename_map: Dict[str, str] = {}
|
| 775 |
used_names: set = set()
|
|
|
|
| 776 |
|
| 777 |
for wav_name in wav_files:
|
| 778 |
src = os.path.join(slices_dir, wav_name)
|
| 779 |
if not os.path.exists(src):
|
| 780 |
continue
|
| 781 |
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
used_names.add(new_name)
|
| 785 |
-
else:
|
| 786 |
new_name = wav_name
|
|
|
|
|
|
|
|
|
|
| 787 |
|
|
|
|
| 788 |
filename_map[wav_name] = new_name
|
| 789 |
dst = os.path.join(export_dir, new_name)
|
| 790 |
shutil.copyfile(src, dst)
|
| 791 |
copied += 1
|
| 792 |
|
|
|
|
|
|
|
|
|
|
| 793 |
return copied, filename_map
|
| 794 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 795 |
def _sanitize_filename(self, filename: str, used_names: set) -> str:
|
| 796 |
"""
|
| 797 |
清理文件名:中文转拼音 + 特殊字符清理 + 防冲突
|
|
@@ -883,25 +872,31 @@ class UTAUOtoExportPlugin(ExportPlugin):
|
|
| 883 |
|
| 884 |
def _write_character_txt(
|
| 885 |
self,
|
| 886 |
-
|
| 887 |
output_path: str,
|
| 888 |
encoding: str
|
| 889 |
):
|
| 890 |
"""写入 character.txt 文件,用于 UTAU 识别音源名称
|
| 891 |
|
| 892 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 893 |
自动将名称转换为拼音/罗马音。
|
| 894 |
"""
|
| 895 |
-
name_to_write =
|
| 896 |
|
| 897 |
# 检测是否能用指定编码
|
| 898 |
try:
|
| 899 |
-
|
| 900 |
except UnicodeEncodeError:
|
| 901 |
# 无法编码,转换为拼音
|
| 902 |
from pypinyin import lazy_pinyin
|
| 903 |
-
pinyin_name = ''.join(lazy_pinyin(
|
| 904 |
-
logger.warning(f"
|
|
|
|
| 905 |
name_to_write = pinyin_name
|
| 906 |
|
| 907 |
with open(output_path, 'w', encoding=encoding) as f:
|
|
|
|
| 233 |
|
| 234 |
def get_options(self) -> List[PluginOption]:
|
| 235 |
return [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
PluginOption(
|
| 237 |
key="cross_language",
|
| 238 |
label="跨语种导出",
|
|
|
|
| 304 |
description="oto.ini 和 character.txt 编码(UTAU 标准为 Shift_JIS)"
|
| 305 |
),
|
| 306 |
PluginOption(
|
| 307 |
+
key="character_name",
|
| 308 |
+
label="角色名称",
|
| 309 |
+
option_type=OptionType.TEXT,
|
| 310 |
+
default="",
|
| 311 |
+
description="character.txt 中的角色名,留空则使用音源名称"
|
| 312 |
),
|
| 313 |
]
|
| 314 |
|
|
|
|
| 320 |
) -> Tuple[bool, str]:
|
| 321 |
"""执行 UTAU oto.ini 导出"""
|
| 322 |
try:
|
| 323 |
+
# 使用基类方法加载语言设置
|
| 324 |
+
language = self.load_language_from_meta(bank_dir, source_name)
|
| 325 |
|
| 326 |
# 获取选项
|
| 327 |
max_samples = int(options.get("max_samples", 5))
|
|
|
|
| 331 |
alias_style = options.get("alias_style", "romaji")
|
| 332 |
overlap_ratio = float(options.get("overlap_ratio", 0.3))
|
| 333 |
encoding = options.get("encoding", "utf-8")
|
| 334 |
+
character_name = options.get("character_name", "").strip()
|
| 335 |
use_hiragana = (alias_style == "hiragana") and language in ('japanese', 'ja', 'jp')
|
| 336 |
|
| 337 |
+
# 使用基类方法解析质量评估维度
|
| 338 |
+
enabled_metrics = self.parse_quality_metrics(quality_metrics)
|
| 339 |
|
| 340 |
paths = self.get_source_paths(bank_dir, source_name)
|
| 341 |
export_dir = self.get_export_dir(bank_dir, source_name, "utau_oto")
|
|
|
|
| 365 |
)
|
| 366 |
self._log(f"筛选后保留 {len(filtered_entries)} 条配置,涉及 {len(used_wavs)} 个音频文件")
|
| 367 |
|
| 368 |
+
# 步骤3: 复制音频文件(自动检测文件名是否需要转拼音)
|
| 369 |
self._log("\n【复制音频文件】")
|
|
|
|
|
|
|
| 370 |
copied, filename_map = self._copy_wav_files(
|
| 371 |
+
used_wavs, paths["slices_dir"], export_dir, encoding
|
| 372 |
)
|
| 373 |
self._log(f"复制了 {copied} 个音频文件")
|
| 374 |
|
|
|
|
| 381 |
# 步骤5: 写入 character.txt
|
| 382 |
self._log("\n【生成 character.txt】")
|
| 383 |
char_path = os.path.join(export_dir, "character.txt")
|
| 384 |
+
# 使用自定义角色名,留空则使用音源名称
|
| 385 |
+
final_character_name = character_name if character_name else source_name
|
| 386 |
+
self._write_character_txt(final_character_name, char_path, encoding)
|
| 387 |
self._log(f"写入: {char_path}")
|
| 388 |
|
| 389 |
# 统计别名数量
|
|
|
|
| 394 |
logger.error(f"UTAU oto.ini 导出失败: {e}", exc_info=True)
|
| 395 |
return False, str(e)
|
| 396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
def _parse_textgrids(
|
| 398 |
self,
|
| 399 |
slices_dir: str,
|
|
|
|
| 659 |
|
| 660 |
# 保留前 N 个,并应用命名规则
|
| 661 |
for idx, entry in enumerate(sorted_group[:max_samples]):
|
| 662 |
+
# 使用基类方法应用命名规则
|
| 663 |
if idx == 0 and first_naming_rule:
|
| 664 |
+
final_alias = self.apply_naming_rule(first_naming_rule, base_alias, idx)
|
| 665 |
else:
|
| 666 |
+
final_alias = self.apply_naming_rule(naming_rule, base_alias, idx)
|
| 667 |
|
| 668 |
entry["alias"] = final_alias
|
| 669 |
filtered.append(entry)
|
|
|
|
| 717 |
|
| 718 |
return entries
|
| 719 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
def _copy_wav_files(
|
| 721 |
self,
|
| 722 |
wav_files: set,
|
| 723 |
slices_dir: str,
|
| 724 |
export_dir: str,
|
| 725 |
+
encoding: str = "shift_jis"
|
| 726 |
) -> Tuple[int, Dict[str, str]]:
|
| 727 |
"""
|
| 728 |
复制音频文件到导出目录
|
|
|
|
| 731 |
wav_files: 需要复制的文件名集合
|
| 732 |
slices_dir: 源目录
|
| 733 |
export_dir: 目标目录
|
| 734 |
+
encoding: 目标编码,用于检测文件名是否合法
|
| 735 |
|
| 736 |
返回:
|
| 737 |
(复制数量, 文件名映射表 {原文件名: 新文件名})
|
|
|
|
| 739 |
copied = 0
|
| 740 |
filename_map: Dict[str, str] = {}
|
| 741 |
used_names: set = set()
|
| 742 |
+
sanitized_count = 0
|
| 743 |
|
| 744 |
for wav_name in wav_files:
|
| 745 |
src = os.path.join(slices_dir, wav_name)
|
| 746 |
if not os.path.exists(src):
|
| 747 |
continue
|
| 748 |
|
| 749 |
+
# 检测文件名是否能用指定编码表示
|
| 750 |
+
if self._is_filename_valid(wav_name, encoding):
|
|
|
|
|
|
|
| 751 |
new_name = wav_name
|
| 752 |
+
else:
|
| 753 |
+
new_name = self._sanitize_filename(wav_name, used_names)
|
| 754 |
+
sanitized_count += 1
|
| 755 |
|
| 756 |
+
used_names.add(new_name)
|
| 757 |
filename_map[wav_name] = new_name
|
| 758 |
dst = os.path.join(export_dir, new_name)
|
| 759 |
shutil.copyfile(src, dst)
|
| 760 |
copied += 1
|
| 761 |
|
| 762 |
+
if sanitized_count > 0:
|
| 763 |
+
self._log(f"已将 {sanitized_count} 个文件名转换为拼音(原文件名无法用 {encoding} 编码)")
|
| 764 |
+
|
| 765 |
return copied, filename_map
|
| 766 |
|
| 767 |
+
def _is_filename_valid(self, filename: str, encoding: str) -> bool:
|
| 768 |
+
"""
|
| 769 |
+
检测文件名是否合法(能否用指定编码表示)
|
| 770 |
+
|
| 771 |
+
参数:
|
| 772 |
+
filename: 文件名
|
| 773 |
+
encoding: 目标编码
|
| 774 |
+
|
| 775 |
+
返回:
|
| 776 |
+
True 表示文件名合法,False 表示需要转换
|
| 777 |
+
"""
|
| 778 |
+
try:
|
| 779 |
+
filename.encode(encoding)
|
| 780 |
+
return True
|
| 781 |
+
except UnicodeEncodeError:
|
| 782 |
+
return False
|
| 783 |
+
|
| 784 |
def _sanitize_filename(self, filename: str, used_names: set) -> str:
|
| 785 |
"""
|
| 786 |
清理文件名:中文转拼音 + 特殊字符清理 + 防冲突
|
|
|
|
| 872 |
|
| 873 |
def _write_character_txt(
|
| 874 |
self,
|
| 875 |
+
character_name: str,
|
| 876 |
output_path: str,
|
| 877 |
encoding: str
|
| 878 |
):
|
| 879 |
"""写入 character.txt 文件,用于 UTAU 识别音源名称
|
| 880 |
|
| 881 |
+
参数:
|
| 882 |
+
character_name: 角色名称(可以是用户自定义的名称或音源名称)
|
| 883 |
+
output_path: 输出路径
|
| 884 |
+
encoding: 文件编码
|
| 885 |
+
|
| 886 |
+
注意:当角色名称包含无法用指定编码表示的字符时,
|
| 887 |
自动将名称转换为拼音/罗马音。
|
| 888 |
"""
|
| 889 |
+
name_to_write = character_name
|
| 890 |
|
| 891 |
# 检测是否能用指定编码
|
| 892 |
try:
|
| 893 |
+
character_name.encode(encoding)
|
| 894 |
except UnicodeEncodeError:
|
| 895 |
# 无法编码,转换为拼音
|
| 896 |
from pypinyin import lazy_pinyin
|
| 897 |
+
pinyin_name = ''.join(lazy_pinyin(character_name))
|
| 898 |
+
logger.warning(f"角色名称 '{character_name}' 无法用 {encoding} 编码,已转换为拼音: {pinyin_name}")
|
| 899 |
+
self._log(f"角色名称 '{character_name}' 无法用 {encoding} 编码,已转换为拼音: {pinyin_name}")
|
| 900 |
name_to_write = pinyin_name
|
| 901 |
|
| 902 |
with open(output_path, 'w', encoding=encoding) as f:
|
src/gui_cloud.py
CHANGED
|
@@ -469,8 +469,14 @@ def process_export_voicebank(
|
|
| 469 |
log("\n" + "=" * 50)
|
| 470 |
log("【打包结果】")
|
| 471 |
|
| 472 |
-
# 根据插件类型确定导出目录
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
export_dir = os.path.join(workspace, "export", source_name, export_subdir)
|
| 475 |
|
| 476 |
# 如果导出目录不存在,尝试其他位置
|
|
@@ -482,16 +488,21 @@ def process_export_voicebank(
|
|
| 482 |
# 再尝试另一个子目录
|
| 483 |
if not os.path.exists(export_dir):
|
| 484 |
other_subdir = "simple_export" if export_subdir == "utau_oto" else "utau_oto"
|
|
|
|
| 485 |
export_dir = os.path.join(workspace, "export", source_name, other_subdir)
|
| 486 |
if not os.path.exists(export_dir):
|
| 487 |
alt_export = os.path.join(os.path.dirname(bank_dir), "export", source_name, other_subdir)
|
| 488 |
if os.path.exists(alt_export):
|
| 489 |
export_dir = alt_export
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
if not os.path.exists(export_dir):
|
| 492 |
return "❌ 未找到导出结果", "\n".join(logs), None
|
| 493 |
|
| 494 |
-
|
|
|
|
| 495 |
result_zip = create_zip(export_dir, zip_name)
|
| 496 |
|
| 497 |
if result_zip:
|
|
|
|
| 469 |
log("\n" + "=" * 50)
|
| 470 |
log("【打包结果】")
|
| 471 |
|
| 472 |
+
# 根据插件类型确定导出目录和导出标识
|
| 473 |
+
if "UTAU" in plugin_name:
|
| 474 |
+
export_subdir = "utau_oto"
|
| 475 |
+
export_id = "utau_oto_export"
|
| 476 |
+
else:
|
| 477 |
+
export_subdir = "simple_export"
|
| 478 |
+
export_id = "simple_export"
|
| 479 |
+
|
| 480 |
export_dir = os.path.join(workspace, "export", source_name, export_subdir)
|
| 481 |
|
| 482 |
# 如果导出目录不存在,尝试其他位置
|
|
|
|
| 488 |
# 再尝试另一个子目录
|
| 489 |
if not os.path.exists(export_dir):
|
| 490 |
other_subdir = "simple_export" if export_subdir == "utau_oto" else "utau_oto"
|
| 491 |
+
other_id = "simple_export" if export_id == "utau_oto_export" else "utau_oto_export"
|
| 492 |
export_dir = os.path.join(workspace, "export", source_name, other_subdir)
|
| 493 |
if not os.path.exists(export_dir):
|
| 494 |
alt_export = os.path.join(os.path.dirname(bank_dir), "export", source_name, other_subdir)
|
| 495 |
if os.path.exists(alt_export):
|
| 496 |
export_dir = alt_export
|
| 497 |
+
export_id = other_id
|
| 498 |
+
else:
|
| 499 |
+
export_id = other_id
|
| 500 |
|
| 501 |
if not os.path.exists(export_dir):
|
| 502 |
return "❌ 未找到导出结果", "\n".join(logs), None
|
| 503 |
|
| 504 |
+
# 命名格式: [音源名称]_[插件标识]
|
| 505 |
+
zip_name = f"{source_name}_{export_id}"
|
| 506 |
result_zip = create_zip(export_dir, zip_name)
|
| 507 |
|
| 508 |
if result_zip:
|