feat(tts): 新增IndexTTS-2语音合成引擎支持

实现兼容IndexTTS2-Pack API的完整TTS调用流程,包含音频下载、错误重试等处理
重构原有IndexTTS-1.5代码,抽象通用逻辑以同时兼容indextts和indextts2两个引擎
新增IndexTTS-2的WebUI配置界面,支持情感控制与高级生成参数调整
更新配置示例文件与中英多语言文案,完善配置迁移逻辑兼容旧版配置
新增对应单元测试覆盖参数处理与配置迁移流程
This commit is contained in:
viccy 2026-06-06 14:31:09 +08:00
parent a2645aebd3
commit d147fe66e4
10 changed files with 689 additions and 125 deletions

View File

@ -10,34 +10,51 @@ root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__fi
config_file = f"{root_dir}/config.toml"
version_file = f"{root_dir}/project_version"
INDEXTTS_ENGINE = "indextts"
INDEXTTS_LEGACY_ENGINE = "indextts2"
INDEXTTS_DISPLAY_NAME = "IndexTTS-1.5"
INDEXTTS2_ENGINE = "indextts2"
INDEXTTS2_DISPLAY_NAME = "IndexTTS-2"
INDEXTTS_VOICE_PREFIX = f"{INDEXTTS_ENGINE}:"
INDEXTTS_LEGACY_VOICE_PREFIX = f"{INDEXTTS_LEGACY_ENGINE}:"
INDEXTTS2_VOICE_PREFIX = f"{INDEXTTS2_ENGINE}:"
def normalize_tts_engine_name(tts_engine: str) -> str:
if tts_engine == INDEXTTS_LEGACY_ENGINE:
return INDEXTTS_ENGINE
return tts_engine
def normalize_indextts_voice_prefix(voice_name: str) -> str:
if isinstance(voice_name, str) and voice_name.startswith(INDEXTTS_LEGACY_VOICE_PREFIX):
return f"{INDEXTTS_VOICE_PREFIX}{voice_name[len(INDEXTTS_LEGACY_VOICE_PREFIX):]}"
return voice_name
def _is_legacy_indextts2_config(indextts2_config) -> bool:
if not isinstance(indextts2_config, dict):
return False
api_url = str(indextts2_config.get("api_url", ""))
has_indextts2_fields = any(
key in indextts2_config
for key in (
"emotion_mode",
"emotion_alpha",
"max_text_tokens_per_segment",
"max_mel_tokens",
"vec_calm",
)
)
return "8081" in api_url and not has_indextts2_fields
def migrate_indextts_config(config_data):
if "indextts" not in config_data and INDEXTTS_LEGACY_ENGINE in config_data:
config_data["indextts"] = config_data[INDEXTTS_LEGACY_ENGINE]
migrated_legacy_indextts2 = _is_legacy_indextts2_config(config_data.get(INDEXTTS2_ENGINE))
if migrated_legacy_indextts2:
if "indextts" not in config_data:
config_data["indextts"] = config_data[INDEXTTS2_ENGINE]
config_data.pop(INDEXTTS2_ENGINE, None)
ui_config = config_data.get("ui")
if isinstance(ui_config, dict):
if "tts_engine" in ui_config:
ui_config["tts_engine"] = normalize_tts_engine_name(ui_config.get("tts_engine", ""))
if "voice_name" in ui_config:
ui_config["voice_name"] = normalize_indextts_voice_prefix(ui_config.get("voice_name", ""))
if migrated_legacy_indextts2 and ui_config.get("tts_engine") == INDEXTTS2_ENGINE:
ui_config["tts_engine"] = INDEXTTS_ENGINE
if ui_config.get("voice_name", "").startswith(INDEXTTS2_VOICE_PREFIX) and ui_config.get("tts_engine") == INDEXTTS_ENGINE:
ui_config["voice_name"] = f"{INDEXTTS_VOICE_PREFIX}{ui_config['voice_name'][len(INDEXTTS2_VOICE_PREFIX):]}"
return config_data
@ -113,7 +130,7 @@ def save_config():
_cfg["tts_qwen"] = tts_qwen
_cfg["fun_asr"] = fun_asr
_cfg["indextts"] = indextts
_cfg.pop(INDEXTTS_LEGACY_ENGINE, None)
_cfg["indextts2"] = indextts2
_cfg["doubaotts"] = doubaotts
f.write(toml.dumps(_cfg))
@ -129,7 +146,8 @@ ui = _cfg.get("ui", {})
frames = _cfg.get("frames", {})
tts_qwen = _cfg.get("tts_qwen", {})
fun_asr = _cfg.get("fun_asr", {})
indextts = _cfg.get("indextts", _cfg.get(INDEXTTS_LEGACY_ENGINE, {}))
indextts = _cfg.get("indextts", {})
indextts2 = _cfg.get("indextts2", {})
doubaotts = _cfg.get("doubaotts", {})
hostname = socket.gethostname()

View File

