fix: adapt IndexTTS-2 MLX Pack API

This commit is contained in:
viccy 2026-07-13 04:41:09 +08:00
parent cd20aebacf
commit 39eae0d511
7 changed files with 533 additions and 214 deletions

View File

@ -10,7 +10,7 @@ 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_DISPLAY_NAME = "IndexTTS-1.5"
INDEXTTS_DISPLAY_NAME = "IndexTTS-1.5-windows"
INDEXTTS2_ENGINE = "indextts2"
INDEXTTS2_DISPLAY_NAME = "IndexTTS-2"
OMNIVOICE_ENGINE = "omnivoice"
@ -18,6 +18,16 @@ OMNIVOICE_DISPLAY_NAME = "OmniVoice"
INDEXTTS_VOICE_PREFIX = f"{INDEXTTS_ENGINE}:"
INDEXTTS2_VOICE_PREFIX = f"{INDEXTTS2_ENGINE}:"
OMNIVOICE_VOICE_PREFIX = f"{OMNIVOICE_ENGINE}:"
INDEXTTS2_EMOTION_VECTOR_FIELDS = (
("happy", "vec_happy"),
("angry", "vec_angry"),
("sad", "vec_sad"),
("afraid", "vec_afraid"),
("disgusted", "vec_disgusted"),
("melancholic", "vec_melancholic"),
("surprised", "vec_surprised"),
("calm", "vec_calm"),
)
def normalize_tts_engine_name(tts_engine: str) -> str:
@ -28,6 +38,32 @@ def normalize_indextts_voice_prefix(voice_name: str) -> str:
return voice_name
def get_indextts2_pack_emotion(indextts2_config) -> str:
"""Return the MLX Pack emotion string for current or legacy settings."""
if not isinstance(indextts2_config, dict):
return ""
configured_emotion = str(indextts2_config.get("emotion", "")).strip()
if configured_emotion:
return configured_emotion
emotion_mode = indextts2_config.get("emotion_mode", "speaker")
if emotion_mode == "text":
return str(indextts2_config.get("emotion_text", "")).strip()
if emotion_mode != "vector":
return ""
weights = []
for emotion, field in INDEXTTS2_EMOTION_VECTOR_FIELDS:
try:
weight = float(indextts2_config.get(field, 0.0))
except (TypeError, ValueError):
continue
if weight > 0:
weights.append(f"{emotion}:{weight:g}")
return ",".join(weights)
def _is_legacy_indextts2_config(indextts2_config) -> bool:
if not isinstance(indextts2_config, dict):
return False

View File

