TNOT commited on
Commit
86371bb
·
1 Parent(s): d824a9d

fix: 导出页面 UI 修复 2

Browse files
docs/流程文档_AI用.md CHANGED
@@ -391,9 +391,11 @@ MFA 环境:
391
  - 音频未上传完成时禁用「开始制作」按钮,防止误操作
392
  - 导出页面提供「使用刚制作的音源」按钮,避免重复上传
393
  - Whisper 模型选项标注速度参考:small 约 4 秒/句,medium 约 12 秒/句(慢 2-3 倍但更准确)
394
- - **导出插件动态选项**: 切换导出插件时自动显示/隐藏对应的配置选项
395
- - 简单单字导出: 基本设置(最大样本数、命名规则)
396
- - UTAU oto.ini 导出: 额外显示质量评估维度、别名风格、Overlap 比例、文件编码等专用选项
 
 
397
 
398
  ### 平台差异
399
 
 
391
  - 音频未上传完成时禁用「开始制作」按钮,防止误操作
392
  - 导出页面提供「使用刚制作的音源」按钮,避免重复上传
393
  - Whisper 模型选项标注速度参考:small 约 4 秒/句,medium 约 12 秒/句(慢 2-3 倍但更准确)
394
+ - **导出插件动态选项系统**:
395
+ - 插件选项完全动态化,根据 `ExportPlugin.get_options()` 自动生成 UI 组件
396
+ - 切换插件时自动显示/隐藏对应的配置选项
397
+ - 支持的选项类型: TEXT(文本)、NUMBER(数字)、SWITCH(开关)、COMBO(下拉)、MULTI_SELECT(多选)、LABEL(说明文字)
398
+ - 新增插件无需修改 GUI 代码,只需在插件中定义 `get_options()` 即可自动生成界面
399
 
400
  ### 平台差异
401
 
src/export_plugins/simple_export.py CHANGED
@@ -41,6 +41,13 @@ class SimpleExportPlugin(ExportPlugin):
41
  max_value=1000,
42
  description="按时长排序,保留最长的N个"
43
  ),
 
 
 
 
 
 
 
44
  PluginOption(
45
  key="naming_rule",
46
  label="命名规则",
@@ -83,6 +90,57 @@ class SimpleExportPlugin(ExportPlugin):
83
  name = rule.replace("%p%", pinyin).replace("%n%", str(index))
84
  return name
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  def export(
87
  self,
88
  source_name: str,
@@ -105,13 +163,19 @@ class SimpleExportPlugin(ExportPlugin):
105
  temp_base = os.path.join(bank_dir, ".temp_segments")
106
  segments_dir = os.path.join(temp_base, source_name)
107
 
 
 
 
108
  # 步骤1: 提取分词片段
109
  self._log("【提取分词片段】")
 
 
110
  success, msg, pinyin_counts = self._extract_segments(
111
  paths["slices_dir"],
112
  paths["textgrid_dir"],
113
  segments_dir,
114
- language
 
115
  )
116
  if not success:
117
  return False, msg
@@ -146,13 +210,17 @@ class SimpleExportPlugin(ExportPlugin):
146
  slices_dir: str,
147
  textgrid_dir: str,
148
  segments_dir: str,
149
- language: str
 
150
  ) -> Tuple[bool, str, Dict[str, int]]:
151
  """
152
  提取分词片段
153
 
154
  中文:使用words层按字切分,用char_to_pinyin获取拼音名称
155
  日语:使用phones层按音素切分,合并辅音+元音为音节
 
 
 
156
  """
157
  try:
158
  import textgrid
@@ -169,11 +237,11 @@ class SimpleExportPlugin(ExportPlugin):
169
  # 根据语言选择提取方法
170
  if language in ("japanese", "ja", "jp"):
171
  return self._extract_japanese_segments(
172
- tg_files, slices_dir, segments_dir
173
  )
174
  else:
175
  return self._extract_chinese_segments(
176
- tg_files, slices_dir, segments_dir, language
177
  )
178
 
179
  except Exception as e:
@@ -185,12 +253,16 @@ class SimpleExportPlugin(ExportPlugin):
185
  tg_files: List[str],
186
  slices_dir: str,
187
  segments_dir: str,
188
- language: str
 
189
  ) -> Tuple[bool, str, Dict[str, int]]:
190
  """
191
  中文音频提取
192
 
193
  使用words层的时间边界,按字符切分,用char_to_pinyin获取拼音
 
 
 
194
  """
195
  import textgrid
196
  import soundfile as sf
@@ -208,6 +280,7 @@ class SimpleExportPlugin(ExportPlugin):
208
 
209
  tg = textgrid.TextGrid.fromFile(tg_path)
210
  audio, sr = sf.read(wav_path, dtype='float32')
 
211
 
212
  # 使用words层(第一层)
213
  words_tier = tg[0]
@@ -240,6 +313,11 @@ class SimpleExportPlugin(ExportPlugin):
240
  char_start = start_time + i * char_duration
241
  char_end = char_start + char_duration
242
 
 
 
 
 
 
243
  pinyin_dir = os.path.join(segments_dir, pinyin)
244
  os.makedirs(pinyin_dir, exist_ok=True)
245
 
@@ -247,8 +325,8 @@ class SimpleExportPlugin(ExportPlugin):
247
  index = current_count + 1
248
  pinyin_counts[pinyin] = index
249
 
250
- start_sample = int(round(char_start * sr))
251
- end_sample = int(round(char_end * sr))
252
  segment = audio[start_sample:end_sample]
253
 
254
  if len(segment) == 0:
@@ -267,12 +345,16 @@ class SimpleExportPlugin(ExportPlugin):
267
  self,
268
  tg_files: List[str],
269
  slices_dir: str,
270
- segments_dir: str
 
271
  ) -> Tuple[bool, str, Dict[str, int]]:
272
  """
273
  日语音频提取
274
 
275
  使用phones层,将辅音+元音合并为音节
 
 
 
276
  """
277
  import textgrid
278
  import soundfile as sf
@@ -289,6 +371,7 @@ class SimpleExportPlugin(ExportPlugin):
289
 
290
  tg = textgrid.TextGrid.fromFile(tg_path)
291
  audio, sr = sf.read(wav_path, dtype='float32')
 
292
 
293
  # 查找phones层
294
  phones_tier = None
@@ -316,6 +399,11 @@ class SimpleExportPlugin(ExportPlugin):
316
  if not normalized:
317
  continue
318
 
 
 
 
 
 
319
  phone_dir = os.path.join(segments_dir, normalized)
320
  os.makedirs(phone_dir, exist_ok=True)
321
 
@@ -323,8 +411,8 @@ class SimpleExportPlugin(ExportPlugin):
323
  index = current_count + 1
324
  phone_counts[normalized] = index
325
 
326
- start_sample = int(round(start_time * sr))
327
- end_sample = int(round(end_time * sr))
328
  segment = audio[start_sample:end_sample]
329
 
330
  if len(segment) == 0:
 
41
  max_value=1000,
42
  description="按时长排序,保留最长的N个"
43
  ),