@ -64,7 +64,7 @@ hide_config = true
self.assertEqual("Pro/zai-org/GLM-5", saved_config["app"]["text_openai_model_name"])
self.assertTrue(saved_config["app"]["hide_config"])
def test_indextts_legacy_config_is_migrated(self):
def test_legacy_indextts2_config_is_migrated_to_indextts_15(self):
migrated = cfg.migrate_indextts_config(
{
"indextts2": {"api_url": "http://127.0.0.1:8081/tts"},
@ -76,9 +76,30 @@ hide_config = true
)
self.assertEqual("http://127.0.0.1:8081/tts", migrated["indextts"]["api_url"])
self.assertNotIn("indextts2", migrated)
self.assertEqual("indextts", migrated["ui"]["tts_engine"])
self.assertEqual("indextts:/tmp/reference.wav", migrated["ui"]["voice_name"])
def test_indextts2_config_is_kept_as_separate_engine(self):
migrated = cfg.migrate_indextts_config(
{
"indextts": {"api_url": "http://127.0.0.1:8081/tts"},
"indextts2": {
"api_url": "http://192.168.3.6:7863/tts",
"emotion_mode": "speaker",
},
"ui": {
"tts_engine": "indextts2",
"voice_name": "indextts2:/tmp/reference.wav",
},
}
)
self.assertEqual("http://127.0.0.1:8081/tts", migrated["indextts"]["api_url"])
self.assertEqual("http://192.168.3.6:7863/tts", migrated["indextts2"]["api_url"])
self.assertEqual("indextts2", migrated["ui"]["tts_engine"])
self.assertEqual("indextts2:/tmp/reference.wav", migrated["ui"]["voice_name"])
class OpenAICompatibleModelDefaultsTests(unittest.TestCase):
def test_ui_keeps_full_model_name_and_openai_provider(self):

View File

@ -49,14 +49,20 @@ def get_audio_duration_ffprobe(audio_file: str) -> float:
return get_media_duration_ffprobe(audio_file)
def _strip_indextts_prefix(voice_name: str) -> str:
voice_name = config.normalize_indextts_voice_prefix(voice_name or "")
prefix = config.INDEXTTS_VOICE_PREFIX
def _strip_tts_voice_prefix(voice_name: str, prefix: str) -> str:
voice_name = voice_name or ""
if voice_name.startswith(prefix):
return voice_name[len(prefix):]
return voice_name
def _strip_indextts_prefix(voice_name: str) -> str:
return _strip_tts_voice_prefix(
config.normalize_indextts_voice_prefix(voice_name or ""),
config.INDEXTTS_VOICE_PREFIX,
)
def _floor_duration_to_milliseconds(duration: float) -> float:
return int(duration * 1000) / 1000.0
@ -101,24 +107,32 @@ def _clamp_duration_to_media(
def _normalize_indextts_reference_audio(params: VideoClipParams) -> None:
"""Ensure IndexTTS-1.5 uses the configured reference audio instead of a stale UI voice."""
"""Ensure IndexTTS engines use the configured reference audio instead of a stale UI voice."""
params.tts_engine = config.normalize_tts_engine_name(params.tts_engine)
if params.tts_engine != config.INDEXTTS_ENGINE:
if params.tts_engine == config.INDEXTTS_ENGINE:
tts_config = config.indextts
voice_prefix = config.INDEXTTS_VOICE_PREFIX
display_name = "IndexTTS-1.5"
elif params.tts_engine == config.INDEXTTS2_ENGINE:
tts_config = config.indextts2
voice_prefix = config.INDEXTTS2_VOICE_PREFIX
display_name = "IndexTTS-2"
else:
return
candidate = _strip_indextts_prefix(getattr(params, "voice_name", "") or "")
candidate = _strip_tts_voice_prefix(getattr(params, "voice_name", "") or "", voice_prefix)
if candidate and os.path.isfile(candidate):
params.voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{candidate}"
logger.info(f"IndexTTS-1.5 使用参考音频: {candidate}")
params.voice_name = f"{voice_prefix}{candidate}"
logger.info(f"{display_name} 使用参考音频: {candidate}")
return
configured_ref = _strip_indextts_prefix(config.indextts.get("reference_audio", "") or "")
configured_ref = _strip_tts_voice_prefix(tts_config.get("reference_audio", "") or "", voice_prefix)
if configured_ref and os.path.isfile(configured_ref):
params.voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{configured_ref}"
logger.info(f"IndexTTS-1.5 使用配置中的参考音频: {configured_ref}")
params.voice_name = f"{voice_prefix}{configured_ref}"
logger.info(f"{display_name} 使用配置中的参考音频: {configured_ref}")
return
raise ValueError("IndexTTS-1.5 参考音频不存在,请在音频设置中上传或选择有效的参考音频")
raise ValueError(f"{display_name} 参考音频不存在,请在音频设置中上传或选择有效的参考音频")
def start_export_jianying_draft(task_id: str, params: VideoClipParams):

View File

@ -31,14 +31,25 @@ class JianyingTaskTests(unittest.TestCase):
self.assertEqual(f"indextts:{ref_path}", params.voice_name)
def test_normalize_indextts_accepts_legacy_engine_and_prefix(self):
def test_normalize_indextts2_uses_valid_param_reference(self):
with tempfile.NamedTemporaryFile(suffix=".wav") as ref:
params = VideoClipParams(tts_engine="indextts2", voice_name=f"indextts2:{ref.name}")
jianying_task._normalize_indextts_reference_audio(params)
self.assertEqual("indextts", params.tts_engine)
self.assertEqual(f"indextts:{ref.name}", params.voice_name)
self.assertEqual("indextts2", params.tts_engine)
self.assertEqual(f"indextts2:{ref.name}", params.voice_name)
def test_normalize_indextts2_uses_config_reference_when_param_is_stale(self):
with tempfile.TemporaryDirectory() as temp_dir:
ref_path = Path(temp_dir) / "reference.wav"
ref_path.write_bytes(b"fake wav")
params = VideoClipParams(tts_engine="indextts2", voice_name="zh-CN-YunjianNeural")
with patch.dict(jianying_task.config.indextts2, {"reference_audio": str(ref_path)}, clear=False):
jianying_task._normalize_indextts_reference_audio(params)
self.assertEqual(f"indextts2:{ref_path}", params.voice_name)
def test_normalize_indextts_requires_existing_reference_audio(self):
params = VideoClipParams(tts_engine="indextts", voice_name="zh-CN-YunjianNeural")

View File

@ -21,6 +21,7 @@ except ImportError:
MOVIEPY_AVAILABLE = False
logger.warning("moviepy 未安装,将使用估算方法计算音频时长")
import time
from urllib.parse import urljoin
from app.config import config
from app.utils import utils
@ -1293,6 +1294,10 @@ def tts(
if tts_engine == config.INDEXTTS_ENGINE:
logger.info("分发到 IndexTTS-1.5")
return indextts_tts(text, voice_name, voice_file, speed=voice_rate)
if tts_engine == config.INDEXTTS2_ENGINE:
logger.info("分发到 IndexTTS-2")
return indextts2_tts(text, voice_name, voice_file)
if tts_engine == "doubaotts":
logger.info("分发到豆包语音 TTS")
@ -1778,12 +1783,13 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f
voice_name = config.normalize_indextts_voice_prefix(parse_voice_name(voice_name))
output_dir = utils.task_dir(task_id)
tts_results = []
audio_extension = ".wav" if tts_engine in (config.INDEXTTS_ENGINE, config.INDEXTTS2_ENGINE) else ".mp3"
for item in list_script:
if item['OST'] != 1:
# 将时间戳中的冒号替换为下划线
timestamp = item['timestamp'].replace(':', '_')
audio_file = os.path.join(output_dir, f"audio_{timestamp}.mp3")
audio_file = os.path.join(output_dir, f"audio_{timestamp}{audio_extension}")
subtitle_file = os.path.join(output_dir, f"subtitle_{timestamp}.srt")
text = item['narration']
@ -1803,8 +1809,13 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f
f"或者使用其他 tts 引擎")
continue
else:
# SoulVoice、Qwen3、IndexTTS-1.5、豆包语音 引擎不生成字幕文件
if is_soulvoice_voice(voice_name) or is_qwen_engine(tts_engine) or tts_engine == config.INDEXTTS_ENGINE or tts_engine == "doubaotts":
# SoulVoice、Qwen3、IndexTTS、豆包语音 引擎不生成精确字幕文件
if (
is_soulvoice_voice(voice_name)
or is_qwen_engine(tts_engine)
or tts_engine in (config.INDEXTTS_ENGINE, config.INDEXTTS2_ENGINE)
or tts_engine == "doubaotts"
):
# 获取实际音频文件的时长
duration = get_audio_duration_from_file(audio_file)
if duration <= 0:
@ -2234,6 +2245,17 @@ def parse_indextts_voice(voice_name: str) -> str:
return voice_name
def parse_indextts2_voice(voice_name: str) -> str:
"""
解析 IndexTTS-2 语音名称
支持格式indextts2:reference_audio_path
返回参考音频文件路径
"""
if isinstance(voice_name, str) and voice_name.startswith(config.INDEXTTS2_VOICE_PREFIX):
return voice_name[len(config.INDEXTTS2_VOICE_PREFIX):]
return voice_name
def indextts_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0) -> Union[SubMaker, None]:
"""
使用 IndexTTS-1.5 API 进行零样本语音克隆
@ -2344,3 +2366,130 @@ def indextts_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0
logger.error("IndexTTS-1.5 TTS 生成失败,已达到最大重试次数")
return None
def _normalize_indextts2_api_url(api_url: str) -> str:
api_url = (api_url or "http://192.168.3.6:7863/tts").strip()
if api_url.endswith("/tts"):
return api_url
return f"{api_url.rstrip('/')}/tts"
def _get_configured_proxies() -> dict:
if not config.proxy.get("http"):
return {}
return {
"http": config.proxy.get("http"),
"https": config.proxy.get("https", config.proxy.get("http")),
}
def _download_indextts2_audio(response: requests.Response, api_url: str, voice_file: str, proxies: dict) -> bool:
content_type = response.headers.get("content-type", "").lower()
if "application/json" not in content_type:
with open(voice_file, "wb") as f:
f.write(response.content)
return os.path.getsize(voice_file) > 0
result = response.json()
downloads = result.get("downloads") if isinstance(result, dict) else {}
download_url = downloads.get("wav") if isinstance(downloads, dict) else ""
if not download_url:
logger.error(f"IndexTTS-2 API 响应中没有音频下载地址: {result}")
return False
audio_url = urljoin(api_url, download_url)
audio_response = requests.get(audio_url, proxies=proxies, timeout=120)
if audio_response.status_code != 200:
logger.error(f"IndexTTS-2 音频下载失败: {audio_response.status_code} - {audio_response.text}")
return False
with open(voice_file, "wb") as f:
f.write(audio_response.content)
return os.path.getsize(voice_file) > 0
def indextts2_tts(text: str, voice_name: str, voice_file: str) -> Union[SubMaker, None]:
"""
使用 IndexTTS-2 API 进行零样本语音克隆
接口兼容 IndexTTS2-Pack POST /tts multipart form
"""
api_url = _normalize_indextts2_api_url(config.indextts2.get("api_url", "http://192.168.3.6:7863/tts"))
reference_audio_path = parse_indextts2_voice(voice_name)
if not reference_audio_path or not os.path.exists(reference_audio_path):
logger.error(f"IndexTTS-2 参考音频文件不存在: {reference_audio_path}")
return None
emotion_mode = config.indextts2.get("emotion_mode", "speaker")
emotion_audio_path = config.indextts2.get("emotion_audio", "")
data = {
"text": text.strip(),
"emotion_mode": emotion_mode,
"emotion_alpha": config.indextts2.get("emotion_alpha", 0.65),
"emotion_text": config.indextts2.get("emotion_text", ""),
"use_random": str(bool(config.indextts2.get("use_random", False))).lower(),
"max_text_tokens_per_segment": config.indextts2.get("max_text_tokens_per_segment", 120),
"vec_happy": config.indextts2.get("vec_happy", 0.0),
"vec_angry": config.indextts2.get("vec_angry", 0.0),
"vec_sad": config.indextts2.get("vec_sad", 0.0),
"vec_afraid": config.indextts2.get("vec_afraid", 0.0),
"vec_disgusted": config.indextts2.get("vec_disgusted", 0.0),
"vec_melancholic": config.indextts2.get("vec_melancholic", 0.0),
"vec_surprised": config.indextts2.get("vec_surprised", 0.0),
"vec_calm": config.indextts2.get("vec_calm", 0.8),
"temperature": config.indextts2.get("temperature", 0.8),
"top_p": config.indextts2.get("top_p", 0.8),
"top_k": config.indextts2.get("top_k", 30),
"num_beams": config.indextts2.get("num_beams", 3),
"repetition_penalty": config.indextts2.get("repetition_penalty", 10.0),
"max_mel_tokens": config.indextts2.get("max_mel_tokens", 1500),
}
proxies = _get_configured_proxies()
for attempt in range(3):
files = {}
try:
files["speaker_audio"] = open(reference_audio_path, "rb")
if emotion_mode == "audio":
if not emotion_audio_path or not os.path.exists(emotion_audio_path):
logger.error(f"IndexTTS-2 情感参考音频文件不存在: {emotion_audio_path}")
return None
files["emotion_audio"] = open(emotion_audio_path, "rb")
logger.info(f"{attempt + 1} 次调用 IndexTTS-2 API: {api_url}")
response = requests.post(
api_url,
files=files,
data=data,
proxies=proxies,
timeout=180,
)
if response.status_code == 200 and _download_indextts2_audio(response, api_url, voice_file, proxies):
logger.info(f"IndexTTS-2 成功生成音频: {voice_file}, 大小: {os.path.getsize(voice_file)} 字节")
sub_maker = new_sub_maker()
duration = get_audio_duration_from_file(voice_file)
duration_ms = int(duration * 1000) if duration > 0 else max(1000, int(len(text) * 200))
add_subtitle_event(sub_maker, 0, duration_ms * 10000, text)
return sub_maker
logger.error(f"IndexTTS-2 API 调用失败: {response.status_code} - {response.text}")
except requests.exceptions.Timeout:
logger.error(f"IndexTTS-2 API 调用超时 (尝试 {attempt + 1}/3)")
except requests.exceptions.RequestException as e:
logger.error(f"IndexTTS-2 API 网络错误: {str(e)} (尝试 {attempt + 1}/3)")
except Exception as e:
logger.error(f"IndexTTS-2 TTS 处理错误: {str(e)} (尝试 {attempt + 1}/3)")
finally:
for file_obj in files.values():
try:
file_obj.close()
except Exception:
pass
if attempt < 2:
time.sleep(2)
logger.error("IndexTTS-2 TTS 生成失败,已达到最大重试次数")
return None

View File

@ -113,21 +113,21 @@
# 使用阿里百炼在线 fun-asr 时,访问 https://bailian.console.aliyun.com/?tab=model#/api-key 获取 API Key
api_key = ""
model = "fun-asr"
[indextts]
# IndexTTS-1.5 语音克隆配置
# 这是一个开源的零样本语音克隆项目,需要自行部署
# 项目地址https://github.com/index-tts/index-tts
# 默认 API 地址(本地部署)
api_url = "http://127.0.0.1:8081/tts"
# 默认参考音频(可选)
reference_audio_source = "resource"
# reference_audio = "/path/to/reference_audio.wav"
# 推理模式:普通推理 / 快速推理
infer_mode = "普通推理"
# 高级参数
temperature = 1.0
top_p = 0.8
@ -135,6 +135,42 @@
do_sample = true
num_beams = 3
repetition_penalty = 10.0
[indextts2]
# IndexTTS-2 语音克隆配置
# 支持 IndexTTS2-Pack FastAPI 接口POST /tts
api_url = "http://192.168.3.6:7863/tts"
# 默认参考音频(可选),音色列表复用 IndexTTS-1.5 的资源目录
reference_audio_source = "resource"
# reference_audio = "/path/to/reference_audio.wav"
# 情感控制speaker / audio / vector / text
emotion_mode = "speaker"
emotion_audio = ""
emotion_alpha = 0.65
emotion_text = ""
use_random = false
max_text_tokens_per_segment = 120
# 8 维情感向量顺序happy, angry, sad, afraid, disgusted, melancholic, surprised, calm
vec_happy = 0.0
vec_angry = 0.0
vec_sad = 0.0
vec_afraid = 0.0
vec_disgusted = 0.0
vec_melancholic = 0.0
vec_surprised = 0.0
vec_calm = 0.8
# 高级生成参数
temperature = 0.8
top_p = 0.8
top_k = 30
num_beams = 3
repetition_penalty = 10.0
max_mel_tokens = 1500
[doubaotts]
# 豆包语音 TTS 配置
# 申请流程:
@ -153,7 +189,7 @@
silence_duration = 0.125
[ui]
# TTS引擎选择 (indextts, edge_tts, qwen3_tts, tencent_tts, doubaotts, azure_speech)
# TTS引擎选择 (indextts, indextts2, edge_tts, qwen3_tts, tencent_tts, doubaotts, azure_speech)
tts_engine = "indextts"
# Edge TTS 配置

View File

@ -233,6 +233,11 @@ def get_voice_name_for_tts_engine(tts_engine: str) -> str:
return f"tencent:{config.ui.get('tencent_voice_type', '101001')}"
if tts_engine == 'qwen3_tts':
return f"qwen3:{config.ui.get('qwen_voice_type', 'Cherry')}"
if tts_engine == config.INDEXTTS2_ENGINE:
reference_audio = config.indextts2.get('reference_audio', '')
if reference_audio:
return f"{config.INDEXTTS2_VOICE_PREFIX}{reference_audio}"
return config.ui.get('voice_name', '')
if config.normalize_tts_engine_name(tts_engine) == config.INDEXTTS_ENGINE:
reference_audio = config.indextts.get('reference_audio', '')
if reference_audio:

View File

@ -57,6 +57,7 @@ def get_tts_engine_options(tr=lambda key: key):
"""获取TTS引擎选项"""
return {
config.INDEXTTS_ENGINE: config.INDEXTTS_DISPLAY_NAME,
config.INDEXTTS2_ENGINE: config.INDEXTTS2_DISPLAY_NAME,
"edge_tts": "Edge TTS",
"qwen3_tts": tr("Tongyi Qwen3 TTS"),
"tencent_tts": tr("Tencent Cloud TTS"),
@ -98,6 +99,12 @@ def get_tts_engine_descriptions(tr=lambda key: key):
"use_case": tr("IndexTTS use case"),
"registration": None
},
config.INDEXTTS2_ENGINE: {
"title": config.INDEXTTS2_DISPLAY_NAME,
"features": tr("IndexTTS2 features"),
"use_case": tr("IndexTTS2 use case"),
"registration": None
},
"doubaotts": {
"title": tr("Doubao TTS"),
"features": tr("Doubao TTS features"),
@ -325,7 +332,7 @@ def get_audio_mime_type(audio_path):
return "audio/mp3"
def render_reference_audio_preview_button(reference_audio, key, tr):
def render_reference_audio_preview_button(reference_audio, key, tr, preview_state_key="indextts_reference_audio_preview_path"):
"""渲染参考音频试听按钮"""
can_preview = bool(reference_audio and os.path.isfile(reference_audio))
if st.button(
@ -336,7 +343,102 @@ def render_reference_audio_preview_button(reference_audio, key, tr):
disabled=not can_preview,
use_container_width=True,
):
st.session_state["indextts_reference_audio_preview_path"] = reference_audio
st.session_state[preview_state_key] = reference_audio
def render_indextts_reference_audio_selector(tr, tts_config, key_prefix):
"""渲染 IndexTTS 系列共用的参考音频选择器。"""
saved_reference_audio = tts_config.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 = tts_config.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
)
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=f"{key_prefix}_reference_audio_source_selection",
help=tr("Reference Audio Source Help"),
label_visibility="collapsed",
width="stretch",
)
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
preview_state_key = f"{key_prefix}_reference_audio_preview_path"
reference_audio_options = get_indextts_reference_audio_options()
if reference_audio_source == "resource" and reference_audio_options:
selected_audio_index = get_indextts_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_indextts_reference_audio_option(reference_audio_options[x]),
help=tr("Reference Audio Path Help"),
label_visibility="collapsed",
key=f"{key_prefix}_reference_audio_select",
)]
reference_audio = copy_indextts_reference_audio(selected_audio_option["path"])
with preview_col:
render_reference_audio_preview_button(
reference_audio,
f"{key_prefix}_resource_reference_audio_preview",
tr,
preview_state_key=preview_state_key,
)
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",
key=f"{key_prefix}_reference_audio_upload",
)
if uploaded_file is not None:
target_dir = utils.storage_dir(INDEXTTS_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,
f"{key_prefix}_upload_reference_audio_preview",
tr,
preview_state_key=preview_state_key,
)
preview_audio_path = st.session_state.get(preview_state_key, "")
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))
return reference_audio_source, reference_audio
def render_bgm_preview_button(bgm_file, key, tr):
@ -442,6 +544,8 @@ def render_tts_settings(tr):
render_qwen3_tts_settings(tr)
elif selected_engine == config.INDEXTTS_ENGINE:
render_indextts_tts_settings(tr)
elif selected_engine == config.INDEXTTS2_ENGINE:
render_indextts2_tts_settings(tr)
elif selected_engine == "doubaotts":
render_doubaotts_settings(tr)
@ -861,90 +965,11 @@ def render_indextts_tts_settings(tr):
help=tr("IndexTTS API URL Help")
)
saved_reference_audio = config.indextts.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.indextts.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
reference_audio_source, reference_audio = render_indextts_reference_audio_selector(
tr,
config.indextts,
"indextts",
)
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="indextts_reference_audio_source_selection",
help=tr("Reference Audio Source Help"),
label_visibility="collapsed",
width="stretch",
)
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_indextts_reference_audio_options()
if reference_audio_source == "resource" and reference_audio_options:
selected_audio_index = get_indextts_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_indextts_reference_audio_option(reference_audio_options[x]),
help=tr("Reference Audio Path Help"),
label_visibility="collapsed"
)]
reference_audio = copy_indextts_reference_audio(selected_audio_option["path"])
with preview_col:
render_reference_audio_preview_button(
reference_audio,
"indextts_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(INDEXTTS_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,
"indextts_upload_reference_audio_preview",
tr,
)
preview_audio_path = st.session_state.get("indextts_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 = [
@ -1038,6 +1063,217 @@ def render_indextts_tts_settings(tr):
config.ui["voice_name"] = f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}"
def render_indextts2_tts_settings(tr):
"""渲染 IndexTTS-2 TTS 设置"""
api_url = st.text_input(
tr("API URL"),
value=config.indextts2.get("api_url", "http://192.168.3.6:7863/tts"),
help=tr("IndexTTS2 API URL Help")
)
reference_audio_source, reference_audio = render_indextts_reference_audio_selector(
tr,
config.indextts2,
"indextts2",
)
emotion_mode_options = [
("speaker", tr("Emotion Mode Speaker")),
("audio", tr("Emotion Mode Audio")),
("vector", tr("Emotion Mode Vector")),
("text", tr("Emotion Mode Text")),
]
saved_emotion_mode = config.indextts2.get("emotion_mode", "speaker")
emotion_mode_values = [item[0] for item in emotion_mode_options]
if saved_emotion_mode not in emotion_mode_values:
saved_emotion_mode = "speaker"
with st.expander(tr("IndexTTS2 Emotion Parameters"), expanded=False):
emotion_mode = emotion_mode_options[st.selectbox(
tr("Emotion Mode"),
options=range(len(emotion_mode_options)),
index=emotion_mode_values.index(saved_emotion_mode),
format_func=lambda x: emotion_mode_options[x][1],
help=tr("Emotion Mode Help"),
)][0]
emotion_alpha = st.slider(
tr("Emotion Alpha"),
min_value=0.0,
max_value=1.0,
value=float(config.indextts2.get("emotion_alpha", 0.65)),
step=0.05,
help=tr("Emotion Alpha Help"),
)
emotion_audio = config.indextts2.get("emotion_audio", "")
emotion_text = config.indextts2.get("emotion_text", "")
if emotion_mode == "audio":
emotion_audio_col, emotion_preview_col = st.columns([5, 1])
with emotion_audio_col:
emotion_audio = st.text_input(
tr("Emotion Reference Audio Path"),
value=emotion_audio,
help=tr("Emotion Reference Audio Path Help"),
)
with emotion_preview_col:
render_reference_audio_preview_button(
emotion_audio,
"indextts2_emotion_audio_preview",
tr,
preview_state_key="indextts2_emotion_audio_preview_path",
)
preview_audio_path = st.session_state.get("indextts2_emotion_audio_preview_path", "")
if preview_audio_path == emotion_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))
elif emotion_mode == "text":
emotion_text = st.text_input(
tr("Emotion Text"),
value=emotion_text,
help=tr("Emotion Text Help"),
placeholder=tr("Emotion Text Placeholder"),
)
use_random = st.checkbox(
tr("Use Random Emotion"),
value=bool(config.indextts2.get("use_random", False)),
help=tr("Use Random Emotion Help"),
)
emotion_vector_defaults = {
"vec_happy": 0.0,
"vec_angry": 0.0,
"vec_sad": 0.0,
"vec_afraid": 0.0,
"vec_disgusted": 0.0,
"vec_melancholic": 0.0,
"vec_surprised": 0.0,
"vec_calm": 0.8,
}
emotion_vector_labels = {
"vec_happy": tr("Emotion Happy"),
"vec_angry": tr("Emotion Angry"),
"vec_sad": tr("Emotion Sad"),
"vec_afraid": tr("Emotion Afraid"),
"vec_disgusted": tr("Emotion Disgusted"),
"vec_melancholic": tr("Emotion Melancholic"),
"vec_surprised": tr("Emotion Surprised"),
"vec_calm": tr("Emotion Calm"),
}
emotion_vector_values = {}
if emotion_mode == "vector":
vec_cols = st.columns(2)
for index, (field, default_value) in enumerate(emotion_vector_defaults.items()):
with vec_cols[index % 2]:
emotion_vector_values[field] = st.slider(
emotion_vector_labels[field],
min_value=0.0,
max_value=1.0,
value=float(config.indextts2.get(field, default_value)),
step=0.05,
)
else:
emotion_vector_values = {
field: float(config.indextts2.get(field, default_value))
for field, default_value in emotion_vector_defaults.items()
}
with st.expander(tr("Advanced Parameters"), expanded=False):
col1, col2 = st.columns(2)
with col1:
temperature = st.slider(
tr("Sampling Temperature"),
min_value=0.1,
max_value=2.0,
value=float(config.indextts2.get("temperature", 0.8)),
step=0.1,
help=tr("Sampling Temperature Help")
)
top_p = st.slider(
"Top P",
min_value=0.0,
max_value=1.0,
value=float(config.indextts2.get("top_p", 0.8)),
step=0.05,
help=tr("Top P Help")
)
top_k = st.slider(
"Top K",
min_value=0,
max_value=100,
value=int(config.indextts2.get("top_k", 30)),
step=5,
help=tr("Top K Help")
)
max_text_tokens_per_segment = st.slider(
tr("Max Text Tokens Per Segment"),
min_value=20,
max_value=600,
value=int(config.indextts2.get("max_text_tokens_per_segment", 120)),
step=10,
help=tr("Max Text Tokens Per Segment Help")
)
with col2:
num_beams = st.slider(
tr("Num Beams"),
min_value=1,
max_value=10,
value=int(config.indextts2.get("num_beams", 3)),
step=1,
help=tr("Num Beams Help")
)
repetition_penalty = st.slider(
tr("Repetition Penalty"),
min_value=0.1,
max_value=20.0,
value=float(config.indextts2.get("repetition_penalty", 10.0)),
step=0.1,
help=tr("Repetition Penalty Help")
)
max_mel_tokens = st.slider(
tr("Max Mel Tokens"),
min_value=50,
max_value=1815,
value=int(config.indextts2.get("max_mel_tokens", 1500)),
step=10,
help=tr("Max Mel Tokens Help")
)
with st.expander(tr("IndexTTS2 Usage Instructions Title"), expanded=False):
st.markdown(tr("IndexTTS2 Usage Instructions"))
config.indextts2["api_url"] = api_url
config.indextts2["reference_audio_source"] = reference_audio_source
config.indextts2["reference_audio"] = reference_audio
config.indextts2["emotion_mode"] = emotion_mode
config.indextts2["emotion_audio"] = emotion_audio
config.indextts2["emotion_alpha"] = emotion_alpha
config.indextts2["emotion_text"] = emotion_text
config.indextts2["use_random"] = use_random
config.indextts2["max_text_tokens_per_segment"] = max_text_tokens_per_segment
for field, value in emotion_vector_values.items():
config.indextts2[field] = value
config.indextts2["temperature"] = temperature
config.indextts2["top_p"] = top_p
config.indextts2["top_k"] = top_k
config.indextts2["num_beams"] = num_beams
config.indextts2["repetition_penalty"] = repetition_penalty
config.indextts2["max_mel_tokens"] = max_mel_tokens
if reference_audio:
config.ui["voice_name"] = f"{config.INDEXTTS2_VOICE_PREFIX}{reference_audio}"
st.session_state['voice_rate'] = 1.0
st.session_state['voice_pitch'] = 1.0
def render_doubaotts_settings(tr):
"""渲染豆包语音 TTS 设置"""
# AK 输入
@ -1325,6 +1561,12 @@ def render_voice_preview_new(tr, selected_engine):
voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}"
voice_rate = 1.0 # IndexTTS-1.5 不支持速度调节
voice_pitch = 1.0 # IndexTTS-1.5 不支持音调调节
elif selected_engine == config.INDEXTTS2_ENGINE:
reference_audio = config.indextts2.get("reference_audio", "")
if reference_audio:
voice_name = f"{config.INDEXTTS2_VOICE_PREFIX}{reference_audio}"
voice_rate = 1.0 # IndexTTS-2 使用自身生成参数
voice_pitch = 1.0
elif selected_engine == "doubaotts":
voice_type = config.ui.get("doubaotts_voice_type", "BV700_streaming")
voice_name = voice_type
@ -1337,7 +1579,9 @@ def render_voice_preview_new(tr, selected_engine):
with st.spinner(tr("Synthesizing Voice")):
temp_dir = utils.storage_dir("temp", create=True)
audio_file = os.path.join(temp_dir, f"tmp-voice-{str(uuid4())}.mp3")
audio_format = "audio/wav" if selected_engine in (config.INDEXTTS_ENGINE, config.INDEXTTS2_ENGINE) else "audio/mp3"
audio_extension = ".wav" if audio_format == "audio/wav" else ".mp3"
audio_file = os.path.join(temp_dir, f"tmp-voice-{str(uuid4())}{audio_extension}")
sub_maker = voice.tts(
text=play_content,
@ -1354,7 +1598,7 @@ def render_voice_preview_new(tr, selected_engine):
# 播放音频
with open(audio_file, 'rb') as audio_file_obj:
audio_bytes = audio_file_obj.read()
st.audio(audio_bytes, format='audio/mp3')
st.audio(audio_bytes, format=audio_format)
# 清理临时文件
try:

View File

@ -284,6 +284,8 @@
"IndexTTS features": "A locally or privately deployed IndexTTS-1.5 voice-cloning engine. Choose a resource audio file or upload a reference audio file, then synthesize narration in that voice.",
"IndexTTS use case": "Best for fixed narrator voices, character dubbing, or generating multiple videos with the same voice. Start the IndexTTS-1.5 API service before use. Deployment package: https://pan.quark.cn/s/0767c9bcefd5",
"IndexTTS download link": "Download link: https://pan.quark.cn/s/0767c9bcefd5",
"IndexTTS2 features": "A locally or privately deployed IndexTTS-2 voice-cloning engine with emotion control and fuller generation parameters.",
"IndexTTS2 use case": "Best for fixed voices, emotional narration, and local speech synthesis workflows that need finer sampling controls. Start the IndexTTS-2 API service before use.",
"Doubao TTS features": "Volcengine Doubao speech synthesis with multiple voices and emotions, plus fast access in mainland China.",
"Select TTS Engine": "Select TTS Engine",
"Select TTS Engine Help": "Choose the text-to-speech engine you want to use.",
@ -440,6 +442,7 @@
"Select Qwen3 TTS Voice": "Select a Qwen3 TTS voice",
"API URL": "API URL",
"IndexTTS API URL Help": "IndexTTS-1.5 API service URL",
"IndexTTS2 API URL Help": "IndexTTS-2 API service URL. You can enter the service root or the full /tts endpoint.",
"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",
@ -469,6 +472,36 @@
"Enable Sampling Help": "Enable sampling for more natural speech.",
"IndexTTS Usage Instructions Title": "💡 IndexTTS-1.5 Usage Instructions",
"IndexTTS Usage Instructions": "**Zero-shot voice cloning**\n\n1. **Prepare reference audio**: upload or specify a clear audio file (3-10 seconds recommended)\n2. **Set API URL**: make sure the IndexTTS-1.5 service is running\n3. **Start synthesis**: the system will use the reference voice to synthesize new speech\n\n**Notes**:\n- Reference audio quality directly affects synthesis quality\n- Use clean audio without background noise when possible\n- Keep text length within a reasonable range\n- The first synthesis may take longer",
"IndexTTS2 Emotion Parameters": "🎭 Emotion Parameters",
"Emotion Mode": "Emotion Mode",
"Emotion Mode Help": "Choose the emotion control source for IndexTTS-2.",
"Emotion Mode Speaker": "Same as speaker reference",
"Emotion Mode Audio": "Use emotion reference audio",
"Emotion Mode Vector": "Use emotion vector",
"Emotion Mode Text": "Use emotion text",
"Emotion Alpha": "Emotion Alpha",
"Emotion Alpha Help": "Controls how strongly the emotion condition affects generation. 0 is weak, 1 is strong.",
"Emotion Reference Audio Path": "Emotion Reference Audio Path",
"Emotion Reference Audio Path Help": "Local emotion reference audio path used when emotion_mode=audio.",
"Emotion Text": "Emotion Text",
"Emotion Text Help": "Emotion description used when emotion_mode=text, such as happy, nervous, or aggrieved.",
"Emotion Text Placeholder": "e.g. calm, nervous, happy",
"Use Random Emotion": "Use Random Emotion",
"Use Random Emotion Help": "Let IndexTTS-2 use random emotion sampling during generation.",
"Emotion Happy": "Happy",
"Emotion Angry": "Angry",
"Emotion Sad": "Sad",
"Emotion Afraid": "Afraid",
"Emotion Disgusted": "Disgusted",
"Emotion Melancholic": "Melancholic",
"Emotion Surprised": "Surprised",
"Emotion Calm": "Calm",
"Max Text Tokens Per Segment": "Max Text Tokens Per Segment",
"Max Text Tokens Per Segment Help": "Maximum text tokens per segment for IndexTTS-2 inference.",
"Max Mel Tokens": "Max Mel Tokens",
"Max Mel Tokens Help": "Controls the maximum mel tokens generated in one request. Higher values can produce longer audio.",
"IndexTTS2 Usage Instructions Title": "💡 IndexTTS-2 Usage Instructions",
"IndexTTS2 Usage Instructions": "**IndexTTS-2 voice cloning**\n\n1. **Choose a voice**: reuse IndexTTS-1.5 resource audio or upload a reference audio file\n2. **Set API URL**: for example http://192.168.3.6:7863/tts, or enter the service root\n3. **Tune emotion**: speaker is the default; switch to audio, vector, or text when needed\n4. **Tune generation**: temperature, top_p, top_k, num_beams, repetition_penalty, and max_mel_tokens are sent directly to the IndexTTS-2 API\n\n**Notes**:\n- Reference audio quality directly affects cloning quality\n- The first request may load the model and take longer\n- CPU deployments are much slower than GPU deployments",
"Volcengine Access Key Help": "Volcengine Access Key",
"Volcengine Secret Key Help": "Volcengine Secret Key",
"Doubao AppID Help": "Doubao TTS application AppID",

View File

@ -265,6 +265,8 @@
"IndexTTS features": "本地/私有部署的 IndexTTS-1.5 语音克隆引擎。选择资源目录音频或上传参考音频后,可按该音色合成旁白。",
"IndexTTS use case": "适合需要固定旁白音色、角色配音或批量生成同一音色视频的场景。使用前请先启动 IndexTTS-1.5 API 服务部署包下载https://pan.quark.cn/s/0767c9bcefd5",
"IndexTTS download link": "下载地址https://pan.quark.cn/s/0767c9bcefd5",
"IndexTTS2 features": "本地/私有部署的 IndexTTS-2 语音克隆引擎,支持情感控制和更完整的生成参数。",
"IndexTTS2 use case": "适合需要固定音色、情绪化旁白或更细致采样控制的本地语音合成场景。使用前请先启动 IndexTTS-2 API 服务。",
"Doubao TTS features": "火山引擎豆包语音合成,支持多种音色和情感,国内访问速度快",
"Select TTS Engine": "选择 TTS 引擎",
"Select TTS Engine Help": "选择您要使用的文本转语音引擎",
@ -422,6 +424,7 @@
"Select Qwen3 TTS Voice": "选择 Qwen3 TTS 音色",
"API URL": "API 地址",
"IndexTTS API URL Help": "IndexTTS-1.5 API 服务地址",
"IndexTTS2 API URL Help": "IndexTTS-2 API 服务地址,可填写服务根地址或完整 /tts 地址",
"Reference Audio Source": "参考音频来源",
"Reference Audio Source Help": "选择从资源目录选择参考音频,或上传新的参考音频",
"Select from Resource Directory": "从资源目录选择",
@ -451,6 +454,36 @@
"Enable Sampling Help": "启用采样可以获得更自然的语音",
"IndexTTS Usage Instructions Title": "💡 IndexTTS-1.5 使用说明",
"IndexTTS Usage Instructions": "**零样本语音克隆**\n\n1. **准备参考音频**:上传或指定一段清晰的音频文件(建议 3-10 秒)\n2. **设置 API 地址**:确保 IndexTTS-1.5 服务正常运行\n3. **开始合成**:系统会自动使用参考音频的音色合成新语音\n\n**注意事项**\n- 参考音频质量直接影响合成效果\n- 建议使用无背景噪音的清晰音频\n- 文本长度建议控制在合理范围内\n- 首次合成可能需要较长时间",
"IndexTTS2 Emotion Parameters": "🎭 情感参数",
"Emotion Mode": "情感控制方式",
"Emotion Mode Help": "选择 IndexTTS-2 的情感控制来源",
"Emotion Mode Speaker": "与音色参考相同",
"Emotion Mode Audio": "使用情感参考音频",
"Emotion Mode Vector": "使用情感向量",
"Emotion Mode Text": "使用情感描述文本",
"Emotion Alpha": "情感权重",
"Emotion Alpha Help": "控制情感条件的影响强度0 表示弱1 表示强",
"Emotion Reference Audio Path": "情感参考音频路径",
"Emotion Reference Audio Path Help": "emotion_mode=audio 时使用的本地情感参考音频路径",
"Emotion Text": "情感描述文本",
"Emotion Text Help": "emotion_mode=text 时使用的情感描述,例如开心、紧张、委屈",
"Emotion Text Placeholder": "例如:沉稳、紧张、开心",
"Use Random Emotion": "启用随机情感",
"Use Random Emotion Help": "让 IndexTTS-2 在生成时使用随机情感采样",
"Emotion Happy": "开心",
"Emotion Angry": "愤怒",
"Emotion Sad": "悲伤",
"Emotion Afraid": "害怕",
"Emotion Disgusted": "厌恶",
"Emotion Melancholic": "忧郁",
"Emotion Surprised": "惊讶",
"Emotion Calm": "平静",
"Max Text Tokens Per Segment": "单段最大文本 Token",
"Max Text Tokens Per Segment Help": "IndexTTS-2 分段推理的最大文本 token 数",
"Max Mel Tokens": "最大 Mel Tokens",
"Max Mel Tokens Help": "控制单次生成的最大 mel token 数,值越大可生成更长音频",
"IndexTTS2 Usage Instructions Title": "💡 IndexTTS-2 使用说明",
"IndexTTS2 Usage Instructions": "**IndexTTS-2 语音克隆**\n\n1. **选择音色**:复用 IndexTTS-1.5 的资源音频或上传参考音频\n2. **设置 API 地址**:例如 http://192.168.3.6:7863/tts也可以填写服务根地址\n3. **调整情感参数**:默认使用 speaker可按需切换到 audio、vector 或 text\n4. **调整生成参数**temperature、top_p、top_k、num_beams、repetition_penalty 和 max_mel_tokens 会直接传给 IndexTTS-2 接口\n\n**注意事项**\n- 参考音频质量会直接影响克隆效果\n- 首次请求可能需要加载模型,耗时更长\n- CPU 部署生成速度会明显慢于 GPU",
"Volcengine Access Key Help": "火山引擎 Access Key",
"Volcengine Secret Key Help": "火山引擎 Secret Key",
"Doubao AppID Help": "豆包语音应用 AppID",