mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-07-17 11:38:09 +00:00
feat: add IndexTTS 1.5 macOS engine
This commit is contained in:
parent
f80ae97679
commit
b56222725d
@ -11,11 +11,14 @@ config_file = f"{root_dir}/config.toml"
|
||||
version_file = f"{root_dir}/project_version"
|
||||
INDEXTTS_ENGINE = "indextts"
|
||||
INDEXTTS_DISPLAY_NAME = "IndexTTS-1.5-windows"
|
||||
INDEXTTS_MACOS_ENGINE = "indextts_macos"
|
||||
INDEXTTS_MACOS_DISPLAY_NAME = "IndexTTS-1.5-macOS"
|
||||
INDEXTTS2_ENGINE = "indextts2"
|
||||
INDEXTTS2_DISPLAY_NAME = "IndexTTS-2"
|
||||
OMNIVOICE_ENGINE = "omnivoice"
|
||||
OMNIVOICE_DISPLAY_NAME = "OmniVoice"
|
||||
INDEXTTS_VOICE_PREFIX = f"{INDEXTTS_ENGINE}:"
|
||||
INDEXTTS_MACOS_VOICE_PREFIX = f"{INDEXTTS_MACOS_ENGINE}:"
|
||||
INDEXTTS2_VOICE_PREFIX = f"{INDEXTTS2_ENGINE}:"
|
||||
OMNIVOICE_VOICE_PREFIX = f"{OMNIVOICE_ENGINE}:"
|
||||
INDEXTTS2_EMOTION_VECTOR_FIELDS = (
|
||||
@ -169,6 +172,7 @@ def save_config():
|
||||
_cfg["tts_qwen"] = tts_qwen
|
||||
_cfg["fun_asr"] = fun_asr
|
||||
_cfg["indextts"] = indextts
|
||||
_cfg["indextts_macos"] = indextts_macos
|
||||
_cfg["indextts2"] = indextts2
|
||||
_cfg["omnivoice"] = omnivoice
|
||||
_cfg["doubaotts"] = doubaotts
|
||||
@ -187,6 +191,7 @@ frames = _cfg.get("frames", {})
|
||||
tts_qwen = _cfg.get("tts_qwen", {})
|
||||
fun_asr = _cfg.get("fun_asr", {})
|
||||
indextts = _cfg.get("indextts", {})
|
||||
indextts_macos = _cfg.get("indextts_macos", {})
|
||||
indextts2 = _cfg.get("indextts2", {})
|
||||
omnivoice = _cfg.get("omnivoice", {})
|
||||
doubaotts = _cfg.get("doubaotts", {})
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
@ -15,6 +16,30 @@ from app.config.defaults import (
|
||||
|
||||
|
||||
class ConfigBootstrapDefaultsTests(unittest.TestCase):
|
||||
def test_save_config_keeps_macos_tts_settings_independent(self):
|
||||
macos_settings = {
|
||||
"api_url": "http://127.0.0.1:7866",
|
||||
"reference_audio": "/tmp/macos-reference.wav",
|
||||
"speed": 1.1,
|
||||
}
|
||||
windows_settings = {
|
||||
"api_url": "http://127.0.0.1:8081/tts",
|
||||
"reference_audio": "/tmp/windows-reference.wav",
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_path = Path(tmp_dir) / "config.toml"
|
||||
with (
|
||||
patch.object(cfg, "config_file", str(config_path)),
|
||||
patch.object(cfg, "indextts_macos", dict(macos_settings)),
|
||||
patch.object(cfg, "indextts", dict(windows_settings)),
|
||||
):
|
||||
cfg.save_config()
|
||||
saved_config = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
self.assertEqual(macos_settings, saved_config["indextts_macos"])
|
||||
self.assertEqual(windows_settings, saved_config["indextts"])
|
||||
|
||||
def test_load_config_bootstraps_webui_llm_defaults(self):
|
||||
original_root_dir = cfg.root_dir
|
||||
original_config_file = cfg.config_file
|
||||
|
||||
@ -114,6 +114,10 @@ def _normalize_indextts_reference_audio(params: VideoClipParams) -> None:
|
||||
tts_config = config.indextts
|
||||
voice_prefix = config.INDEXTTS_VOICE_PREFIX
|
||||
display_name = "IndexTTS-1.5"
|
||||
elif params.tts_engine == config.INDEXTTS_MACOS_ENGINE:
|
||||
tts_config = config.indextts_macos
|
||||
voice_prefix = config.INDEXTTS_MACOS_VOICE_PREFIX
|
||||
display_name = config.INDEXTTS_MACOS_DISPLAY_NAME
|
||||
elif params.tts_engine == config.INDEXTTS2_ENGINE:
|
||||
tts_config = config.indextts2
|
||||
voice_prefix = config.INDEXTTS2_VOICE_PREFIX
|
||||
|
||||
102
app/services/test_indextts_macos_tts_unittest.py
Normal file
102
app/services/test_indextts_macos_tts_unittest.py
Normal file
@ -0,0 +1,102 @@
|
||||
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 IndexTTSMacOSTtsTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.original_config = dict(voice.config.indextts_macos)
|
||||
self.original_proxy = dict(voice.config.proxy)
|
||||
|
||||
def tearDown(self):
|
||||
voice.config.indextts_macos.clear()
|
||||
voice.config.indextts_macos.update(self.original_config)
|
||||
voice.config.proxy.clear()
|
||||
voice.config.proxy.update(self.original_proxy)
|
||||
|
||||
def test_uploads_reference_audio_and_downloads_pack_output_url(self):
|
||||
voice.config.indextts_macos.clear()
|
||||
voice.config.indextts_macos.update(
|
||||
{
|
||||
"api_url": "http://127.0.0.1:7866",
|
||||
"speed": 1.1,
|
||||
"seed": 42,
|
||||
"max_mel_tokens": 800,
|
||||
"max_text_tokens_per_segment": 120,
|
||||
"interval_silence": 200,
|
||||
"temperature": 1.0,
|
||||
"top_p": 0.8,
|
||||
"top_k": 30,
|
||||
"repetition_penalty": 10.0,
|
||||
"segment_overlap_ms": 50,
|
||||
}
|
||||
)
|
||||
voice.config.proxy.clear()
|
||||
|
||||
generation_response = FakeResponse(payload={"output_url": "/outputs/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.indextts_macos_tts(
|
||||
text=" macOS 接口测试。 ",
|
||||
voice_name=f"indextts_macos:{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:7866/v1/audio/speech/upload",
|
||||
)
|
||||
self.assertEqual(
|
||||
post.call_args.kwargs["data"],
|
||||
{
|
||||
"text": "macOS 接口测试。",
|
||||
"speed": 1.1,
|
||||
"seed": 42,
|
||||
"max_mel_tokens": 800,
|
||||
"max_text_tokens_per_segment": 120,
|
||||
"interval_silence": 200,
|
||||
"temperature": 1.0,
|
||||
"top_p": 0.8,
|
||||
"top_k": 30,
|
||||
"repetition_penalty": 10.0,
|
||||
"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:7866/outputs/speech.wav",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1302,6 +1302,10 @@ def tts(
|
||||
logger.info("分发到 IndexTTS-1.5")
|
||||
return indextts_tts(text, voice_name, voice_file, speed=voice_rate)
|
||||
|
||||
if tts_engine == config.INDEXTTS_MACOS_ENGINE:
|
||||
logger.info("分发到 IndexTTS-1.5-macOS")
|
||||
return indextts_macos_tts(text, voice_name, voice_file)
|
||||
|
||||
if tts_engine == config.INDEXTTS2_ENGINE:
|
||||
logger.info("分发到 IndexTTS-2")
|
||||
return indextts2_tts(text, voice_name, voice_file)
|
||||
@ -1796,6 +1800,7 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f
|
||||
tts_results = []
|
||||
audio_extension = ".wav" if tts_engine in (
|
||||
config.INDEXTTS_ENGINE,
|
||||
config.INDEXTTS_MACOS_ENGINE,
|
||||
config.INDEXTTS2_ENGINE,
|
||||
config.OMNIVOICE_ENGINE,
|
||||
) else ".mp3"
|
||||
@ -1828,7 +1833,12 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f
|
||||
if (
|
||||
is_soulvoice_voice(voice_name)
|
||||
or is_qwen_engine(tts_engine)
|
||||
or tts_engine in (config.INDEXTTS_ENGINE, config.INDEXTTS2_ENGINE, config.OMNIVOICE_ENGINE)
|
||||
or tts_engine in (
|
||||
config.INDEXTTS_ENGINE,
|
||||
config.INDEXTTS_MACOS_ENGINE,
|
||||
config.INDEXTTS2_ENGINE,
|
||||
config.OMNIVOICE_ENGINE,
|
||||
)
|
||||
or tts_engine == "doubaotts"
|
||||
):
|
||||
# 获取实际音频文件的时长
|
||||
@ -2271,6 +2281,13 @@ def parse_indextts2_voice(voice_name: str) -> str:
|
||||
return voice_name
|
||||
|
||||
|
||||
def parse_indextts_macos_voice(voice_name: str) -> str:
|
||||
"""解析 IndexTTS-1.5-macOS 参考音频路径。"""
|
||||
if isinstance(voice_name, str) and voice_name.startswith(config.INDEXTTS_MACOS_VOICE_PREFIX):
|
||||
return voice_name[len(config.INDEXTTS_MACOS_VOICE_PREFIX):]
|
||||
return voice_name
|
||||
|
||||
|
||||
def parse_omnivoice_voice(voice_name: str) -> str:
|
||||
"""
|
||||
解析 OmniVoice 语音名称
|
||||
@ -2414,6 +2431,149 @@ def _normalize_indextts2_api_url(api_url: str) -> str:
|
||||
return f"{api_url}{upload_path}"
|
||||
|
||||
|
||||
def _normalize_indextts_macos_api_url(api_url: str) -> str:
|
||||
"""Return the IndexTTS 1.5 MLX Pack multipart upload endpoint."""
|
||||
api_url = (api_url or "http://127.0.0.1:7866").strip().rstrip("/")
|
||||
upload_path = "/v1/audio/speech/upload"
|
||||
speech_path = "/v1/audio/speech"
|
||||
if api_url.endswith(upload_path):
|
||||
return api_url
|
||||
if api_url.endswith(speech_path):
|
||||
return f"{api_url}/upload"
|
||||
return f"{api_url}{upload_path}"
|
||||
|
||||
|
||||
def _get_indextts_macos_number(
|
||||
key: str,
|
||||
default: float | int,
|
||||
minimum: float | int,
|
||||
maximum: float | int,
|
||||
*,
|
||||
integer: bool = False,
|
||||
) -> float | int:
|
||||
try:
|
||||
raw_value = config.indextts_macos.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_indextts_macos_seed() -> int | None:
|
||||
seed = config.indextts_macos.get("seed")
|
||||
if seed in (None, ""):
|
||||
return None
|
||||
try:
|
||||
return int(seed)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning("IndexTTS-1.5-macOS 随机种子无效,将使用随机采样: {}", seed)
|
||||
return None
|
||||
|
||||
|
||||
def _download_indextts_macos_audio(
|
||||
response: requests.Response,
|
||||
api_url: str,
|
||||
voice_file: str,
|
||||
proxies: dict,
|
||||
) -> bool:
|
||||
try:
|
||||
result = response.json()
|
||||
except ValueError:
|
||||
logger.error("IndexTTS-1.5-macOS API 返回了无效的 JSON 响应")
|
||||
return False
|
||||
|
||||
download_url = result.get("output_url") if isinstance(result, dict) else ""
|
||||
if not download_url:
|
||||
logger.error(f"IndexTTS-1.5-macOS API 响应中没有音频下载地址: {result}")
|
||||
return False
|
||||
|
||||
audio_response = requests.get(
|
||||
urljoin(api_url, download_url),
|
||||
proxies=proxies,
|
||||
timeout=120,
|
||||
)
|
||||
if audio_response.status_code != 200:
|
||||
logger.error(
|
||||
f"IndexTTS-1.5-macOS 音频下载失败: "
|
||||
f"{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 indextts_macos_tts(text: str, voice_name: str, voice_file: str) -> Union[SubMaker, None]:
|
||||
"""使用 IndexTTS-MLX-1.5-Pack 的上传接口进行零样本语音克隆。"""
|
||||
api_url = _normalize_indextts_macos_api_url(
|
||||
config.indextts_macos.get("api_url", "http://127.0.0.1:7866")
|
||||
)
|
||||
reference_audio_path = parse_indextts_macos_voice(voice_name)
|
||||
if not reference_audio_path or not os.path.exists(reference_audio_path):
|
||||
logger.error(f"IndexTTS-1.5-macOS 参考音频文件不存在: {reference_audio_path}")
|
||||
return None
|
||||
|
||||
data = {
|
||||
"text": text.strip(),
|
||||
"speed": _get_indextts_macos_number("speed", 1.0, 0.5, 2.0),
|
||||
"max_mel_tokens": _get_indextts_macos_number(
|
||||
"max_mel_tokens", 800, 64, 1600, integer=True
|
||||
),
|
||||
"max_text_tokens_per_segment": _get_indextts_macos_number(
|
||||
"max_text_tokens_per_segment", 120, 20, 600, integer=True
|
||||
),
|
||||
"interval_silence": _get_indextts_macos_number(
|
||||
"interval_silence", 200, 0, 2000, integer=True
|
||||
),
|
||||
"temperature": _get_indextts_macos_number("temperature", 1.0, 0.0, 2.0),
|
||||
"top_p": _get_indextts_macos_number("top_p", 0.8, 0.05, 1.0),
|
||||
"top_k": _get_indextts_macos_number("top_k", 30, 0, 200, integer=True),
|
||||
"repetition_penalty": _get_indextts_macos_number(
|
||||
"repetition_penalty", 10.0, 1.0, 20.0
|
||||
),
|
||||
"segment_overlap_ms": _get_indextts_macos_number(
|
||||
"segment_overlap_ms", 50, 0, 500, integer=True
|
||||
),
|
||||
}
|
||||
seed = _get_indextts_macos_seed()
|
||||
if seed is not None:
|
||||
data["seed"] = seed
|
||||
|
||||
proxies = _get_configured_proxies()
|
||||
for attempt in range(3):
|
||||
try:
|
||||
with open(reference_audio_path, "rb") as reference_audio:
|
||||
logger.info(f"第 {attempt + 1} 次调用 IndexTTS-1.5-macOS API: {api_url}")
|
||||
response = requests.post(
|
||||
api_url,
|
||||
files={"reference_audio": reference_audio},
|
||||
data=data,
|
||||
proxies=proxies,
|
||||
timeout=600,
|
||||
)
|
||||
if response.status_code == 200 and _download_indextts_macos_audio(
|
||||
response, api_url, voice_file, proxies
|
||||
):
|
||||
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-1.5-macOS API 调用失败: {response.status_code} - {response.text}"
|
||||
)
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error(f"IndexTTS-1.5-macOS API 调用超时 (尝试 {attempt + 1}/3)")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"IndexTTS-1.5-macOS API 网络错误: {str(e)} (尝试 {attempt + 1}/3)")
|
||||
except Exception as e:
|
||||
logger.error(f"IndexTTS-1.5-macOS TTS 处理错误: {str(e)} (尝试 {attempt + 1}/3)")
|
||||
if attempt < 2:
|
||||
time.sleep(2)
|
||||
return None
|
||||
|
||||
|
||||
def _get_configured_proxies() -> dict:
|
||||
if not config.proxy.get("http"):
|
||||
return {}
|
||||
|
||||
@ -160,6 +160,24 @@
|
||||
num_beams = 3
|
||||
repetition_penalty = 10.0
|
||||
|
||||
[indextts_macos]
|
||||
# IndexTTS-1.5 macOS MLX Pack 语音克隆配置(仅适用于 Apple Silicon)
|
||||
# 参考音频会通过 POST /v1/audio/speech/upload 上传
|
||||
api_url = "http://127.0.0.1:7866"
|
||||
reference_audio_source = "resource"
|
||||
# reference_audio = "/path/to/reference_audio.wav"
|
||||
|
||||
speed = 1.0
|
||||
# seed = 42
|
||||
max_mel_tokens = 800
|
||||
max_text_tokens_per_segment = 120
|
||||
interval_silence = 200
|
||||
temperature = 1.0
|
||||
top_p = 0.8
|
||||
top_k = 30
|
||||
repetition_penalty = 10.0
|
||||
segment_overlap_ms = 50
|
||||
|
||||
[indextts2]
|
||||
# IndexTTS-2 MLX Pack 语音克隆配置
|
||||
# 参考音频会通过 POST /v1/audio/speech/upload 上传;可填写服务根地址或完整接口地址
|
||||
|
||||
@ -42,6 +42,7 @@ BGM_UPLOAD_SUBDIR = "uploaded_bgms"
|
||||
BGM_AUDIO_EXTENSIONS = (".mp3", ".wav", ".flac", ".m4a", ".aac", ".ogg")
|
||||
LOCAL_TTS_ENGINES = {
|
||||
config.INDEXTTS_ENGINE,
|
||||
config.INDEXTTS_MACOS_ENGINE,
|
||||
config.INDEXTTS2_ENGINE,
|
||||
config.OMNIVOICE_ENGINE,
|
||||
}
|
||||
@ -74,6 +75,7 @@ def get_tts_engine_options(tr=lambda key: key):
|
||||
"""获取TTS引擎选项"""
|
||||
engine_options = {
|
||||
config.INDEXTTS_ENGINE: config.INDEXTTS_DISPLAY_NAME,
|
||||
config.INDEXTTS_MACOS_ENGINE: config.INDEXTTS_MACOS_DISPLAY_NAME,
|
||||
config.INDEXTTS2_ENGINE: config.INDEXTTS2_DISPLAY_NAME,
|
||||
config.OMNIVOICE_ENGINE: config.OMNIVOICE_DISPLAY_NAME,
|
||||
"edge_tts": "Edge TTS",
|
||||
@ -136,6 +138,12 @@ def get_tts_engine_descriptions(tr=lambda key: key):
|
||||
"use_case": tr("IndexTTS use case"),
|
||||
"registration": None
|
||||
},
|
||||
config.INDEXTTS_MACOS_ENGINE: {
|
||||
"title": config.INDEXTTS_MACOS_DISPLAY_NAME,
|
||||
"features": tr("IndexTTS macOS features"),
|
||||
"use_case": tr("IndexTTS macOS use case"),
|
||||
"registration": None
|
||||
},
|
||||
config.INDEXTTS2_ENGINE: {
|
||||
"title": config.INDEXTTS2_DISPLAY_NAME,
|
||||
"features": tr("IndexTTS2 features"),
|
||||
@ -588,6 +596,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.INDEXTTS_MACOS_ENGINE:
|
||||
render_indextts_macos_tts_settings(tr)
|
||||
elif selected_engine == config.INDEXTTS2_ENGINE:
|
||||
render_indextts2_tts_settings(tr)
|
||||
elif selected_engine == config.OMNIVOICE_ENGINE:
|
||||
@ -1122,6 +1132,102 @@ def render_indextts_tts_settings(tr):
|
||||
config.ui["voice_name"] = f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}"
|
||||
|
||||
|
||||
def render_indextts_macos_tts_settings(tr):
|
||||
"""渲染 IndexTTS-1.5 macOS MLX Pack 设置。"""
|
||||
tts_config = config.indextts_macos
|
||||
|
||||
def bounded_value(key, default, min_value, max_value):
|
||||
try:
|
||||
value = float(tts_config.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=tts_config.get("api_url", "http://127.0.0.1:7866"),
|
||||
help=tr("IndexTTS macOS API URL Help"),
|
||||
)
|
||||
reference_audio_source, reference_audio = render_indextts_reference_audio_selector(
|
||||
tr,
|
||||
tts_config,
|
||||
"indextts_macos",
|
||||
)
|
||||
|
||||
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(tts_config.get("seed", "") or ""),
|
||||
help=tr("IndexTTS2 Seed Help"),
|
||||
placeholder=tr("IndexTTS2 Seed Placeholder"),
|
||||
)
|
||||
|
||||
with st.expander(tr("Advanced Parameters"), expanded=False):
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
temperature = st.slider(
|
||||
tr("Sampling Temperature"), 0.0, 2.0,
|
||||
bounded_value("temperature", 1.0, 0.0, 2.0), 0.05,
|
||||
)
|
||||
top_p = st.slider(
|
||||
"Top P", 0.05, 1.0,
|
||||
bounded_value("top_p", 0.8, 0.05, 1.0), 0.05,
|
||||
)
|
||||
top_k = st.slider(
|
||||
"Top K", 0, 200,
|
||||
int(bounded_value("top_k", 30, 0, 200)), 1,
|
||||
)
|
||||
max_text_tokens_per_segment = st.slider(
|
||||
tr("Max Text Tokens Per Segment"), 20, 600,
|
||||
int(bounded_value("max_text_tokens_per_segment", 120, 20, 600)), 10,
|
||||
)
|
||||
with col2:
|
||||
repetition_penalty = st.slider(
|
||||
tr("Repetition Penalty"), 1.0, 20.0,
|
||||
bounded_value("repetition_penalty", 10.0, 1.0, 20.0), 0.1,
|
||||
)
|
||||
max_mel_tokens = st.slider(
|
||||
tr("Max Mel Tokens"), 64, 1600,
|
||||
int(bounded_value("max_mel_tokens", 800, 64, 1600)), 1,
|
||||
)
|
||||
interval_silence = st.slider(
|
||||
tr("Interval Silence"), 0, 2000,
|
||||
int(bounded_value("interval_silence", 200, 0, 2000)), 50,
|
||||
)
|
||||
segment_overlap_ms = st.slider(
|
||||
tr("Segment Overlap"), 0, 500,
|
||||
int(bounded_value("segment_overlap_ms", 50, 0, 500)), 10,
|
||||
)
|
||||
|
||||
with st.expander(tr("IndexTTS macOS Usage Instructions Title"), expanded=False):
|
||||
st.markdown(tr("IndexTTS macOS Usage Instructions"))
|
||||
|
||||
tts_config["api_url"] = api_url
|
||||
tts_config["reference_audio_source"] = reference_audio_source
|
||||
tts_config["reference_audio"] = reference_audio
|
||||
tts_config["speed"] = speed
|
||||
tts_config["seed"] = seed.strip()
|
||||
tts_config["temperature"] = temperature
|
||||
tts_config["top_p"] = top_p
|
||||
tts_config["top_k"] = top_k
|
||||
tts_config["max_text_tokens_per_segment"] = max_text_tokens_per_segment
|
||||
tts_config["repetition_penalty"] = repetition_penalty
|
||||
tts_config["max_mel_tokens"] = max_mel_tokens
|
||||
tts_config["interval_silence"] = interval_silence
|
||||
tts_config["segment_overlap_ms"] = segment_overlap_ms
|
||||
if reference_audio:
|
||||
config.ui["voice_name"] = f"{config.INDEXTTS_MACOS_VOICE_PREFIX}{reference_audio}"
|
||||
st.session_state["voice_rate"] = 1.0
|
||||
st.session_state["voice_pitch"] = 1.0
|
||||
|
||||
|
||||
def render_indextts2_tts_settings(tr):
|
||||
"""渲染 IndexTTS-2 MLX Pack TTS 设置"""
|
||||
|
||||
@ -1748,6 +1854,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.INDEXTTS_MACOS_ENGINE:
|
||||
reference_audio = config.indextts_macos.get("reference_audio", "")
|
||||
if reference_audio:
|
||||
voice_name = f"{config.INDEXTTS_MACOS_VOICE_PREFIX}{reference_audio}"
|
||||
voice_rate = 1.0 # 语速由 macOS Pack 配置传递
|
||||
voice_pitch = 1.0
|
||||
elif selected_engine == config.INDEXTTS2_ENGINE:
|
||||
reference_audio = config.indextts2.get("reference_audio", "")
|
||||
if reference_audio:
|
||||
@ -1777,6 +1889,7 @@ def render_voice_preview_new(tr, selected_engine):
|
||||
temp_dir = utils.storage_dir("temp", create=True)
|
||||
audio_format = "audio/wav" if selected_engine in (
|
||||
config.INDEXTTS_ENGINE,
|
||||
config.INDEXTTS_MACOS_ENGINE,
|
||||
config.INDEXTTS2_ENGINE,
|
||||
config.OMNIVOICE_ENGINE,
|
||||
) else "audio/mp3"
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import unittest
|
||||
|
||||
from webui.components.audio_settings import _normalize_source_pills_value
|
||||
from webui.components.audio_settings import (
|
||||
_normalize_source_pills_value,
|
||||
get_tts_engine_options,
|
||||
)
|
||||
|
||||
|
||||
def zh_tr(key):
|
||||
@ -12,6 +15,17 @@ def zh_tr(key):
|
||||
|
||||
|
||||
class AudioSettingsSourcePillsTests(unittest.TestCase):
|
||||
def test_tts_engine_options_include_indextts_15_macos_as_local_engine(self):
|
||||
options = get_tts_engine_options(lambda key: {
|
||||
"Local Deployment": "本地部署",
|
||||
"Cloud Service": "云服务",
|
||||
}.get(key, key))
|
||||
|
||||
self.assertEqual(
|
||||
"IndexTTS-1.5-macOS [本地部署]",
|
||||
options["indextts_macos"],
|
||||
)
|
||||
|
||||
def test_normalize_source_pills_value_keeps_canonical_value(self):
|
||||
options = {
|
||||
"resource": "Select from Resource Directory",
|
||||
|
||||
@ -373,9 +373,11 @@
|
||||
"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",
|
||||
"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 packages:\n\n- **macOS**: [Download](https://cutagent.online/resources/indextts15-mlx-macos)\n- **Windows**: [Download](https://cutagent.online/resources/indextts15-windows)",
|
||||
"IndexTTS features": "A locally or privately deployed IndexTTS-1.5 voice-cloning engine for Windows. Choose or upload reference audio to synthesize narration in that voice.",
|
||||
"IndexTTS use case": "Best for fixed narrator voices, character dubbing, and batch generation on Windows. Start the IndexTTS-1.5 API service before use. [Download the deployment package](https://cutagent.online/resources/indextts15-windows).",
|
||||
"IndexTTS download link": "Download packages:\n\n- **macOS**: [Download](https://cutagent.online/resources/indextts15-mlx-macos)\n- **Windows**: [Download](https://cutagent.online/resources/indextts15-windows)",
|
||||
"IndexTTS macOS features": "A local IndexTTS-1.5 MLX voice-cloning engine for Apple Silicon that uploads reference audio to synthesize narration.",
|
||||
"IndexTTS macOS use case": "Best for local fixed narrator voices and character dubbing on Apple Silicon Macs. Start IndexTTS-MLX-1.5-Pack before use. [Download the deployment package](https://cutagent.online/resources/indextts15-mlx-macos).",
|
||||
"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. Deployment packages:\n\n- **macOS**: [Download](https://cutagent.online/resources/indextts2-full-macos)\n- **Windows**: Coming soon",
|
||||
"OmniVoice features": "A locally or privately deployed OmniVoice-Pack multilingual TTS engine with automatic voice generation, voice design, and reference-audio cloning.",
|
||||
@ -564,6 +566,7 @@
|
||||
"Select Qwen3 TTS Voice": "Select a Qwen3 TTS voice",
|
||||
"API URL": "API URL",
|
||||
"IndexTTS API URL Help": "IndexTTS-1.5 API service URL",
|
||||
"IndexTTS macOS API URL Help": "IndexTTS-MLX-1.5-Pack service URL. Enter the service root or the full /v1/audio/speech/upload 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",
|
||||
@ -618,6 +621,8 @@
|
||||
"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",
|
||||
"IndexTTS macOS Usage Instructions Title": "IndexTTS-1.5-macOS Usage Instructions",
|
||||
"IndexTTS macOS Usage Instructions": "**Local voice cloning on Apple Silicon**\n\n1. Double-click `start.command` in the Pack to start the service\n2. Keep the default API URL `http://127.0.0.1:7866`, or enter the full upload endpoint\n3. Select clean reference audio and start synthesis\n\nThe first request usually takes longer while the MLX model loads. This engine requires an Apple Silicon Mac.",
|
||||
"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.",
|
||||
|
||||
@ -311,9 +311,11 @@
|
||||
"Tencent Cloud TTS use case": "个人和企业用户,需要稳定的中文语音合成",
|
||||
"Tongyi Qwen3 TTS features": "阿里云通义千问语音合成,音质优秀,支持多种音色",
|
||||
"High-quality Chinese speech synthesis use case": "需要高质量中文语音合成的用户",
|
||||
"IndexTTS features": "本地/私有部署的 IndexTTS-1.5 语音克隆引擎。选择资源目录音频或上传参考音频后,可按该音色合成旁白。",
|
||||
"IndexTTS use case": "适合需要固定旁白音色、角色配音或批量生成同一音色视频的场景。使用前请先启动 IndexTTS-1.5 API 服务;部署包下载:\n\n- **macOS**:[下载地址](https://cutagent.online/resources/indextts15-mlx-macos)\n- **Windows**:[下载地址](https://cutagent.online/resources/indextts15-windows)",
|
||||
"IndexTTS features": "Windows 本地/私有部署的 IndexTTS-1.5 语音克隆引擎。选择资源目录音频或上传参考音频后,可按该音色合成旁白。",
|
||||
"IndexTTS use case": "适合在 Windows 上生成固定旁白音色、角色配音或批量生成同一音色视频。使用前请先启动 IndexTTS-1.5 API 服务;[下载部署包](https://cutagent.online/resources/indextts15-windows)。",
|
||||
"IndexTTS download link": "下载地址:\n\n- **macOS**:[下载地址](https://cutagent.online/resources/indextts15-mlx-macos)\n- **Windows**:[下载地址](https://cutagent.online/resources/indextts15-windows)",
|
||||
"IndexTTS macOS features": "面向 Apple Silicon 的 IndexTTS-1.5 MLX 本地语音克隆引擎,通过上传参考音频合成旁白。",
|
||||
"IndexTTS macOS use case": "适合在 Apple Silicon Mac 上本地生成固定旁白音色和角色配音。使用前请启动 IndexTTS-MLX-1.5-Pack;[下载部署包](https://cutagent.online/resources/indextts15-mlx-macos)。",
|
||||
"IndexTTS2 features": "本地部署的 IndexTTS-2 MLX Pack 语音克隆引擎,支持情感控制和更完整的生成参数。",
|
||||
"IndexTTS2 use case": "适合需要固定音色、情绪化旁白或更细致采样控制的本地语音合成场景。使用前请先启动 IndexTTS-2 MLX Pack 服务;部署包下载:\n\n- **macOS**:[下载地址](https://cutagent.online/resources/indextts2-full-macos)\n- **Windows**:待更新",
|
||||
"OmniVoice features": "本地/私有部署的 OmniVoice-Pack 多语种语音合成引擎,支持自动音色、指令音色和参考音频克隆。",
|
||||
@ -503,6 +505,7 @@
|
||||
"Select Qwen3 TTS Voice": "选择 Qwen3 TTS 音色",
|
||||
"API URL": "API 地址",
|
||||
"IndexTTS API URL Help": "IndexTTS-1.5 API 服务地址",
|
||||
"IndexTTS macOS API URL Help": "IndexTTS-MLX-1.5-Pack 服务地址,可填写服务根地址或完整 /v1/audio/speech/upload 地址",
|
||||
"IndexTTS2 API URL Help": "IndexTTS-2 MLX Pack 服务地址,可填写服务根地址或完整 /v1/audio/speech/upload 地址",
|
||||
"OmniVoice API URL Help": "OmniVoice-Pack API 服务地址,可填写服务根地址或完整 /tts 地址",
|
||||
"OmniVoice Language Code": "合成语言",
|
||||
@ -557,6 +560,8 @@
|
||||
"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- 首次合成可能需要较长时间",
|
||||
"IndexTTS macOS Usage Instructions Title": "IndexTTS-1.5-macOS 使用说明",
|
||||
"IndexTTS macOS Usage Instructions": "**Apple Silicon 本地语音克隆**\n\n1. 双击 Pack 中的 `start.command` 启动服务\n2. 保持默认 API 地址 `http://127.0.0.1:7866`,或填写完整上传接口\n3. 选择清晰的参考音频后开始合成\n\n首次请求需要加载 MLX 模型,耗时通常更长。该引擎仅适用于 Apple Silicon Mac。",
|
||||
"IndexTTS2 Emotion Parameters": "情感参数",
|
||||
"IndexTTS2 Emotion": "情感覆盖",
|
||||
"IndexTTS2 Emotion Help": "留空时保留参考音频的情感;可填写单个情感,如 happy,或权重混合,如 happy:0.7,calm:0.3。",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user