cockolo terada commited on
Commit
0ffa97e
·
verified ·
1 Parent(s): 22ba782

Update gradio_tabs/single.py

Browse files
Files changed (1) hide show
  1. gradio_tabs/single.py +115 -172
gradio_tabs/single.py CHANGED
@@ -27,6 +27,9 @@ from typing import Dict, Any, List, Tuple, Optional, Set
27
  # --- タイムゾーン定義 ---
28
  # グローバルな定数としてJSTを定義
29
  JST = timezone(timedelta(hours=9), 'JST')
 
 
 
30
 
31
 
32
  # --- モック(本来はライブラリからインポート) ---
@@ -72,14 +75,6 @@ class TTSModelHolder:
72
  }
73
  with open(model2_path / "style_settings.json", "w", encoding="utf-8") as f:
74
  json.dump(style_settings_data, f, indent=2, ensure_ascii=False)
75
-
76
- # Sample Merged Model
77
- merged_model_path = p / "miku_90p_rinu_10p"
78
- merged_model_path.mkdir(parents=True, exist_ok=True)
79
- (merged_model_path / "G_merged.safetensors").touch()
80
- with open(merged_model_path / "config.json", "w", encoding="utf-8") as f:
81
- json.dump(config1, f, indent=2)
82
-
83
 
84
  def refresh(self) -> List[str]:
85
  """
@@ -87,7 +82,6 @@ class TTSModelHolder:
87
  更新後のモデルリストを返す。
88
  """
89
  if self.root_dir.is_dir():
90
- # is_dir()はシンボリックリンクされたディレクトリもTrueを返す
91
  self.model_names = sorted([d.name for d in self.root_dir.iterdir() if d.is_dir()])
92
  print(f"TTSModelHolder model list refreshed. Known models: {self.model_names}")
93
  else:
@@ -101,7 +95,7 @@ class TTSModelHolder:
101
  error_msg = (
102
  f"Model '{model_name}' is not in the known list of TTSModelHolder. "
103
  f"Current list: {self.model_names}. "
104
- "Please refresh the model list by clicking the refresh button."
105
  )
106
  print(f"[ERROR] {error_msg}")
107
  raise ValueError(error_msg)
@@ -141,10 +135,8 @@ DEFAULT_STYLE_WEIGHT=1.0
141
  DEFAULT_WORKBENCH_PAUSE = 250
142
  # ------------------------------------------------
143
 
144
- # ▼▼▼ 変更・追加点 1: 容量制限の定数を定義 ▼▼▼
145
- OUTPUT_SIZE_LIMIT_GB = 5 # outputフォルダの容量上限をGB単位で指定
146
  OUTPUT_SIZE_LIMIT_BYTES = OUTPUT_SIZE_LIMIT_GB * 1024**3
147
- # ▲▲▲ 変更・追加点 1 ▲▲▲
148
 
149
 
150
  # --- ヘルパー関数 ---
@@ -190,6 +182,16 @@ def format_and_sort_model_names(dir_list: List[str]) -> List[Tuple[str, str]]:
190
  result_list.extend(sorted(unparsed_models))
191
  return result_list
192
 
 
 
 
 
 
 
 
 
 
 
193
  def set_random_seed(seed: int):
194
  if seed >= 0:
195
  print(f"Setting random seed to: {seed}")
@@ -200,7 +202,6 @@ def set_random_seed(seed: int):
200
  np.random.seed(seed)
201
  random.seed(seed)
202
 
203
- # ▼▼▼ 変更・追加点 2: 容量計算用のヘルパー関数を追加 ▼▼▼
204
  def get_directory_size(directory_path: Path) -> int:
205
  """指定されたディレクトリの合計サイズをバイト単位で返す。"""
206
  total_size = 0
@@ -228,7 +229,6 @@ def format_bytes(size_bytes: int) -> str:
228
  p = math.pow(1024, i)
229
  s = round(size_bytes / p, 2)
230
  return f"{s} {size_name[i]}"
231
- # ▲▲▲ 変更・追加点 2 ▲▲▲
232
 
233
 
234
  # --- pyopenjtalk関連ヘルパー関数 ---