44
+ PluginOption(
45
+ key="extend_duration",
46
+ label="头尾拓展(秒)",
47
+ option_type=OptionType.TEXT,
48
+ default="0",
49
+ description="裁剪时头尾各拓展的时长,最大1.5秒。若一边到达边界,另一边继续拓展"
50
+ ),
51
  PluginOption(
52
  key="naming_rule",
53
  label="命名规则",
 
90
  name = rule.replace("%p%", pinyin).replace("%n%", str(index))
91
  return name
92
 
93
+ def _apply_extend(
94
+ self,
95
+ start_time: float,
96
+ end_time: float,
97
+ extend_duration: float,
98
+ audio_duration: float
99
+ ) -> Tuple[float, float]:
100
+ """
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
+ if start_time - head_extend < 0:
125
+ head_actual = start_time # 头部只能拓展到0
126
+ head_remaining = head_extend - head_actual # 剩余量转给尾部
127
+ tail_extend += head_remaining
128
+ head_extend = head_actual
129
+
130
+ # 检查尾部是否到达边界
131
+ if end_time + tail_extend > audio_duration:
132
+ tail_actual = audio_duration - end_time # 尾部只能拓展到边界
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
+ actual_start = max(0, start_time - head_extend)
140
+ actual_end = min(audio_duration, end_time + tail_extend)
141
+
142
+ return actual_start, actual_end
143
+
144
  def export(
145
  self,
146
  source_name: str,
 
163
  temp_base = os.path.join(bank_dir, ".temp_segments")
164
  segments_dir = os.path.join(temp_base, source_name)
165
 
166
+ # 获取头尾拓展参数
167
+ extend_duration = min(float(options.get("extend_duration", 0)), 1.5)
168
+
169
  # 步骤1: 提取分词片段
170
  self._log("【提取分词片段】")
171
+ if extend_duration > 0:
172
+ self._log(f"头尾拓展: {extend_duration}s(单边到达边界时另一边继续拓展)")
173
  success, msg, pinyin_counts = self._extract_segments(
174
  paths["slices_dir"],
175
  paths["textgrid_dir"],
176
  segments_dir,
177
+ language,
178
+ extend_duration
179
  )
180
  if not success:
181
  return False, msg
 
210
  slices_dir: str,
211
  textgrid_dir: str,
212
  segments_dir: str,
213
+ language: str,
214
+ extend_duration: float = 0.0
215
  ) -> Tuple[bool, str, Dict[str, int]]:
216
  """
217
  提取分词片段
218
 
219
  中文:使用words层按字切分,用char_to_pinyin获取拼音名称
220
  日语:使用phones层按音素切分,合并辅音+元音为音节
221
+
222
+ 参数:
223
+ extend_duration: 头尾拓展总时长(秒),单边到达边界时另一边继续拓展
224
  """
225
  try:
226
  import textgrid
 
237
  # 根据语言选择提取方法
238
  if language in ("japanese", "ja", "jp"):
239
  return self._extract_japanese_segments(
240
+ tg_files, slices_dir, segments_dir, extend_duration
241
  )
242
  else:
243
  return self._extract_chinese_segments(
244
+ tg_files, slices_dir, segments_dir, language, extend_duration
245
  )
246
 
247
  except Exception as e:
 
253
  tg_files: List[str],
254
  slices_dir: str,
255
  segments_dir: str,
256
+ language: str,
257
+ extend_duration: float = 0.0
258
  ) -> Tuple[bool, str, Dict[str, int]]:
259
  """
260
  中文音频提取
261
 
262
  使用words层的时间边界,按字符切分,用char_to_pinyin获取拼音
263
+
264
+ 参数:
265
+ extend_duration: 头尾拓展总时长(秒),单边到达边界时另一边继续拓展
266
  """
267
  import textgrid
268
  import soundfile as sf
 
280
 
281
  tg = textgrid.TextGrid.fromFile(tg_path)
282
  audio, sr = sf.read(wav_path, dtype='float32')
283
+ audio_duration = len(audio) / sr
284
 
285
  # 使用words层(第一层)
286
  words_tier = tg[0]
 
313
  char_start = start_time + i * char_duration
314
  char_end = char_start + char_duration
315
 
316
+ # 应用头尾拓展,单边到达边界时另一边继续拓展
317
+ actual_start, actual_end = self._apply_extend(
318
+ char_start, char_end, extend_duration, audio_duration
319
+ )
320
+
321
  pinyin_dir = os.path.join(segments_dir, pinyin)
322
  os.makedirs(pinyin_dir, exist_ok=True)
323
 
 
325
  index = current_count + 1
326
  pinyin_counts[pinyin] = index
327
 
328
+ start_sample = int(round(actual_start * sr))
329
+ end_sample = int(round(actual_end * sr))
330
  segment = audio[start_sample:end_sample]
331
 
332
  if len(segment) == 0:
 
345
  self,
346
  tg_files: List[str],
347
  slices_dir: str,
348
+ segments_dir: str,
349
+ extend_duration: float = 0.0
350
  ) -> Tuple[bool, str, Dict[str, int]]:
351
  """
352
  日语音频提取
353
 
354
  使用phones层,将辅音+元音合并为音节
355
+
356
+ 参数:
357
+ extend_duration: 头尾拓展总时长(秒),单边到达边界时另一边继续拓展
358
  """
359
  import textgrid
360
  import soundfile as sf
 
371
 
372
  tg = textgrid.TextGrid.fromFile(tg_path)
373
  audio, sr = sf.read(wav_path, dtype='float32')
374
+ audio_duration = len(audio) / sr
375
 
376
  # 查找phones层
377
  phones_tier = None
 
399
  if not normalized:
400
  continue
401
 
402
+ # 应用头尾拓展,单边到达边界时另一边继续拓展
403
+ actual_start, actual_end = self._apply_extend(
404
+ start_time, end_time, extend_duration, audio_duration
405
+ )
406
+
407
  phone_dir = os.path.join(segments_dir, normalized)
408
  os.makedirs(phone_dir, exist_ok=True)
409
 
 
411
  index = current_count + 1
412
  phone_counts[normalized] = index
413
 
414
+ start_sample = int(round(actual_start * sr))
415
+ end_sample = int(round(actual_end * sr))
416
  segment = audio[start_sample:end_sample]
417
 
418
  if len(segment) == 0:
src/export_plugins/utau_oto_export.py CHANGED
@@ -238,6 +238,13 @@ class UTAUOtoExportPlugin(ExportPlugin):
238
  label="从 TextGrid phones 层提取音素,生成 oto.ini(音频不裁剪)",
239
  option_type=OptionType.LABEL
240
  ),
 
 
 
 
 
 
 
241
  PluginOption(
242
  key="max_samples",
243
  label="每个别名最大样本数",
@@ -286,6 +293,13 @@ class UTAUOtoExportPlugin(ExportPlugin):
286
  max_value=0.5,
287
  description="Overlap = Preutterance × 此比例"
288
  ),
 
 
 
 
 
 
 
289
  PluginOption(
290
  key="encoding",
291
  label="文件编码",
 
238
  label="从 TextGrid phones 层提取音素,生成 oto.ini(音频不裁剪)",
239
  option_type=OptionType.LABEL
240
  ),
241
+ PluginOption(
242
+ key="cross_language",
243
+ label="跨语种导出",
244
+ option_type=OptionType.SWITCH,
245
+ default=False,
246
+ description="【TODO】启用中跨日或日跨中的音素映射导出"
247
+ ),
248
  PluginOption(
249
  key="max_samples",
250
  label="每个别名最大样本数",
 
293
  max_value=0.5,
294
  description="Overlap = Preutterance × 此比例"
295
  ),
296
+ PluginOption(
297
+ key="auto_phoneme_combine",
298
+ label="自动拼字",
299
+ option_type=OptionType.SWITCH,
300
+ default=False,
301
+ description="【TODO】尽可能用已有的音素拆分拼接成缺失的音素"
302
+ ),
303
  PluginOption(
304
  key="encoding",
305
  label="文件编码",
src/gui_cloud.py CHANGED
@@ -534,31 +534,13 @@ def get_plugin_options_config(plugins: Dict[str, Any]) -> Dict[str, List[Dict]]:
534
  "choices": opt.choices,
535
  "min_value": opt.min_value,
536
  "max_value": opt.max_value,
 
537
  }
538
  options.append(opt_config)
539
  config[name] = options
540
  return config
541
 
542
 
543
- def build_plugin_options_html(plugin_name: str, plugins_config: Dict) -> str:
544
- """
545
- 根据插件选项生成 HTML 表单
546
-
547
- 这个方法生成一个简单的 HTML 表单,用于在 Gradio 中显示插件选项
548
- """
549
- if plugin_name not in plugins_config:
550
- return "<p>未找到插件配置</p>"
551
-
552
- options = plugins_config[plugin_name]
553
- html_parts = []
554
-
555
- for opt in options:
556
- if opt["type"] == "label":
557
- html_parts.append(f'<p style="color: #666; font-style: italic;">{opt["label"]}</p>')
558
-
559
- return "\n".join(html_parts) if html_parts else ""
560
-
561
-
562
  def get_default_options_json(plugin_name: str, plugins_config: Dict) -> str:
563
  """获取插件的默认选项 JSON"""
564
  if plugin_name not in plugins_config:
@@ -573,6 +555,145 @@ def get_default_options_json(plugin_name: str, plugins_config: Dict) -> str:
573
  return json.dumps(defaults, ensure_ascii=False)
574
 
575
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  # ==================== 构建界面 ====================
577
 
578
  def create_cloud_ui():
@@ -743,128 +864,126 @@ def create_cloud_ui():
743
  label="导出插件"
744
  )
745
 
746
- # 插件描述
747
- plugin_desc = gr.Markdown(
748
- value=f"> {plugins[plugin_names[0]].description}" if plugin_names and plugin_names[0] in plugins else ""
749
- )
750
-
751
- # 存储当前选项的 JSON(隐藏)
752
- options_state = gr.State(
753
- value=get_default_options_json(plugin_names[0], plugins_config) if plugin_names else "{}"
754
- )
755
-
756
  # ===== 动态选项区域 =====
757
- # 使用 gr.Group 包裹所有可能的组件,通过 visible 控制显示
758
-
759
- # 通用选项(两个插件都有)
760
- with gr.Group():
761
- gr.Markdown("#### 基本设置")
762
- with gr.Row():
763
- export_max_samples = gr.Number(
764
- label="每个拼音/别名最大样本数",
765
- value=10,
766
- minimum=1,
767
- maximum=1000
768
- )
769
-
770
- with gr.Row():
771
- export_naming = gr.Textbox(
772
- label="命名规则",
773
- value="%p%%n%",
774
- info="%p%=拼音/罗马音, %n%=序号"
775
- )
776
- export_first_naming = gr.Textbox(
777
- label="首个样本命名",
778
- value="%p%",
779
- info="第0个样本的特殊规则"
780
- )
781
-
782
- # UTAU 专用选项
783
- with gr.Group(visible=False) as utau_options_group:
784
- gr.Markdown("#### UTAU 专用设置")
785
- with gr.Row():
786
- export_quality_metrics = gr.Dropdown(
787
- label="质量评估维度",
788
- choices=["duration", "duration+rms", "duration+f0", "all"],
789
- value="duration+rms",
790
- info="duration=仅时长, +rms=音量稳定性, +f0=音高稳定性"
791
- )
792
- export_alias_style = gr.Dropdown(
793
- label="别名风格(日语)",
794
- choices=["romaji", "hiragana"],
795
- value="hiragana",
796
- info="日语音源的别名格式"
797
- )
798
-
799
- with gr.Row():
800
- export_overlap_ratio = gr.Number(
801
- label="Overlap 比例",
802
- value=0.3,
803
- minimum=0.1,
804
- maximum=0.5,
805
- info="Overlap = Preutterance × 此比例"
806
- )
807
- export_encoding = gr.Dropdown(
808
- label="文件编码",
809
- choices=["shift_jis", "utf-8", "gbk"],
810
- value="shift_jis",
811
- info="oto.ini 编码(UTAU 标准为 Shift_JIS)"
812
- )
813
-
814
- export_sanitize_filename = gr.Checkbox(
815
- label="文件名转拼音(防止 UTAU 识别故障)",
816
- value=False
817
- )
818
 
819
- # 插件切换时更新选项显示
820
- def on_plugin_change(plugin_name):
821
- """切换插件时更新选项区域"""
822
- is_utau = "UTAU" in plugin_name
823
-
824
- # 获取插件描述
825
- desc = ""
826
- if plugin_name in plugins:
827
- desc = f"> {plugins[plugin_name].description}"
828
 
829
- # 获取默认值
830
- default_max = 5 if is_utau else 10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
831
 
832
- return (
833
- gr.update(visible=is_utau), # utau_options_group
834
- desc, # plugin_desc
835
- default_max, # export_max_samples
836
- )
 
 
 
 
 
837
 
 
838
  export_plugin.change(
839
  fn=on_plugin_change,
840
  inputs=[export_plugin],
841
- outputs=[utau_options_group, plugin_desc, export_max_samples]
842
  )
843
 
844
  # 收集选项并导出
845
- def collect_and_export(
846
- zip_file, plugin_name,
847
- max_samples, naming_rule, first_naming_rule,
848
- quality_metrics, alias_style, overlap_ratio, encoding, sanitize_filename,
849
- progress=gr.Progress()
850
- ):
851
- """收集所有选项并执行导出"""
852
- # 构建选项字典
853
- options = {
854
- "max_samples": int(max_samples),
855
- "naming_rule": naming_rule,
856
- "first_naming_rule": first_naming_rule,
857
- }
858
 
859
- # UTAU 专用选项
860
- if "UTAU" in plugin_name:
861
- options.update({
862
- "quality_metrics": quality_metrics,
863
- "alias_style": alias_style,
864
- "overlap_ratio": float(overlap_ratio),
865
- "encoding": encoding,
866
- "sanitize_filename": sanitize_filename,
867
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
868
 
869
  options_json = json.dumps(options, ensure_ascii=False)
870
  return process_export_voicebank(zip_file, plugin_name, options_json, progress)
@@ -884,14 +1003,17 @@ def create_cloud_ui():
884
  > - 导出为适配其他软件的音源格式
885
  """)
