feat(indextts2, webui): 完善 IndexTTS2 支持,新增参考音频选择与预览功能

修改内容包括:
- 修正缺失参考音频的错误提示文本
- 更新示例配置文件,新增参考音频来源配置项并将 IndexTTS2 设为默认TTS引擎
- 更新语音服务模块的注释与文档字符串
- 新增多语言适配文案,支持新UI的所有提示内容
- 重构 IndexTTS2 设置页面:支持从资源目录选择音频、上传本地音频、预览音频效果
- 调整TTS引擎选项的排序与默认选中项
This commit is contained in:
viccy 2026-06-05 23:52:31 +08:00
parent 0bd001ce33
commit 5b2487e879
6 changed files with 290 additions and 46 deletions

View File

@ -71,7 +71,7 @@ def _normalize_indextts2_reference_audio(params: VideoClipParams) -> None:
logger.info(f"IndexTTS2 使用配置中的参考音频: {configured_ref}")
return
raise ValueError("IndexTTS2 参考音频不存在,请在音频设置中上传或填写有效的参考音频路径")
raise ValueError("IndexTTS2 参考音频不存在,请在音频设置中上传或选择有效的参考音频")
def start_export_jianying_draft(task_id: str, params: VideoClipParams):

View File

@ -2236,7 +2236,7 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.
Args:
text: 要转换的文本
voice_name: 参考音频路径格式indextts2:path/to/audio.wav
voice_name: 参考音频文件格式indextts2:path/to/audio.wav
voice_file: 输出音频文件路径
speed: 语音速度此引擎暂不支持速度调节
@ -2253,7 +2253,7 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.
num_beams = config.indextts2.get("num_beams", 3)
repetition_penalty = config.indextts2.get("repetition_penalty", 10.0)
# 解析参考音频路径
# 解析参考音频文件
reference_audio_path = parse_indextts2_voice(voice_name)
if not reference_audio_path or not os.path.exists(reference_audio_path):

View File

@ -120,7 +120,8 @@
# 默认 API 地址(本地部署)
api_url = "http://127.0.0.1:8081/tts"
# 默认参考音频路径(可选)
# 默认参考音频(可选)
reference_audio_source = "resource"
# reference_audio = "/path/to/reference_audio.wav"
# 推理模式:普通推理 / 快速推理
@ -151,8 +152,8 @@
silence_duration = 0.125
[ui]
# TTS引擎选择 (edge_tts, azure_speech, soulvoice, tencent_tts, tts_qwen, doubaotts)
tts_engine = "edge_tts"
# TTS引擎选择 (indextts2, edge_tts, qwen3_tts, tencent_tts, doubaotts, azure_speech)
tts_engine = "indextts2"
# Edge TTS 配置
edge_voice_name = "zh-CN-XiaoyiNeural-Female"

View File