@@ -422,15 +422,7 @@ def process_single_synthesis_webui(
422
  return True, log_messages, (sr, audio_data)
423
 
424
 
425
- # ▼▼▼ 変更点: action_save_audio 関数を削除 ▼▼▼
426
- # この関数は不要になったため削除されました。
427
- # ▲▲▲ 変更点 ▲▲▲
428
-
429
  def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
430
- # ▼▼▼ 変更点: is_shm_available 変数を削除 ▼▼▼
431
- # is_shm_available = sys.platform != "win32" and Path("/dev/shm").exists() and Path("/dev/shm").is_dir()
432
- # ▲▲▲ 変更点 ▲▲▲
433
-
434
  custom_css = """
435
  .audio-output-row { display: flex !important; flex-wrap: wrap !important; gap: 10px !important; }
436
  .audio-item-column { flex-grow: 0 !important; flex-shrink: 0 !important; width: var(--audio-width, 250px) !important; background-color: #f8f9fa; padding: 8px; border-radius: 8px; border: 1px solid #dee2e6; }
@@ -445,18 +437,13 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
445
  with gr.Blocks(css=custom_css) as app:
446
  MAX_AUDIO_OUTPUTS = 4
447
  ITEMS_PER_ROW = 4
448
- # ▼▼▼ 変更点: キープの最大アイテム数を8に変更 ▼▼▼
449
  MAX_WORKBENCH_ITEMS = 8
450
- # ▲▲▲ 変更点 ▲▲▲
451
 
452
  all_styles_data_state = gr.State({})
453
  workbench_state = gr.State([])
454
  merged_preview_state = gr.State({})
455
- # ▼▼▼ 変更点: saved_audio_hashes_stateを削除 ▼▼▼
456
- # saved_audio_hashes_state = gr.State(set())
457
- # ▲▲▲ 変更点 ▲▲▲
458
 
459
- # --- キープUI更新ヘルパー ---
460
  def update_workbench_ui(workbench_list: List[Dict]) -> Tuple:
461
  updates = []
462
  for i in range(MAX_WORKBENCH_ITEMS):
@@ -506,9 +493,6 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
506
  with gr.Row(elem_classes="audio-output-row"):
507
  audio_item_columns = []
508
  audio_outputs = []
509
- # ▼▼▼ 変更点: save_buttons を削除 ▼▼▼
510
- # save_buttons = []
511
- # ▲▲▲ 変更点 ▲▲▲
512
  to_workbench_buttons = []
513
  synthesized_text_states = []
514
  dummy_audio_item_columns = []
@@ -521,10 +505,7 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
521
  type="filepath", interactive=False
522
  ))
523
  with gr.Row():
524
- # ▼▼▼ 変更点: 保存ボタンを削除し、キープボタンの幅を広げる ▼▼▼
525
- # save_buttons.append(gr.Button("💾 保存", scale=1))
526
- to_workbench_buttons.append(gr.Button("🛠️ キープに追加", scale=2))
527
- # ▲▲▲ 変更点 ▲▲▲
528
  audio_item_columns.append(audio_col)
529
 
530
  for i in range(ITEMS_PER_ROW - 1):
@@ -532,27 +513,16 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
532
  pass
533
  dummy_audio_item_columns.append(dummy_col)
534
 
535
- # ▼▼▼ 変更点: 保存設定アコーディオン(ID入力欄)を削除 ▼▼▼
536
- # with gr.Accordion("保存設定", open=True):
537
- # metadata_textbox = gr.Textbox(
538
- # label="ID",
539
- # lines=1, placeholder="音声ファイルに埋め込むメモを入力してください(必須)",
540
- # interactive=True
541
- # )
542
- # ▲▲▲ 変更点 ▲▲▲
543
  with gr.Accordion("ステータス", open=True):
544
  status_textbox = gr.Textbox(interactive=False, lines=5, max_lines=5, autoscroll=True, show_label=False, placeholder="ここにログが表示されます...")
545
 
546
  with gr.Column(scale=1):
547
- # ▼▼▼ 変更点: 「融☆合モデルを使う」チェックボックスを削除し、レイアウトを調整 ▼▼▼
548
  with gr.Row():
549
- selected_model_dropdown = gr.Dropdown(label="話者", choices=[], value=None, interactive=True, scale=4)
 
 
550
  refresh_model_list_button = gr.Button("再読込", scale=1)
551
- # ▲▲▲ 変更点 ▲▲▲
552
-
553
- # ▼▼▼ 変更点: モデルファイル選択ドロップダウンを削除 ▼▼▼
554
- # selected_model_file_dropdown = gr.Dropdown(label="モデルファイル (.safetensors)", choices=[], value=None, interactive=True)
555
- # ▲▲▲ 変更点 ▲▲▲
556
  current_styles_dropdown = gr.Dropdown(label="スタイル", choices=[], type="value", interactive=True)
557
  style_weight_for_synth_slider = gr.Slider(label="スタイル強度", minimum=0.0, maximum=20.0, value=DEFAULT_STYLE_WEIGHT, step=0.1, info="初期値は推奨強度", interactive=True)
558
  batch_count_slider = gr.Slider(label="生成数", value=1, minimum=1, maximum=MAX_AUDIO_OUTPUTS, step=1, interactive=True)
@@ -579,23 +549,17 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
579
  random_text_mode_slider = gr.Slider(label="分割の単位", minimum=1, maximum=4, value=1, step=1, info="1:形態素, 2:チャンク, 3:文節, 4:節", interactive=True)
580
  random_text_ratio_textbox = gr.Textbox(label="カタカナ化の割合", value="0.5, 1", info="カンマ区切りで複数指定可。指定値からランダムに1つ使用。", interactive=True)
581
 
582
- # ▼▼▼ 変更点 2: キープのレイアウトを2列に変更 ▼▼▼
583
- with gr.Tab("キープ"):
584
- gr.Markdown("## キープ\n読み上げタブで生成した音声をここにストックし、結合や保存ができます。最大8個まで保持できます。")
585
 
586
  workbench_items = []
587
  all_workbench_ui_components = []
588
 
589
  with gr.Row(variant="panel"):
590
- # 左側の音声リストエリア(2列)
591
  with gr.Column(scale=3):
592
  with gr.Row():
593
- # 左列 (アイテム 1-4)
594
  left_workbench_col = gr.Column(scale=1)
595
- # 右列 (アイテム 5-8)
596
  right_workbench_col = gr.Column(scale=1)
597
-
598
- # 右側の操作パネル
599
  with gr.Column(scale=1):
600
  with gr.Blocks():
601
  with gr.Row():
@@ -612,38 +576,13 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
612
 
613
  with gr.Row():
614
  merge_preview_button = gr.Button("1.結合&プレビュー", variant="primary")
615
- add_merged_to_workbench_button = gr.Button("2.キープに追加", variant="primary")
616
  delete_originals_checkbox = gr.Checkbox(label="結合時に自動で元ファイルを削除", value=False, interactive=True)
617
 
618
  preview_audio_player = gr.Audio(label="結合結果プレビュー", interactive=False, type="filepath")
619
 
620
- # ▼▼▼ 変更点: キープの保存機能を削除 ▼▼▼
621
- # gr.Markdown("---")
622
- #
623
- # with gr.Blocks():
624
- # gr.Markdown("#### キープの音声を保存")
625
- # with gr.Row():
626
- # with gr.Column(scale=1):
627
- # audio_to_save_num_input = gr.Number(label="保存する音声の番号", value=1, minimum=1, step=1, precision=0, interactive=True)
628
- # with gr.Column(scale=3):
629
- # creative_filename_input = gr.Textbox(
630
- # label="保存する時のファイル名",
631
- # placeholder=f"例: れいさな",
632
- # interactive=True
633
- # )
634
- # workbench_metadata_textbox = gr.Textbox(
635
- # label="ID (必須)",
636
- # lines=1, placeholder="この音声に関するメモを入力してください",
637
- # interactive=True
638
- # )
639
- # save_creative_button = gr.Button("💾 指定番号の音声を保存", variant="primary")
640
- # ▲▲▲ 変更点 ▲▲▲
641
-
642
-
643
- # 2列にアイテムを配置するロジック
644
  ITEMS_PER_COLUMN = 4
645
  for i in range(MAX_WORKBENCH_ITEMS):
646
- # iの値に応じて、アイテムを追加する親のColumnを決定
647
  parent_column = left_workbench_col if i < ITEMS_PER_COLUMN else right_workbench_col
648
 
649
  with parent_column:
@@ -658,20 +597,13 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
658
  "audio": audio, "info": info, "delete_btn": delete_btn
659
  })
660
 
661
- # all_workbench_ui_componentsの生成ロジックは変更なし
662
  for item in workbench_items:
663
  all_workbench_ui_components.extend([
664
  item["row"], item["item_num_display"], item["audio"], item["info"]
665
  ])
666
- # ▲▲▲ 変更点 2 ▲▲▲
667
 
668
 
669
  # --- UIイベントハンドラ関数 ---
670
-
671
- # ▼▼▼ 変更点: update_model_files_dropdown 関数を削除 ▼▼▼
672
- # この関数はモデルファイル選択UIの削除に伴い不要になりました。
673
- # ▲▲▲ 変更点 ▲▲▲
674
-
675
  def load_styles_for_ui(selected_model_name: Optional[str]):
676
  if not selected_model_name: return gr.update(choices=[], value=None), gr.update(value=DEFAULT_STYLE_WEIGHT), {}
677
  model_path = assets_root_path / selected_model_name
@@ -687,29 +619,60 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
687
  default_weight = styles_map[first_key].get("weight", DEFAULT_STYLE_WEIGHT)
688
  return gr.update(choices=display_names, value=default_display_name), gr.update(value=default_weight), styles_map
689
 
690
- # ▼▼▼ 変更点: action_refresh_model_list関数からシンボリックリンク関連のロジックを削除 ▼▼▼
691
- def action_refresh_model_list():
692
  """モデルリストを再読み込みし、UIとバックエンドの状態を同期させる。"""
693
- # バックエンドのリストを更新
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  model_holder.refresh()
695
 
696
- # UIのドロップダウンを更新
697
- # マージモデルのパースとソートは維持する
698
- ui_model_list = model_holder.model_names
699
- formatted_choices = format_and_sort_model_names(ui_model_list)
700
- value = formatted_choices[0][1] if formatted_choices else None
701
- model_dropdown_update = gr.update(choices=formatted_choices, value=value)
702
-
703
- style_dropdown_update, style_weight_update, styles_data_state_update = load_styles_for_ui(value)
 
 
 
 
 
 
 
 
704
 
705
- return model_dropdown_update, style_dropdown_update, style_weight_update, styles_data_state_update
706
  # ▲▲▲ 変更点 ▲▲▲
707
 
 
708
  def on_model_select_change(selected_model_name: Optional[str]):
709
- # ▼▼▼ 変更点: モデルファイル選択UIがなくなったため、関連処理を削除 ▼▼▼
710
  style_dropdown_update, style_weight_update, styles_data_state_update = load_styles_for_ui(selected_model_name)
711
  return style_dropdown_update, style_weight_update, styles_data_state_update
712
- # ▲▲▲ 変更点 ▲▲▲
713
 
714
  def on_style_dropdown_select(selected_display_name: Optional[str], styles_data: Dict[str, Any]):
715
  if not selected_display_name or not styles_data: return gr.update(value=DEFAULT_STYLE_WEIGHT)
@@ -720,9 +683,6 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
720
 
721
  def action_run_synthesis(
722
  model_name: Optional[str],
723
- # ▼▼▼ 変更点: selected_model_file 引数を削除 ▼▼▼
724
- # selected_model_file: Optional[str],
725
- # ▲▲▲ 変更点 ▲▲▲
726
  style_display_name: Optional[str], style_weight_for_synth: float,
727
  text: str, generation_mode: str, batch_count: int,
728
  lang: str, seed: int, speaker: str, ref_audio: Optional[str],
@@ -769,17 +729,14 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
769
 
770
  all_logs = []
771
 
772
- # ▼▼▼ 変更点: モデルファイルを自動で選択するロジック ▼▼▼
773
  model_path = assets_root_path / model_name
774
  files = find_safetensors_files_webui(str(model_path))
775
  if not files:
776
  error_outputs[0] = f"❌ [エラー] モデルフォルダ '{model_name}' に .safetensors ファイルが見つかりません。"
777
  return tuple(error_outputs)
778
 
779
- # ソートされたリストの最初のファイルを使用する
780
  actual_model_file_to_load = str(model_path / files[0])
781
  all_logs.append(f"[自動選択] 使用モデルファイル: {files[0]}")
782
- # ▲▲▲ 変更点 ▲▲▲
783
 
784
  batch_count = int(batch_count)
785
  if batch_count <= 0: batch_count = 1
@@ -897,7 +854,7 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
897
 
898
  return tuple(final_outputs)
899
 
900
- # --- キープイベントハンドラ ---
901
  def add_to_workbench(
902
  current_status: str,
903
  current_workbench_list: List[Dict],
@@ -907,12 +864,12 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
907
  safe_workbench_list = current_workbench_list or []
908
 
909
  if not audio_path or not Path(audio_path).exists():
910
- log_messages.append("⚠️ [キープ追加エラー] 追加する音声ファイルが見つかりません。")
911
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
912
  return (final_status, safe_workbench_list) + update_workbench_ui(safe_workbench_list)
913
 
914
  if any(item['audio_path'] == audio_path for item in safe_workbench_list):
915
- log_messages.append("ℹ️ この音声はすでにキープに存在します。")
916
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
917
  return (final_status, safe_workbench_list) + update_workbench_ui(safe_workbench_list)
918
 
@@ -941,10 +898,10 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
941
  path_to_delete.unlink()
942
  except Exception as e:
943
  print(f"Warning: Failed to delete old workbench audio file: {e}")
944
- log_messages.append(f"ℹ️ キープのアイテムが最大数({MAX_WORKBENCH_ITEMS})に達したため、一番古いアイテムを削除しました。")
945
 
946
  ui_updates = update_workbench_ui(updated_list)
947
- log_messages.append("✅ キープに音声を追加しました。")
948
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
949
  return (final_status, updated_list) + ui_updates
950
 
@@ -959,13 +916,13 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
959
  path_to_delete = Path(item_to_remove['audio_path'])
960
  if path_to_delete.exists() and str(path_to_delete.parent) == tempfile.gettempdir():
961
  path_to_delete.unlink()
962
- log_messages.append(f"✅ キープからアイテム #{index_to_remove + 1} を削除し、一時ファイルをクリーンアップしました。")
963
  elif path_to_delete.exists():
964
- log_messages.append(f"✅ キープからアイテム #{index_to_remove + 1} を削除しました。(ファイルは保持: {path_to_delete.name})")
965
  else:
966
- log_messages.append(f"✅ キープからアイテム #{index_to_remove + 1} を削除しました。(関連ファイルなし)")
967
  except Exception as e:
968
- log_messages.append(f"⚠️ キープのアイテム #{index_to_remove + 1} のファイル削除中にエラー: {e}")
969
 
970
  updated_list = [item for i, item in enumerate(safe_workbench_list) if i != index_to_remove]
971
  ui_updates = update_workbench_ui(updated_list)
@@ -980,7 +937,7 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
980
  ):
981
  log_messages = []
982
  if not workbench_list:
983
- log_messages.append("⚠️ [結合プレビュー警告] キープに音声がありません。")
984
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
985
  return final_status, None, {}
986
 
@@ -1008,33 +965,21 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1008
  pause_duration = int(pause_ms)
1009
 
1010
  if pause_duration >= 0:
1011
- # 正の値: 無音を挿入
1012
  pause_segment = AudioSegment.silent(duration=pause_duration)
1013
  combined_audio = segment1 + pause_segment + segment2
1014
  log_messages.append(f"✅ 音声 #{first_audio_num} と #{second_audio_num} を {pause_duration}ms のポーズを挟んで結合しました。")
1015
  else:
1016
- # 負の値: そのまま重ねる(オーバーレイ)
1017
  overlap_duration = abs(pause_duration)
1018
-
1019
- # オーバーラップ長がどちらかの音声の長さを超えないように制限
1020
  max_possible_overlap = min(len(segment1), len(segment2))
1021
  if overlap_duration > max_possible_overlap:
1022
  log_messages.append(f"ℹ️ オーバーラップ長({overlap_duration}ms)が可能な最大値({max_possible_overlap}ms)を超えるため、自動的に調整されました。")
1023
  overlap_duration = max_possible_overlap
1024
 
1025
- # 結合後の最終的な音声の長さを計算
1026
  final_duration = len(segment1) + len(segment2) - overlap_duration
1027
-
1028
- # 最終的な長さの無音キャンバスを作成
1029
  combined_audio = AudioSegment.silent(duration=final_duration)
1030
-
1031
- # 1番目の音声をキャンバスの先頭から重ねる
1032
  combined_audio = combined_audio.overlay(segment1, position=0)
1033
-
1034
- # 2番目の音声を、1番目の音声の末尾からオーバーラップする位置に重ねる
1035
  overlay_position = len(segment1) - overlap_duration
1036
  combined_audio = combined_audio.overlay(segment2, position=overlay_position)
1037
-
1038
  log_messages.append(f"✅ 音声 #{first_audio_num} と #{second_audio_num} を {overlap_duration}ms 重ねて(オーバーレイして)結合しました。")
1039
 
1040
  progress(1, desc="結合完了")
@@ -1065,10 +1010,6 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1065
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
1066
  return final_status, str(temp_path), metadata
1067
 
1068
- # ▼▼▼ 変更点: action_save_workbench_audio 関数を削除 ▼▼▼
1069
- # この関数は保存機能の削除に伴い不要になりました。
1070
- # ▲▲▲ 変更点 ▲▲▲
1071
-
1072
  def action_add_merged_to_workbench(
1073
  current_status: str,
1074
  preview_data: Dict,
@@ -1080,13 +1021,13 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1080
  log_messages = []
1081
  safe_workbench_list = current_workbench_list or []
1082
  if not preview_data or "audio_path" not in preview_data:
1083
- log_messages.append("⚠️ [キープ追加エラー] 追加する結合済み音声がありません。先にプレビューを生成してください。")
1084
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
1085
  return (final_status, safe_workbench_list) + update_workbench_ui(safe_workbench_list)
1086
 
1087
  src_path = Path(preview_data["audio_path"])
1088
  if not src_path.exists():
1089
- log_messages.append("⚠️ [キープ追加エラー] 結合済み音声ファイルが見つかりません。")
1090
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
1091
  return (final_status, safe_workbench_list) + update_workbench_ui(safe_workbench_list)
1092
 
@@ -1118,10 +1059,10 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1118
  log_messages.append(f"⚠️ 元の音声ファイル削除中にエラー: {e}")
1119
 
1120
  final_workbench_list = [new_merged_item] + remaining_list
1121
- log_messages.append(f"✅ 結合音声をキープに追加し、元の音声(#{idx1+1}, #{idx2+1})を削除しました。")
1122
  else:
1123
  final_workbench_list = [new_merged_item] + safe_workbench_list
1124
- log_messages.append("✅ 結合済みの音声をキープの一番上に追加しました。")
1125
 
1126
  if len(final_workbench_list) > MAX_WORKBENCH_ITEMS:
1127
  item_to_remove = final_workbench_list.pop(-1)
@@ -1131,7 +1072,7 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1131
  path_to_delete.unlink()
1132
  except Exception as e:
1133
  print(f"Warning: Failed to delete old workbench audio file: {e}")
1134
- log_messages.append(f"ℹ️ キープが最大数({MAX_WORKBENCH_ITEMS})に達したため一番古いアイテムを削除しました。")
1135
 
1136
  ui_updates = update_workbench_ui(final_workbench_list)
1137
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
@@ -1139,12 +1080,14 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1139
 
1140
 
1141
  # --- イベントリスナー接続 ---
1142
- # ▼▼▼ 変更点: イベントリスナーの inputs/outputs を修正 ▼▼▼
1143
- refresh_model_list_button.click(
1144
- action_refresh_model_list,
1145
- inputs=[],
1146
- outputs=[selected_model_dropdown, current_styles_dropdown, style_weight_for_synth_slider, all_styles_data_state]
1147
- )
 
 
1148
 
1149
  selected_model_dropdown.change(on_model_select_change,
1150
  inputs=[selected_model_dropdown],
@@ -1162,8 +1105,7 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1162
  generate_button.click(
1163
  fn=action_run_synthesis,
1164
  inputs=[
1165
- selected_model_dropdown, # selected_model_file_dropdown は削除
1166
- # use_symlink_mode_checkbox は削除
1167
  current_styles_dropdown, style_weight_for_synth_slider,
1168
  text_input, generation_mode_radio, batch_count_slider,
1169
  language_dropdown, seed_input, speaker_name_textbox,
@@ -1178,9 +1120,6 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1178
  )
1179
 
1180
  for i in range(MAX_AUDIO_OUTPUTS):
1181
- # ▼▼▼ 変更点: 保存ボタンのクリックイベントを削除 ▼▼▼
1182
- # save_buttons[i].click(...)
1183
- # ▲▲▲ 変更点 ▲▲▲
1184
  to_workbench_buttons[i].click(
1185
  fn=add_to_workbench,
1186
  inputs=[
@@ -1209,10 +1148,6 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1209
  outputs=[status_textbox, preview_audio_player, merged_preview_state]
1210
  )
1211
 
1212
- # ▼▼▼ 変更点: キープの保存ボタンのクリックイベントを削除 ▼▼▼
1213
- # save_creative_button.click(...)
1214
- # ▲▲▲ 変更点 ▲▲▲
1215
-
1216
  add_merged_to_workbench_button.click(
1217
  fn=action_add_merged_to_workbench,
1218
  inputs=[
@@ -1228,11 +1163,9 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1228
 
1229
  player_width_slider.release(lambda w: f"<script>document.documentElement.style.setProperty('--audio-width', '{w}px');</script>", inputs=[player_width_slider], outputs=[js_injector_html])
1230
 
1231
- app.load(
1232
- action_refresh_model_list,
1233
- inputs=[],
1234
- outputs=[selected_model_dropdown, current_styles_dropdown, style_weight_for_synth_slider, all_styles_data_state]
1235
- )
1236
  # ▲▲▲ 変更点 ▲▲▲
1237
  return app
1238
 
@@ -1240,10 +1173,13 @@ def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
1240
  if __name__ == "__main__":
1241
  if Path("model_assets").exists(): shutil.rmtree("model_assets")
1242
 
1243
- # ▼▼▼ 変更点: shm_path関連のコードを削除 ▼▼▼
1244
- # shm_path = Path("/dev/shm")
1245
- # ▲▲▲ 変更点 ▲▲▲
1246
-
 
 
 
1247
  mock_model_holder = TTSModelHolder()
1248
  print(f"Initial models loaded by TTSModelHolder: {mock_model_holder.model_names}")
1249
  app = create_synthesis_app(mock_model_holder)
@@ -1252,10 +1188,16 @@ if __name__ == "__main__":
1252
  assets_dir_path.mkdir(exist_ok=True)
1253
  allowed_paths = [str(assets_dir_path)]
1254
 
1255
- # ▼▼▼ 変更点: shm_pathをallowed_pathsに追加するロジックを削除 ▼▼▼
1256
- # if sys.platform != "win32" and shm_path.exists():
1257
- # allowed_paths.append(str(shm_path.resolve()))
1258
- # ▲▲▲ 変更点 ▲▲▲
 
 
 
 
 
 
1259
 
1260
  output_dir_path = Path("output").resolve()
1261
  (output_dir_path / "normal").mkdir(exist_ok=True, parents=True)
@@ -1267,4 +1209,5 @@ if __name__ == "__main__":
1267
 
1268
  print(f"Gradioに次のパスへのアクセスを許可します: {', '.join(allowed_paths)}")
1269
 
1270
- app.launch(allowed_paths=allowed_paths)
 
 
27
  # --- タイムゾーン定義 ---
28
  # グローバルな定数としてJSTを定義
29
  JST = timezone(timedelta(hours=9), 'JST')
30
+ # ▼▼▼ 変更点: FNモデル用のキャッシュパスを定義 ▼▼▼
31
+ FN_MODEL_CACHE_PATH = Path("/tmp/sbv2_merger_cache")
32
+ # ▲▲▲ 変更点 ▲▲▲
33
 
34
 
35
  # --- モック(本来はライブラリからインポート) ---
 
75
  }
76
  with open(model2_path / "style_settings.json", "w", encoding="utf-8") as f:
77
  json.dump(style_settings_data, f, indent=2, ensure_ascii=False)
 
 
 
 
 
 
 
 
78
 
79
  def refresh(self) -> List[str]:
80
  """
 
82
  更新後のモデルリストを返す。
83
  """
84
  if self.root_dir.is_dir():
 
85
  self.model_names = sorted([d.name for d in self.root_dir.iterdir() if d.is_dir()])
86
  print(f"TTSModelHolder model list refreshed. Known models: {self.model_names}")
87
  else:
 
95
  error_msg = (
96
  f"Model '{model_name}' is not in the known list of TTSModelHolder. "
97
  f"Current list: {self.model_names}. "
98
+ "Please refresh the model list by toggling the symlink checkbox or clicking the refresh button."
99
  )
100
  print(f"[ERROR] {error_msg}")
101
  raise ValueError(error_msg)
 
135
  DEFAULT_WORKBENCH_PAUSE = 250
136
  # ------------------------------------------------
137
 
138
+ OUTPUT_SIZE_LIMIT_GB = 5
 
139
  OUTPUT_SIZE_LIMIT_BYTES = OUTPUT_SIZE_LIMIT_GB * 1024**3
 
140
 
141
 
142
  # --- ヘルパー関数 ---
 
182
  result_list.extend(sorted(unparsed_models))
183
  return result_list
184
 
185
+ # ▼▼▼ 変更点: FNモデルソート用のヘルパー関数を追加 ▼▼▼
186
+ def get_fn_model_sort_key(name: str) -> int:
187
+ """FNモデル名からソート用の数値を抽出する。例: 'FN10' -> 10"""
188
+ match = re.search(r'FN(\d+)', name, re.IGNORECASE)
189
+ if match:
190
+ return int(match.group(1))
191
+ return float('inf') # マッチしないものは後ろに配置
192
+ # ▲▲▲ 変更点 ▲▲▲
193
+
194
+
195
  def set_random_seed(seed: int):
196
  if seed >= 0:
197
  print(f"Setting random seed to: {seed}")
 
202
  np.random.seed(seed)
203
  random.seed(seed)
204
 
 
205
  def get_directory_size(directory_path: Path) -> int:
206
  """指定されたディレクトリの合計サイズをバイト単位で返す。"""
207
  total_size = 0
 
229
  p = math.pow(1024, i)
230
  s = round(size_bytes / p, 2)
231
  return f"{s} {size_name[i]}"
 
232
 
233
 
234
  # --- pyopenjtalk関連ヘルパー関数 ---
 
422
  return True, log_messages, (sr, audio_data)
423
 
424
 
 
 
 
 
425
  def create_synthesis_app(model_holder: TTSModelHolder) -> gr.Blocks:
 
 
 
 
426
  custom_css = """
427
  .audio-output-row { display: flex !important; flex-wrap: wrap !important; gap: 10px !important; }
428
  .audio-item-column { flex-grow: 0 !important; flex-shrink: 0 !important; width: var(--audio-width, 250px) !important; background-color: #f8f9fa; padding: 8px; border-radius: 8px; border: 1px solid #dee2e6; }
 
437
  with gr.Blocks(css=custom_css) as app:
438
  MAX_AUDIO_OUTPUTS = 4
439
  ITEMS_PER_ROW = 4
 
440
  MAX_WORKBENCH_ITEMS = 8
 
441
 
442
  all_styles_data_state = gr.State({})
443
  workbench_state = gr.State([])
444
  merged_preview_state = gr.State({})
 
 
 
445
 
446
+ # --- 作業台UI更新ヘルパー ---
447
  def update_workbench_ui(workbench_list: List[Dict]) -> Tuple:
448
  updates = []
449
  for i in range(MAX_WORKBENCH_ITEMS):
 
493
  with gr.Row(elem_classes="audio-output-row"):
494
  audio_item_columns = []
495
  audio_outputs = []
 
 
 
496
  to_workbench_buttons = []
497
  synthesized_text_states = []
498
  dummy_audio_item_columns = []
 
505
  type="filepath", interactive=False
506
  ))