886
 
 
 
 
 
 
 
 
 
887
  export_btn.click(
888
  fn=collect_and_export,
889
- inputs=[
890
- export_upload, export_plugin,
891
- export_max_samples, export_naming, export_first_naming,
892
- export_quality_metrics, export_alias_style,
893
- export_overlap_ratio, export_encoding, export_sanitize_filename
894
- ],
895
  outputs=[export_status, export_log, export_download]
896
  )
897
 
 
534
  "choices": opt.choices,
535
  "min_value": opt.min_value,
536
  "max_value": opt.max_value,
537
+ "step": opt.step,
538
  }
539
  options.append(opt_config)
540
  config[name] = options
541
  return config
542
 
543
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  def get_default_options_json(plugin_name: str, plugins_config: Dict) -> str:
545
  """获取插件的默认选项 JSON"""
546
  if plugin_name not in plugins_config:
 
555
  return json.dumps(defaults, ensure_ascii=False)
556
 
557
 
558
+ def create_dynamic_plugin_options(plugins: Dict[str, Any], plugins_config: Dict) -> Tuple[Dict[str, Any], callable]:
559
+ """
560
+ 创建动态插件选项组件
561
+
562
+ 返回:
563
+ (组件字典, 收集选项函数)
564
+
565
+ 组件字典结构: {
566
+ "container": gr.Column, # 主容器
567
+ "groups": {插件名: gr.Group}, # 每个插件的选项组
568
+ "components": {插件名: {选项key: 组件}}, # 所有组件
569
+ }
570
+ """
571
+ from src.export_plugins.base import OptionType
572
+
573
+ all_groups = {}
574
+ all_components = {}
575
+
576
+ # 为每个插件创建选项组
577
+ for plugin_name, options in plugins_config.items():
578
+ plugin_components = {}
579
+
580
+ # 创建该插件的选项组(初始隐藏,第一个插件除外)
581
+ is_first = (plugin_name == list(plugins_config.keys())[0])
582
+
583
+ with gr.Group(visible=is_first) as plugin_group:
584
+ # 显示插件描述
585
+ if plugin_name in plugins:
586
+ gr.Markdown(f"> {plugins[plugin_name].description}")
587
+
588
+ for opt in options:
589
+ opt_type = opt["type"]
590
+ key = opt["key"]
591
+ label = opt["label"]
592
+ default = opt["default"]
593
+ description = opt.get("description", "")
594
+ choices = opt.get("choices", [])
595
+ min_val = opt.get("min_value")
596
+ max_val = opt.get("max_value")
597
+ step = opt.get("step")
598
+
599
+ # 根据类型创建对应的 Gradio 组件
600
+ if opt_type == "label":
601
+ # 纯文本标签
602
+ gr.Markdown(f"*{label}*")
603
+ continue
604
+
605
+ elif opt_type == "text":
606
+ component = gr.Textbox(
607
+ label=label,
608
+ value=default or "",
609
+ info=description
610
+ )
611
+
612
+ elif opt_type == "number":
613
+ component = gr.Number(
614
+ label=label,
615
+ value=default if default is not None else 0,
616
+ minimum=min_val,
617
+ maximum=max_val,
618
+ step=step or 1,
619
+ info=description
620
+ )
621
+
622
+ elif opt_type == "switch":
623
+ component = gr.Checkbox(
624
+ label=label,
625
+ value=bool(default),
626
+ info=description
627
+ )
628
+
629
+ elif opt_type == "combo":
630
+ component = gr.Dropdown(
631
+ label=label,
632
+ choices=choices,
633
+ value=default if default in choices else (choices[0] if choices else None),
634
+ info=description
635
+ )
636
+
637
+ elif opt_type == "multi_select":
638
+ component = gr.CheckboxGroup(
639
+ label=label,
640
+ choices=choices,
641
+ value=default if isinstance(default, list) else [],
642
+ info=description
643
+ )
644
+
645
+ else:
646
+ # 未知类型,使用文本框
647
+ component = gr.Textbox(
648
+ label=label,
649
+ value=str(default) if default else "",
650
+ info=description
651
+ )
652
+
653
+ plugin_components[key] = component
654
+
655
+ all_groups[plugin_name] = plugin_group
656
+ all_components[plugin_name] = plugin_components
657
+
658
+ return all_groups, all_components
659
+
660
+
661
+ def build_options_collector(plugins_config: Dict, all_components: Dict):
662
+ """
663
+ 构建选项收集函数
664
+
665
+ 返回一个函数,该函数接收插件名和所有组件值,返回选项字典
666
+ """
667
+ # 构建组件到选项的映射
668
+ component_keys = {}
669
+ for plugin_name, components in all_components.items():
670
+ component_keys[plugin_name] = list(components.keys())
671
+
672
+ def collect_options(plugin_name: str, *values) -> Dict[str, Any]:
673
+ """收集当前插件的选项值"""
674
+ if plugin_name not in component_keys:
675
+ return {}
676
+
677
+ keys = component_keys[plugin_name]
678
+ options = {}
679
+
680
+ # 计算当前插件的值在 values 中的起始位置
681
+ start_idx = 0
682
+ for pname in component_keys:
683
+ if pname == plugin_name:
684
+ break
685
+ start_idx += len(component_keys[pname])
686
+
687
+ # 提取当前插件的值
688
+ for i, key in enumerate(keys):
689
+ if start_idx + i < len(values):
690
+ options[key] = values[start_idx + i]
691
+
692
+ return options
693
+
694
+ return collect_options
695
+
696
+
697
  # ==================== 构建界面 ====================