@ -0,0 +1,211 @@
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from app.services import voice
class FakeResponse:
def __init__(self, *, status_code=200, content=b"", payload=None, content_type="application/json"):
self.status_code = status_code
self.content = content
self._payload = payload or {}
self.headers = {"content-type": content_type}
self.text = "OK"
def json(self):
return self._payload
class IndexTTS2TtsTests(unittest.TestCase):
def setUp(self):
self.original_indextts2 = dict(voice.config.indextts2)
self.original_proxy = dict(voice.config.proxy)
def tearDown(self):
voice.config.indextts2.clear()
voice.config.indextts2.update(self.original_indextts2)
voice.config.proxy.clear()
voice.config.proxy.update(self.original_proxy)
def test_uploads_reference_audio_and_downloads_pack_output_url(self):
voice.config.indextts2.clear()
voice.config.indextts2.update(
{
"api_url": "http://127.0.0.1:7860",
"emotion": "happy:0.7,calm:0.3",
"emo_alpha": 0.6,
"speed": 1.15,
"seed": 20260713,
"max_mel_tokens": 1500,
"max_text_tokens_per_segment": 120,
"interval_silence": 200,
"temperature": 0.8,
"top_p": 0.8,
"top_k": 30,
"repetition_penalty": 10.0,
"diffusion_steps": 25,
"cfg_rate": 0.7,
"segment_overlap_ms": 50,
}
)
voice.config.proxy.clear()
generation_response = FakeResponse(
payload={"output": {"url": "/outputs/audio/speech.wav"}}
)
download_response = FakeResponse(content=b"wav-bytes", content_type="audio/wav")
with tempfile.TemporaryDirectory() as temp_dir:
reference_audio = Path(temp_dir) / "reference.wav"
output_file = Path(temp_dir) / "output.wav"
reference_audio.write_bytes(b"reference-wav")
with (
patch("app.services.voice.requests.post", return_value=generation_response) as post,
patch("app.services.voice.requests.get", return_value=download_response) as get,
patch("app.services.voice.get_audio_duration_from_file", return_value=1.25),
):
result = voice.indextts2_tts(
text=" 新版接口测试。 ",
voice_name=f"indextts2:{reference_audio}",
voice_file=str(output_file),
)
output_bytes = output_file.read_bytes() if output_file.exists() else b""
self.assertIsNotNone(result)
self.assertEqual(output_bytes, b"wav-bytes")
self.assertEqual(
post.call_args.args[0],
"http://127.0.0.1:7860/v1/audio/speech/upload",
)
self.assertEqual(
post.call_args.kwargs["data"],
{
"text": "新版接口测试。",
"emotion": "happy:0.7,calm:0.3",
"emo_alpha": 0.6,
"speed": 1.15,
"seed": 20260713,
"max_mel_tokens": 1500,
"max_text_tokens_per_segment": 120,
"interval_silence": 200,
"temperature": 0.8,
"top_p": 0.8,
"top_k": 30,
"repetition_penalty": 10.0,
"diffusion_steps": 25,
"cfg_rate": 0.7,
"segment_overlap_ms": 50,
},
)
self.assertIn("reference_audio", post.call_args.kwargs["files"])
self.assertEqual(
get.call_args.args[0],
"http://127.0.0.1:7860/outputs/audio/speech.wav",
)
def test_maps_legacy_vector_settings_to_pack_emotion_and_endpoint(self):
voice.config.indextts2.clear()
voice.config.indextts2.update(
{
"api_url": "http://127.0.0.1:7860/tts",
"emotion_mode": "vector",
"emotion_alpha": 0.65,
"vec_happy": 0.3,
"vec_calm": 0.7,
}
)
voice.config.proxy.clear()
generation_response = FakeResponse(content=b"wav-bytes", content_type="audio/wav")
with tempfile.TemporaryDirectory() as temp_dir:
reference_audio = Path(temp_dir) / "reference.wav"
output_file = Path(temp_dir) / "output.wav"
reference_audio.write_bytes(b"reference-wav")
with (
patch("app.services.voice.requests.post", return_value=generation_response) as post,
patch("app.services.voice.get_audio_duration_from_file", return_value=1.0),
):
result = voice.indextts2_tts(
text="旧配置兼容测试。",
voice_name=f"indextts2:{reference_audio}",
voice_file=str(output_file),
)
self.assertIsNotNone(result)
self.assertEqual(
post.call_args.args[0],
"http://127.0.0.1:7860/v1/audio/speech/upload",
)
self.assertEqual(post.call_args.kwargs["data"]["emotion"], "happy:0.3,calm:0.7")
self.assertEqual(post.call_args.kwargs["data"]["emo_alpha"], 0.65)
self.assertNotIn("emotion_mode", post.call_args.kwargs["data"])
self.assertNotIn("vec_happy", post.call_args.kwargs["data"])
def test_normalizes_saved_values_to_pack_request_ranges(self):
voice.config.indextts2.clear()
voice.config.indextts2.update(
{
"api_url": "http://127.0.0.1:7860",
"emo_alpha": 2.0,
"speed": 0.1,
"max_mel_tokens": 50,
"max_text_tokens_per_segment": 1,
"interval_silence": -10,
"temperature": 0.0,
"top_p": 0.0,
"top_k": 0,
"repetition_penalty": 0.1,
"diffusion_steps": 0,
"cfg_rate": -1.0,
"segment_overlap_ms": -5,
"seed": "not-an-integer",
}
)
voice.config.proxy.clear()
generation_response = FakeResponse(content=b"wav-bytes", content_type="audio/wav")
with tempfile.TemporaryDirectory() as temp_dir:
reference_audio = Path(temp_dir) / "reference.wav"
output_file = Path(temp_dir) / "output.wav"
reference_audio.write_bytes(b"reference-wav")
with (
patch("app.services.voice.requests.post", return_value=generation_response) as post,
patch("app.services.voice.get_audio_duration_from_file", return_value=1.0),
):
result = voice.indextts2_tts(
text="参数范围测试。",
voice_name=f"indextts2:{reference_audio}",
voice_file=str(output_file),
)
self.assertIsNotNone(result)
self.assertEqual(
post.call_args.kwargs["data"],
{
"text": "参数范围测试。",
"emo_alpha": 1.0,
"speed": 0.5,
"max_mel_tokens": 64,
"max_text_tokens_per_segment": 20,
"interval_silence": 0,
"temperature": 0.05,
"top_p": 0.05,
"top_k": 1,
"repetition_penalty": 1.0,
"diffusion_steps": 1,
"cfg_rate": 0.0,
"segment_overlap_ms": 0,
},
)
if __name__ == "__main__":
unittest.main()

