biosn2 commited on
Commit
b9e0911
·
verified ·
1 Parent(s): e3d26ff

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +951 -134
app.py CHANGED
@@ -1,3 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import json
2
  import os
3
  import sys
@@ -18,15 +49,16 @@ parser.add_argument("--port", type=int, default=7860, help="Port to run the web
18
  parser.add_argument("--host", type=str, default="127.0.0.1", help="Host to run the web UI on") # WebUI 主机地址
19
  parser.add_argument("--model_dir", type=str, default="checkpoints", help="Model checkpoints directory") # 模型目录
20
  cmd_args = parser.parse_args()
 
21
 
22
  # ----------------- 设置模块搜索路径 -----------------
23
  current_dir = os.path.dirname(os.path.abspath(__file__))
24
  sys.path.append(current_dir)
25
  sys.path.append(os.path.join(current_dir, "indextts"))
26
 
27
- # ----------------- 下载模型 -----------------
28
- MODE = 'local'
29
- snapshot_download("IndexTeam/IndexTTS-1.5", local_dir="checkpoints") # 从 Hugging Face 下载模型到本地
30
 
31
  # ----------------- 检查模型文件完整性 -----------------
32
  if not os.path.exists(cmd_args.model_dir):
@@ -43,6 +75,45 @@ for file in [
43
  if not os.path.exists(file_path):
44
  print(f"Required file {file_path} does not exist. Please download it.")
45
  sys.exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  # ----------------- 导入 Gradio 和其他模块 -----------------
48
  import gradio as gr
@@ -52,12 +123,12 @@ from indextts.infer import IndexTTS # 核心 TTS 推理类
52
  from tools.i18n.i18n import I18nAuto # 国际化工具
53
 
54
  # ----------------- 初始化 TTS 模型 -----------------
55
- i18n = I18nAuto(language="zh_CN") # 设置默认中文
56
  tts = IndexTTS(model_dir=cmd_args.model_dir, cfg_path=os.path.join(cmd_args.model_dir, "config.yaml")) # 加载模型
57
 
58
- # ----------------- 创建输出目录 -----------------
59
- os.makedirs("outputs/tasks", exist_ok=True)
60
- os.makedirs("prompts", exist_ok=True)
61
 
62
  # ----------------- 核心函数 -----------------
63
 
@@ -73,142 +144,888 @@ def ensure_wav(file_path):
73
  return wav_path
74
  return file_path
75
 
76
- def progress_print(step, total, info=""):
77
- """
78
- 印生成音频的进度到终端
79
- step: 当前步骤
80
- total: 总步骤数
81
- info: 附加信息
82
- """
83
- percent = int(step / total * 100)
84
- print(f"\r[{percent}%] {info}", end="", flush=True)
85
 
86
- def gen_single(prompt, text, max_text_tokens_per_sentence=120, *args, progress=gr.Progress()):
87
- """
88
- 单句音频生成函数
89
- prompt: 参考音频路径
90
- text: 目标文本
91
- max_text_tokens_per_sentence: 分句最大 Token 数
92
- *args: 高级生成参数(do_sample, top_p, top_k, temperature 等)
93
- progress: Gradio 进度条对象
94
-
95
- 返回生成的音频路径
96
- """
97
- prompt = ensure_wav(prompt) # 转换为 WAV
98
- output_path = os.path.join("outputs", f"spk_{int(time.time())}.wav") # 输出文件名
99
- tts.gr_progress = progress
100
- tts.print_progress = progress_print # 设置进度打印函数
101
-
102
- # 解包高级参数
103
- do_sample, top_p, top_k, temperature, \
104
- length_penalty, num_beams, repetition_penalty, max_mel_tokens = args
105
- kwargs = {
106
- "do_sample": bool(do_sample), # 是否启用随机采样,True生成多样化音频,False贪婪生成固定结果
107
- "top_p": float(top_p), # 核采样概率阈值,只从累计概率 >= top_p 的词集合中采样,值越大生成越自由
108
- "top_k": int(top_k) if int(top_k) > 0 else None, # 从概率最高的 top_k 个词中采样,None表示不限制,值越大多样性越高
109
- "temperature": float(temperature), # 采样温度,值越高随机性越大,值越低生成更确定平稳
110
- "length_penalty": float(length_penalty), # 长度惩罚,正值鼓励生成更长序列,负值鼓励生成更短序列
111
- "num_beams": num_beams, # Beam Search 的束宽,值越大生成越平滑自然,但速度慢
112
- "repetition_penalty": float(repetition_penalty), # 重复惩罚,值越大重复可能性越低
113
- "max_mel_tokens": int(max_mel_tokens), # 最大 mel 频谱长度,控制生成音频最大帧数,值越大生成音频越长,占用显存越多
114
- }
115
 
116
- # 调用 TTS 推理
117
- output = tts.infer(prompt, text, output_path, verbose=cmd_args.verbose,
118
- max_text_tokens_per_sentence=int(max_text_tokens_per_sentence),
119
- **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
- print("\n生成完成:", output_path)
122
- return gr.update(value=output, visible=True)
 
 
 
 
 
 
 
 
 
 
123
 
124
- def update_prompt_audio():
 
 
 
125
  """
126
- 上传参考音频时触发
127
- 激活生成按钮
128
  """
129
- return gr.update(interactive=True)
130
-
131
- # ----------------- Gradio WebUI 构建 -----------------
132
- with gr.Blocks(title="IndexTTS Demo") as demo:
133
- mutex = threading.Lock()
134
- gr.HTML('''
135
- 标题
136
- ''')
137
- with gr.Tab("音频生成"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  with gr.Row():
139
- os.makedirs("prompts", exist_ok=True)
140
- prompt_audio = gr.Audio(label="参考音频", key="prompt_audio",
141
- sources=["upload","microphone"], type="filepath")
 
 
 
142
  with gr.Column():
143
- input_text_single = gr.TextArea(label="文本", key="input_text_single", placeholder="请输入目标文本", info="当前模型版本{}".format(tts.model_version or "1.0"))
144
- gen_button = gr.Button("生成语音", key="gen_button", interactive=True)
145
- output_audio = gr.Audio(label="生成结果", visible=True, key="output_audio")
146
- # 高级参数设置
147
- with gr.Accordion("高级生成参数设置", open=False):
148
- # GPT2 采样参数
149
- with gr.Row():
150
- with gr.Column(scale=1):
151
- gr.Markdown("**GPT2 采样设置** _参数会影响音频多样性和生成速度_")
152
- with gr.Row():
153
- do_sample = gr.Checkbox(label="do_sample", value=True)
154
- temperature = gr.Slider(label="temperature", minimum=0.1, maximum=2.0, value=1.0, step=0.1)
155
- with gr.Row():
156
- top_p = gr.Slider(label="top_p", minimum=0.0, maximum=1.0, value=0.8, step=0.01)
157
- top_k = gr.Slider(label="top_k", minimum=0, maximum=100, value=30, step=1)
158
- num_beams = gr.Slider(label="num_beams", value=3, minimum=1, maximum=10, step=1)
159
- with gr.Row():
160
- repetition_penalty = gr.Number(label="repetition_penalty", precision=None, value=10.0, minimum=0.1, maximum=20.0, step=0.1)
161
- length_penalty = gr.Number(label="length_penalty", precision=None, value=0.0, minimum=-2.0, maximum=2.0, step=0.1)
162
- max_mel_tokens = gr.Slider(label="max_mel_tokens", value=600, minimum=50, maximum=tts.cfg.gpt.max_mel_tokens, step=10)
163
- # 分句设置
164
- with gr.Column(scale=2):
165
- gr.Markdown("**分句设置**")
166
- with gr.Row():
167
- max_text_tokens_per_sentence = gr.Slider(label="分句最大Token数", value=120, minimum=20, maximum=tts.cfg.gpt.max_text_tokens, step=2)
168
- with gr.Accordion("预览分句结果", open=True) as sentences_settings:
169
- sentences_preview = gr.Dataframe(headers=["序号", "分句内容", "Token数"], key="sentences_preview", wrap=True)
170
-
171
- advanced_params = [
172
- do_sample, top_p, top_k, temperature,
173
- length_penalty, num_beams, repetition_penalty, max_mel_tokens,
174
- ]
175
-
176
- # 分句预览逻辑
177
- input_text_single.change(
178
- lambda text, max_tokens_per_sentence: {
179
- sentences_preview: gr.update(value=[
180
- [i, ''.join(s), len(s)] for i, s in enumerate(
181
- tts.tokenizer.split_sentences(tts.tokenizer.tokenize(text), int(max_tokens_per_sentence))
182
  )
183
- ]) if text else gr.update(value=pd.DataFrame([], columns=["序号","分句内容","Token数"]))
184
- },
185
- inputs=[input_text_single, max_text_tokens_per_sentence],
186
- outputs=[sentences_preview]
187
- )
188
- max_text_tokens_per_sentence.change(
189
- lambda text, max_tokens_per_sentence: {
190
- sentences_preview: gr.update(value=[
191
- [i, ''.join(s), len(s)] for i, s in enumerate(
192
- tts.tokenizer.split_sentences(tts.tokenizer.tokenize(text), int(max_tokens_per_sentence))
193
  )
194
- ]) if text else gr.update(value=pd.DataFrame([], columns=["序号","分句内容","Token数"]))
195
- },
196
- inputs=[input_text_single, max_text_tokens_per_sentence],
197
- outputs=[sentences_preview]
198
- )
199
- prompt_audio.upload(update_prompt_audio, inputs=[], outputs=[gen_button])
200
-
201
- # 点击生成按钮调用 gen_single
202
- gen_button.click(gen_single,
203
- inputs=[prompt_audio, input_text_single, max_text_tokens_per_sentence, *advanced_params],
204
- outputs=[output_audio])
205
-
206
- # ----------------- 启动函数 -----------------
207
- def main():
208
- """
209
- 启动 Gradio WebUI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  """
211
- demo.launch(server_name="0.0.0.0", server_port=cmd_args.port)
212
 
213
- if __name__ == "__main__":
214
- main()
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ```python
2
+ #!/usr/bin/env python3
3
+ """
4
+ ChatTTS Gradio 应用 - 改编为使用 ChatterboxTTS
5
+ 适配 Hugging Face Spaces 运行环境
6
+ 基于 Chatterbox-TTS 实现: https://huggingface.co/spaces/ResembleAI/Chatterbox
7
+ """
8
+
9
+ import os
10
+ import random
11
+ import argparse
12
+ import torch
13
+ import numpy as np
14
+ import gradio as gr
15
+ from scipy.io.wavfile import write
16
+ import logging
17
+ from pathlib import Path
18
+ import sys
19
+ import re
20
+ import subprocess
21
+ from textblob import TextBlob
22
+ import pandas as pd
23
+ import base64
24
+ import threading
25
+ import io
26
+ import zipfile
27
+ import shutil
28
+ import math
29
+ from queue import Queue
30
+ from pydub import AudioSegment
31
+ import tempfile
32
  import json
33
  import os
34
  import sys
 
49
  parser.add_argument("--host", type=str, default="127.0.0.1", help="Host to run the web UI on") # WebUI 主机地址
50
  parser.add_argument("--model_dir", type=str, default="checkpoints", help="Model checkpoints directory") # 模型目录
51
  cmd_args = parser.parse_args()
52
+ model_dir="checkpoints"
53
 
54
  # ----------------- 设置模块搜索路径 -----------------
55
  current_dir = os.path.dirname(os.path.abspath(__file__))
56
  sys.path.append(current_dir)
57
  sys.path.append(os.path.join(current_dir, "indextts"))
58
 
59
+ # # ----------------- 下载模型 -----------------
60
+ # MODE = 'local'
61
+ # snapshot_download("IndexTeam/IndexTTS-1.5", local_dir="checkpoints") # 从 Hugging Face 下载模型到本地
62
 
63
  # ----------------- 检查模型文件完整性 -----------------
64
  if not os.path.exists(cmd_args.model_dir):
 
75
  if not os.path.exists(file_path):
76
  print(f"Required file {file_path} does not exist. Please download it.")
77
  sys.exit(1)
78
+ def analyze_sentiment(text):
79
+ """使用 TextBlob 分析文本情感,返回优化后的 IndexTTS 参数字典"""
80
+ try:
81
+ blob = TextBlob(text)
82
+ polarity = blob.sentiment.polarity # -1 (负面) ~ 1 (正面)
83
+ subjectivity = blob.sentiment.subjectivity # 0 (客观) ~ 1 (主观)
84
+
85
+ # temperature: 正面高 (生动),负面低 (平淡);范围 0.5~1.5
86
+ temperature = 1.0 + math.tanh(polarity) * 0.9 # 0.5 ~ 1.5
87
+ # top_p: 主观高 (多样),客观低 (保守);范围 0.7~0.95
88
+ top_p = 0.7 + subjectivity * 0.25
89
+ # top_k: 主观高 (创意),但固定上限;范围 20~50
90
+ top_k = int(20 + subjectivity * 30)
91
+ # repetition_penalty: 客观高 (避免重复),主观低 (允许强调);范围 5.0~15.0
92
+ repetition_penalty = 10.0 + (1 - subjectivity) * 5.0 - polarity * 2.0
93
+ # length_penalty: 正面正 (长表达),负面负 (短);范围 -1.0~1.0
94
+ length_penalty = math.tanh(polarity) * 1.0
95
+ # num_beams: 正面高 (自然),但固定以控制速度;范围 2~5
96
+ num_beams = int(2 + (polarity + 1) * 1.5)
97
+
98
+ return {
99
+ "temperature": max(0.3, min(2, temperature)),
100
+ "top_p": max(0.7, min(0.95, top_p)),
101
+ "top_k": max(20, min(50, top_k)),
102
+ "repetition_penalty": max(5.0, min(15.0, repetition_penalty)),
103
+ "length_penalty": max(-1.0, min(1.0, length_penalty)),
104
+ "num_beams": max(2, min(5, num_beams))
105
+ }
106
+ except Exception as e:
107
+ print(f"情感分析失败: {str(e)}")
108
+ return { # 默认值
109
+ "temperature": 1.0,
110
+ "top_p": 0.8,
111
+ "top_k": 30,
112
+ "repetition_penalty": 10.0,
113
+ "length_penalty": 0.0,
114
+ "num_beams": 3
115
+ }
116
+
117
 
118
  # ----------------- 导入 Gradio 和其他模块 -----------------
119
  import gradio as gr
 
123
  from tools.i18n.i18n import I18nAuto # 国际化工具
124
 
125
  # ----------------- 初始化 TTS 模型 -----------------
126
+ i18n = I18nAuto(language="en") # 设置默认中文
127
  tts = IndexTTS(model_dir=cmd_args.model_dir, cfg_path=os.path.join(cmd_args.model_dir, "config.yaml")) # 加载模型
128
 
129
+ # # ----------------- 创建输出目录 -----------------
130
+ # os.makedirs("outputs/tasks", exist_ok=True)
131
+ # os.makedirs("prompts", exist_ok=True)
132
 
133
  # ----------------- 核心函数 -----------------
134
 
 
144
  return wav_path
145
  return file_path
146
 
147
+ # def progress_print(step, total, info=""):
148
+ # """
149
+ # ��印生成音频的进度到终端
150
+ # step: 当前步骤
151
+ # total: 总步骤数
152
+ # info: 附加信息
153
+ # """
154
+ # percent = int(step / total * 100)
155
+ # print(f"\r[{percent}%] {info}", end="", flush=True)
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
+ # 设置日志
159
+ logging.basicConfig(level=logging.INFO)
160
+ logger = logging.getLogger(__name__)
161
+
162
+ # 全局变量用于存储最后上传的文件
163
+ last_uploaded_file = None
164
+
165
+ # 全局取消标志
166
+ cancel_generation = threading.Event()
167
+
168
+ # 全局生成状态和队列
169
+ is_generating = threading.Event()
170
+ generation_queue = [] # 存储 {'role_index': i, 'txt': str, 'start_idx': int, 'spk_emb_seed_id': str, 'ref_wav': str}
171
+ queued_roles = set()
172
+
173
+ # 存储每个角色的 make_bulk 函数
174
+ make_bulk_functions = [None] * 40
175
+
176
+ # 临时目录用于缓存音频
177
+ audio_cache_dir = tempfile.mkdtemp(prefix='audio_cache_')
178
 
179
+ # 在脚本启动时清空缓存文件夹
180
+ def clear_audio_cache_dir():
181
+ """清空或重新创建 audio_cache_dir"""
182
+ global audio_cache_dir
183
+ try:
184
+ if os.path.exists(audio_cache_dir):
185
+ shutil.rmtree(audio_cache_dir)
186
+ print(f"\033[92m✅ 已清空缓存文件夹: {audio_cache_dir}\033[0m")
187
+ os.makedirs(audio_cache_dir)
188
+ print(f"\033[92m✅ 创建新的缓存文件夹: {audio_cache_dir}\033[0m")
189
+ except Exception as e:
190
+ print(f"\033[91m❌ 清空缓存文件夹失败: {str(e)}\033[0m")
191
 
192
+ # Monkey patch torch.load 以处理设备映射
193
+ original_torch_load = torch.load
194
+
195
+ def patched_torch_load(f, map_location=None, **kwargs):
196
  """
197
+ 修补后的 torch.load,自动将 CUDA 张量映射到 CPU/CUDA
 
198
  """
199
+ if map_location is None:
200
+ map_location = 'cpu'
201
+ logger.info(f"🔧 使用 map_location={map_location} 加载")
202
+ return original_torch_load(f, map_location=map_location, **kwargs)
203
+
204
+ # 在 torch 导入后立即应用补丁
205
+ torch.load = patched_torch_load
206
+ if 'torch' in sys.modules:
207
+ sys.modules['torch'].load = patched_torch_load
208
+
209
+ logger.info("✅ 成功应用 torch.load 设备映射补丁")
210
+
211
+ # 设备检测,适配 Hugging Face Spaces
212
+ if torch.cuda.is_available():
213
+ DEVICE = "cuda"
214
+ logger.info("🚀 使用 CUDA GPU 运行")
215
+ else:
216
+ DEVICE = "cpu"
217
+ logger.info("🚀 使用 CPU 运行")
218
+
219
+ print(f"🚀 在设备上运行: {DEVICE}")
220
+
221
+ # 全局模型变量
222
+ MODEL = True
223
+
224
+ def get_or_load_model():
225
+ """加载 ChatterboxTTS 模型(如果尚未加载),并确保其在正确的设备上运行"""
226
+ global MODEL, DEVICE
227
+ if MODEL is None:
228
+ print("模型未加载,正在初始化...")
229
+ try:
230
+ # 优先尝试官方导入路径
231
+ try:
232
+ from chatterbox.src.chatterbox.tts import ChatterboxTTS
233
+ logger.info("✅ 使用官方 chatterbox.src 导入路径")
234
+ except ImportError:
235
+ # 回退到备用导入路径
236
+ from chatterbox import ChatterboxTTS
237
+ logger.info("✅ 使用 chatterbox 直接导入路径")
238
+
239
+ # 先将模型加载到 CPU 以避免设备问题
240
+ MODEL = ChatterboxTTS.from_pretrained("cpu")
241
+
242
+ # 如果是 CUDA,移动到目标设备
243
+ if DEVICE == "cuda":
244
+ logger.info(f"将模型组件移动到 {DEVICE}...")
245
+ try:
246
+ if hasattr(MODEL, 't3'):
247
+ MODEL.t3 = MODEL.t3.to(DEVICE)
248
+ if hasattr(MODEL, 's3gen'):
249
+ MODEL.s3gen = MODEL.s3gen.to(DEVICE)
250
+ if hasattr(MODEL, 've'):
251
+ MODEL.ve = MODEL.ve.to(DEVICE)
252
+
253
+ MODEL.device = DEVICE
254
+ logger.info(f"✅ 所有模型组件已移动到 {DEVICE}")
255
+ except Exception as e:
256
+ logger.warning(f"⚠️ 无法将某些组件移动到 {DEVICE}: {e}")
257
+ logger.info("🔄 为确保稳定性回退到 CPU 模式")
258
+ DEVICE = "cpu"
259
+ MODEL.device = "cpu"
260
+
261
+ logger.info(f"✅ 模型在 {DEVICE} 上加载成功")
262
+ return MODEL, "模型加载成功"
263
+
264
+ except Exception as e:
265
+ logger.error(f"❌ 加载模型失败: {e}")
266
+ return None, f"模型加载失败: {str(e)}"
267
+ return MODEL, "模型已加载"
268
+
269
+ def parse_role_mappings(mapping_text):
270
+ """解析角色映射文本,返回 {角色代号: {'display_name': 显示名, 'voice_file': 'xxx.wav', 'seed_id': seed_id}} 字典"""
271
+ role_map = {}
272
+ if not mapping_text:
273
+ return role_map
274
+ lines = mapping_text.strip().splitlines()
275
+ pattern = re.compile(r'角色代号:\s*(\w+)\s*→\s*显示名:\s*([^→]+?)(?:\s+voice:\s*([\w_-]+))?(?:\s+seed_id:\s*(\w+))?\s*$')
276
+
277
+ for line in lines:
278
+ match = pattern.match(line.strip())
279
+ if match:
280
+ role_code = match.group(1).strip()
281
+ display_name = match.group(2).strip()
282
+ voice_name = match.group(3).strip() if match.group(3) else None
283
+ seed_id = match.group(4).strip() if match.group(4) else None
284
+
285
+ voice_file = None
286
+ if voice_name:
287
+ voice_file = voice_name + ".wav"
288
+
289
+ role_map[role_code] = {'display_name': display_name, 'voice_file': voice_file, 'seed_id': seed_id}
290
+
291
+ return role_map
292
+
293
+ def filter_lines(lines, filter_text):
294
+ """根据筛选条件过滤对话行"""
295
+ if not filter_text:
296
+ return lines
297
+ filtered = []
298
+ conditions = filter_text.strip().split(';')
299
+ for line in lines:
300
+ include = True
301
+ for condition in conditions:
302
+ condition = condition.strip()
303
+ if not condition:
304
+ continue
305
+ if condition.startswith('min_len='):
306
+ try:
307
+ min_len = int(condition.split('=')[1])
308
+ if len(re.sub(r'[^A-Za-z0-9]', '', line)) < min_len:
309
+ include = False
310
+ except ValueError:
311
+ print(f"警告: 无效的 min_len 条件: {condition}")
312
+ elif condition.startswith('keyword='):
313
+ keyword = condition.split('=')[1].lower()
314
+ if keyword not in line.lower():
315
+ include = False
316
+ else:
317
+ print(f"警告: 未知筛选条件: {condition}")
318
+ if include:
319
+ filtered.append(line)
320
+ return filtered
321
+
322
+ def parse_roles_from_rpy_file(file_bytes, filter_text=""):
323
+ try:
324
+ lines = file_bytes.decode("utf-8").splitlines()
325
+ role_pattern = re.compile(r'^#\s*(?:(\w+)\s*)?\"(.*?)\"')
326
+ role_dict = {}
327
+
328
+ for line in lines:
329
+ match = role_pattern.match(line.strip())
330
+ if match:
331
+ role = match.group(1) if match.group(1) else 'noname'
332
+ content = match.group(2)
333
+ if '(' in content or ')' in content:
334
+ continue
335
+ content = re.sub(r'\[.*?\]', ',', content)
336
+ content = re.sub(r'\{.*?\}', '', content)
337
+ content = re.sub(r'\\', '', content)
338
+ content = re.sub(r'\*.*?\*', '', content)
339
+ content = re.sub(r'\s+', ' ', content).strip()
340
+ if len(content) < 2:
341
+ continue
342
+ if role not in role_dict:
343
+ role_dict[role] = []
344
+ role_dict[role].append(content)
345
+
346
+ for role in role_dict:
347
+ role_dict[role] = filter_lines(role_dict[role], filter_text)
348
+
349
+ return role_dict, lines
350
+ except Exception as e:
351
+ raise ValueError(f"解析 .rpy 文件失败: {str(e)}")
352
+
353
+ def save_wav(audio_data, sample_rate, output_path):
354
+ try:
355
+ if audio_data.dtype != np.int16:
356
+ audio_int16 = np.clip(audio_data * 32767, -32768, 32767).astype(np.int16)
357
+ else:
358
+ audio_int16 = audio_data
359
+ write(output_path, sample_rate, audio_int16)
360
+ except Exception as e:
361
+ raise RuntimeError(f"保存音频文件失败: {str(e)}")
362
+
363
+ # def analyze_sentiment(text):
364
+ # """使用 TextBlob 分析文本情感,返回 temperature, cfg_weight 和 exaggeration 参数"""
365
+ # try:
366
+ # blob = TextBlob(text)
367
+ # polarity = blob.sentiment.polarity
368
+ # subjectivity = blob.sentiment.subjectivity
369
+
370
+ # temperature = 0.5 + math.tanh(polarity) * 0.75
371
+ # temperature = max(0.05, min(2.0, temperature))
372
+
373
+ # cfg_weight = 0.5 + (1 - subjectivity) * 0.45
374
+ # cfg_weight = max(0.1, min(1.0, cfg_weight))
375
+
376
+ # exaggeration = 0.55 + math.tanh(polarity) * 0.45
377
+ # exaggeration = max(0.1, min(1.0, exaggeration))
378
+
379
+ # return temperature, cfg_weight, exaggeration
380
+ # except Exception as e:
381
+ # print(f"情感分析失败: {str(e)}")
382
+ # return 0.3, 0.5, 0.25
383
+ def pre_analyze_lines(texts):
384
+ """为每行文本分析情感,返回参数字典列表"""
385
+ context_window = 2
386
+ params_list = []
387
+ for idx, line in enumerate(texts):
388
+ if not line.strip():
389
+ params_list.append({
390
+ "temperature": 1.0,
391
+ "top_p": 0.8,
392
+ "top_k": 30,
393
+ "repetition_penalty": 10.0,
394
+ "length_penalty": 0.0,
395
+ "num_beams": 3
396
+ })
397
+ continue
398
+ start_idx = max(0, idx - context_window)
399
+ end_idx = min(len(texts), idx + context_window + 1)
400
+ context_text = " ".join(texts[start_idx:end_idx])
401
+ params = analyze_sentiment(context_text)
402
+ params_list.append(params)
403
+ return params_list
404
+
405
+ def get_pt_file(seed_id, csv_path=os.path.join(os.path.dirname(__file__), "evaluation_results.csv")):
406
+ """根据 seed_id 从 CSV 文件获取 .pt 数据并加载为 PyTorch tensor"""
407
+ try:
408
+ if seed_id and not seed_id.startswith("seed_"):
409
+ seed_id = f"seed_{seed_id}"
410
+
411
+ df = pd.read_csv(csv_path, encoding="utf-8")
412
+ row = df[df["seed_id"] == seed_id]
413
+ if row.empty:
414
+ return None, f"未找到 seed_id: {seed_id}"
415
+
416
+ emb_data = row.iloc[0]["emb_data"]
417
+ emb_bytes = base64.b64decode(emb_data)
418
+ emb_buffer = io.BytesIO(emb_bytes)
419
+ spk_emb = torch.load(emb_buffer)
420
+
421
+ return spk_emb, f"成功加载 seed_id: {seed_id} 的 spk_emb 数据"
422
+ except Exception as e:
423
+ return None, f"加载 spk_emb 数据失败: {str(e)}"
424
+
425
+ def zip_outputs_folder():
426
+ try:
427
+ outputs_dir = "Outputs"
428
+ zip_path = os.path.join("tmp", "outputs.zip")
429
+ os.makedirs("tmp", exist_ok=True)
430
+
431
+ if os.path.exists(outputs_dir) and os.path.isdir(outputs_dir):
432
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
433
+ for root, _, files in os.walk(outputs_dir):
434
+ for file in files:
435
+ file_path = os.path.join(root, file)
436
+ arcname = os.path.relpath(file_path, outputs_dir)
437
+ zipf.write(file_path, os.path.join("Outputs", arcname))
438
+ return zip_path, "Outputs 文件夹已成功打包为 outputs.zip"
439
+ else:
440
+ return None, "Outputs 文件夹不存在或为空"
441
+ except Exception as e:
442
+ return None, f"打包 Outputs 文件夹失败: {str(e)}"
443
+
444
+ def compress_wav_to_mp3():
445
+ """将 Outputs 文件夹及其子目录中的 .wav 文件转换为 80kbps 的 .mp3 文件,并替换原文件"""
446
+ try:
447
+ outputs_dir = "Outputs"
448
+ if not os.path.exists(outputs_dir) or not os.path.isdir(outputs_dir):
449
+ return "Outputs 文件夹不存在或为空"
450
+
451
+ converted_files = 0
452
+ for root, _, files in os.walk(outputs_dir):
453
+ for file in files:
454
+ if file.lower().endswith(".wav"):
455
+ wav_path = os.path.join(root, file)
456
+ mp3_path = os.path.join(root, file[:-4] + ".mp3")
457
+ try:
458
+ # 读取 WAV 文件
459
+ audio = AudioSegment.from_wav(wav_path)
460
+ # 转换为 MP3,设置比特率为 80kbps
461
+ audio.export(mp3_path, format="mp3", bitrate="80k")
462
+ # 删除原 WAV 文件
463
+ os.remove(wav_path)
464
+ print(f"\033[92m✅ 转换并替换: {wav_path} -> {mp3_path}\033[0m")
465
+ converted_files += 1
466
+ except Exception as e:
467
+ print(f"\033[91m❌ 转换 {wav_path} 失败: {str(e)}\033[0m")
468
+ continue
469
+
470
+ if converted_files == 0:
471
+ return "未找到 .wav 文件进行转换"
472
+ return f"成功将 {converted_files} 个 .wav 文件转换为 .mp3 并替换"
473
+ except Exception as e:
474
+ print(f"\033[91m❌ 压缩 WAV 到 MP3 失败: {str(e)}\033[0m")
475
+ return f"压缩 WAV 到 MP3 失败: {str(e)}"
476
+
477
+
478
+ def generate_all_lines_audio(role, texts, lines, start_index, role_map, spk_emb_seed_id=None, ref_wav=None, role_index=0, button=None, status_output=None, use_sentiment=True):
479
+ """批量生成角色语音,基于情感分析动态调整 IndexTTS 参数"""
480
+ try:
481
+ display_name = role_map.get(role, {'display_name': role})['display_name']
482
+
483
+ if cancel_generation.is_set():
484
+ return None, f"{display_name}:生成已取消", button
485
+
486
+ # 检查是否提供 spk_emb_seed_id 或 ref_wav
487
+ if not spk_emb_seed_id and not ref_wav:
488
+ print(f"\033[93m⚠️ 角色 {display_name} 未提供 spk_emb_seed_id 或参考音频,跳过生成\033[0m")
489
+ return None, f"{display_name}:未提供 spk_emb_seed_id 或参考音频,跳过生成", gr.Button(interactive=True)
490
+
491
+ current_model = get_or_load_model()
492
+ if current_model[0] is None:
493
+ raise RuntimeError(f"ChatterboxTTS 模型未加载:{current_model[1]}")
494
+
495
+ params_list = pre_analyze_lines(texts[start_index-1:])
496
+ saved_paths = []
497
+ os.makedirs("Outputs", exist_ok=True)
498
+
499
+ role_pattern = re.compile(r'^#\s*(?:' + re.escape(role) + r'\s*)?\"(.*?)\"')
500
+ translate_pattern = re.compile(r'^translate\s+\w+\s+([\w_-]+)\s*:')
501
+
502
+ print(f"\033[94m🚀 开始生成角色 {display_name} 的语音,从第 {start_index} 条开始,共 {len(texts[start_index-1:])} 条\033[0m")
503
+
504
+ def generate_single_line(idx, line, params):
505
+ if cancel_generation.is_set():
506
+ return None
507
+ if not line.strip():
508
+ return None
509
+ text_index = None
510
+ for i, file_line in enumerate(lines):
511
+ if role_pattern.match(file_line.strip()):
512
+ raw_content = role_pattern.match(file_line.strip()).group(1)
513
+ raw_content = re.sub(r'\[.*?\]', ',', raw_content)
514
+ raw_content = re.sub(r'\{.*?\}', '', raw_content)
515
+ raw_content = re.sub(r'\\', '', raw_content)
516
+ raw_content = re.sub(r'\*.*?\*', '', raw_content)
517
+ if line == raw_content.strip():
518
+ text_index = i
519
+ break
520
+ identifier = None
521
+ if text_index is not None:
522
+ for j in range(1, 5):
523
+ if text_index - j >= 0:
524
+ line_to_check = lines[text_index - j].strip()
525
+ translate_match = translate_pattern.match(line_to_check)
526
+ if translate_match:
527
+ identifier = translate_match.group(1)
528
+ break
529
+ if identifier is None:
530
+ print(f"\033[93m跳过: 未找到 translate id for {line} [角色: {display_name}, 索引: {idx+1}]\033[0m")
531
+ return None
532
+
533
+ processed_line = line
534
+ # print(f"\033[92m📢 生成 {display_name} 第 {idx+1}/{len(texts)} 条语音:{processed_line[:30]}...\033[0m")
535
+
536
+ filename = f"{identifier}.wav"
537
+ output_path = os.path.join("Outputs", filename)
538
+
539
+ # 默认参数
540
+ kwargs = {
541
+ "do_sample": True,
542
+ "top_p": 0.8,
543
+ "top_k": 30,
544
+ "temperature": 1.0,
545
+ "length_penalty": 0.0,
546
+ "num_beams": 3,
547
+ "repetition_penalty": 10.0,
548
+ "max_mel_tokens": 600
549
+ }
550
+
551
+ # 如果启用情感分析,覆盖参数
552
+ if use_sentiment:
553
+ kwargs.update(params)
554
+
555
+ # 调用 TTS 推理
556
+ output = tts.infer(ref_wav, processed_line, output_path, verbose=cmd_args.verbose,
557
+ max_text_tokens_per_sentence=120, **kwargs)
558
+
559
+ print(f"\033[91m-------\n温度: {kwargs['temperature']:.2f}, Top_p: {kwargs['top_p']:.2f}, Top_k: {kwargs['top_k']}, "
560
+ f"RP: {kwargs['repetition_penalty']:.2f}, Length_penalty: {kwargs['length_penalty']:.2f}, "
561
+ f"Num_beams: {kwargs['num_beams']} {line}\n[角色: {display_name}: {idx+1}/{len(texts)}]{line}\n----------\033[0m")
562
+ return output_path
563
+
564
+ for idx, (line, params) in enumerate(zip(texts[start_index-1:], params_list), start=start_index-1):
565
+ if cancel_generation.is_set():
566
+ print(f"\033[93m⚠️ {display_name}:生成被取消,停止于第 {idx+1} 条\033[0m")
567
+ return None, f"{display_name}:生成已取消", button
568
+ result = generate_single_line(idx, line, params)
569
+ if result:
570
+ saved_paths.append(result)
571
+
572
+ print(f"\033[94m✅ {display_name}:生成完成,共生成 {len(saved_paths)} 条语音\033[0m")
573
+ return None, f"{display_name}:从第 {start_index} 条开始,共生成 {len(saved_paths)} 条语音", gr.Button(interactive=True)
574
+ except Exception as e:
575
+ print(f"\033[91m❌ {display_name}:批量生成失败: {str(e)}\033[0m")
576
+ return None, f"{display_name}:批量生成失败: {str(e)}", gr.Button(interactive=True)
577
+ finally:
578
+ queued_roles.discard(role_index)
579
+ is_generating.clear()
580
+ process_queue()
581
+
582
+ def process_queue():
583
+ """处理队列中的下一个角色"""
584
+ global generation_queue, is_generating, queued_roles
585
+ while generation_queue and not is_generating.is_set() and not cancel_generation.is_set():
586
+ role_config = generation_queue.pop(0)
587
+ role_index = role_config['role_index']
588
+ role_data = role_texts[role_index]
589
+ if not role_data:
590
+ continue
591
+ role = role_data.get("role", f"角色{role_index+1}")
592
+ display_name = role_data.get("display_name", role)
593
+ txt = role_config['txt']
594
+ start_idx = role_config['start_idx']
595
+ spk_emb_seed_id = role_config['spk_emb_seed_id']
596
+ ref_wav = role_config['ref_wav']
597
+
598
+ # 检查是否提供 spk_emb_seed_id 或 ref_wav
599
+ if not spk_emb_seed_id and not ref_wav:
600
+ print(f"\033[93m⚠️ 角色 {display_name} 未提供 spk_emb_seed_id 或参考音频,跳过生成\033[0m")
601
+ role_components[role_index][4].value = f"{display_name}:未提供 spk_emb_seed_id 或参考音频,跳过生成"
602
+ role_components[role_index][-1].value = gr.Button(interactive=True)
603
+ queued_roles.discard(role_index)
604
+ continue
605
+
606
+ print(f"\033[94m🚀 从队列中取出角色 {display_name} 开始生成\033[0m")
607
+ # 设置 UI 状态
608
+ role_components[role_index][4].value = f"{display_name}:开始生成"
609
+ role_components[role_index][0].value = txt
610
+ role_components[role_index][1].value = start_idx
611
+ role_components[role_index][2].value = spk_emb_seed_id
612
+ role_components[role_index][3].value = ref_wav
613
+ is_generating.set()
614
+ # 使用缓存的配置调用 generate_all_lines_audio
615
+ file_lines = []
616
+ if last_uploaded_file:
617
+ try:
618
+ _, file_lines = parse_roles_from_rpy_file(last_uploaded_file, "")
619
+ except Exception as e:
620
+ role_components[role_index][4].value = f"文件解析失败: {str(e)}"
621
+ role_components[role_index][-1].value = gr.Button(interactive=True)
622
+ queued_roles.discard(role_index)
623
+ is_generating.clear()
624
+ continue
625
+ role_map = parse_role_mappings(role_mapping_input.value)
626
+ result = generate_all_lines_audio(
627
+ role, role_data.get("lines", []), file_lines, int(start_idx), role_map,
628
+ spk_emb_seed_id, ref_wav, role_index=role_index,
629
+ button=role_components[role_index][-1], status_output=role_components[role_index][4]
630
+ )
631
+ # 更新 UI 组件
632
+ role_components[role_index][4].value = result[1]
633
+ role_components[role_index][-1].value = result[2]
634
+ break
635
+
636
+ role_texts = [{} for _ in range(40)]
637
+ role_components = [] # 全局存储角色组件
638
+ role_mapping_input = None # 全局存储 role_mapping_input
639
+
640
+ def process_file(file, mapping_text, filter_text):
641
+ global last_uploaded_file
642
+ last_uploaded_file = file
643
+ if file is None:
644
+ return ["", 1, "", None, "", f"### 角色{i+1}", False, True] * 40 + ["请上传文件"]
645
+
646
+ try:
647
+ mapping_file_path = os.path.join(os.path.dirname(__file__), "角色映射.txt")
648
+ with open(mapping_file_path, "w", encoding="utf-8") as f:
649
+ f.write(mapping_text.strip() if mapping_text else "")
650
+
651
+ role_map = parse_role_mappings(mapping_text)
652
+ role_dict, lines = parse_roles_from_rpy_file(file, filter_text)
653
+
654
+ filtered_role_dict = {}
655
+ for role, lines in role_dict.items():
656
+ if len(lines) >= 100:
657
+ filtered_role_dict[role] = lines
658
+ else:
659
+ print(f"角色 {role} 的对话条数 ({len(lines)}) 小于100,已被过滤")
660
+
661
+ # 按 display_name 字母顺序排序
662
+ role_items = [(role, lines) for role, lines in filtered_role_dict.items()]
663
+ role_items.sort(key=lambda x: role_map.get(x[0], {'display_name': x[0]})['display_name'].lower())
664
+ # print(f"\033[94m📋 角色按字母顺序排序:{[role_map.get(role, {'display_name': role})['display_name'] for role, _ in role_items]}\033[0m")
665
+
666
+ except Exception as e:
667
+ return ["", 1, "", None, "", f"### 角色{i+1}", False, True] * 40 + [f"文件解析失败: {str(e)}"]
668
+
669
+ return_values = []
670
+ for i in range(40):
671
+ if i < len(role_items):
672
+ role, lines = role_items[i]
673
+ display_name = role_map.get(role, {'display_name': role, 'voice_file': None, 'seed_id': None})['display_name']
674
+ seed_id = role_map.get(role, {'display_name': role, 'voice_file': None, 'seed_id': None})['seed_id']
675
+ voice_file = role_map.get(role, {'display_name': role, 'voice_file': None, 'seed_id': None})['voice_file']
676
+ ref_wav_original = None
677
+ if voice_file:
678
+ ref_wav_original = os.path.join('voice', voice_file)
679
+ # 如果 voice_file 未指定或不存在,尝试使用 display_name.wav
680
+ if not ref_wav_original or not os.path.exists(ref_wav_original):
681
+ ref_wav_original = os.path.join('voice', display_name + '.wav')
682
+ if ref_wav_original and os.path.exists(ref_wav_original):
683
+ # 缓存到临时目录
684
+ cached_path = os.path.join(audio_cache_dir, f"role_{i}_{display_name}.wav")
685
+ shutil.copy(ref_wav_original, cached_path)
686
+ ref_wav_path = cached_path
687
+ print(f"\033[92m✅ 加载并缓存参考音频 for {display_name}: {ref_wav_original} -> {cached_path}\033[0m")
688
+ else:
689
+ ref_wav_path = None
690
+ # print(f"\033[93m⚠️ 未找到参考音频 for {display_name}: {ref_wav_original}\033[0m")
691
+ role_texts[i] = {"role": role, "display_name": display_name, "lines": lines, "ref_wav": ref_wav_path, "seed_id": seed_id}
692
+ joined = "\n".join(lines)
693
+ return_values.extend([joined, 1, seed_id, ref_wav_path, f"{display_name}:共 {len(lines)} 句", f"### {display_name}", True, True])
694
+ else:
695
+ role_texts[i] = {}
696
+ return_values.extend(["", 1, "", None, "", f"### 角色{i+1}", False, True])
697
+
698
+ return_values.append("文件处理成功,请为每个角色点击批量生成")
699
+ return return_values
700
+
701
+ def stop_all_generation():
702
+ """停止所有正在进行的生成任务,并释放所有按钮"""
703
+ global generation_queue, is_generating, queued_roles
704
+ cancel_generation.set()
705
+ generation_queue.clear()
706
+ queued_roles.clear()
707
+ is_generating.clear()
708
+ print(f"\033[93m🛑 停止所有生成任务\033[0m")
709
+ button_states = [gr.Button(interactive=True) for _ in range(40)]
710
+ status_outputs = [f"{role_texts[i].get('display_name', f'角色{i+1}')}:生成已取消" if role_texts[i] else "" for i in range(40)]
711
+ # 确保所有按钮恢复为可交互状态
712
+ for i in range(40):
713
+ if role_components[i][-1]: # bulk_btn
714
+ role_components[i][-1].value = gr.Button(interactive=True)
715
+ return "正在停止所有生成任务...", status_outputs, button_states
716
+
717
+ def get_pt_file_for_download(seed_id):
718
+ """为下载按钮生成 .pt 文件并返回路径和状态"""
719
+ spk_emb, message = get_pt_file(seed_id)
720
+ if spk_emb is not None:
721
+ os.makedirs("tmp", exist_ok=True)
722
+ output_path = os.path.join("tmp", f"{seed_id}_restored_emb.pt")
723
+ torch.save(spk_emb, output_path)
724
+ return gr.DownloadButton(value=output_path, label=f"Download .pt File [{seed_id}]", visible=True), message
725
+ return gr.DownloadButton(value=None, label="Download .pt File", visible=False), message
726
+
727
+ def get_download_zip_state():
728
+ """检查 zip 文件状态并返回下载按钮状态"""
729
+ if os.path.exists(os.path.join("tmp", "outputs.zip")):
730
+ return gr.DownloadButton(value=os.path.join("tmp", "outputs.zip"), label="Download Outputs.zip", visible=True), "已准备好下载 outputs.zip"
731
+ return gr.DownloadButton(value=None, label="Download Outputs.zip", visible=False), "未找到 outputs.zip"
732
+
733
+ def main():
734
+ # 在脚本启动时清空缓存文件夹
735
+ # clear_audio_cache_dir()
736
+
737
+ global role_components, make_bulk_functions, audio_cache_dir, role_mapping_input
738
+ MAX_ROLES = 40
739
+ mapping_file_path = os.path.join(os.path.dirname(__file__), "角色映射.txt")
740
+ try:
741
+ with open(mapping_file_path, "r", encoding="utf-8") as f:
742
+ role_mapping_placeholder = f.read().strip()
743
+ except FileNotFoundError:
744
+ role_mapping_placeholder = "角色代号: jud → 显示名: Judge voice:jud seed_id:1403\n角色代号: jury → 显示名: Members of The Jury voice:jury seed_id:1404\n角色代号: noname → 显示名: 无名角色"
745
+
746
+ with gr.Blocks() as demo:
747
+ gr.Markdown("https://huggingface.co/spaces/ResembleAI/Chatterbox")
748
+
749
+ model_status = gr.Textbox(label="模型加载状态", interactive=False, value="正在加载模型...")
750
+
751
  with gr.Row():
752
+ default_rpy_path = os.path.join(os.path.dirname(__file__), "dialogue.rpy")
753
+ file_input = gr.File(
754
+ label="上传 .rpy 文件",
755
+ type="binary",
756
+ value=default_rpy_path if os.path.exists(default_rpy_path) else None
757
+ )
758
  with gr.Column():
759
+ role_mapping_input = gr.Textbox(
760
+ label="角色映射(格式:角色代号: xxx → 显示名: xxx voice:xxx seed_id:xxx)",
761
+ lines=5,
762
+ placeholder=role_mapping_placeholder,
763
+ value=role_mapping_placeholder
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
  )
765
+ filter_input = gr.Textbox(
766
+ label="对话筛选(格式:min_len=X;keyword=Y,留空显示全部)",
767
+ lines=2,
768
+ placeholder="示例:min_len=20;keyword=hello\n(最小长度20字符,包含'hello'的对话)",
769
+ value="min_len=3"
 
 
 
 
 
770
  )
771
+ with gr.Row():
772
+ process_button = gr.Button("处理文件")
773
+ stop_button = gr.Button("停止所有生成")
774
+ zip_button = gr.Button("打包 Outputs 文件夹")
775
+ compress_button = gr.Button("压缩 WAV 到 MP3")
776
+ all_status = gr.Textbox(label="总状态", interactive=False, visible=False)
777
+ download_zip_button = gr.DownloadButton(label="Download Outputs.zip", visible=False)
778
+ zip_status = gr.Textbox(label="打包状态", interactive=False)
779
+
780
+ with gr.Row():
781
+ seed_id_input = gr.Textbox(
782
+ label="输入 seed_id 获取 .pt 文件",
783
+ placeholder="例如: 1403 seed_1403",
784
+ visible=False
785
+ )
786
+ pt_download_button = gr.DownloadButton(label="Download .pt File", visible=False)
787
+ pt_status = gr.Textbox(label="生成 .pt 文件状态", interactive=False)
788
+
789
+ def load_model_on_start():
790
+ model, status = get_or_load_model()
791
+ return status
792
+
793
+ demo.load(
794
+ fn=load_model_on_start,
795
+ inputs=[],
796
+ outputs=[model_status]
797
+ )
798
+
799
+ zip_button.click(
800
+ fn=zip_outputs_folder,
801
+ inputs=[],
802
+ outputs=[zip_status, download_zip_button]
803
+ )
804
+
805
+ compress_button.click(
806
+ fn=compress_wav_to_mp3,
807
+ inputs=[],
808
+ outputs=[zip_status]
809
+ )
810
+
811
+ demo.load(
812
+ fn=get_download_zip_state,
813
+ inputs=[],
814
+ outputs=[download_zip_button, zip_status]
815
+ )
816
+
817
+ seed_id_input.change(
818
+ fn=get_pt_file_for_download,
819
+ inputs=[seed_id_input],
820
+ outputs=[pt_download_button, pt_status]
821
+ )
822
+
823
+ role_components = []
824
+ visibility_states = [gr.State(value=False) for _ in range(MAX_ROLES)]
825
+ button_states = [gr.State(value=True) for _ in range(MAX_ROLES)]
826
+
827
+ ROLES_PER_ROW = 4
828
+ for row_idx in range((MAX_ROLES + ROLES_PER_ROW - 1) // ROLES_PER_ROW):
829
+ with gr.Row():
830
+ for i in range(row_idx * ROLES_PER_ROW, min((row_idx + 1) * ROLES_PER_ROW, MAX_ROLES)):
831
+ with gr.Group(visible=False, elem_classes="compact-group") as group:
832
+ role_display = gr.Markdown(f"### 角色{i+1}", elem_classes="compact-header")
833
+ text_input = gr.Textbox(
834
+ label="文本",
835
+ lines=2,
836
+ max_lines=4,
837
+ elem_classes="compact-textbox",
838
+ container=False
839
+ )
840
+ start_index = gr.Number(
841
+ value=1,
842
+ label="从第几条开始",
843
+ minimum=1,
844
+ step=1,
845
+ elem_classes="compact-number"
846
+ )
847
+ spk_emb_seed_id = gr.Textbox(
848
+ label="输入 spk_emb 的 seed_id",
849
+ placeholder="例如: 1403 或 seed_1403",
850
+ elem_classes="compact-textbox",
851
+ visible=False
852
+ )
853
+ ref_wav = gr.Audio(
854
+ type="filepath",
855
+ label="参考音频文件(可选,建议 6 秒以上)",
856
+ sources=["upload", "microphone"],
857
+ elem_classes="compact-audio"
858
+ )
859
+ audio_output = gr.Audio(
860
+ label="输出音频",
861
+ elem_classes="compact-audio",
862
+ visible=False
863
+ )
864
+ status = gr.Textbox(
865
+ label="状态",
866
+ interactive=False,
867
+ elem_classes="compact-textbox",
868
+ container=False
869
+ )
870
+ bulk_btn = gr.Button(
871
+ f"生成角色{i+1}",
872
+ size="sm",
873
+ elem_classes="compact-button",
874
+ interactive=True
875
+ )
876
+
877
+ role_components.append([text_input, start_index, spk_emb_seed_id, ref_wav, status, role_display, bulk_btn])
878
+
879
+ def make_bulk(i, bulk_btn):
880
+ def inner(txt, start_idx, spk_emb_seed_id, ref_wav, button):
881
+ global generation_queue, is_generating, queued_roles
882
+ role_data = role_texts[i]
883
+ if not role_data:
884
+ return None, f"角色{i+1} 无数据", gr.Button(interactive=True)
885
+ role = role_data.get("role", f"角色{i+1}")
886
+ display_name = role_data.get("display_name", role)
887
+
888
+ # 优先使用 UI 提供的 ref_wav,但检查是否需要缓存
889
+ ref_wav_path = ref_wav
890
+ if ref_wav and os.path.exists(ref_wav):
891
+ # 检查 ref_wav 是否已经是缓存目录中的文件
892
+ if not ref_wav.startswith(audio_cache_dir):
893
+ cached_path = os.path.join(audio_cache_dir, f"role_{i}_{display_name}_ui.wav")
894
+ shutil.copy(ref_wav, cached_path)
895
+ role_texts[i]["ref_wav"] = cached_path
896
+ print(f"\033[92m✅ 缓存 UI 提供的参考音频 for {display_name}: {ref_wav} -> {cached_path}\033[0m")
897
+ else:
898
+ # 已经是缓存文件,直接使用
899
+ role_texts[i]["ref_wav"] = ref_wav
900
+ print(f"\033[92m✅ 使用已缓存的参考音频 for {display_name}: {ref_wav}\033[0m")
901
+ else:
902
+ ref_wav_path = role_texts[i].get("ref_wav")
903
+
904
+ # 检查是否提供 spk_emb_seed_id 或 ref_wav
905
+ if not spk_emb_seed_id and not ref_wav_path:
906
+ print(f"\033[93m⚠️ 角色 {display_name} 未提供 spk_emb_seed_id 或参考音频,跳过生成\033[0m")
907
+ return None, f"{display_name}:未提供 spk_emb_seed_id 或参考音频,跳过生成", gr.Button(interactive=True)
908
+
909
+ # 缓存角色配置到队列
910
+ role_config = {
911
+ 'role_index': i,
912
+ 'txt': txt,
913
+ 'start_idx': int(start_idx),
914
+ 'spk_emb_seed_id': spk_emb_seed_id,
915
+ 'ref_wav': ref_wav_path
916
+ }
917
+
918
+ if is_generating.is_set():
919
+ if i not in queued_roles:
920
+ generation_queue.append(role_config)
921
+ queued_roles.add(i)
922
+ pos = len(generation_queue)
923
+ print(f"\033[94m⏳ 角色 {display_name} 已加入队列,位置 {pos}/{pos}\033[0m")
924
+ return None, f"{display_name}:等待队列中,位置 {pos}/{pos}", gr.Button(interactive=False)
925
+ else:
926
+ is_generating.set()
927
+ queued_roles.add(i)
928
+ cancel_generation.clear()
929
+ print(f"\033[94m🚀 单角色生成:{display_name},从第 {start_idx} 条开始\033[0m")
930
+ file_lines = []
931
+ if last_uploaded_file:
932
+ try:
933
+ _, file_lines = parse_roles_from_rpy_file(last_uploaded_file, "")
934
+ except Exception as e:
935
+ return None, f"文件解析失败: {str(e)}", gr.Button(interactive=True)
936
+ role_map = parse_role_mappings(role_mapping_input.value)
937
+ result = generate_all_lines_audio(
938
+ role, role_data.get("lines", []), file_lines, int(start_idx), role_map,
939
+ spk_emb_seed_id, ref_wav_path, role_index=i, button=button, status_output=status
940
+ )
941
+ return result
942
+ return inner
943
+
944
+ # 存储 make_bulk 函数,传递 bulk_btn
945
+ make_bulk_functions[i] = make_bulk(i, bulk_btn)
946
+
947
+ bulk_btn.click(
948
+ fn=make_bulk_functions[i],
949
+ inputs=[text_input, start_index, spk_emb_seed_id, ref_wav, bulk_btn],
950
+ outputs=[audio_output, status, bulk_btn]
951
+ )
952
+
953
+ visibility_states[i].change(
954
+ fn=lambda x, g=group: {"__type__": "update", "visible": x},
955
+ inputs=visibility_states[i],
956
+ outputs=group
957
+ )
958
+
959
+ outputs = []
960
+ for i in range(MAX_ROLES):
961
+ for comp in role_components[i][:-1]:
962
+ outputs.append(comp)
963
+ outputs.append(visibility_states[i])
964
+ outputs.append(role_components[i][-1]) # Button state
965
+ outputs.append(all_status)
966
+
967
+ process_button.click(
968
+ fn=process_file,
969
+ inputs=[file_input, role_mapping_input, filter_input],
970
+ outputs=outputs
971
+ )
972
+
973
+ stop_button.click(
974
+ fn=stop_all_generation,
975
+ inputs=[],
976
+ outputs=[all_status, *[comp[4] for comp in role_components], *button_states]
977
+ )
978
+
979
+ demo.css = """
980
+ body {
981
+ background-color: #808080 !important;
982
+ }
983
+ .compact-group {
984
+ width: 250px !important;
985
+ min-width: 230px !important;
986
+ padding: 5px !important;
987
+ margin: 5px !important;
988
+ }
989
+ .compact-header {
990
+ font-size: 14px !important;
991
+ margin: 2px 0 !important;
992
+ }
993
+ .compact-textbox {
994
+ font-size: 12px !important;
995
+ line-height: 1.2 !important;
996
+ padding: 2px !important;
997
+ margin: 2px 0 !important;
998
+ }
999
+ .compact-number {
1000
+ width: 80px !important;
1001
+ font-size: 12px !important;
1002
+ padding: 2px !important;
1003
+ margin: 2px 0 !important;
1004
+ }
1005
+ .compact-button {
1006
+ font-size: 12px !important;
1007
+ padding: 4px !important;
1008
+ margin: 2px 0 !important;
1009
+ }
1010
+ .compact-audio {
1011
+ max-width: 220px !important;
1012
+ min-width: 200px !important;
1013
+ font-size: 12px !important;
1014
+ margin: 2px 0 !important;
1015
+ overflow: visible !important;
1016
+ }
1017
  """
 
1018
 
1019
+ parser = argparse.ArgumentParser()
1020
+ parser.add_argument("--host", type=str, default="0.0.0.0")
1021
+ parser.add_argument("--port", type=int, default=7860)
1022
+ args = parser.parse_args()
1023
+
1024
+ os.environ["GRADIO_SERVER_NAME"] = args.host
1025
+ os.environ["GRADIO_SERVER_PORT"] = str(args.port)
1026
+
1027
+ demo.launch(server_name=args.host, server_port=args.port, share=False)
1028
+
1029
+ if __name__ == '__main__':
1030
+ main()
1031
+ # ```