698
 
699
  def create_cloud_ui():
 
864
  label="导出插件"
865
  )
866
 
 
 
 
 
 
 
 
 
 
 
867
  # ===== 动态选项区域 =====
868
+ # 为每个插件动态创建���组件
869
+ all_plugin_groups = {}
870
+ all_plugin_components = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
871
 
872
+ for idx, (pname, poptions) in enumerate(plugins_config.items()):
873
+ is_first = (idx == 0)
874
+ plugin_components = {}
 
 
 
 
 
 
875
 
876
+ with gr.Group(visible=is_first) as plugin_group:
877
+ # 插件描述
878
+ if pname in plugins:
879
+ gr.Markdown(f"> {plugins[pname].description}")
880
+
881
+ # 动态创建选项组件
882
+ for opt in poptions:
883
+ opt_type = opt["type"]
884
+ key = opt["key"]
885
+ label = opt["label"]
886
+ default = opt["default"]
887
+ description = opt.get("description", "")
888
+ choices = opt.get("choices", [])
889
+ min_val = opt.get("min_value")
890
+ max_val = opt.get("max_value")
891
+ step = opt.get("step")
892
+
893
+ if opt_type == "label":
894
+ gr.Markdown(f"*{label}*")
895
+ continue
896
+ elif opt_type == "text":
897
+ component = gr.Textbox(
898
+ label=label,
899
+ value=default or "",
900
+ info=description
901
+ )
902
+ elif opt_type == "number":
903
+ component = gr.Number(
904
+ label=label,
905
+ value=default if default is not None else 0,
906
+ minimum=min_val,
907
+ maximum=max_val,
908
+ step=step or 1,
909
+ info=description
910
+ )
911
+ elif opt_type == "switch":
912
+ component = gr.Checkbox(
913
+ label=label,
914
+ value=bool(default),
915
+ info=description
916
+ )
917
+ elif opt_type == "combo":
918
+ component = gr.Dropdown(
919
+ label=label,
920
+ choices=choices,
921
+ value=default if default in choices else (choices[0] if choices else None),
922
+ info=description
923
+ )
924
+ elif opt_type == "multi_select":
925
+ component = gr.CheckboxGroup(
926
+ label=label,
927
+ choices=choices,
928
+ value=default if isinstance(default, list) else [],
929
+ info=description
930
+ )
931
+ else:
932
+ component = gr.Textbox(
933
+ label=label,
934
+ value=str(default) if default else "",
935
+ info=description
936
+ )
937
+
938
+ plugin_components[key] = component
939
 
