From d147fe66e4d6591d2977ff1a497d1856d22366b9 Mon Sep 17 00:00:00 2001 From: viccy Date: Sat, 6 Jun 2026 14:31:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(tts):=20=E6=96=B0=E5=A2=9EIndexTTS-2?= =?UTF-8?q?=E8=AF=AD=E9=9F=B3=E5=90=88=E6=88=90=E5=BC=95=E6=93=8E=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现兼容IndexTTS2-Pack API的完整TTS调用流程,包含音频下载、错误重试等处理 重构原有IndexTTS-1.5代码,抽象通用逻辑以同时兼容indextts和indextts2两个引擎 新增IndexTTS-2的WebUI配置界面,支持情感控制与高级生成参数调整 更新配置示例文件与中英多语言文案,完善配置迁移逻辑兼容旧版配置 新增对应单元测试覆盖参数处理与配置迁移流程 --- app/config/config.py | 46 +- app/config/test_config_bootstrap_unittest.py | 23 +- app/services/jianying_task.py | 38 +- app/services/test_jianying_task_unittest.py | 17 +- app/services/voice.py | 155 ++++++- config.example.toml | 46 +- webui.py | 5 + webui/components/audio_settings.py | 418 +++++++++++++++---- webui/i18n/en.json | 33 ++ webui/i18n/zh.json | 33 ++ 10 files changed, 689 insertions(+), 125 deletions(-) diff --git a/app/config/config.py b/app/config/config.py index ac44df1..ae19945 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -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() diff --git a/app/config/test_config_bootstrap_unittest.py b/app/config/test_config_bootstrap_unittest.py index 691b6da..8034d02 100644 --- a/app/config/test_config_bootstrap_unittest.py +++ b/app/config/test_config_bootstrap_unittest.py @@ -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): diff --git a/app/services/jianying_task.py b/app/services/jianying_task.py index dd9db81..345f6b7 100644 --- a/app/services/jianying_task.py +++ b/app/services/jianying_task.py @@ -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): diff --git a/app/services/test_jianying_task_unittest.py b/app/services/test_jianying_task_unittest.py index c073d3f..18897a4 100644 --- a/app/services/test_jianying_task_unittest.py +++ b/app/services/test_jianying_task_unittest.py @@ -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") diff --git a/app/services/voice.py b/app/services/voice.py index 38d70ee..2be5c87 100644 --- a/app/services/voice.py +++ b/app/services/voice.py @@ -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 diff --git a/config.example.toml b/config.example.toml index 652ffb0..0b807e3 100644 --- a/config.example.toml +++ b/config.example.toml @@ -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 配置 diff --git a/webui.py b/webui.py index f57d240..68c24a7 100644 --- a/webui.py +++ b/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: diff --git a/webui/components/audio_settings.py b/webui/components/audio_settings.py index 5b12cdd..c5ec08c 100644 --- a/webui/components/audio_settings.py +++ b/webui/components/audio_settings.py @@ -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: diff --git a/webui/i18n/en.json b/webui/i18n/en.json index bf8ffe3..8e3356c 100644 --- a/webui/i18n/en.json +++ b/webui/i18n/en.json @@ -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", diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json index 84ea88e..4b16d7e 100644 --- a/webui/i18n/zh.json +++ b/webui/i18n/zh.json @@ -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",