507
  with gr.Row():
508
+ to_workbench_buttons.append(gr.Button("🛠️ 作業台に追加", scale=2))
 
 
 
509
  audio_item_columns.append(audio_col)
510
 
511
  for i in range(ITEMS_PER_ROW - 1):
 
513
  pass
514
  dummy_audio_item_columns.append(dummy_col)
515
 
 
 
 
 
 
 
 
 
516
  with gr.Accordion("ステータス", open=True):
517
  status_textbox = gr.Textbox(interactive=False, lines=5, max_lines=5, autoscroll=True, show_label=False, placeholder="ここにログが表示されます...")
518
 
519
  with gr.Column(scale=1):
 
520
  with gr.Row():
521
+ # ▼▼▼ 変更点: 「融☆合モデルを使う」を「FNモデルモード」に変更し、最初は非表示に ▼▼▼
522
+ use_fn_model_mode_checkbox = gr.Checkbox(label="FNモデルモード", value=False, interactive=True, scale=3, visible=False)
523
+ # ▲▲▲ 変更点 ▲▲▲
524
  refresh_model_list_button = gr.Button("再読込", scale=1)
525
+ selected_model_dropdown = gr.Dropdown(label="話者", choices=[], value=None, interactive=True)
 
 
 
 
526
  current_styles_dropdown = gr.Dropdown(label="スタイル", choices=[], type="value", interactive=True)