940
+ all_plugin_groups[pname] = plugin_group
941
+ all_plugin_components[pname] = plugin_components
942
+
943
+ # 插件切换时更新选项组可见性
944
+ def on_plugin_change(selected_plugin):
945
+ """切换插件时更新选项区域可见性"""
946
+ updates = []
947
+ for pname in plugins_config.keys():
948
+ updates.append(gr.update(visible=(pname == selected_plugin)))
949
+ return updates
950
 
951
+ # 绑定插件切换事件
952
  export_plugin.change(
953
  fn=on_plugin_change,
954
  inputs=[export_plugin],
955
+ outputs=list(all_plugin_groups.values())
956
  )
957
 
958
  # 收集选项并导出
959
+ def collect_and_export(zip_file, plugin_name, *all_values, progress=gr.Progress()):
960
+ """收集当前插件的选项并执行导出"""
961
+ # 根据插件名找到对应的选项配置
962
+ if plugin_name not in plugins_config:
963
+ return "❌ 未找到插件配置", "", None
 
 
 
 
 
 
 
 
964
 
965
+ # 计算当前插件的值在 all_values 中的位置
966
+ start_idx = 0
967
+ for pname in plugins_config.keys():
968
+ if pname == plugin_name:
969
+ break
970
+ # 统计该插件的非 label 选项数量
971
+ start_idx += sum(1 for opt in plugins_config[pname] if opt["type"] != "label")
972
+
973
+ # 提取当前插件的选项值
974
+ options = {}
975
+ current_idx = start_idx
976
+ for opt in plugins_config[plugin_name]:
977
+ if opt["type"] == "label":
978
+ continue
979
+ key = opt["key"]
980
+ if current_idx < len(all_values):
981
+ value = all_values[current_idx]
982
+ # 类型转换
983
+ if opt["type"] == "number":
984
+ value = float(value) if value is not None else opt["default"]
985
+ options[key] = value
986
+ current_idx += 1
987
 
