mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-07-17 11:38:09 +00:00
fix: adapt IndexTTS-2 MLX Pack API
This commit is contained in:
parent
cd20aebacf
commit
39eae0d511
@ -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
|
||||
|
||||
211
app/services/test_indextts2_tts_unittest.py
Normal file
211
app/services/test_indextts2_tts_unittest.py
Normal 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()
|
||||
@ -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)
|
||||
|
||||
@ -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 语音合成配置
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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.5x–2.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",
|
||||
|
||||
@ -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.5x–2.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:7860;NarratoAI 会调用 /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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user