View File

@ -2395,10 +2395,23 @@ def indextts_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0
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 the IndexTTS-2 MLX Pack upload endpoint for a configured URL.
The Pack accepts a server root, the JSON speech endpoint, or the multipart
upload endpoint. Treat an old ``/tts`` value as a server root so existing
saved settings move to the new Pack route instead of continuing to 404.
"""
api_url = (api_url or "http://127.0.0.1:7860").strip().rstrip("/")
upload_path = "/v1/audio/speech/upload"
speech_path = "/v1/audio/speech"
if api_url.endswith(upload_path):
return api_url
return f"{api_url.rstrip('/')}/tts"
if api_url.endswith(speech_path):
return f"{api_url}/upload"
if api_url.endswith("/tts"):
api_url = api_url[: -len("/tts")]
return f"{api_url}{upload_path}"
def _get_configured_proxies() -> dict:
@ -2410,6 +2423,49 @@ def _get_configured_proxies() -> dict:
}
def _get_indextts2_number(
key: str,
default: float | int,
minimum: float | int,
maximum: float | int,
*,
integer: bool = False,
) -> float | int:
"""Read an IndexTTS-2 option and constrain it to the MLX Pack schema."""
try:
raw_value = config.indextts2.get(key, default)
value = int(float(raw_value)) if integer else float(raw_value)
except (TypeError, ValueError):
value = default
return max(minimum, min(maximum, value))
def _get_indextts2_seed() -> int | None:
"""Return the optional Pack seed, omitting invalid legacy text values."""
seed = config.indextts2.get("seed")
if seed in (None, ""):
return None
try:
return int(seed)
except (TypeError, ValueError):
logger.warning("IndexTTS-2 随机种子无效,将使用随机采样: {}", seed)
return None
def _get_indextts2_emotion() -> str:
"""Map current and legacy IndexTTS-2 emotion settings to the MLX Pack API."""
emotion = config.get_indextts2_pack_emotion(config.indextts2)
if emotion:
return emotion
emotion_mode = config.indextts2.get("emotion_mode", "speaker")
if emotion_mode == "audio" and config.indextts2.get("emotion_audio"):
logger.warning(
"IndexTTS-2 MLX Pack 不支持单独的情感参考音频,将使用音色参考音频的情感。"
)
return ""
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:
@ -2417,9 +2473,14 @@ def _download_indextts2_audio(response: requests.Response, api_url: str, voice_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 ""
try:
result = response.json()
except ValueError:
logger.error("IndexTTS-2 API 返回了无效的 JSON 响应")
return False
output = result.get("output") if isinstance(result, dict) else {}
download_url = output.get("url") if isinstance(output, dict) else ""
if not download_url:
logger.error(f"IndexTTS-2 API 响应中没有音频下载地址: {result}")
return False
@ -2437,68 +2498,74 @@ def _download_indextts2_audio(response: requests.Response, api_url: str, voice_f
def indextts2_tts(text: str, voice_name: str, voice_file: str) -> Union[SubMaker, None]:
"""
使用 IndexTTS-2 API 进行零样本语音克隆
接口兼容 IndexTTS2-Pack POST /tts multipart form
使用 IndexTTS-2 MLX Pack API 进行零样本语音克隆
参考音频通过 ``POST /v1/audio/speech/upload`` 上传这样 Pack 即使
运行在另一台机器上也不需要访问 NarratoAI 的本地文件路径
"""
api_url = _normalize_indextts2_api_url(config.indextts2.get("api_url", "http://192.168.3.6:7863/tts"))
api_url = _normalize_indextts2_api_url(config.indextts2.get("api_url", "http://127.0.0.1:7860"))
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),
"emo_alpha": _get_indextts2_number(
"emo_alpha", config.indextts2.get("emotion_alpha", 0.6), 0.0, 1.0
),
"speed": _get_indextts2_number("speed", 1.0, 0.5, 2.0),
"max_mel_tokens": _get_indextts2_number(
"max_mel_tokens", 1500, 64, 1815, integer=True
),
"max_text_tokens_per_segment": _get_indextts2_number(
"max_text_tokens_per_segment", 120, 20, 600, integer=True
),
"interval_silence": _get_indextts2_number(
"interval_silence", 200, 0, 5000, integer=True
),
"temperature": _get_indextts2_number("temperature", 0.8, 0.05, 2.0),
"top_p": _get_indextts2_number("top_p", 0.8, 0.05, 1.0),
"top_k": _get_indextts2_number("top_k", 30, 1, 200, integer=True),
"repetition_penalty": _get_indextts2_number(
"repetition_penalty", 10.0, 1.0, 30.0
),
"diffusion_steps": _get_indextts2_number(
"diffusion_steps", 25, 1, 100, integer=True
),
"cfg_rate": _get_indextts2_number("cfg_rate", 0.7, 0.0, 2.0),
"segment_overlap_ms": _get_indextts2_number(
"segment_overlap_ms", 50, 0, 1000, integer=True
),
}
emotion = _get_indextts2_emotion()
if emotion:
data["emotion"] = emotion
seed = _get_indextts2_seed()
if seed is not None:
data["seed"] = seed
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")
with open(reference_audio_path, "rb") as reference_audio:
logger.info(f"{attempt + 1} 次调用 IndexTTS-2 API: {api_url}")
response = requests.post(
api_url,
files={"reference_audio": reference_audio},
data=data,
proxies=proxies,
timeout=180,
)
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
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:
@ -2507,12 +2574,6 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str) -> Union[SubMaker
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)

View File

@ -161,39 +161,32 @@
repetition_penalty = 10.0
[indextts2]
# IndexTTS-2 语音克隆配置
# 支持 IndexTTS2-Pack FastAPI 接口POST /tts
api_url = "http://192.168.3.6:7863/tts"
# IndexTTS-2 MLX Pack 语音克隆配置
# 参考音频会通过 POST /v1/audio/speech/upload 上传;可填写服务根地址或完整接口地址
api_url = "http://127.0.0.1:7860"
# 默认参考音频(可选),音色列表复用 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
# 留空时保留参考音频情绪;支持单个情绪或混合权重,例如 happy:0.7,calm:0.3
emotion = ""
emo_alpha = 0.6
speed = 1.0
# 可选:设置固定随机种子以复现生成结果
# seed = 20260713
# 高级生成参数
max_text_tokens_per_segment = 120
interval_silence = 200
temperature = 0.8
top_p = 0.8
top_k = 30
num_beams = 3
repetition_penalty = 10.0
max_mel_tokens = 1500
diffusion_steps = 25
cfg_rate = 0.7
segment_overlap_ms = 50
[omnivoice]
# OmniVoice-Pack 语音合成配置

View File

@ -1123,10 +1123,18 @@ def render_indextts_tts_settings(tr):
def render_indextts2_tts_settings(tr):
"""渲染 IndexTTS-2 TTS 设置"""
"""渲染 IndexTTS-2 MLX Pack TTS 设置"""
def bounded_value(key, default, min_value, max_value):
try:
value = float(config.indextts2.get(key, default))
except (TypeError, ValueError):
value = default
return max(min_value, min(max_value, value))
api_url = st.text_input(
tr("API URL"),
value=config.indextts2.get("api_url", "http://192.168.3.6:7863/tts"),
value=config.indextts2.get("api_url", "http://127.0.0.1:7860"),
help=tr("IndexTTS2 API URL Help")
)
@ -1135,108 +1143,43 @@ def render_indextts2_tts_settings(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"
initial_emotion = config.get_indextts2_pack_emotion(config.indextts2)
legacy_emotion_audio = (
config.indextts2.get("emotion_mode") == "audio"
and bool(config.indextts2.get("emotion_audio"))
)
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(
emotion = st.text_input(
tr("IndexTTS2 Emotion"),
value=initial_emotion,
help=tr("IndexTTS2 Emotion Help"),
placeholder=tr("IndexTTS2 Emotion Placeholder"),
)
if legacy_emotion_audio and not emotion.strip():
st.warning(tr("IndexTTS2 Emotion Audio Unsupported"))
emo_alpha = st.slider(
tr("Emotion Alpha"),
min_value=0.0,
max_value=1.0,
value=float(config.indextts2.get("emotion_alpha", 0.65)),
value=bounded_value("emo_alpha", config.indextts2.get("emotion_alpha", 0.6), 0.0, 1.0),
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"),
speed = st.slider(
tr("IndexTTS2 Speed"),
min_value=0.5,
max_value=2.0,
value=bounded_value("speed", 1.0, 0.5, 2.0),
step=0.05,
help=tr("IndexTTS2 Speed Help"),
)
seed = st.text_input(
tr("IndexTTS2 Seed"),
value=str(config.indextts2.get("seed", "") or ""),
help=tr("IndexTTS2 Seed Help"),
placeholder=tr("IndexTTS2 Seed Placeholder"),
)
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)
@ -1244,65 +1187,92 @@ def render_indextts2_tts_settings(tr):
with col1:
temperature = st.slider(
tr("Sampling Temperature"),
min_value=0.1,
min_value=0.05,
max_value=2.0,
value=float(config.indextts2.get("temperature", 0.8)),
step=0.1,
value=bounded_value("temperature", 0.8, 0.05, 2.0),
step=0.05,
help=tr("Sampling Temperature Help")
)
top_p = st.slider(
"Top P",
min_value=0.0,
min_value=0.05,
max_value=1.0,
value=float(config.indextts2.get("top_p", 0.8)),
value=bounded_value("top_p", 0.8, 0.05, 1.0),
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")
min_value=1,
max_value=200,
value=int(bounded_value("top_k", 30, 1, 200)),
step=1,
help=tr("IndexTTS2 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)),
value=int(bounded_value("max_text_tokens_per_segment", 120, 20, 600)),
step=10,
help=tr("Max Text Tokens Per Segment Help")
)
interval_silence = st.slider(
tr("Interval Silence"),
min_value=0,
max_value=5000,
value=int(bounded_value("interval_silence", 200, 0, 5000)),
step=50,
help=tr("Interval Silence Help"),
)
segment_overlap_ms = st.slider(
tr("Segment Overlap"),
min_value=0,
max_value=1000,
value=int(bounded_value("segment_overlap_ms", 50, 0, 1000)),
step=10,
help=tr("Segment Overlap Help"),
)
with col2:
num_beams = st.slider(
tr("Num Beams"),
diffusion_steps = st.slider(
tr("Diffusion Steps"),
min_value=1,
max_value=10,
value=int(config.indextts2.get("num_beams", 3)),
max_value=100,
value=int(bounded_value("diffusion_steps", 25, 1, 100)),
step=1,
help=tr("Num Beams Help")
help=tr("Diffusion Steps Help")
)
cfg_rate = st.slider(
tr("CFG Rate"),
min_value=0.0,
max_value=2.0,
value=bounded_value("cfg_rate", 0.7, 0.0, 2.0),
step=0.05,
help=tr("CFG Rate 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)),
min_value=1.0,
max_value=30.0,
value=bounded_value("repetition_penalty", 10.0, 1.0, 30.0),
step=0.1,
help=tr("Repetition Penalty Help")
)
max_mel_tokens = st.slider(
tr("Max Mel Tokens"),
min_value=50,
min_value=64,
max_value=1815,
value=int(config.indextts2.get("max_mel_tokens", 1500)),
step=10,
value=int(bounded_value("max_mel_tokens", 1500, 64, 1815)),
step=1,
help=tr("Max Mel Tokens Help")
)
@ -1312,20 +1282,32 @@ 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["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["emotion"] = emotion
config.indextts2["emo_alpha"] = emo_alpha
config.indextts2["speed"] = speed
config.indextts2["seed"] = seed.strip()
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["interval_silence"] = interval_silence
config.indextts2["segment_overlap_ms"] = segment_overlap_ms
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
config.indextts2["diffusion_steps"] = diffusion_steps
config.indextts2["cfg_rate"] = cfg_rate
legacy_fields = (
"emotion_mode", "emotion_audio", "emotion_alpha", "emotion_text", "use_random",
"num_beams", "vec_happy", "vec_angry", "vec_sad", "vec_afraid",
"vec_disgusted", "vec_melancholic", "vec_surprised", "vec_calm",
)
if legacy_emotion_audio and not emotion.strip():
legacy_fields = tuple(
field for field in legacy_fields if field not in {"emotion_mode", "emotion_audio"}
)
for field in legacy_fields:
config.indextts2.pop(field, None)
if reference_audio:
config.ui["voice_name"] = f"{config.INDEXTTS2_VOICE_PREFIX}{reference_audio}"

View File

@ -376,8 +376,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.",
"IndexTTS2 features": "A locally deployed IndexTTS-2 MLX Pack voice-cloning engine with emotion control and advanced 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 MLX Pack service before use.",
"OmniVoice features": "A locally or privately deployed OmniVoice-Pack multilingual TTS engine with automatic voice generation, voice design, and reference-audio cloning.",
"OmniVoice use case": "Best for local controllable multilingual narration, voice design, or reference-audio cloning. Start the OmniVoice-Pack API service before use.",
"Doubao TTS features": "Volcengine Doubao speech synthesis with multiple voices and emotions, plus fast access in mainland China.",
@ -564,7 +564,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.",
"IndexTTS2 API URL Help": "IndexTTS-2 MLX Pack service URL. Enter the service root or the full /v1/audio/speech/upload endpoint.",
"OmniVoice API URL Help": "OmniVoice-Pack API service URL. You can enter the service root or the full /tts endpoint.",
"OmniVoice Language Code": "Synthesis Language",
"OmniVoice Language Code Help": "The language parameter sent to OmniVoice-Pack, such as zh or en.",
@ -619,6 +619,15 @@
"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",
"IndexTTS2 Emotion": "Emotion Override",
"IndexTTS2 Emotion Help": "Leave blank to preserve the reference audio emotion. Use a label such as happy or a weighted mix such as happy:0.7,calm:0.3.",
"IndexTTS2 Emotion Placeholder": "e.g. happy:0.7,calm:0.3",
"IndexTTS2 Emotion Audio Unsupported": "The IndexTTS-2 MLX Pack does not support a separate emotion reference audio. The speaker reference audio emotion will be used.",
"IndexTTS2 Speed": "Speech Speed",
"IndexTTS2 Speed Help": "Speech speed sent to the IndexTTS-2 MLX Pack (0.5x2.0x).",
"IndexTTS2 Seed": "Seed",
"IndexTTS2 Seed Help": "Optional integer seed for reproducible generation. Leave blank for random sampling.",
"IndexTTS2 Seed Placeholder": "Random",
"Emotion Mode": "Emotion Mode",
"Emotion Mode Help": "Choose the emotion control source for IndexTTS-2.",
"Emotion Mode Speaker": "Same as speaker reference",
@ -642,12 +651,21 @@
"Emotion Melancholic": "Melancholic",
"Emotion Surprised": "Surprised",
"Emotion Calm": "Calm",
"Diffusion Steps": "Diffusion Steps",
"Diffusion Steps Help": "Number of diffusion steps. Higher values can improve quality but take longer.",
"CFG Rate": "CFG Rate",
"CFG Rate Help": "Classifier-free guidance strength for the diffusion stage.",
"Interval Silence": "Inter-segment Silence (ms)",
"Interval Silence Help": "Silence inserted between text segments by the Pack.",
"Segment Overlap": "Segment Overlap (ms)",
"Segment Overlap Help": "Crossfade overlap between generated text segments.",
"IndexTTS2 Top K Help": "Top-k sampling value for IndexTTS-2 MLX Pack. It must be at least 1.",
"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",
"IndexTTS2 Usage Instructions": "**IndexTTS-2 MLX Pack voice cloning**\n\n1. **Choose a voice**: reuse IndexTTS-1.5 resource audio or upload a reference audio file\n2. **Set API URL**: the default Pack service is http://127.0.0.1:7860; NarratoAI calls /v1/audio/speech/upload and uploads the reference audio\n3. **Tune emotion**: leave it blank to preserve the reference audio emotion, or enter a label/mix such as happy:0.7,calm:0.3\n4. **Tune generation**: speed, temperature, top_p, top_k, repetition_penalty, diffusion_steps, cfg_rate, and max_mel_tokens are sent to the Pack\n\n**Notes**:\n- Reference audio quality directly affects cloning quality\n- The first request may load the model and take longer\n- The Pack binds to localhost by default",
"OmniVoice Usage Instructions Title": "OmniVoice Usage Instructions",
"OmniVoice Usage Instructions": "**OmniVoice-Pack speech synthesis**\n\n1. **Automatic voice**: set the API URL and language, then synthesize directly.\n2. **Voice design**: fill instruct with the desired gender, pitch, accent, or style.\n3. **Reference-audio clone**: upload or choose reference audio and fill its matching transcript.\n\n**Notes**:\n- The default service URL is http://127.0.0.1:7866/tts\n- Reference-audio cloning requires reference text when the service has no ASR model loaded\n- OmniVoice returns WAV audio, and NarratoAI estimates subtitle segment timing from the audio duration",
"Volcengine Access Key Help": "Volcengine Access Key",

View File

@ -314,8 +314,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 服务。",
"IndexTTS2 features": "本地部署的 IndexTTS-2 MLX Pack 语音克隆引擎,支持情感控制和更完整的生成参数。",
"IndexTTS2 use case": "适合需要固定音色、情绪化旁白或更细致采样控制的本地语音合成场景。使用前请先启动 IndexTTS-2 MLX Pack 服务。",
"OmniVoice features": "本地/私有部署的 OmniVoice-Pack 多语种语音合成引擎,支持自动音色、指令音色和参考音频克隆。",
"OmniVoice use case": "适合需要本地可控、多语言旁白、音色设计或参考音频克隆的场景。使用前请先启动 OmniVoice-Pack API 服务。",
"Doubao TTS features": "火山引擎豆包语音合成,支持多种音色和情感,国内访问速度快",
@ -503,7 +503,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 地址",
"IndexTTS2 API URL Help": "IndexTTS-2 MLX Pack 服务地址,可填写服务根地址或完整 /v1/audio/speech/upload 地址",
"OmniVoice API URL Help": "OmniVoice-Pack API 服务地址,可填写服务根地址或完整 /tts 地址",
"OmniVoice Language Code": "合成语言",
"OmniVoice Language Code Help": "传给 OmniVoice-Pack 的 language 参数,例如 zh、en。",
@ -558,6 +558,15 @@
"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": "情感参数",
"IndexTTS2 Emotion": "情感覆盖",
"IndexTTS2 Emotion Help": "留空时保留参考音频的情感;可填写单个情感,如 happy或权重混合如 happy:0.7,calm:0.3。",
"IndexTTS2 Emotion Placeholder": "例如happy:0.7,calm:0.3",
"IndexTTS2 Emotion Audio Unsupported": "IndexTTS-2 MLX Pack 不支持单独的情感参考音频,将使用音色参考音频的情感。",
"IndexTTS2 Speed": "语速",
"IndexTTS2 Speed Help": "传给 IndexTTS-2 MLX Pack 的语速0.5x2.0x)。",
"IndexTTS2 Seed": "随机种子",
"IndexTTS2 Seed Help": "可选整数,用于复现生成结果;留空则随机采样。",
"IndexTTS2 Seed Placeholder": "随机",
"Emotion Mode": "情感控制方式",
"Emotion Mode Help": "选择 IndexTTS-2 的情感控制来源",
"Emotion Mode Speaker": "与音色参考相同",
@ -581,12 +590,21 @@
"Emotion Melancholic": "忧郁",
"Emotion Surprised": "惊讶",
"Emotion Calm": "平静",
"Diffusion Steps": "扩散步数",
"Diffusion Steps Help": "扩散生成的步数;值越大可能提升质量,但生成更慢。",
"CFG Rate": "CFG 引导强度",
"CFG Rate Help": "扩散阶段的 classifier-free guidance 强度。",
"Interval Silence": "分段静音(毫秒)",
"Interval Silence Help": "Pack 在文本分段之间插入的静音时长。",
"Segment Overlap": "分段重叠(毫秒)",
"Segment Overlap Help": "生成的文本片段之间用于交叉淡化的重叠时长。",
"IndexTTS2 Top K Help": "IndexTTS-2 MLX Pack 的 top-k 采样值,必须不小于 1。",
"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",
"IndexTTS2 Usage Instructions": "**IndexTTS-2 MLX Pack 语音克隆**\n\n1. **选择音色**:复用 IndexTTS-1.5 的资源音频或上传参考音频\n2. **设置 API 地址**默认 Pack 服务为 http://127.0.0.1:7860NarratoAI 会调用 /v1/audio/speech/upload 并上传参考音频\n3. **调整情感参数**:留空时保留参考音频情感;也可输入 happy:0.7,calm:0.3 这类情感或权重混合\n4. **调整生成参数**speed、temperature、top_p、top_k、repetition_penalty、diffusion_steps、cfg_rate 和 max_mel_tokens 会传给 Pack\n\n**注意事项**\n- 参考音频质量会直接影响克隆效果\n- 首次请求可能需要加载模型,耗时更长\n- Pack 默认仅监听本机地址",
"OmniVoice Usage Instructions Title": "OmniVoice 使用说明",
"OmniVoice Usage Instructions": "**OmniVoice-Pack 语音合成**\n\n1. **自动音色**:只需要设置 API 地址和语言,可直接合成。\n2. **指令音色**:填写 instruct 描述想要的性别、音高、口音或风格。\n3. **参考音频克隆**:上传或选择参考音频,并填写该音频对应文本。\n\n**注意事项**\n- 当前默认服务地址为 http://127.0.0.1:7866/tts\n- 参考音频克隆在服务未加载 ASR 模型时必须填写参考文本\n- OmniVoice 返回 WAV 音频,系统会按音频时长估算字幕段落",
"Volcengine Access Key Help": "火山引擎 Access Key",