988
  options_json = json.dumps(options, ensure_ascii=False)
989
  return process_export_voicebank(zip_file, plugin_name, options_json, progress)
 
1003
  > - 导出为适配其他软件的音源格式
1004
  """)
1005
 
1006
+ # 收集所有插件的所有组件作为输入
1007
+ all_option_components = []
1008
+ for pname in plugins_config.keys():
1009
+ if pname in all_plugin_components:
1010
+ for opt in plugins_config[pname]:
1011
+ if opt["type"] != "label" and opt["key"] in all_plugin_components[pname]:
1012
+ all_option_components.append(all_plugin_components[pname][opt["key"]])
1013
+
1014
  export_btn.click(
1015
  fn=collect_and_export,
1016
+ inputs=[export_upload, export_plugin] + all_option_components,
 
 
 
 
 
1017
  outputs=[export_status, export_log, export_download]
1018
  )
1019
 
tools/README.md CHANGED
@@ -30,5 +30,5 @@ tools/
30
  ## 注意事项
31
 
32
  - MFA 引擎仅支持 Windows 64位系统
33
- - 解压后约占用 500MB 磁盘空间
34
  - 首次运行可能需要较长时间初始化
 
30
  ## 注意事项
31
 
32
  - MFA 引擎仅支持 Windows 64位系统
33
+ - 解压后约占用 2GB 磁盘空间
34
  - 首次运行可能需要较长时间初始化
tools/mfa_engine.7z ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5ccbaac4541b3ae414ddecb8d6e092e622887b6a30155f01c3ecc84cd6aba86b
3
+ size 395100074