@ -1,5 +1,6 @@
import streamlit as st
import os
import shutil
from uuid import uuid4
from app.config import config
from app.services import voice
@ -8,6 +9,35 @@ from app.utils import utils
from webui.utils.cache import get_songs_cache
INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR = "/Users/viccy/Downloads/tts-mp3-clone/mp3"
INDEXTTS2_REFERENCE_AUDIO_COPY_SUBDIR = "indextts2_refs"
INDEXTTS2_REFERENCE_AUDIO_MAP = [
("yingshijieshuo-zh-male.mp3", "影视解说", "Film Narration"),
("maikeashe-zh-male.mp3", "麦克阿瑟", "Macintosh"),
("dong-yuhui-zh-male.mp3", "董宇辉", "Dong Yuhui"),
("fangzhenren-ad-fake-news-zh-male.mp3", "仿真人", "Realistic Human"),
("fengyin-jilupian-jieshuo-zh-male.mp3", "风吟纪录片解说", "Fengyin Documentary Narration"),
("guwo-dianying-jieshuo-zh-male.mp3", "顾我电影解说", "Guwo Film Narration"),
("jia-xiaojun-final-zh-male.mp3", "贾小军", "Jia Xiaojun"),
("junshi-zh-male.mp3", "军事解说", "Military Narration"),
("qi-tongwei-v2-zh-male.mp3", "祁同伟", "Qi Tongwei"),
("saima-niang-mambo-oye-zh-female.mp3", "赛马娘曼波欧耶版", "Uma Musume Mambo Oye Version"),
("shejian-shangde-zhongguo-zh-male.mp3", "舌尖上的中国", "A Bite of China"),
("xiaoming-jianmo-zh-male.mp3", "小明剑魔", "Xiaoming Sword Demon"),
("xin-youxi-jieshuo-zh-male.mp3", "新游戏解说", "New Game Narration"),
("xinzhong-zhicheng-zh-male.mp3", "心中之城", "City in the Heart"),
("alex-chikna-en-male.mp3", "亚历克斯", "Alex Chikna"),
("alle-en-unknown.mp3", "艾莉", "ALLE"),
("calm-normal-en-unknown.mp3", "沉稳男声", "Calm Normal"),
("donald-j-trump-noise-reduction-en-male.mp3", "唐纳德·特朗普", "Donald J. Trump"),
("elite-en-unknown.mp3", "精英男声", "ELITE"),
("horror-en-unknown.mp3", "惊悚男声", "Horror"),
("meiqu-kelong-en-unknown.mp3", "美式男声", "US Clone"),
("sarah-en-female.mp3", "莎拉", "Sarah"),
]
INDEXTTS2_REFERENCE_AUDIO_EXTENSIONS = (".mp3", ".wav", ".flac", ".m4a", ".aac", ".ogg")
def get_soulvoice_voices():
"""获取 SoulVoice 语音列表"""
# 检查是否配置了 SoulVoice API key
@ -22,12 +52,12 @@ def get_soulvoice_voices():
def get_tts_engine_options(tr=lambda key: key):
"""获取TTS引擎选项"""
return {
"indextts2": "IndexTTS2",
"edge_tts": "Edge TTS",
"azure_speech": "Azure Speech Services",
"tencent_tts": tr("Tencent Cloud TTS"),
"qwen3_tts": tr("Tongyi Qwen3 TTS"),
"indextts2": tr("IndexTTS2 Voice Clone"),
"doubaotts": tr("Doubao TTS")
"tencent_tts": tr("Tencent Cloud TTS"),
"doubaotts": tr("Doubao TTS"),
"azure_speech": "Azure Speech Services"
}
@ -59,9 +89,9 @@ def get_tts_engine_descriptions(tr=lambda key: key):
"registration": "https://dashscope.aliyuncs.com/"
},
"indextts2": {
"title": tr("IndexTTS2 Voice Clone"),
"title": "IndexTTS2",
"features": tr("IndexTTS2 features"),
"use_case": tr("IndexTTS2 download link"),
"use_case": tr("IndexTTS2 use case"),
"registration": None
},
"doubaotts": {
@ -73,6 +103,143 @@ def get_tts_engine_descriptions(tr=lambda key: key):
}
def infer_indextts2_reference_audio_language(filename):
"""根据文件名推断参考音频语言"""
lower_filename = filename.lower()
if "-zh-" in lower_filename:
return "zh"
if "-en-" in lower_filename:
return "en"
return "unknown"
def get_indextts2_reference_audio_options():
"""获取本地 IndexTTS2 参考音频选项"""
options = []
mapped_files = set()
for filename, zh_name, en_name in INDEXTTS2_REFERENCE_AUDIO_MAP:
audio_path = os.path.join(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR, filename)
if os.path.isfile(audio_path):
options.append({
"filename": filename,
"path": audio_path,
"zh": zh_name,
"en": en_name,
"language": infer_indextts2_reference_audio_language(filename),
})
mapped_files.add(filename)
if os.path.isdir(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR):
for filename in sorted(os.listdir(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR)):
if filename in mapped_files:
continue
if not filename.lower().endswith(INDEXTTS2_REFERENCE_AUDIO_EXTENSIONS):
continue
audio_path = os.path.join(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR, filename)
if not os.path.isfile(audio_path):
continue
fallback_name = os.path.splitext(filename)[0]
options.append({
"filename": filename,
"path": audio_path,
"zh": fallback_name,
"en": fallback_name,
"language": infer_indextts2_reference_audio_language(filename),
})
return options
def format_indextts2_reference_audio_option(option):
"""格式化 IndexTTS2 参考音频下拉显示名"""
zh_name = option.get("zh", "")
en_name = option.get("en", "")
language = option.get("language", "unknown")
ui_language = str(st.session_state.get("ui_language", "zh-CN")).lower()
if ui_language.startswith("en"):
display_name = en_name or zh_name or option.get("filename", "")
language_labels = {
"zh": "Chinese",
"en": "English",
}
else:
display_name = zh_name or en_name or option.get("filename", "")
language_labels = {
"zh": "中文",
"en": "英文",
}
language_label = language_labels.get(language)
if not language_label:
return display_name
return f"{display_name} ({language_label})"
def get_indextts2_reference_audio_index(options, saved_reference_audio):
"""根据已保存的参考音频文件匹配下拉选项索引"""
if not options:
return 0
saved_filename = os.path.basename(saved_reference_audio or "")
for index, option in enumerate(options):
if option["filename"] == saved_filename:
return index
return 0
def copy_indextts2_reference_audio(source_path):
"""复制一份参考音频到项目存储目录,并返回复制后的路径"""
if not source_path or not os.path.isfile(source_path):
return ""
target_dir = utils.storage_dir(INDEXTTS2_REFERENCE_AUDIO_COPY_SUBDIR, create=True)
target_path = os.path.join(target_dir, os.path.basename(source_path))
if os.path.abspath(source_path) == os.path.abspath(target_path):
return target_path
should_copy = True
if os.path.exists(target_path):
should_copy = os.path.getsize(source_path) != os.path.getsize(target_path)
if should_copy:
shutil.copy2(source_path, target_path)
return target_path
def get_audio_mime_type(audio_path):
"""根据音频文件扩展名返回 MIME 类型"""
extension = os.path.splitext(audio_path or "")[1].lower()
if extension == ".wav":
return "audio/wav"
if extension == ".ogg":
return "audio/ogg"
if extension == ".m4a":
return "audio/mp4"
if extension == ".aac":
return "audio/aac"
return "audio/mp3"
def render_reference_audio_preview_button(reference_audio, key, tr):
"""渲染参考音频试听按钮"""
can_preview = bool(reference_audio and os.path.isfile(reference_audio))
if st.button(
" ",
key=key,
icon=":material/play_arrow:",
help=tr("Preview Reference Audio Help"),
disabled=not can_preview,
use_container_width=True,
):
st.session_state["indextts2_reference_audio_preview_path"] = reference_audio
def is_valid_azure_voice_name(voice_name: str) -> bool:
"""检查是否为有效的Azure音色名称格式"""
if not voice_name or not isinstance(voice_name, str):
@ -109,11 +276,11 @@ def render_tts_settings(tr):
engine_descriptions = get_tts_engine_descriptions(tr)
# 获取保存的TTS引擎设置
saved_tts_engine = config.ui.get("tts_engine", "edge_tts")
saved_tts_engine = config.ui.get("tts_engine", "indextts2")
# 确保保存的引擎在可用选项中
if saved_tts_engine not in engine_options:
saved_tts_engine = "edge_tts"
saved_tts_engine = "indextts2"
# TTS引擎选择下拉框
selected_engine = st.selectbox(
@ -566,8 +733,6 @@ def render_qwen3_tts_settings(tr):
def render_indextts2_tts_settings(tr):
"""渲染 IndexTTS2 TTS 设置"""
import os
# API 地址配置
api_url = st.text_input(
tr("API URL"),
@ -575,29 +740,90 @@ def render_indextts2_tts_settings(tr):
help=tr("IndexTTS2 API URL Help")
)
# 参考音频文件路径
reference_audio = st.text_input(
tr("Reference Audio Path"),
value=config.indextts2.get("reference_audio", ""),
help=tr("Reference Audio Path Help")
saved_reference_audio = config.indextts2.get("reference_audio", "")
reference_audio_source_options = {
tr("Select from Resource Directory"): "resource",
tr("Upload Reference Audio"): "upload",
}
reference_audio_source_labels = list(reference_audio_source_options.keys())
saved_reference_audio_source = config.indextts2.get("reference_audio_source", "resource")
if saved_reference_audio_source not in reference_audio_source_options.values():
saved_reference_audio_source = "resource"
default_reference_audio_source_label = next(
label
for label, source_value in reference_audio_source_options.items()
if source_value == saved_reference_audio_source
)
# 文件上传功能
uploaded_file = st.file_uploader(
tr("Upload Reference Audio File"),
type=["wav", "mp3"],
help=tr("Upload Reference Audio Help")
st.markdown(f"**{tr('Reference Audio Path')}**")
reference_audio_source_label = st.pills(
tr("Reference Audio Source"),
options=reference_audio_source_labels,
selection_mode="single",
default=default_reference_audio_source_label,
key="indextts2_reference_audio_source_selection",
help=tr("Reference Audio Source Help"),
label_visibility="collapsed",
width="stretch",
)
if uploaded_file is not None:
# 保存上传的文件
import tempfile
temp_dir = tempfile.gettempdir()
audio_path = os.path.join(temp_dir, f"indextts2_ref_{uploaded_file.name}")
with open(audio_path, "wb") as f:
f.write(uploaded_file.getbuffer())
reference_audio = audio_path
st.success(tr("Audio uploaded").format(path=audio_path))
if not reference_audio_source_label:
reference_audio_source_label = default_reference_audio_source_label
reference_audio_source = reference_audio_source_options[reference_audio_source_label]
reference_audio = saved_reference_audio
reference_audio_options = get_indextts2_reference_audio_options()
if reference_audio_source == "resource" and reference_audio_options:
selected_audio_index = get_indextts2_reference_audio_index(reference_audio_options, saved_reference_audio)
select_col, preview_col = st.columns([5, 1])
with select_col:
selected_audio_option = reference_audio_options[st.selectbox(
tr("Reference Audio Path"),
options=range(len(reference_audio_options)),
index=selected_audio_index,
format_func=lambda x: format_indextts2_reference_audio_option(reference_audio_options[x]),
help=tr("Reference Audio Path Help"),
label_visibility="collapsed"
)]
reference_audio = copy_indextts2_reference_audio(selected_audio_option["path"])
with preview_col:
render_reference_audio_preview_button(
reference_audio,
"indextts2_resource_reference_audio_preview",
tr,
)
elif reference_audio_source == "resource":
st.warning(tr("No Reference Audio Resources Found"))
if reference_audio_source == "upload":
if saved_reference_audio_source != "upload":
reference_audio = ""
upload_col, preview_col = st.columns([5, 1])
with upload_col:
uploaded_file = st.file_uploader(
tr("Upload Reference Audio File"),
type=["wav", "mp3"],
help=tr("Upload Reference Audio Help"),
label_visibility="collapsed"
)
if uploaded_file is not None:
target_dir = utils.storage_dir(INDEXTTS2_REFERENCE_AUDIO_COPY_SUBDIR, create=True)
audio_path = os.path.join(target_dir, f"uploaded_{uploaded_file.name}")
with open(audio_path, "wb") as f:
f.write(uploaded_file.getbuffer())
reference_audio = audio_path
st.success(tr("Audio uploaded").format(path=audio_path))
with preview_col:
render_reference_audio_preview_button(
reference_audio,
"indextts2_upload_reference_audio_preview",
tr,
)
preview_audio_path = st.session_state.get("indextts2_reference_audio_preview_path", "")
if preview_audio_path == reference_audio and os.path.isfile(preview_audio_path):
with open(preview_audio_path, "rb") as audio_file:
st.audio(audio_file.read(), format=get_audio_mime_type(preview_audio_path))
# 推理模式
infer_mode_options = [
@ -676,6 +902,7 @@ def render_indextts2_tts_settings(tr):
# 保存配置
config.indextts2["api_url"] = api_url
config.indextts2["reference_audio_source"] = reference_audio_source
config.indextts2["reference_audio"] = reference_audio
config.indextts2["infer_mode"] = infer_mode
config.indextts2["temperature"] = temperature
@ -1175,5 +1402,5 @@ def get_audio_params():
'bgm_type': st.session_state.get('bgm_type', 'random'),
'bgm_file': st.session_state.get('bgm_file', ''),
'bgm_volume': st.session_state.get('bgm_volume', AudioVolumeDefaults.BGM_VOLUME),
'tts_engine': st.session_state.get('tts_engine', "edge_tts"),
'tts_engine': st.session_state.get('tts_engine', "indextts2"),
}

View File

@ -235,7 +235,8 @@
"Tencent Cloud TTS use case": "Personal and enterprise users who need stable Chinese speech synthesis",
"Tongyi Qwen3 TTS features": "Alibaba Cloud Tongyi Qwen speech synthesis with high-quality voices and multiple voice options.",
"High-quality Chinese speech synthesis use case": "Users who need high-quality Chinese speech synthesis",
"IndexTTS2 features": "Zero-shot voice cloning. Upload a reference audio file to synthesize speech with a matching voice. Requires local or private deployment.",
"IndexTTS2 features": "A locally or privately deployed voice-cloning engine. Choose a resource audio file or upload a reference audio file, then synthesize narration in that voice.",
"IndexTTS2 use case": "Best for fixed narrator voices, character dubbing, or generating multiple videos with the same voice. Start the IndexTTS2 API service before use. Deployment package: https://pan.quark.cn/s/0767c9bcefd5",
"IndexTTS2 download link": "Download link: https://pan.quark.cn/s/0767c9bcefd5",
"Doubao TTS features": "Volcengine Doubao speech synthesis with multiple voices and emotions, plus fast access in mainland China.",
"Select TTS Engine": "Select TTS Engine",
@ -387,9 +388,16 @@
"Select Qwen3 TTS Voice": "Select a Qwen3 TTS voice",
"API URL": "API URL",
"IndexTTS2 API URL Help": "IndexTTS2 API service URL",
"Reference Audio Path": "Reference Audio Path",
"Reference Audio Path Help": "Reference audio file path for voice cloning (WAV format, 3-10 seconds recommended)",
"Upload Reference Audio File": "Or Upload Reference Audio File",
"Reference Audio Source": "Reference Audio Source",
"Reference Audio Source Help": "Choose a reference audio from the resource directory or upload a new one.",
"Select from Resource Directory": "Select from Resource Directory",
"Upload Reference Audio": "Upload Reference Audio",
"Reference Audio Path": "Reference Audio",
"Reference Audio Path Help": "Choose the reference audio for voice cloning (WAV/MP3, 3-10 seconds recommended)",
"No Reference Audio Resources Found": "No reference audio resources found. Please upload a reference audio file.",
"Preview Reference Audio": "Preview",
"Preview Reference Audio Help": "Play the selected reference audio.",
"Upload Reference Audio File": "Upload Reference Audio File",
"Upload Reference Audio Help": "Upload a clear audio clip for voice cloning",
"Audio uploaded": "✅ Audio uploaded: {path}",
"Inference Mode": "Inference Mode",

View File

@ -216,7 +216,8 @@
"Tencent Cloud TTS use case": "个人和企业用户,需要稳定的中文语音合成",
"Tongyi Qwen3 TTS features": "阿里云通义千问语音合成,音质优秀,支持多种音色",
"High-quality Chinese speech synthesis use case": "需要高质量中文语音合成的用户",
"IndexTTS2 features": "零样本语音克隆,上传参考音频即可合成相同音色的语音,需要本地或私有部署",
"IndexTTS2 features": "本地/私有部署的语音克隆引擎。选择资源目录音频或上传参考音频后,可按该音色合成旁白。",
"IndexTTS2 use case": "适合需要固定旁白音色、角色配音或批量生成同一音色视频的场景。使用前请先启动 IndexTTS2 API 服务部署包下载https://pan.quark.cn/s/0767c9bcefd5",
"IndexTTS2 download link": "下载地址https://pan.quark.cn/s/0767c9bcefd5",
"Doubao TTS features": "火山引擎豆包语音合成,支持多种音色和情感,国内访问速度快",
"Select TTS Engine": "选择 TTS 引擎",
@ -369,9 +370,16 @@
"Select Qwen3 TTS Voice": "选择 Qwen3 TTS 音色",
"API URL": "API 地址",
"IndexTTS2 API URL Help": "IndexTTS2 API 服务地址",
"Reference Audio Path": "参考音频路径",
"Reference Audio Path Help": "用于语音克隆的参考音频文件路径WAV 格式,建议 3-10 秒)",
"Upload Reference Audio File": "或上传参考音频文件",
"Reference Audio Source": "参考音频来源",
"Reference Audio Source Help": "选择从资源目录选择参考音频,或上传新的参考音频",
"Select from Resource Directory": "从资源目录选择",
"Upload Reference Audio": "上传参考音频",
"Reference Audio Path": "参考音频",
"Reference Audio Path Help": "选择用于语音克隆的参考音频WAV/MP3 格式,建议 3-10 秒)",
"No Reference Audio Resources Found": "未找到资源目录中的参考音频,请上传参考音频文件",
"Preview Reference Audio": "试听",
"Preview Reference Audio Help": "播放当前参考音频",
"Upload Reference Audio File": "上传参考音频文件",
"Upload Reference Audio Help": "上传一段清晰的音频用于语音克隆",
"Audio uploaded": "✅ 音频已上传: {path}",
"Inference Mode": "推理模式",