527
  style_weight_for_synth_slider = gr.Slider(label="スタイル強度", minimum=0.0, maximum=20.0, value=DEFAULT_STYLE_WEIGHT, step=0.1, info="初期値は推奨強度", interactive=True)
528
  batch_count_slider = gr.Slider(label="生成数", value=1, minimum=1, maximum=MAX_AUDIO_OUTPUTS, step=1, interactive=True)
 
549
  random_text_mode_slider = gr.Slider(label="分割の単位", minimum=1, maximum=4, value=1, step=1, info="1:形態素, 2:チャンク, 3:文節, 4:節", interactive=True)
550
  random_text_ratio_textbox = gr.Textbox(label="カタカナ化の割合", value="0.5, 1", info="カンマ区切りで複数指定可。指定値からランダムに1つ使用。", interactive=True)
551
 
552
+ with gr.Tab("作業台"):
553
+ gr.Markdown("## 作業台\n読み上げタブで生成した音声をここにストックし、結合や保存ができます。最大8個まで保持できます。")
 
554
 
555
  workbench_items = []
556
  all_workbench_ui_components = []
557
 
558
  with gr.Row(variant="panel"):
 
559
  with gr.Column(scale=3):
560
  with gr.Row():
 
561
  left_workbench_col = gr.Column(scale=1)
 
