mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-07-28 08:56:00 +00:00
feat(tts): 新增IndexTTS-2语音合成引擎支持
实现兼容IndexTTS2-Pack API的完整TTS调用流程,包含音频下载、错误重试等处理 重构原有IndexTTS-1.5代码,抽象通用逻辑以同时兼容indextts和indextts2两个引擎 新增IndexTTS-2的WebUI配置界面,支持情感控制与高级生成参数调整 更新配置示例文件与中英多语言文案,完善配置迁移逻辑兼容旧版配置 新增对应单元测试覆盖参数处理与配置迁移流程
This commit is contained in:
parent
a2645aebd3
commit
d147fe66e4
@ -10,34 +10,51 @@ root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__fi
|
||||
config_file = f"{root_dir}/config.toml"
|
||||
version_file = f"{root_dir}/project_version"
|
||||
INDEXTTS_ENGINE = "indextts"
|
||||
INDEXTTS_LEGACY_ENGINE = "indextts2"
|
||||
INDEXTTS_DISPLAY_NAME = "IndexTTS-1.5"
|
||||
INDEXTTS2_ENGINE = "indextts2"
|
||||
INDEXTTS2_DISPLAY_NAME = "IndexTTS-2"
|
||||
INDEXTTS_VOICE_PREFIX = f"{INDEXTTS_ENGINE}:"
|
||||
INDEXTTS_LEGACY_VOICE_PREFIX = f"{INDEXTTS_LEGACY_ENGINE}:"
|
||||
INDEXTTS2_VOICE_PREFIX = f"{INDEXTTS2_ENGINE}:"
|
||||
|
||||
|
||||
def normalize_tts_engine_name(tts_engine: str) -> str:
|
||||
if tts_engine == INDEXTTS_LEGACY_ENGINE:
|
||||
return INDEXTTS_ENGINE
|
||||
return tts_engine
|
||||
|
||||
|
||||
def normalize_indextts_voice_prefix(voice_name: str) -> str:
|
||||
if isinstance(voice_name, str) and voice_name.startswith(INDEXTTS_LEGACY_VOICE_PREFIX):
|
||||
return f"{INDEXTTS_VOICE_PREFIX}{voice_name[len(INDEXTTS_LEGACY_VOICE_PREFIX):]}"
|
||||
return voice_name
|
||||
|
||||
|
||||
def _is_legacy_indextts2_config(indextts2_config) -> bool:
|
||||
if not isinstance(indextts2_config, dict):
|
||||
return False
|
||||
api_url = str(indextts2_config.get("api_url", ""))
|
||||
has_indextts2_fields = any(
|
||||
key in indextts2_config
|
||||
for key in (
|
||||
"emotion_mode",
|
||||
"emotion_alpha",
|
||||
"max_text_tokens_per_segment",
|
||||
"max_mel_tokens",
|
||||
"vec_calm",
|
||||
)
|
||||
)
|
||||
return "8081" in api_url and not has_indextts2_fields
|
||||
|
||||
|
||||
def migrate_indextts_config(config_data):
|
||||
if "indextts" not in config_data and INDEXTTS_LEGACY_ENGINE in config_data:
|
||||
config_data["indextts"] = config_data[INDEXTTS_LEGACY_ENGINE]
|
||||
migrated_legacy_indextts2 = _is_legacy_indextts2_config(config_data.get(INDEXTTS2_ENGINE))
|
||||
if migrated_legacy_indextts2:
|
||||
if "indextts" not in config_data:
|
||||
config_data["indextts"] = config_data[INDEXTTS2_ENGINE]
|
||||
config_data.pop(INDEXTTS2_ENGINE, None)
|
||||
|
||||
ui_config = config_data.get("ui")
|
||||
if isinstance(ui_config, dict):
|
||||
if "tts_engine" in ui_config:
|
||||
ui_config["tts_engine"] = normalize_tts_engine_name(ui_config.get("tts_engine", ""))
|
||||
if "voice_name" in ui_config:
|
||||
ui_config["voice_name"] = normalize_indextts_voice_prefix(ui_config.get("voice_name", ""))
|
||||
if migrated_legacy_indextts2 and ui_config.get("tts_engine") == INDEXTTS2_ENGINE:
|
||||
ui_config["tts_engine"] = INDEXTTS_ENGINE
|
||||
if ui_config.get("voice_name", "").startswith(INDEXTTS2_VOICE_PREFIX) and ui_config.get("tts_engine") == INDEXTTS_ENGINE:
|
||||
ui_config["voice_name"] = f"{INDEXTTS_VOICE_PREFIX}{ui_config['voice_name'][len(INDEXTTS2_VOICE_PREFIX):]}"
|
||||
return config_data
|
||||
|
||||
|
||||
@ -113,7 +130,7 @@ def save_config():
|
||||
_cfg["tts_qwen"] = tts_qwen
|
||||
_cfg["fun_asr"] = fun_asr
|
||||
_cfg["indextts"] = indextts
|
||||
_cfg.pop(INDEXTTS_LEGACY_ENGINE, None)
|
||||
_cfg["indextts2"] = indextts2
|
||||
_cfg["doubaotts"] = doubaotts
|
||||
f.write(toml.dumps(_cfg))
|
||||
|
||||
@ -129,7 +146,8 @@ ui = _cfg.get("ui", {})
|
||||
frames = _cfg.get("frames", {})
|
||||
tts_qwen = _cfg.get("tts_qwen", {})
|
||||
fun_asr = _cfg.get("fun_asr", {})
|
||||
indextts = _cfg.get("indextts", _cfg.get(INDEXTTS_LEGACY_ENGINE, {}))
|
||||
indextts = _cfg.get("indextts", {})
|
||||
indextts2 = _cfg.get("indextts2", {})
|
||||
doubaotts = _cfg.get("doubaotts", {})
|
||||
|
||||
hostname = socket.gethostname()
|
||||
|
||||
@ -64,7 +64,7 @@ hide_config = true
|
||||
self.assertEqual("Pro/zai-org/GLM-5", saved_config["app"]["text_openai_model_name"])
|
||||
self.assertTrue(saved_config["app"]["hide_config"])
|
||||
|
||||
def test_indextts_legacy_config_is_migrated(self):
|
||||
def test_legacy_indextts2_config_is_migrated_to_indextts_15(self):
|
||||
migrated = cfg.migrate_indextts_config(
|
||||
{
|
||||
"indextts2": {"api_url": "http://127.0.0.1:8081/tts"},
|
||||
@ -76,9 +76,30 @@ hide_config = true
|
||||
)
|
||||
|
||||
self.assertEqual("http://127.0.0.1:8081/tts", migrated["indextts"]["api_url"])
|
||||
self.assertNotIn("indextts2", migrated)
|
||||
self.assertEqual("indextts", migrated["ui"]["tts_engine"])
|
||||
self.assertEqual("indextts:/tmp/reference.wav", migrated["ui"]["voice_name"])
|
||||
|
||||
def test_indextts2_config_is_kept_as_separate_engine(self):
|
||||
migrated = cfg.migrate_indextts_config(
|
||||
{
|
||||
"indextts": {"api_url": "http://127.0.0.1:8081/tts"},
|
||||
"indextts2": {
|
||||
"api_url": "http://192.168.3.6:7863/tts",
|
||||
"emotion_mode": "speaker",
|
||||
},
|
||||
"ui": {
|
||||
"tts_engine": "indextts2",
|
||||
"voice_name": "indextts2:/tmp/reference.wav",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual("http://127.0.0.1:8081/tts", migrated["indextts"]["api_url"])
|
||||
self.assertEqual("http://192.168.3.6:7863/tts", migrated["indextts2"]["api_url"])
|
||||
self.assertEqual("indextts2", migrated["ui"]["tts_engine"])
|
||||
self.assertEqual("indextts2:/tmp/reference.wav", migrated["ui"]["voice_name"])
|
||||
|
||||
|
||||
class OpenAICompatibleModelDefaultsTests(unittest.TestCase):
|
||||
def test_ui_keeps_full_model_name_and_openai_provider(self):
|
||||
|
||||
@ -49,14 +49,20 @@ def get_audio_duration_ffprobe(audio_file: str) -> float:
|
||||
return get_media_duration_ffprobe(audio_file)
|
||||
|
||||
|
||||
def _strip_indextts_prefix(voice_name: str) -> str:
|
||||
voice_name = config.normalize_indextts_voice_prefix(voice_name or "")
|
||||
prefix = config.INDEXTTS_VOICE_PREFIX
|
||||
def _strip_tts_voice_prefix(voice_name: str, prefix: str) -> str:
|
||||
voice_name = voice_name or ""
|
||||
if voice_name.startswith(prefix):
|
||||
return voice_name[len(prefix):]
|
||||
return voice_name
|
||||
|
||||
|
||||
def _strip_indextts_prefix(voice_name: str) -> str:
|
||||
return _strip_tts_voice_prefix(
|
||||
config.normalize_indextts_voice_prefix(voice_name or ""),
|
||||
config.INDEXTTS_VOICE_PREFIX,
|
||||
)
|
||||
|
||||
|
||||
def _floor_duration_to_milliseconds(duration: float) -> float:
|
||||
return int(duration * 1000) / 1000.0
|
||||
|
||||
@ -101,24 +107,32 @@ def _clamp_duration_to_media(
|
||||
|
||||
|
||||
def _normalize_indextts_reference_audio(params: VideoClipParams) -> None:
|
||||
"""Ensure IndexTTS-1.5 uses the configured reference audio instead of a stale UI voice."""
|
||||
"""Ensure IndexTTS engines use the configured reference audio instead of a stale UI voice."""
|
||||
params.tts_engine = config.normalize_tts_engine_name(params.tts_engine)
|
||||
if params.tts_engine != config.INDEXTTS_ENGINE:
|
||||
if params.tts_engine == config.INDEXTTS_ENGINE:
|
||||
tts_config = config.indextts
|
||||
voice_prefix = config.INDEXTTS_VOICE_PREFIX
|
||||
display_name = "IndexTTS-1.5"
|
||||
elif params.tts_engine == config.INDEXTTS2_ENGINE:
|
||||
tts_config = config.indextts2
|
||||
voice_prefix = config.INDEXTTS2_VOICE_PREFIX
|
||||
display_name = "IndexTTS-2"
|
||||
else:
|
||||
return
|
||||
|
||||
candidate = _strip_indextts_prefix(getattr(params, "voice_name", "") or "")
|
||||
candidate = _strip_tts_voice_prefix(getattr(params, "voice_name", "") or "", voice_prefix)
|
||||
if candidate and os.path.isfile(candidate):
|
||||
params.voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{candidate}"
|
||||
logger.info(f"IndexTTS-1.5 使用参考音频: {candidate}")
|
||||
params.voice_name = f"{voice_prefix}{candidate}"
|
||||
logger.info(f"{display_name} 使用参考音频: {candidate}")
|
||||
return
|
||||
|
||||
configured_ref = _strip_indextts_prefix(config.indextts.get("reference_audio", "") or "")
|
||||
configured_ref = _strip_tts_voice_prefix(tts_config.get("reference_audio", "") or "", voice_prefix)
|
||||
if configured_ref and os.path.isfile(configured_ref):
|
||||
params.voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{configured_ref}"
|
||||
logger.info(f"IndexTTS-1.5 使用配置中的参考音频: {configured_ref}")
|
||||
params.voice_name = f"{voice_prefix}{configured_ref}"
|
||||
logger.info(f"{display_name} 使用配置中的参考音频: {configured_ref}")
|
||||
return
|
||||
|
||||
raise ValueError("IndexTTS-1.5 参考音频不存在,请在音频设置中上传或选择有效的参考音频")
|
||||
raise ValueError(f"{display_name} 参考音频不存在,请在音频设置中上传或选择有效的参考音频")
|
||||
|
||||
|
||||
def start_export_jianying_draft(task_id: str, params: VideoClipParams):
|
||||
|
||||
@ -31,14 +31,25 @@ class JianyingTaskTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(f"indextts:{ref_path}", params.voice_name)
|
||||
|
||||
def test_normalize_indextts_accepts_legacy_engine_and_prefix(self):
|
||||
def test_normalize_indextts2_uses_valid_param_reference(self):
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav") as ref:
|
||||
params = VideoClipParams(tts_engine="indextts2", voice_name=f"indextts2:{ref.name}")
|
||||
|
||||
jianying_task._normalize_indextts_reference_audio(params)
|
||||
|
||||
self.assertEqual("indextts", params.tts_engine)
|
||||
self.assertEqual(f"indextts:{ref.name}", params.voice_name)
|
||||
self.assertEqual("indextts2", params.tts_engine)
|
||||
self.assertEqual(f"indextts2:{ref.name}", params.voice_name)
|
||||
|
||||
def test_normalize_indextts2_uses_config_reference_when_param_is_stale(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
ref_path = Path(temp_dir) / "reference.wav"
|
||||
ref_path.write_bytes(b"fake wav")
|
||||
params = VideoClipParams(tts_engine="indextts2", voice_name="zh-CN-YunjianNeural")
|
||||
|
||||
with patch.dict(jianying_task.config.indextts2, {"reference_audio": str(ref_path)}, clear=False):
|
||||
jianying_task._normalize_indextts_reference_audio(params)
|
||||
|
||||
self.assertEqual(f"indextts2:{ref_path}", params.voice_name)
|
||||
|
||||
def test_normalize_indextts_requires_existing_reference_audio(self):
|
||||
params = VideoClipParams(tts_engine="indextts", voice_name="zh-CN-YunjianNeural")
|
||||
|
||||
@ -21,6 +21,7 @@ except ImportError:
|
||||
MOVIEPY_AVAILABLE = False
|
||||
logger.warning("moviepy 未安装,将使用估算方法计算音频时长")
|
||||
import time
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from app.config import config
|
||||
from app.utils import utils
|
||||
@ -1293,6 +1294,10 @@ def tts(
|
||||
if tts_engine == config.INDEXTTS_ENGINE:
|
||||
logger.info("分发到 IndexTTS-1.5")
|
||||
return indextts_tts(text, voice_name, voice_file, speed=voice_rate)
|
||||
|
||||
if tts_engine == config.INDEXTTS2_ENGINE:
|
||||
logger.info("分发到 IndexTTS-2")
|
||||
return indextts2_tts(text, voice_name, voice_file)
|
||||
|
||||
if tts_engine == "doubaotts":
|
||||
logger.info("分发到豆包语音 TTS")
|
||||
@ -1778,12 +1783,13 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f
|
||||
voice_name = config.normalize_indextts_voice_prefix(parse_voice_name(voice_name))
|
||||
output_dir = utils.task_dir(task_id)
|
||||
tts_results = []
|
||||
audio_extension = ".wav" if tts_engine in (config.INDEXTTS_ENGINE, config.INDEXTTS2_ENGINE) else ".mp3"
|
||||
|
||||
for item in list_script:
|
||||
if item['OST'] != 1:
|
||||
# 将时间戳中的冒号替换为下划线
|
||||
timestamp = item['timestamp'].replace(':', '_')
|
||||
audio_file = os.path.join(output_dir, f"audio_{timestamp}.mp3")
|
||||
audio_file = os.path.join(output_dir, f"audio_{timestamp}{audio_extension}")
|
||||
subtitle_file = os.path.join(output_dir, f"subtitle_{timestamp}.srt")
|
||||
|
||||
text = item['narration']
|
||||
@ -1803,8 +1809,13 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f
|
||||
f"或者使用其他 tts 引擎")
|
||||
continue
|
||||
else:
|
||||
# SoulVoice、Qwen3、IndexTTS-1.5、豆包语音 引擎不生成字幕文件
|
||||
if is_soulvoice_voice(voice_name) or is_qwen_engine(tts_engine) or tts_engine == config.INDEXTTS_ENGINE or tts_engine == "doubaotts":
|
||||
# SoulVoice、Qwen3、IndexTTS、豆包语音 引擎不生成精确字幕文件
|
||||
if (
|
||||
is_soulvoice_voice(voice_name)
|
||||
or is_qwen_engine(tts_engine)
|
||||
or tts_engine in (config.INDEXTTS_ENGINE, config.INDEXTTS2_ENGINE)
|
||||
or tts_engine == "doubaotts"
|
||||
):
|
||||
# 获取实际音频文件的时长
|
||||
duration = get_audio_duration_from_file(audio_file)
|
||||
if duration <= 0:
|
||||
@ -2234,6 +2245,17 @@ def parse_indextts_voice(voice_name: str) -> str:
|
||||
return voice_name
|
||||
|
||||
|
||||
def parse_indextts2_voice(voice_name: str) -> str:
|
||||
"""
|
||||
解析 IndexTTS-2 语音名称
|
||||
支持格式:indextts2:reference_audio_path
|
||||
返回参考音频文件路径
|
||||
"""
|
||||
if isinstance(voice_name, str) and voice_name.startswith(config.INDEXTTS2_VOICE_PREFIX):
|
||||
return voice_name[len(config.INDEXTTS2_VOICE_PREFIX):]
|
||||
return voice_name
|
||||
|
||||
|
||||
def indextts_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0) -> Union[SubMaker, None]:
|
||||
"""
|
||||
使用 IndexTTS-1.5 API 进行零样本语音克隆
|
||||
@ -2344,3 +2366,130 @@ def indextts_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0
|
||||
|
||||
logger.error("IndexTTS-1.5 TTS 生成失败,已达到最大重试次数")
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_indextts2_api_url(api_url: str) -> str:
|
||||
api_url = (api_url or "http://192.168.3.6:7863/tts").strip()
|
||||
if api_url.endswith("/tts"):
|
||||
return api_url
|
||||
return f"{api_url.rstrip('/')}/tts"
|
||||
|
||||
|
||||
def _get_configured_proxies() -> dict:
|
||||
if not config.proxy.get("http"):
|
||||
return {}
|
||||
return {
|
||||
"http": config.proxy.get("http"),
|
||||
"https": config.proxy.get("https", config.proxy.get("http")),
|
||||
}
|
||||
|
||||
|
||||
def _download_indextts2_audio(response: requests.Response, api_url: str, voice_file: str, proxies: dict) -> bool:
|
||||
content_type = response.headers.get("content-type", "").lower()
|
||||
if "application/json" not in content_type:
|
||||
with open(voice_file, "wb") as f:
|
||||
f.write(response.content)
|
||||
return os.path.getsize(voice_file) > 0
|
||||
|
||||
result = response.json()
|
||||
downloads = result.get("downloads") if isinstance(result, dict) else {}
|
||||
download_url = downloads.get("wav") if isinstance(downloads, dict) else ""
|
||||
if not download_url:
|
||||
logger.error(f"IndexTTS-2 API 响应中没有音频下载地址: {result}")
|
||||
return False
|
||||
|
||||
audio_url = urljoin(api_url, download_url)
|
||||
audio_response = requests.get(audio_url, proxies=proxies, timeout=120)
|
||||
if audio_response.status_code != 200:
|
||||
logger.error(f"IndexTTS-2 音频下载失败: {audio_response.status_code} - {audio_response.text}")
|
||||
return False
|
||||
|
||||
with open(voice_file, "wb") as f:
|
||||
f.write(audio_response.content)
|
||||
return os.path.getsize(voice_file) > 0
|
||||
|
||||
|
||||
def indextts2_tts(text: str, voice_name: str, voice_file: str) -> Union[SubMaker, None]:
|
||||
"""
|
||||
使用 IndexTTS-2 API 进行零样本语音克隆。
|
||||
接口兼容 IndexTTS2-Pack 的 POST /tts multipart form。
|
||||
"""
|
||||
api_url = _normalize_indextts2_api_url(config.indextts2.get("api_url", "http://192.168.3.6:7863/tts"))
|
||||
reference_audio_path = parse_indextts2_voice(voice_name)
|
||||
|
||||
if not reference_audio_path or not os.path.exists(reference_audio_path):
|
||||
logger.error(f"IndexTTS-2 参考音频文件不存在: {reference_audio_path}")
|
||||
return None
|
||||
|
||||
emotion_mode = config.indextts2.get("emotion_mode", "speaker")
|
||||
emotion_audio_path = config.indextts2.get("emotion_audio", "")
|
||||
data = {
|
||||
"text": text.strip(),
|
||||
"emotion_mode": emotion_mode,
|
||||
"emotion_alpha": config.indextts2.get("emotion_alpha", 0.65),
|
||||
"emotion_text": config.indextts2.get("emotion_text", ""),
|
||||
"use_random": str(bool(config.indextts2.get("use_random", False))).lower(),
|
||||
"max_text_tokens_per_segment": config.indextts2.get("max_text_tokens_per_segment", 120),
|
||||
"vec_happy": config.indextts2.get("vec_happy", 0.0),
|
||||
"vec_angry": config.indextts2.get("vec_angry", 0.0),
|
||||
"vec_sad": config.indextts2.get("vec_sad", 0.0),
|
||||
"vec_afraid": config.indextts2.get("vec_afraid", 0.0),
|
||||
"vec_disgusted": config.indextts2.get("vec_disgusted", 0.0),
|
||||
"vec_melancholic": config.indextts2.get("vec_melancholic", 0.0),
|
||||
"vec_surprised": config.indextts2.get("vec_surprised", 0.0),
|
||||
"vec_calm": config.indextts2.get("vec_calm", 0.8),
|
||||
"temperature": config.indextts2.get("temperature", 0.8),
|
||||
"top_p": config.indextts2.get("top_p", 0.8),
|
||||
"top_k": config.indextts2.get("top_k", 30),
|
||||
"num_beams": config.indextts2.get("num_beams", 3),
|
||||
"repetition_penalty": config.indextts2.get("repetition_penalty", 10.0),
|
||||
"max_mel_tokens": config.indextts2.get("max_mel_tokens", 1500),
|
||||
}
|
||||
|
||||
proxies = _get_configured_proxies()
|
||||
for attempt in range(3):
|
||||
files = {}
|
||||
try:
|
||||
files["speaker_audio"] = open(reference_audio_path, "rb")
|
||||
if emotion_mode == "audio":
|
||||
if not emotion_audio_path or not os.path.exists(emotion_audio_path):
|
||||
logger.error(f"IndexTTS-2 情感参考音频文件不存在: {emotion_audio_path}")
|
||||
return None
|
||||
files["emotion_audio"] = open(emotion_audio_path, "rb")
|
||||
|
||||
logger.info(f"第 {attempt + 1} 次调用 IndexTTS-2 API: {api_url}")
|
||||
response = requests.post(
|
||||
api_url,
|
||||
files=files,
|
||||
data=data,
|
||||
proxies=proxies,
|
||||
timeout=180,
|
||||
)
|
||||
|
||||
if response.status_code == 200 and _download_indextts2_audio(response, api_url, voice_file, proxies):
|
||||
logger.info(f"IndexTTS-2 成功生成音频: {voice_file}, 大小: {os.path.getsize(voice_file)} 字节")
|
||||
sub_maker = new_sub_maker()
|
||||
duration = get_audio_duration_from_file(voice_file)
|
||||
duration_ms = int(duration * 1000) if duration > 0 else max(1000, int(len(text) * 200))
|
||||
add_subtitle_event(sub_maker, 0, duration_ms * 10000, text)
|
||||
return sub_maker
|
||||
|
||||
logger.error(f"IndexTTS-2 API 调用失败: {response.status_code} - {response.text}")
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error(f"IndexTTS-2 API 调用超时 (尝试 {attempt + 1}/3)")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"IndexTTS-2 API 网络错误: {str(e)} (尝试 {attempt + 1}/3)")
|
||||
except Exception as e:
|
||||
logger.error(f"IndexTTS-2 TTS 处理错误: {str(e)} (尝试 {attempt + 1}/3)")
|
||||
finally:
|
||||
for file_obj in files.values():
|
||||
try:
|
||||
file_obj.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if attempt < 2:
|
||||
time.sleep(2)
|
||||
|
||||
logger.error("IndexTTS-2 TTS 生成失败,已达到最大重试次数")
|
||||
return None
|
||||
|
||||
@ -113,21 +113,21 @@
|
||||
# 使用阿里百炼在线 fun-asr 时,访问 https://bailian.console.aliyun.com/?tab=model#/api-key 获取 API Key
|
||||
api_key = ""
|
||||
model = "fun-asr"
|
||||
|
||||
|
||||
[indextts]
|
||||
# IndexTTS-1.5 语音克隆配置
|
||||
# 这是一个开源的零样本语音克隆项目,需要自行部署
|
||||
# 项目地址:https://github.com/index-tts/index-tts
|
||||
# 默认 API 地址(本地部署)
|
||||
api_url = "http://127.0.0.1:8081/tts"
|
||||
|
||||
|
||||
# 默认参考音频(可选)
|
||||
reference_audio_source = "resource"
|
||||
# reference_audio = "/path/to/reference_audio.wav"
|
||||
|
||||
|
||||
# 推理模式:普通推理 / 快速推理
|
||||
infer_mode = "普通推理"
|
||||
|
||||
|
||||
# 高级参数
|
||||
temperature = 1.0
|
||||
top_p = 0.8
|
||||
@ -135,6 +135,42 @@
|
||||
do_sample = true
|
||||
num_beams = 3
|
||||
repetition_penalty = 10.0
|
||||
|
||||
[indextts2]
|
||||
# IndexTTS-2 语音克隆配置
|
||||
# 支持 IndexTTS2-Pack FastAPI 接口:POST /tts
|
||||
api_url = "http://192.168.3.6:7863/tts"
|
||||
|
||||
# 默认参考音频(可选),音色列表复用 IndexTTS-1.5 的资源目录
|
||||
reference_audio_source = "resource"
|
||||
# reference_audio = "/path/to/reference_audio.wav"
|
||||
|
||||
# 情感控制:speaker / audio / vector / text
|
||||
emotion_mode = "speaker"
|
||||
emotion_audio = ""
|
||||
emotion_alpha = 0.65
|
||||
emotion_text = ""
|
||||
use_random = false
|
||||
max_text_tokens_per_segment = 120
|
||||
|
||||
# 8 维情感向量,顺序:happy, angry, sad, afraid, disgusted, melancholic, surprised, calm
|
||||
vec_happy = 0.0
|
||||
vec_angry = 0.0
|
||||
vec_sad = 0.0
|
||||
vec_afraid = 0.0
|
||||
vec_disgusted = 0.0
|
||||
vec_melancholic = 0.0
|
||||
vec_surprised = 0.0
|
||||
vec_calm = 0.8
|
||||
|
||||
# 高级生成参数
|
||||
temperature = 0.8
|
||||
top_p = 0.8
|
||||
top_k = 30
|
||||
num_beams = 3
|
||||
repetition_penalty = 10.0
|
||||
max_mel_tokens = 1500
|
||||
|
||||
[doubaotts]
|
||||
# 豆包语音 TTS 配置
|
||||
# 申请流程:
|
||||
@ -153,7 +189,7 @@
|
||||
silence_duration = 0.125
|
||||
|
||||
[ui]
|
||||
# TTS引擎选择 (indextts, edge_tts, qwen3_tts, tencent_tts, doubaotts, azure_speech)
|
||||
# TTS引擎选择 (indextts, indextts2, edge_tts, qwen3_tts, tencent_tts, doubaotts, azure_speech)
|
||||
tts_engine = "indextts"
|
||||
|
||||
# Edge TTS 配置
|
||||
|
||||
5
webui.py
5
webui.py
@ -233,6 +233,11 @@ def get_voice_name_for_tts_engine(tts_engine: str) -> str:
|
||||
return f"tencent:{config.ui.get('tencent_voice_type', '101001')}"
|
||||
if tts_engine == 'qwen3_tts':
|
||||
return f"qwen3:{config.ui.get('qwen_voice_type', 'Cherry')}"
|
||||
if tts_engine == config.INDEXTTS2_ENGINE:
|
||||
reference_audio = config.indextts2.get('reference_audio', '')
|
||||
if reference_audio:
|
||||
return f"{config.INDEXTTS2_VOICE_PREFIX}{reference_audio}"
|
||||
return config.ui.get('voice_name', '')
|
||||
if config.normalize_tts_engine_name(tts_engine) == config.INDEXTTS_ENGINE:
|
||||
reference_audio = config.indextts.get('reference_audio', '')
|
||||
if reference_audio:
|
||||
|
||||
@ -57,6 +57,7 @@ def get_tts_engine_options(tr=lambda key: key):
|
||||
"""获取TTS引擎选项"""
|
||||
return {
|
||||
config.INDEXTTS_ENGINE: config.INDEXTTS_DISPLAY_NAME,
|
||||
config.INDEXTTS2_ENGINE: config.INDEXTTS2_DISPLAY_NAME,
|
||||
"edge_tts": "Edge TTS",
|
||||
"qwen3_tts": tr("Tongyi Qwen3 TTS"),
|
||||
"tencent_tts": tr("Tencent Cloud TTS"),
|
||||
@ -98,6 +99,12 @@ def get_tts_engine_descriptions(tr=lambda key: key):
|
||||
"use_case": tr("IndexTTS use case"),
|
||||
"registration": None
|
||||
},
|
||||
config.INDEXTTS2_ENGINE: {
|
||||
"title": config.INDEXTTS2_DISPLAY_NAME,
|
||||
"features": tr("IndexTTS2 features"),
|
||||
"use_case": tr("IndexTTS2 use case"),
|
||||
"registration": None
|
||||
},
|
||||
"doubaotts": {
|
||||
"title": tr("Doubao TTS"),
|
||||
"features": tr("Doubao TTS features"),
|
||||
@ -325,7 +332,7 @@ def get_audio_mime_type(audio_path):
|
||||
return "audio/mp3"
|
||||
|
||||
|
||||
def render_reference_audio_preview_button(reference_audio, key, tr):
|
||||
def render_reference_audio_preview_button(reference_audio, key, tr, preview_state_key="indextts_reference_audio_preview_path"):
|
||||
"""渲染参考音频试听按钮"""
|
||||
can_preview = bool(reference_audio and os.path.isfile(reference_audio))
|
||||
if st.button(
|
||||
@ -336,7 +343,102 @@ def render_reference_audio_preview_button(reference_audio, key, tr):
|
||||
disabled=not can_preview,
|
||||
use_container_width=True,
|
||||
):
|
||||
st.session_state["indextts_reference_audio_preview_path"] = reference_audio
|
||||
st.session_state[preview_state_key] = reference_audio
|
||||
|
||||
|
||||
def render_indextts_reference_audio_selector(tr, tts_config, key_prefix):
|
||||
"""渲染 IndexTTS 系列共用的参考音频选择器。"""
|
||||
saved_reference_audio = tts_config.get("reference_audio", "")
|
||||
reference_audio_source_options = {
|
||||
tr("Select from Resource Directory"): "resource",
|
||||
tr("Upload Reference Audio"): "upload",
|
||||
}
|
||||
reference_audio_source_labels = list(reference_audio_source_options.keys())
|
||||
saved_reference_audio_source = tts_config.get("reference_audio_source", "resource")
|
||||
if saved_reference_audio_source not in reference_audio_source_options.values():
|
||||
saved_reference_audio_source = "resource"
|
||||
default_reference_audio_source_label = next(
|
||||
label
|
||||
for label, source_value in reference_audio_source_options.items()
|
||||
if source_value == saved_reference_audio_source
|
||||
)
|
||||
|
||||
st.markdown(f"**{tr('Reference Audio Path')}**")
|
||||
reference_audio_source_label = st.pills(
|
||||
tr("Reference Audio Source"),
|
||||
options=reference_audio_source_labels,
|
||||
selection_mode="single",
|
||||
default=default_reference_audio_source_label,
|
||||
key=f"{key_prefix}_reference_audio_source_selection",
|
||||
help=tr("Reference Audio Source Help"),
|
||||
label_visibility="collapsed",
|
||||
width="stretch",
|
||||
)
|
||||
if not reference_audio_source_label:
|
||||
reference_audio_source_label = default_reference_audio_source_label
|
||||
reference_audio_source = reference_audio_source_options[reference_audio_source_label]
|
||||
|
||||
reference_audio = saved_reference_audio
|
||||
preview_state_key = f"{key_prefix}_reference_audio_preview_path"
|
||||
reference_audio_options = get_indextts_reference_audio_options()
|
||||
if reference_audio_source == "resource" and reference_audio_options:
|
||||
selected_audio_index = get_indextts_reference_audio_index(reference_audio_options, saved_reference_audio)
|
||||
select_col, preview_col = st.columns([5, 1])
|
||||
with select_col:
|
||||
selected_audio_option = reference_audio_options[st.selectbox(
|
||||
tr("Reference Audio Path"),
|
||||
options=range(len(reference_audio_options)),
|
||||
index=selected_audio_index,
|
||||
format_func=lambda x: format_indextts_reference_audio_option(reference_audio_options[x]),
|
||||
help=tr("Reference Audio Path Help"),
|
||||
label_visibility="collapsed",
|
||||
key=f"{key_prefix}_reference_audio_select",
|
||||
)]
|
||||
reference_audio = copy_indextts_reference_audio(selected_audio_option["path"])
|
||||
with preview_col:
|
||||
render_reference_audio_preview_button(
|
||||
reference_audio,
|
||||
f"{key_prefix}_resource_reference_audio_preview",
|
||||
tr,
|
||||
preview_state_key=preview_state_key,
|
||||
)
|
||||
elif reference_audio_source == "resource":
|
||||
st.warning(tr("No Reference Audio Resources Found"))
|
||||
|
||||
if reference_audio_source == "upload":
|
||||
if saved_reference_audio_source != "upload":
|
||||
reference_audio = ""
|
||||
upload_col, preview_col = st.columns([5, 1])
|
||||
with upload_col:
|
||||
uploaded_file = st.file_uploader(
|
||||
tr("Upload Reference Audio File"),
|
||||
type=["wav", "mp3"],
|
||||
help=tr("Upload Reference Audio Help"),
|
||||
label_visibility="collapsed",
|
||||
key=f"{key_prefix}_reference_audio_upload",
|
||||
)
|
||||
|
||||
if uploaded_file is not None:
|
||||
target_dir = utils.storage_dir(INDEXTTS_REFERENCE_AUDIO_COPY_SUBDIR, create=True)
|
||||
audio_path = os.path.join(target_dir, f"uploaded_{uploaded_file.name}")
|
||||
with open(audio_path, "wb") as f:
|
||||
f.write(uploaded_file.getbuffer())
|
||||
reference_audio = audio_path
|
||||
st.success(tr("Audio uploaded").format(path=audio_path))
|
||||
with preview_col:
|
||||
render_reference_audio_preview_button(
|
||||
reference_audio,
|
||||
f"{key_prefix}_upload_reference_audio_preview",
|
||||
tr,
|
||||
preview_state_key=preview_state_key,
|
||||
)
|
||||
|
||||
preview_audio_path = st.session_state.get(preview_state_key, "")
|
||||
if preview_audio_path == reference_audio and os.path.isfile(preview_audio_path):
|
||||
with open(preview_audio_path, "rb") as audio_file:
|
||||
st.audio(audio_file.read(), format=get_audio_mime_type(preview_audio_path))
|
||||
|
||||
return reference_audio_source, reference_audio
|
||||
|
||||
|
||||
def render_bgm_preview_button(bgm_file, key, tr):
|
||||
@ -442,6 +544,8 @@ def render_tts_settings(tr):
|
||||
render_qwen3_tts_settings(tr)
|
||||
elif selected_engine == config.INDEXTTS_ENGINE:
|
||||
render_indextts_tts_settings(tr)
|
||||
elif selected_engine == config.INDEXTTS2_ENGINE:
|
||||
render_indextts2_tts_settings(tr)
|
||||
elif selected_engine == "doubaotts":
|
||||
render_doubaotts_settings(tr)
|
||||
|
||||
@ -861,90 +965,11 @@ def render_indextts_tts_settings(tr):
|
||||
help=tr("IndexTTS API URL Help")
|
||||
)
|
||||
|
||||
saved_reference_audio = config.indextts.get("reference_audio", "")
|
||||
reference_audio_source_options = {
|
||||
tr("Select from Resource Directory"): "resource",
|
||||
tr("Upload Reference Audio"): "upload",
|
||||
}
|
||||
reference_audio_source_labels = list(reference_audio_source_options.keys())
|
||||
saved_reference_audio_source = config.indextts.get("reference_audio_source", "resource")
|
||||
if saved_reference_audio_source not in reference_audio_source_options.values():
|
||||
saved_reference_audio_source = "resource"
|
||||
default_reference_audio_source_label = next(
|
||||
label
|
||||
for label, source_value in reference_audio_source_options.items()
|
||||
if source_value == saved_reference_audio_source
|
||||
reference_audio_source, reference_audio = render_indextts_reference_audio_selector(
|
||||
tr,
|
||||
config.indextts,
|
||||
"indextts",
|
||||
)
|
||||
|
||||
st.markdown(f"**{tr('Reference Audio Path')}**")
|
||||
reference_audio_source_label = st.pills(
|
||||
tr("Reference Audio Source"),
|
||||
options=reference_audio_source_labels,
|
||||
selection_mode="single",
|
||||
default=default_reference_audio_source_label,
|
||||
key="indextts_reference_audio_source_selection",
|
||||
help=tr("Reference Audio Source Help"),
|
||||
label_visibility="collapsed",
|
||||
width="stretch",
|
||||
)
|
||||
if not reference_audio_source_label:
|
||||
reference_audio_source_label = default_reference_audio_source_label
|
||||
reference_audio_source = reference_audio_source_options[reference_audio_source_label]
|
||||
|
||||
reference_audio = saved_reference_audio
|
||||
reference_audio_options = get_indextts_reference_audio_options()
|
||||
if reference_audio_source == "resource" and reference_audio_options:
|
||||
selected_audio_index = get_indextts_reference_audio_index(reference_audio_options, saved_reference_audio)
|
||||
select_col, preview_col = st.columns([5, 1])
|
||||
with select_col:
|
||||
selected_audio_option = reference_audio_options[st.selectbox(
|
||||
tr("Reference Audio Path"),
|
||||
options=range(len(reference_audio_options)),
|
||||
index=selected_audio_index,
|
||||
format_func=lambda x: format_indextts_reference_audio_option(reference_audio_options[x]),
|
||||
help=tr("Reference Audio Path Help"),
|
||||
label_visibility="collapsed"
|
||||
)]
|
||||
reference_audio = copy_indextts_reference_audio(selected_audio_option["path"])
|
||||
with preview_col:
|
||||
render_reference_audio_preview_button(
|
||||
reference_audio,
|
||||
"indextts_resource_reference_audio_preview",
|
||||
tr,
|
||||
)
|
||||
elif reference_audio_source == "resource":
|
||||
st.warning(tr("No Reference Audio Resources Found"))
|
||||
|
||||
if reference_audio_source == "upload":
|
||||
if saved_reference_audio_source != "upload":
|
||||
reference_audio = ""
|
||||
upload_col, preview_col = st.columns([5, 1])
|
||||
with upload_col:
|
||||
uploaded_file = st.file_uploader(
|
||||
tr("Upload Reference Audio File"),
|
||||
type=["wav", "mp3"],
|
||||
help=tr("Upload Reference Audio Help"),
|
||||
label_visibility="collapsed"
|
||||
)
|
||||
|
||||
if uploaded_file is not None:
|
||||
target_dir = utils.storage_dir(INDEXTTS_REFERENCE_AUDIO_COPY_SUBDIR, create=True)
|
||||
audio_path = os.path.join(target_dir, f"uploaded_{uploaded_file.name}")
|
||||
with open(audio_path, "wb") as f:
|
||||
f.write(uploaded_file.getbuffer())
|
||||
reference_audio = audio_path
|
||||
st.success(tr("Audio uploaded").format(path=audio_path))
|
||||
with preview_col:
|
||||
render_reference_audio_preview_button(
|
||||
reference_audio,
|
||||
"indextts_upload_reference_audio_preview",
|
||||
tr,
|
||||
)
|
||||
|
||||
preview_audio_path = st.session_state.get("indextts_reference_audio_preview_path", "")
|
||||
if preview_audio_path == reference_audio and os.path.isfile(preview_audio_path):
|
||||
with open(preview_audio_path, "rb") as audio_file:
|
||||
st.audio(audio_file.read(), format=get_audio_mime_type(preview_audio_path))
|
||||
|
||||
# 推理模式
|
||||
infer_mode_options = [
|
||||
@ -1038,6 +1063,217 @@ def render_indextts_tts_settings(tr):
|
||||
config.ui["voice_name"] = f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}"
|
||||
|
||||
|
||||
def render_indextts2_tts_settings(tr):
|
||||
"""渲染 IndexTTS-2 TTS 设置"""
|
||||
api_url = st.text_input(
|
||||
tr("API URL"),
|
||||
value=config.indextts2.get("api_url", "http://192.168.3.6:7863/tts"),
|
||||
help=tr("IndexTTS2 API URL Help")
|
||||
)
|
||||
|
||||
reference_audio_source, reference_audio = render_indextts_reference_audio_selector(
|
||||
tr,
|
||||
config.indextts2,
|
||||
"indextts2",
|
||||
)
|
||||
|
||||
emotion_mode_options = [
|
||||
("speaker", tr("Emotion Mode Speaker")),
|
||||
("audio", tr("Emotion Mode Audio")),
|
||||
("vector", tr("Emotion Mode Vector")),
|
||||
("text", tr("Emotion Mode Text")),
|
||||
]
|
||||
saved_emotion_mode = config.indextts2.get("emotion_mode", "speaker")
|
||||
emotion_mode_values = [item[0] for item in emotion_mode_options]
|
||||
if saved_emotion_mode not in emotion_mode_values:
|
||||
saved_emotion_mode = "speaker"
|
||||
|
||||
with st.expander(tr("IndexTTS2 Emotion Parameters"), expanded=False):
|
||||
emotion_mode = emotion_mode_options[st.selectbox(
|
||||
tr("Emotion Mode"),
|
||||
options=range(len(emotion_mode_options)),
|
||||
index=emotion_mode_values.index(saved_emotion_mode),
|
||||
format_func=lambda x: emotion_mode_options[x][1],
|
||||
help=tr("Emotion Mode Help"),
|
||||
)][0]
|
||||
|
||||
emotion_alpha = st.slider(
|
||||
tr("Emotion Alpha"),
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
value=float(config.indextts2.get("emotion_alpha", 0.65)),
|
||||
step=0.05,
|
||||
help=tr("Emotion Alpha Help"),
|
||||
)
|
||||
|
||||
emotion_audio = config.indextts2.get("emotion_audio", "")
|
||||
emotion_text = config.indextts2.get("emotion_text", "")
|
||||
if emotion_mode == "audio":
|
||||
emotion_audio_col, emotion_preview_col = st.columns([5, 1])
|
||||
with emotion_audio_col:
|
||||
emotion_audio = st.text_input(
|
||||
tr("Emotion Reference Audio Path"),
|
||||
value=emotion_audio,
|
||||
help=tr("Emotion Reference Audio Path Help"),
|
||||
)
|
||||
with emotion_preview_col:
|
||||
render_reference_audio_preview_button(
|
||||
emotion_audio,
|
||||
"indextts2_emotion_audio_preview",
|
||||
tr,
|
||||
preview_state_key="indextts2_emotion_audio_preview_path",
|
||||
)
|
||||
preview_audio_path = st.session_state.get("indextts2_emotion_audio_preview_path", "")
|
||||
if preview_audio_path == emotion_audio and os.path.isfile(preview_audio_path):
|
||||
with open(preview_audio_path, "rb") as audio_file:
|
||||
st.audio(audio_file.read(), format=get_audio_mime_type(preview_audio_path))
|
||||
elif emotion_mode == "text":
|
||||
emotion_text = st.text_input(
|
||||
tr("Emotion Text"),
|
||||
value=emotion_text,
|
||||
help=tr("Emotion Text Help"),
|
||||
placeholder=tr("Emotion Text Placeholder"),
|
||||
)
|
||||
|
||||
use_random = st.checkbox(
|
||||
tr("Use Random Emotion"),
|
||||
value=bool(config.indextts2.get("use_random", False)),
|
||||
help=tr("Use Random Emotion Help"),
|
||||
)
|
||||
|
||||
emotion_vector_defaults = {
|
||||
"vec_happy": 0.0,
|
||||
"vec_angry": 0.0,
|
||||
"vec_sad": 0.0,
|
||||
"vec_afraid": 0.0,
|
||||
"vec_disgusted": 0.0,
|
||||
"vec_melancholic": 0.0,
|
||||
"vec_surprised": 0.0,
|
||||
"vec_calm": 0.8,
|
||||
}
|
||||
emotion_vector_labels = {
|
||||
"vec_happy": tr("Emotion Happy"),
|
||||
"vec_angry": tr("Emotion Angry"),
|
||||
"vec_sad": tr("Emotion Sad"),
|
||||
"vec_afraid": tr("Emotion Afraid"),
|
||||
"vec_disgusted": tr("Emotion Disgusted"),
|
||||
"vec_melancholic": tr("Emotion Melancholic"),
|
||||
"vec_surprised": tr("Emotion Surprised"),
|
||||
"vec_calm": tr("Emotion Calm"),
|
||||
}
|
||||
emotion_vector_values = {}
|
||||
if emotion_mode == "vector":
|
||||
vec_cols = st.columns(2)
|
||||
for index, (field, default_value) in enumerate(emotion_vector_defaults.items()):
|
||||
with vec_cols[index % 2]:
|
||||
emotion_vector_values[field] = st.slider(
|
||||
emotion_vector_labels[field],
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
value=float(config.indextts2.get(field, default_value)),
|
||||
step=0.05,
|
||||
)
|
||||
else:
|
||||
emotion_vector_values = {
|
||||
field: float(config.indextts2.get(field, default_value))
|
||||
for field, default_value in emotion_vector_defaults.items()
|
||||
}
|
||||
|
||||
with st.expander(tr("Advanced Parameters"), expanded=False):
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
temperature = st.slider(
|
||||
tr("Sampling Temperature"),
|
||||
min_value=0.1,
|
||||
max_value=2.0,
|
||||
value=float(config.indextts2.get("temperature", 0.8)),
|
||||
step=0.1,
|
||||
help=tr("Sampling Temperature Help")
|
||||
)
|
||||
|
||||
top_p = st.slider(
|
||||
"Top P",
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
value=float(config.indextts2.get("top_p", 0.8)),
|
||||
step=0.05,
|
||||
help=tr("Top P Help")
|
||||
)
|
||||
|
||||
top_k = st.slider(
|
||||
"Top K",
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
value=int(config.indextts2.get("top_k", 30)),
|
||||
step=5,
|
||||
help=tr("Top K Help")
|
||||
)
|
||||
|
||||
max_text_tokens_per_segment = st.slider(
|
||||
tr("Max Text Tokens Per Segment"),
|
||||
min_value=20,
|
||||
max_value=600,
|
||||
value=int(config.indextts2.get("max_text_tokens_per_segment", 120)),
|
||||
step=10,
|
||||
help=tr("Max Text Tokens Per Segment Help")
|
||||
)
|
||||
|
||||
with col2:
|
||||
num_beams = st.slider(
|
||||
tr("Num Beams"),
|
||||
min_value=1,
|
||||
max_value=10,
|
||||
value=int(config.indextts2.get("num_beams", 3)),
|
||||
step=1,
|
||||
help=tr("Num Beams Help")
|
||||
)
|
||||
|
||||
repetition_penalty = st.slider(
|
||||
tr("Repetition Penalty"),
|
||||
min_value=0.1,
|
||||
max_value=20.0,
|
||||
value=float(config.indextts2.get("repetition_penalty", 10.0)),
|
||||
step=0.1,
|
||||
help=tr("Repetition Penalty Help")
|
||||
)
|
||||
|
||||
max_mel_tokens = st.slider(
|
||||
tr("Max Mel Tokens"),
|
||||
min_value=50,
|
||||
max_value=1815,
|
||||
value=int(config.indextts2.get("max_mel_tokens", 1500)),
|
||||
step=10,
|
||||
help=tr("Max Mel Tokens Help")
|
||||
)
|
||||
|
||||
with st.expander(tr("IndexTTS2 Usage Instructions Title"), expanded=False):
|
||||
st.markdown(tr("IndexTTS2 Usage Instructions"))
|
||||
|
||||
config.indextts2["api_url"] = api_url
|
||||
config.indextts2["reference_audio_source"] = reference_audio_source
|
||||
config.indextts2["reference_audio"] = reference_audio
|
||||
config.indextts2["emotion_mode"] = emotion_mode
|
||||
config.indextts2["emotion_audio"] = emotion_audio
|
||||
config.indextts2["emotion_alpha"] = emotion_alpha
|
||||
config.indextts2["emotion_text"] = emotion_text
|
||||
config.indextts2["use_random"] = use_random
|
||||
config.indextts2["max_text_tokens_per_segment"] = max_text_tokens_per_segment
|
||||
for field, value in emotion_vector_values.items():
|
||||
config.indextts2[field] = value
|
||||
config.indextts2["temperature"] = temperature
|
||||
config.indextts2["top_p"] = top_p
|
||||
config.indextts2["top_k"] = top_k
|
||||
config.indextts2["num_beams"] = num_beams
|
||||
config.indextts2["repetition_penalty"] = repetition_penalty
|
||||
config.indextts2["max_mel_tokens"] = max_mel_tokens
|
||||
|
||||
if reference_audio:
|
||||
config.ui["voice_name"] = f"{config.INDEXTTS2_VOICE_PREFIX}{reference_audio}"
|
||||
st.session_state['voice_rate'] = 1.0
|
||||
st.session_state['voice_pitch'] = 1.0
|
||||
|
||||
|
||||
def render_doubaotts_settings(tr):
|
||||
"""渲染豆包语音 TTS 设置"""
|
||||
# AK 输入
|
||||
@ -1325,6 +1561,12 @@ def render_voice_preview_new(tr, selected_engine):
|
||||
voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}"
|
||||
voice_rate = 1.0 # IndexTTS-1.5 不支持速度调节
|
||||
voice_pitch = 1.0 # IndexTTS-1.5 不支持音调调节
|
||||
elif selected_engine == config.INDEXTTS2_ENGINE:
|
||||
reference_audio = config.indextts2.get("reference_audio", "")
|
||||
if reference_audio:
|
||||
voice_name = f"{config.INDEXTTS2_VOICE_PREFIX}{reference_audio}"
|
||||
voice_rate = 1.0 # IndexTTS-2 使用自身生成参数
|
||||
voice_pitch = 1.0
|
||||
elif selected_engine == "doubaotts":
|
||||
voice_type = config.ui.get("doubaotts_voice_type", "BV700_streaming")
|
||||
voice_name = voice_type
|
||||
@ -1337,7 +1579,9 @@ def render_voice_preview_new(tr, selected_engine):
|
||||
|
||||
with st.spinner(tr("Synthesizing Voice")):
|
||||
temp_dir = utils.storage_dir("temp", create=True)
|
||||
audio_file = os.path.join(temp_dir, f"tmp-voice-{str(uuid4())}.mp3")
|
||||
audio_format = "audio/wav" if selected_engine in (config.INDEXTTS_ENGINE, config.INDEXTTS2_ENGINE) else "audio/mp3"
|
||||
audio_extension = ".wav" if audio_format == "audio/wav" else ".mp3"
|
||||
audio_file = os.path.join(temp_dir, f"tmp-voice-{str(uuid4())}{audio_extension}")
|
||||
|
||||
sub_maker = voice.tts(
|
||||
text=play_content,
|
||||
@ -1354,7 +1598,7 @@ def render_voice_preview_new(tr, selected_engine):
|
||||
# 播放音频
|
||||
with open(audio_file, 'rb') as audio_file_obj:
|
||||
audio_bytes = audio_file_obj.read()
|
||||
st.audio(audio_bytes, format='audio/mp3')
|
||||
st.audio(audio_bytes, format=audio_format)
|
||||
|
||||
# 清理临时文件
|
||||
try:
|
||||
|
||||
@ -284,6 +284,8 @@
|
||||
"IndexTTS features": "A locally or privately deployed IndexTTS-1.5 voice-cloning engine. Choose a resource audio file or upload a reference audio file, then synthesize narration in that voice.",
|
||||
"IndexTTS use case": "Best for fixed narrator voices, character dubbing, or generating multiple videos with the same voice. Start the IndexTTS-1.5 API service before use. Deployment package: https://pan.quark.cn/s/0767c9bcefd5",
|
||||
"IndexTTS download link": "Download link: https://pan.quark.cn/s/0767c9bcefd5",
|
||||
"IndexTTS2 features": "A locally or privately deployed IndexTTS-2 voice-cloning engine with emotion control and fuller generation parameters.",
|
||||
"IndexTTS2 use case": "Best for fixed voices, emotional narration, and local speech synthesis workflows that need finer sampling controls. Start the IndexTTS-2 API service before use.",
|
||||
"Doubao TTS features": "Volcengine Doubao speech synthesis with multiple voices and emotions, plus fast access in mainland China.",
|
||||
"Select TTS Engine": "Select TTS Engine",
|
||||
"Select TTS Engine Help": "Choose the text-to-speech engine you want to use.",
|
||||
@ -440,6 +442,7 @@
|
||||
"Select Qwen3 TTS Voice": "Select a Qwen3 TTS voice",
|
||||
"API URL": "API URL",
|
||||
"IndexTTS API URL Help": "IndexTTS-1.5 API service URL",
|
||||
"IndexTTS2 API URL Help": "IndexTTS-2 API service URL. You can enter the service root or the full /tts endpoint.",
|
||||
"Reference Audio Source": "Reference Audio Source",
|
||||
"Reference Audio Source Help": "Choose a reference audio from the resource directory or upload a new one.",
|
||||
"Select from Resource Directory": "Select from Resource Directory",
|
||||
@ -469,6 +472,36 @@
|
||||
"Enable Sampling Help": "Enable sampling for more natural speech.",
|
||||
"IndexTTS Usage Instructions Title": "💡 IndexTTS-1.5 Usage Instructions",
|
||||
"IndexTTS Usage Instructions": "**Zero-shot voice cloning**\n\n1. **Prepare reference audio**: upload or specify a clear audio file (3-10 seconds recommended)\n2. **Set API URL**: make sure the IndexTTS-1.5 service is running\n3. **Start synthesis**: the system will use the reference voice to synthesize new speech\n\n**Notes**:\n- Reference audio quality directly affects synthesis quality\n- Use clean audio without background noise when possible\n- Keep text length within a reasonable range\n- The first synthesis may take longer",
|
||||
"IndexTTS2 Emotion Parameters": "🎭 Emotion Parameters",
|
||||
"Emotion Mode": "Emotion Mode",
|
||||
"Emotion Mode Help": "Choose the emotion control source for IndexTTS-2.",
|
||||
"Emotion Mode Speaker": "Same as speaker reference",
|
||||
"Emotion Mode Audio": "Use emotion reference audio",
|
||||
"Emotion Mode Vector": "Use emotion vector",
|
||||
"Emotion Mode Text": "Use emotion text",
|
||||
"Emotion Alpha": "Emotion Alpha",
|
||||
"Emotion Alpha Help": "Controls how strongly the emotion condition affects generation. 0 is weak, 1 is strong.",
|
||||
"Emotion Reference Audio Path": "Emotion Reference Audio Path",
|
||||
"Emotion Reference Audio Path Help": "Local emotion reference audio path used when emotion_mode=audio.",
|
||||
"Emotion Text": "Emotion Text",
|
||||
"Emotion Text Help": "Emotion description used when emotion_mode=text, such as happy, nervous, or aggrieved.",
|
||||
"Emotion Text Placeholder": "e.g. calm, nervous, happy",
|
||||
"Use Random Emotion": "Use Random Emotion",
|
||||
"Use Random Emotion Help": "Let IndexTTS-2 use random emotion sampling during generation.",
|
||||
"Emotion Happy": "Happy",
|
||||
"Emotion Angry": "Angry",
|
||||
"Emotion Sad": "Sad",
|
||||
"Emotion Afraid": "Afraid",
|
||||
"Emotion Disgusted": "Disgusted",
|
||||
"Emotion Melancholic": "Melancholic",
|
||||
"Emotion Surprised": "Surprised",
|
||||
"Emotion Calm": "Calm",
|
||||
"Max Text Tokens Per Segment": "Max Text Tokens Per Segment",
|
||||
"Max Text Tokens Per Segment Help": "Maximum text tokens per segment for IndexTTS-2 inference.",
|
||||
"Max Mel Tokens": "Max Mel Tokens",
|
||||
"Max Mel Tokens Help": "Controls the maximum mel tokens generated in one request. Higher values can produce longer audio.",
|
||||
"IndexTTS2 Usage Instructions Title": "💡 IndexTTS-2 Usage Instructions",
|
||||
"IndexTTS2 Usage Instructions": "**IndexTTS-2 voice cloning**\n\n1. **Choose a voice**: reuse IndexTTS-1.5 resource audio or upload a reference audio file\n2. **Set API URL**: for example http://192.168.3.6:7863/tts, or enter the service root\n3. **Tune emotion**: speaker is the default; switch to audio, vector, or text when needed\n4. **Tune generation**: temperature, top_p, top_k, num_beams, repetition_penalty, and max_mel_tokens are sent directly to the IndexTTS-2 API\n\n**Notes**:\n- Reference audio quality directly affects cloning quality\n- The first request may load the model and take longer\n- CPU deployments are much slower than GPU deployments",
|
||||
"Volcengine Access Key Help": "Volcengine Access Key",
|
||||
"Volcengine Secret Key Help": "Volcengine Secret Key",
|
||||
"Doubao AppID Help": "Doubao TTS application AppID",
|
||||
|
||||
@ -265,6 +265,8 @@
|
||||
"IndexTTS features": "本地/私有部署的 IndexTTS-1.5 语音克隆引擎。选择资源目录音频或上传参考音频后,可按该音色合成旁白。",
|
||||
"IndexTTS use case": "适合需要固定旁白音色、角色配音或批量生成同一音色视频的场景。使用前请先启动 IndexTTS-1.5 API 服务;部署包下载:https://pan.quark.cn/s/0767c9bcefd5",
|
||||
"IndexTTS download link": "下载地址:https://pan.quark.cn/s/0767c9bcefd5",
|
||||
"IndexTTS2 features": "本地/私有部署的 IndexTTS-2 语音克隆引擎,支持情感控制和更完整的生成参数。",
|
||||
"IndexTTS2 use case": "适合需要固定音色、情绪化旁白或更细致采样控制的本地语音合成场景。使用前请先启动 IndexTTS-2 API 服务。",
|
||||
"Doubao TTS features": "火山引擎豆包语音合成,支持多种音色和情感,国内访问速度快",
|
||||
"Select TTS Engine": "选择 TTS 引擎",
|
||||
"Select TTS Engine Help": "选择您要使用的文本转语音引擎",
|
||||
@ -422,6 +424,7 @@
|
||||
"Select Qwen3 TTS Voice": "选择 Qwen3 TTS 音色",
|
||||
"API URL": "API 地址",
|
||||
"IndexTTS API URL Help": "IndexTTS-1.5 API 服务地址",
|
||||
"IndexTTS2 API URL Help": "IndexTTS-2 API 服务地址,可填写服务根地址或完整 /tts 地址",
|
||||
"Reference Audio Source": "参考音频来源",
|
||||
"Reference Audio Source Help": "选择从资源目录选择参考音频,或上传新的参考音频",
|
||||
"Select from Resource Directory": "从资源目录选择",
|
||||
@ -451,6 +454,36 @@
|
||||
"Enable Sampling Help": "启用采样可以获得更自然的语音",
|
||||
"IndexTTS Usage Instructions Title": "💡 IndexTTS-1.5 使用说明",
|
||||
"IndexTTS Usage Instructions": "**零样本语音克隆**\n\n1. **准备参考音频**:上传或指定一段清晰的音频文件(建议 3-10 秒)\n2. **设置 API 地址**:确保 IndexTTS-1.5 服务正常运行\n3. **开始合成**:系统会自动使用参考音频的音色合成新语音\n\n**注意事项**:\n- 参考音频质量直接影响合成效果\n- 建议使用无背景噪音的清晰音频\n- 文本长度建议控制在合理范围内\n- 首次合成可能需要较长时间",
|
||||
"IndexTTS2 Emotion Parameters": "🎭 情感参数",
|
||||
"Emotion Mode": "情感控制方式",
|
||||
"Emotion Mode Help": "选择 IndexTTS-2 的情感控制来源",
|
||||
"Emotion Mode Speaker": "与音色参考相同",
|
||||
"Emotion Mode Audio": "使用情感参考音频",
|
||||
"Emotion Mode Vector": "使用情感向量",
|
||||
"Emotion Mode Text": "使用情感描述文本",
|
||||
"Emotion Alpha": "情感权重",
|
||||
"Emotion Alpha Help": "控制情感条件的影响强度,0 表示弱,1 表示强",
|
||||
"Emotion Reference Audio Path": "情感参考音频路径",
|
||||
"Emotion Reference Audio Path Help": "emotion_mode=audio 时使用的本地情感参考音频路径",
|
||||
"Emotion Text": "情感描述文本",
|
||||
"Emotion Text Help": "emotion_mode=text 时使用的情感描述,例如开心、紧张、委屈",
|
||||
"Emotion Text Placeholder": "例如:沉稳、紧张、开心",
|
||||
"Use Random Emotion": "启用随机情感",
|
||||
"Use Random Emotion Help": "让 IndexTTS-2 在生成时使用随机情感采样",
|
||||
"Emotion Happy": "开心",
|
||||
"Emotion Angry": "愤怒",
|
||||
"Emotion Sad": "悲伤",
|
||||
"Emotion Afraid": "害怕",
|
||||
"Emotion Disgusted": "厌恶",
|
||||
"Emotion Melancholic": "忧郁",
|
||||
"Emotion Surprised": "惊讶",
|
||||
"Emotion Calm": "平静",
|
||||
"Max Text Tokens Per Segment": "单段最大文本 Token",
|
||||
"Max Text Tokens Per Segment Help": "IndexTTS-2 分段推理的最大文本 token 数",
|
||||
"Max Mel Tokens": "最大 Mel Tokens",
|
||||
"Max Mel Tokens Help": "控制单次生成的最大 mel token 数,值越大可生成更长音频",
|
||||
"IndexTTS2 Usage Instructions Title": "💡 IndexTTS-2 使用说明",
|
||||
"IndexTTS2 Usage Instructions": "**IndexTTS-2 语音克隆**\n\n1. **选择音色**:复用 IndexTTS-1.5 的资源音频或上传参考音频\n2. **设置 API 地址**:例如 http://192.168.3.6:7863/tts,也可以填写服务根地址\n3. **调整情感参数**:默认使用 speaker,可按需切换到 audio、vector 或 text\n4. **调整生成参数**:temperature、top_p、top_k、num_beams、repetition_penalty 和 max_mel_tokens 会直接传给 IndexTTS-2 接口\n\n**注意事项**:\n- 参考音频质量会直接影响克隆效果\n- 首次请求可能需要加载模型,耗时更长\n- CPU 部署生成速度会明显慢于 GPU",
|
||||
"Volcengine Access Key Help": "火山引擎 Access Key",
|
||||
"Volcengine Secret Key Help": "火山引擎 Secret Key",
|
||||
"Doubao AppID Help": "豆包语音应用 AppID",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user