Upload app.py with huggingface_hub
Browse files
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="
|
| 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 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 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 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
| 125 |
"""
|
| 126 |
-
|
| 127 |
-
激活生成按钮
|
| 128 |
"""
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
with gr.Row():
|
| 139 |
-
os.
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
| 142 |
with gr.Column():
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 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 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 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 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
"""
|
| 211 |
-
demo.launch(server_name="0.0.0.0", server_port=cmd_args.port)
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# ```
|