562
  right_workbench_col = gr.Column(scale=1)
 
 
563
  with gr.Column(scale=1):
564
  with gr.Blocks():
565
  with gr.Row():
 
576
 
577
  with gr.Row():
578
  merge_preview_button = gr.Button("1.結合&プレビュー", variant="primary")
579
+ add_merged_to_workbench_button = gr.Button("2.作業台に追加", variant="primary")
580
  delete_originals_checkbox = gr.Checkbox(label="結合時に自動で元ファイルを削除", value=False, interactive=True)
581
 
582
  preview_audio_player = gr.Audio(label="結合結果プレビュー", interactive=False, type="filepath")
583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  ITEMS_PER_COLUMN = 4
585
  for i in range(MAX_WORKBENCH_ITEMS):
 
586
  parent_column = left_workbench_col if i < ITEMS_PER_COLUMN else right_workbench_col
587
 
588
  with parent_column:
 
597
  "audio": audio, "info": info, "delete_btn": delete_btn
598
  })
599
 
 
600
  for item in workbench_items:
601
  all_workbench_ui_components.extend([
602
  item["row"], item["item_num_display"], item["audio"], item["info"]
603
  ])
 
604
 
605
 
606
  # --- UIイベントハンドラ関数 ---
 
 
 
 
 
607
  def load_styles_for_ui(selected_model_name: Optional[str]):
608
  if not selected_model_name: return gr.update(choices=[], value=None), gr.update(value=DEFAULT_STYLE_WEIGHT), {}
609
  model_path = assets_root_path / selected_model_name
 
619
  default_weight = styles_map[first_key].get("weight", DEFAULT_STYLE_WEIGHT)
620
  return gr.update(choices=display_names, value=default_display_name), gr.update(value=default_weight), styles_map
621
 
622
+ # ▼▼▼ 変更点: FNモデルモードに対応したモデルリスト更新関数 ▼▼▼
623
+ def action_refresh_model_list(use_fn_model_mode: bool):
624
  """モデルリストを再読み込みし、UIとバックエンドの状態を同期させる。"""
625
+ # 既存のシンボリックリンクをクリア
626
+ if assets_root_path.exists():
627
+ for item in assets_root_path.iterdir():
628
+ if item.is_symlink():
629
+ try:
630
+ item.unlink()
631
+ except OSError as e:
632
+ print(f"Failed to remove symlink {item}: {e}")
633
+
634
+ # FNモデルキャッシュパスからFNモデルを探し、シンボリックリンクを作成
635
+ fn_models_found = []
636
+ if FN_MODEL_CACHE_PATH.exists() and FN_MODEL_CACHE_PATH.is_dir():
637
+ for item in FN_MODEL_CACHE_PATH.iterdir():
638
+ if item.is_dir() and re.fullmatch(r'FN\d+', item.name, re.IGNORECASE):
639
+ fn_models_found.append(item.name)
640
+ target_link = assets_root_path / item.name
641
+ if not target_link.exists():
642
+ try:
643
+ os.symlink(item, target_link)
644
+ except OSError as e:
645
+ print(f"Warning: Could not create symlink for {item.name}: {e}")
646
+
647
+ fn_mode_checkbox_update = gr.update(visible=bool(fn_models_found))
648
+
649
+ # バックエンドのモデルリストを更新
650
  model_holder.refresh()
651
 
652
+ model_dropdown_update = gr.update(choices=[], value=None)
653
+ if use_fn_model_mode and fn_models_found:
654
+ # FNモデルモードの場合: FNモデルを数値でソートして表示
655
+ sorted_fn_models = sorted(fn_models_found, key=get_fn_model_sort_key)
656
+ value = sorted_fn_models[0] if sorted_fn_models else None
657
+ model_dropdown_update = gr.update(choices=sorted_fn_models, value=value)
658
+ else:
659
+ # 通常モードの場合: 通常モデルとマージモデルを表示
660
+ ui_model_list = [p.name for p in assets_root_path.iterdir() if p.is_dir() and not p.is_symlink()]
661
+ formatted_choices = format_and_sort_model_names(ui_model_list)
662
+ value = formatted_choices[0][1] if formatted_choices else None
663
+ model_dropdown_update = gr.update(choices=formatted_choices, value=value)
664
+
665
+ # 選択されたモデルに基づいてスタイルUIを更新
666
+ selected_model_for_style = model_dropdown_update.value
667
+ style_dropdown_update, style_weight_update, styles_data_state_update = load_styles_for_ui(selected_model_for_style)
668
 
669
+ return fn_mode_checkbox_update, model_dropdown_update, style_dropdown_update, style_weight_update, styles_data_state_update
670
  # ▲▲▲ 変更点 ▲▲▲
671
 
672
+
673
  def on_model_select_change(selected_model_name: Optional[str]):
 
674
  style_dropdown_update, style_weight_update, styles_data_state_update = load_styles_for_ui(selected_model_name)
675
  return style_dropdown_update, style_weight_update, styles_data_state_update
 
676
 
677
  def on_style_dropdown_select(selected_display_name: Optional[str], styles_data: Dict[str, Any]):
678
  if not selected_display_name or not styles_data: return gr.update(value=DEFAULT_STYLE_WEIGHT)
 
683
 
684
  def action_run_synthesis(
685
  model_name: Optional[str],
 
 
 
686
  style_display_name: Optional[str], style_weight_for_synth: float,
687
  text: str, generation_mode: str, batch_count: int,
688
  lang: str, seed: int, speaker: str, ref_audio: Optional[str],
 
729
 
730
  all_logs = []
731
 
 
732
  model_path = assets_root_path / model_name
733
  files = find_safetensors_files_webui(str(model_path))
734
  if not files:
735
  error_outputs[0] = f"❌ [エラー] モデルフォルダ '{model_name}' に .safetensors ファイルが見つかりません。"
736
  return tuple(error_outputs)
737
 
 
738
  actual_model_file_to_load = str(model_path / files[0])
739
  all_logs.append(f"[自動選択] 使用モデルファイル: {files[0]}")
 
740
 
741
  batch_count = int(batch_count)
742
  if batch_count <= 0: batch_count = 1
 
854
 
855
  return tuple(final_outputs)
856
 
857
+ # --- 作業台イベントハンドラ ---
858
  def add_to_workbench(
859
  current_status: str,
860
  current_workbench_list: List[Dict],
 
864
  safe_workbench_list = current_workbench_list or []
865
 
866
  if not audio_path or not Path(audio_path).exists():
867
+ log_messages.append("⚠️ [作業台追加エラー] 追加する音声ファイルが見つかりません。")
868
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
869
  return (final_status, safe_workbench_list) + update_workbench_ui(safe_workbench_list)
870
 
871
  if any(item['audio_path'] == audio_path for item in safe_workbench_list):
872
+ log_messages.append("ℹ️ この音声はすでに作業台に存在します。")
873
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
874
  return (final_status, safe_workbench_list) + update_workbench_ui(safe_workbench_list)
875
 
 
898
  path_to_delete.unlink()
899
  except Exception as e:
900
  print(f"Warning: Failed to delete old workbench audio file: {e}")
901
+ log_messages.append(f"ℹ️ 作業台のアイテムが最大数({MAX_WORKBENCH_ITEMS})に達したため、一番古いアイテムを削除しました。")
902
 
903
  ui_updates = update_workbench_ui(updated_list)
904
+ log_messages.append("✅ 作業台に音声を追加しました。")
905
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
906
  return (final_status, updated_list) + ui_updates
907
 
 
916
  path_to_delete = Path(item_to_remove['audio_path'])
917
  if path_to_delete.exists() and str(path_to_delete.parent) == tempfile.gettempdir():
918
  path_to_delete.unlink()
919
+ log_messages.append(f"✅ 作業台からアイテム #{index_to_remove + 1} を削除し、一時ファイルをクリーンアップしました。")
920
  elif path_to_delete.exists():
921
+ log_messages.append(f"✅ 作業台からアイテム #{index_to_remove + 1} を削除しました。(ファイルは保持: {path_to_delete.name})")
922
  else:
923
+ log_messages.append(f"✅ 作業台からアイテム #{index_to_remove + 1} を削除しました。(関連ファイルなし)")
924
  except Exception as e:
925
+ log_messages.append(f"⚠️ 作業台のアイテム #{index_to_remove + 1} のファイル削除中にエラー: {e}")
926
 
927
  updated_list = [item for i, item in enumerate(safe_workbench_list) if i != index_to_remove]
928
  ui_updates = update_workbench_ui(updated_list)
 
937
  ):
938
  log_messages = []
939
  if not workbench_list:
940
+ log_messages.append("⚠️ [結合プレビュー警告] 作業台に音声がありません。")
941
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
942
  return final_status, None, {}
943
 
 
965
  pause_duration = int(pause_ms)
966
 
967
  if pause_duration >= 0:
 
968
  pause_segment = AudioSegment.silent(duration=pause_duration)
969
  combined_audio = segment1 + pause_segment + segment2
970
  log_messages.append(f"✅ 音声 #{first_audio_num} と #{second_audio_num} を {pause_duration}ms のポーズを挟んで結合しました。")
971
  else:
 
972
  overlap_duration = abs(pause_duration)
 
 
973
  max_possible_overlap = min(len(segment1), len(segment2))
974
  if overlap_duration > max_possible_overlap:
975
  log_messages.append(f"ℹ️ オーバーラップ長({overlap_duration}ms)が可能な最大値({max_possible_overlap}ms)を超えるため、自動的に調整されました。")
976
  overlap_duration = max_possible_overlap
977
 
 
978
  final_duration = len(segment1) + len(segment2) - overlap_duration
 
 
979
  combined_audio = AudioSegment.silent(duration=final_duration)
 
 
980
  combined_audio = combined_audio.overlay(segment1, position=0)
 
 
981
  overlay_position = len(segment1) - overlap_duration
982
  combined_audio = combined_audio.overlay(segment2, position=overlay_position)
 
983
  log_messages.append(f"✅ 音声 #{first_audio_num} と #{second_audio_num} を {overlap_duration}ms 重ねて(オーバーレイして)結合しました。")
984
 
985
  progress(1, desc="結合完了")
 
1010
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
1011
  return final_status, str(temp_path), metadata
1012
 
 
 
 
 
1013
  def action_add_merged_to_workbench(
1014
  current_status: str,
1015
  preview_data: Dict,
 
1021
  log_messages = []
1022
  safe_workbench_list = current_workbench_list or []
1023
  if not preview_data or "audio_path" not in preview_data:
1024
+ log_messages.append("⚠️ [作業台追加エラー] 追加する結合済み音声がありません。先にプレビューを生成してください。")
1025
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
1026
  return (final_status, safe_workbench_list) + update_workbench_ui(safe_workbench_list)
1027
 
1028
  src_path = Path(preview_data["audio_path"])
1029
  if not src_path.exists():
1030
+ log_messages.append("⚠️ [作業台追加エラー] 結合済み音声ファイルが見つかりません。")
1031
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
1032
  return (final_status, safe_workbench_list) + update_workbench_ui(safe_workbench_list)
1033
 
 
1059
  log_messages.append(f"⚠️ 元の音声ファイル削除中にエラー: {e}")
1060
 
1061
  final_workbench_list = [new_merged_item] + remaining_list
1062
+ log_messages.append(f"✅ 結合音声を作業台に追加し、元の音声(#{idx1+1}, #{idx2+1})を削除しました。")
1063
  else:
1064
  final_workbench_list = [new_merged_item] + safe_workbench_list
1065
+ log_messages.append("✅ 結合済みの音声を作業台の一番上に追加しました。")
1066
 
1067
  if len(final_workbench_list) > MAX_WORKBENCH_ITEMS:
1068
  item_to_remove = final_workbench_list.pop(-1)
 
1072
  path_to_delete.unlink()
1073
  except Exception as e:
1074
  print(f"Warning: Failed to delete old workbench audio file: {e}")
1075
+ log_messages.append(f"ℹ️ 作業台が最大数({MAX_WORKBENCH_ITEMS})に達したため一番古いアイテムを削除しました。")
1076
 
1077
  ui_updates = update_workbench_ui(final_workbench_list)
1078
  final_status = (current_status + "\n" + "\n".join(log_messages)).strip()
 
1080
 
1081
 
1082
  # --- イベントリスナー接続 ---
1083
+ # ▼▼▼ 変更点: FNモデルモードに対応したイベントリスナーの定義 ▼▼▼
1084
+ refresh_triggers = [refresh_model_list_button.click, use_fn_model_mode_checkbox.change]
1085
+ outputs_for_refresh = [use_fn_model_mode_checkbox, selected_model_dropdown, current_styles_dropdown, style_weight_for_synth_slider, all_styles_data_state]
1086
+
1087
+ for trigger in refresh_triggers:
1088
+ trigger(action_refresh_model_list,
1089
+ inputs=[use_fn_model_mode_checkbox],
1090
+ outputs=outputs_for_refresh)
1091
 
1092
  selected_model_dropdown.change(on_model_select_change,
1093
  inputs=[selected_model_dropdown],
 
1105
  generate_button.click(
1106
  fn=action_run_synthesis,
1107
  inputs=[
1108
+ selected_model_dropdown,
 
1109
  current_styles_dropdown, style_weight_for_synth_slider,
1110
  text_input, generation_mode_radio, batch_count_slider,
1111
  language_dropdown, seed_input, speaker_name_textbox,
 
1120
  )
1121
 
1122
  for i in range(MAX_AUDIO_OUTPUTS):
 
 
 
1123
  to_workbench_buttons[i].click(
1124
  fn=add_to_workbench,
1125
  inputs=[
 
1148
  outputs=[status_textbox, preview_audio_player, merged_preview_state]
1149
  )
1150
 
 
 
 
 
1151
  add_merged_to_workbench_button.click(
1152
  fn=action_add_merged_to_workbench,
1153
  inputs=[
 
1163
 
1164
  player_width_slider.release(lambda w: f"<script>document.documentElement.style.setProperty('--audio-width', '{w}px');</script>", inputs=[player_width_slider], outputs=[js_injector_html])
1165
 
1166
+ app.load(action_refresh_model_list,
1167
+ inputs=[use_fn_model_mode_checkbox],
1168
+ outputs=outputs_for_refresh)
 
 
1169
  # ▲▲▲ 変更点 ▲▲▲
1170
  return app
1171
 
 
1173
  if __name__ == "__main__":
1174
  if Path("model_assets").exists(): shutil.rmtree("model_assets")
1175
 
1176
+ # ▼▼▼ 変更点: FNモデルキャッシュパスの作成とGradioへの許可 ▼▼▲
1177
+ # モックFNモデルを作成してテスト
1178
+ FN_MODEL_CACHE_PATH.mkdir(exist_ok=True, parents=True)
1179
+ (FN_MODEL_CACHE_PATH / "FN1").mkdir(exist_ok=True)
1180
+ (FN_MODEL_CACHE_PATH / "FN10").mkdir(exist_ok=True)
1181
+ (FN_MODEL_CACHE_PATH / "FN2").mkdir(exist_ok=True)
1182
+
1183
  mock_model_holder = TTSModelHolder()
1184
  print(f"Initial models loaded by TTSModelHolder: {mock_model_holder.model_names}")
1185
  app = create_synthesis_app(mock_model_holder)
 
1188
  assets_dir_path.mkdir(exist_ok=True)
1189
  allowed_paths = [str(assets_dir_path)]
1190
 
1191
+ # FNモデルのキャッシュパスへのアクセスを許可
1192
+ if FN_MODEL_CACHE_PATH.exists() and FN_MODEL_CACHE_PATH.is_dir():
1193
+ allowed_paths.append(str(FN_MODEL_CACHE_PATH.resolve()))
1194
+ else:
1195
+ try:
1196
+ FN_MODEL_CACHE_PATH.mkdir(exist_ok=True, parents=True)
1197
+ allowed_paths.append(str(FN_MODEL_CACHE_PATH.resolve()))
1198
+ print(f"Created FN model cache directory at: {FN_MODEL_CACHE_PATH}")
1199
+ except OSError as e:
1200
+ print(f"Could not create FN model cache directory at {FN_MODEL_CACHE_PATH}: {e}")
1201
 
1202
  output_dir_path = Path("output").resolve()
1203
  (output_dir_path / "normal").mkdir(exist_ok=True, parents=True)
 
1209
 
1210
  print(f"Gradioに次のパスへのアクセスを許可します: {', '.join(allowed_paths)}")
1211
 
1212
+ app.launch(allowed_paths=allowed_paths)
1213
+ # ▲▲▲ 変更点 ▲▲▲