From a2645aebd381c5e75d8f57ec83abb34fc9a260b7 Mon Sep 17 00:00:00 2001 From: viccy Date: Sat, 6 Jun 2026 12:43:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(webui):=20=E4=BC=98=E5=8C=96=E5=89=AA?= =?UTF-8?q?=E6=98=A0=E8=8D=89=E7=A8=BF=E5=AF=BC=E5=87=BA=E7=9A=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新streamlit依赖至1.57.0以支持原生弹窗组件 - 重构剪映导出逻辑,使用原生弹窗替代旧的内联表单 - 新增带样式的导出确认面板并补充多语言翻译 - 简化导出状态渲染与会话状态管理逻辑 --- README.md | 2 +- app/config/config.py | 41 +++- app/config/test_config_bootstrap_unittest.py | 15 ++ app/services/jianying_task.py | 28 ++- app/services/test_jianying_task_unittest.py | 37 +-- app/services/voice.py | 70 +++--- config.example.toml | 8 +- requirements.txt | 2 +- webui.py | 241 +++++++++++++------ webui/components/audio_settings.py | 144 +++++------ webui/i18n/en.json | 18 +- webui/i18n/zh.json | 18 +- 12 files changed, 398 insertions(+), 226 deletions(-) diff --git a/README.md b/README.md index 60afdda..7edab9b 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ NarratoAI 是一款自动化影视解说工具,基于 LLM 实现文案撰写 - 2026.04.27 发布新版本 0.7.9,新增 **Fun-ASR一键转录字幕** - 2026.04.03 发布新版本 0.7.8,重构纪录片逐帧分析链路,统一共享服务并优化抽帧、缓存、视觉并发与文案生成流程 - 2026.03.27 发布新版本 0.7.7,出于安全考虑,已移除 LiteLLM 依赖,统一使用 OpenAI 兼容请求链路 -- 2025.11.20 发布新版本 0.7.5,新增 [IndexTTS2](https://github.com/index-tts/index-tts) 语音克隆支持 +- 2025.11.20 发布新版本 0.7.5,新增 [IndexTTS-1.5](https://github.com/index-tts/index-tts) 语音克隆支持 - 2025.10.15 发布新版本 0.7.3,升级大模型供应商管理能力 - 2025.09.10 发布新版本 0.7.2,新增腾讯云tts - 2025.08.18 发布新版本 0.7.1,支持 **语音克隆** 和 最新大模型 diff --git a/app/config/config.py b/app/config/config.py index e157f94..ac44df1 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -9,6 +9,36 @@ from app.config.defaults import build_default_app_config, merge_missing_app_defa root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 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" +INDEXTTS_VOICE_PREFIX = f"{INDEXTTS_ENGINE}:" +INDEXTTS_LEGACY_VOICE_PREFIX = f"{INDEXTTS_LEGACY_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 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] + + 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", "")) + return config_data def get_version_from_file(): @@ -32,13 +62,13 @@ def load_config(): _config_ = build_default_config() write_config_file(_config_) logger.info("create config.toml with shared defaults") - return _config_ + return migrate_indextts_config(_config_) logger.info(f"load config from file: {config_file}") _config_ = load_toml_file(config_file) _config_["app"] = merge_missing_app_defaults(_config_.get("app", {})) - return _config_ + return migrate_indextts_config(_config_) def load_toml_file(file_path): @@ -60,7 +90,7 @@ def build_default_config(): config_data = load_toml_file(example_file) config_data["app"] = build_default_app_config(config_data.get("app", {})) - return config_data + return migrate_indextts_config(config_data) def write_config_file(config_data): @@ -82,7 +112,8 @@ def save_config(): _cfg["ui"] = ui _cfg["tts_qwen"] = tts_qwen _cfg["fun_asr"] = fun_asr - _cfg["indextts2"] = indextts2 + _cfg["indextts"] = indextts + _cfg.pop(INDEXTTS_LEGACY_ENGINE, None) _cfg["doubaotts"] = doubaotts f.write(toml.dumps(_cfg)) @@ -98,7 +129,7 @@ ui = _cfg.get("ui", {}) frames = _cfg.get("frames", {}) tts_qwen = _cfg.get("tts_qwen", {}) fun_asr = _cfg.get("fun_asr", {}) -indextts2 = _cfg.get("indextts2", {}) +indextts = _cfg.get("indextts", _cfg.get(INDEXTTS_LEGACY_ENGINE, {})) 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 8398fea..691b6da 100644 --- a/app/config/test_config_bootstrap_unittest.py +++ b/app/config/test_config_bootstrap_unittest.py @@ -64,6 +64,21 @@ 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): + migrated = cfg.migrate_indextts_config( + { + "indextts2": {"api_url": "http://127.0.0.1:8081/tts"}, + "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("indextts", migrated["ui"]["tts_engine"]) + self.assertEqual("indextts:/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 e988435..dd9db81 100644 --- a/app/services/jianying_task.py +++ b/app/services/jianying_task.py @@ -49,8 +49,9 @@ def get_audio_duration_ffprobe(audio_file: str) -> float: return get_media_duration_ffprobe(audio_file) -def _strip_indextts2_prefix(voice_name: str) -> str: - prefix = "indextts2:" +def _strip_indextts_prefix(voice_name: str) -> str: + voice_name = config.normalize_indextts_voice_prefix(voice_name or "") + prefix = config.INDEXTTS_VOICE_PREFIX if voice_name.startswith(prefix): return voice_name[len(prefix):] return voice_name @@ -99,24 +100,25 @@ def _clamp_duration_to_media( return safe_duration -def _normalize_indextts2_reference_audio(params: VideoClipParams) -> None: - """Ensure IndexTTS2 uses the configured reference audio instead of a stale UI voice.""" - if params.tts_engine != "indextts2": +def _normalize_indextts_reference_audio(params: VideoClipParams) -> None: + """Ensure IndexTTS-1.5 uses 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: return - candidate = _strip_indextts2_prefix(getattr(params, "voice_name", "") or "") + candidate = _strip_indextts_prefix(getattr(params, "voice_name", "") or "") if candidate and os.path.isfile(candidate): - params.voice_name = f"indextts2:{candidate}" - logger.info(f"IndexTTS2 使用参考音频: {candidate}") + params.voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{candidate}" + logger.info(f"IndexTTS-1.5 使用参考音频: {candidate}") return - configured_ref = _strip_indextts2_prefix(config.indextts2.get("reference_audio", "") or "") + configured_ref = _strip_indextts_prefix(config.indextts.get("reference_audio", "") or "") if configured_ref and os.path.isfile(configured_ref): - params.voice_name = f"indextts2:{configured_ref}" - logger.info(f"IndexTTS2 使用配置中的参考音频: {configured_ref}") + params.voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{configured_ref}" + logger.info(f"IndexTTS-1.5 使用配置中的参考音频: {configured_ref}") return - raise ValueError("IndexTTS2 参考音频不存在,请在音频设置中上传或选择有效的参考音频") + raise ValueError("IndexTTS-1.5 参考音频不存在,请在音频设置中上传或选择有效的参考音频") def start_export_jianying_draft(task_id: str, params: VideoClipParams): @@ -159,7 +161,7 @@ def start_export_jianying_draft(task_id: str, params: VideoClipParams): 2. 使用 TTS 生成音频素材 """ logger.info("\n\n## 2. 根据OST设置生成音频列表") - _normalize_indextts2_reference_audio(params) + _normalize_indextts_reference_audio(params) tts_segments = [ segment for segment in list_script if segment['OST'] in [0, 2] diff --git a/app/services/test_jianying_task_unittest.py b/app/services/test_jianying_task_unittest.py index 5e25478..c073d3f 100644 --- a/app/services/test_jianying_task_unittest.py +++ b/app/services/test_jianying_task_unittest.py @@ -12,31 +12,40 @@ DraftPathPlaceholder = "##_draftpath_placeholder_0E685133-18CE-45ED-8CB8-2904A21 class JianyingTaskTests(unittest.TestCase): - def test_normalize_indextts2_uses_valid_param_reference(self): + def test_normalize_indextts_uses_valid_param_reference(self): with tempfile.NamedTemporaryFile(suffix=".wav") as ref: - params = VideoClipParams(tts_engine="indextts2", voice_name=ref.name) + params = VideoClipParams(tts_engine="indextts", voice_name=ref.name) - jianying_task._normalize_indextts2_reference_audio(params) + jianying_task._normalize_indextts_reference_audio(params) - self.assertEqual(f"indextts2:{ref.name}", params.voice_name) + self.assertEqual(f"indextts:{ref.name}", params.voice_name) - def test_normalize_indextts2_uses_config_reference_when_param_is_stale(self): + def test_normalize_indextts_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") + params = VideoClipParams(tts_engine="indextts", voice_name="zh-CN-YunjianNeural") - with patch.dict(jianying_task.config.indextts2, {"reference_audio": str(ref_path)}, clear=False): - jianying_task._normalize_indextts2_reference_audio(params) + with patch.dict(jianying_task.config.indextts, {"reference_audio": str(ref_path)}, clear=False): + jianying_task._normalize_indextts_reference_audio(params) - self.assertEqual(f"indextts2:{ref_path}", params.voice_name) + self.assertEqual(f"indextts:{ref_path}", params.voice_name) - def test_normalize_indextts2_requires_existing_reference_audio(self): - params = VideoClipParams(tts_engine="indextts2", voice_name="zh-CN-YunjianNeural") + def test_normalize_indextts_accepts_legacy_engine_and_prefix(self): + with tempfile.NamedTemporaryFile(suffix=".wav") as ref: + params = VideoClipParams(tts_engine="indextts2", voice_name=f"indextts2:{ref.name}") - with patch.dict(jianying_task.config.indextts2, {"reference_audio": ""}, clear=False): - with self.assertRaisesRegex(ValueError, "IndexTTS2 参考音频不存在"): - jianying_task._normalize_indextts2_reference_audio(params) + jianying_task._normalize_indextts_reference_audio(params) + + self.assertEqual("indextts", params.tts_engine) + self.assertEqual(f"indextts:{ref.name}", params.voice_name) + + def test_normalize_indextts_requires_existing_reference_audio(self): + params = VideoClipParams(tts_engine="indextts", voice_name="zh-CN-YunjianNeural") + + with patch.dict(jianying_task.config.indextts, {"reference_audio": ""}, clear=False): + with self.assertRaisesRegex(ValueError, "IndexTTS-1.5 参考音频不存在"): + jianying_task._normalize_indextts_reference_audio(params) def test_floor_duration_to_milliseconds(self): self.assertAlmostEqual(6.997, jianying_task._floor_duration_to_milliseconds(6.997333)) diff --git a/app/services/voice.py b/app/services/voice.py index 58cd1c9..38d70ee 100644 --- a/app/services/voice.py +++ b/app/services/voice.py @@ -1263,6 +1263,8 @@ def doubaotts_tts(text: str, voice_name: str, voice_file: str, speed: float = 1. def tts( text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str, tts_engine: str ) -> Union[SubMaker, None]: + tts_engine = config.normalize_tts_engine_name(tts_engine) + voice_name = config.normalize_indextts_voice_prefix(voice_name) logger.info(f"使用 TTS 引擎: '{tts_engine}', 语音: '{voice_name}'") if tts_engine == "tencent_tts": @@ -1288,9 +1290,9 @@ def tts( logger.info("分发到 Edge TTS") return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file) - if tts_engine == "indextts2": - logger.info("分发到 IndexTTS2") - return indextts2_tts(text, voice_name, voice_file, speed=voice_rate) + 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 == "doubaotts": logger.info("分发到豆包语音 TTS") @@ -1772,7 +1774,8 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f :param tts_engine: TTS 引擎 :return: 生成的音频文件列表 """ - voice_name = parse_voice_name(voice_name) + tts_engine = config.normalize_tts_engine_name(tts_engine) + voice_name = config.normalize_indextts_voice_prefix(parse_voice_name(voice_name)) output_dir = utils.task_dir(task_id) tts_results = [] @@ -1800,8 +1803,8 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f f"或者使用其他 tts 引擎") continue else: - # SoulVoice、Qwen3、IndexTTS2、豆包语音 引擎不生成字幕文件 - if is_soulvoice_voice(voice_name) or is_qwen_engine(tts_engine) or tts_engine == "indextts2" or tts_engine == "doubaotts": + # 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": # 获取实际音频文件的时长 duration = get_audio_duration_from_file(audio_file) if duration <= 0: @@ -2219,24 +2222,25 @@ def parse_soulvoice_voice(voice_name: str) -> str: return voice_name -def parse_indextts2_voice(voice_name: str) -> str: +def parse_indextts_voice(voice_name: str) -> str: """ - 解析 IndexTTS2 语音名称 - 支持格式:indextts2:reference_audio_path + 解析 IndexTTS-1.5 语音名称 + 支持格式:indextts:reference_audio_path 返回参考音频文件路径 """ - if voice_name.startswith("indextts2:"): - return voice_name[10:] # 移除 "indextts2:" 前缀 + voice_name = config.normalize_indextts_voice_prefix(voice_name) + if voice_name.startswith(config.INDEXTTS_VOICE_PREFIX): + return voice_name[len(config.INDEXTTS_VOICE_PREFIX):] return voice_name -def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0) -> Union[SubMaker, None]: +def indextts_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0) -> Union[SubMaker, None]: """ - 使用 IndexTTS2 API 进行零样本语音克隆 + 使用 IndexTTS-1.5 API 进行零样本语音克隆 Args: text: 要转换的文本 - voice_name: 参考音频文件(格式:indextts2:path/to/audio.wav) + voice_name: 参考音频文件(格式:indextts:path/to/audio.wav) voice_file: 输出音频文件路径 speed: 语音速度(此引擎暂不支持速度调节) @@ -2244,20 +2248,20 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1. SubMaker: 包含时间戳信息的字幕制作器,失败时返回 None """ # 获取配置 - api_url = config.indextts2.get("api_url", "http://192.168.3.6:8081/tts") - infer_mode = config.indextts2.get("infer_mode", "普通推理") - temperature = config.indextts2.get("temperature", 1.0) - top_p = config.indextts2.get("top_p", 0.8) - top_k = config.indextts2.get("top_k", 30) - do_sample = config.indextts2.get("do_sample", True) - num_beams = config.indextts2.get("num_beams", 3) - repetition_penalty = config.indextts2.get("repetition_penalty", 10.0) + api_url = config.indextts.get("api_url", "http://192.168.3.6:8081/tts") + infer_mode = config.indextts.get("infer_mode", "普通推理") + temperature = config.indextts.get("temperature", 1.0) + top_p = config.indextts.get("top_p", 0.8) + top_k = config.indextts.get("top_k", 30) + do_sample = config.indextts.get("do_sample", True) + num_beams = config.indextts.get("num_beams", 3) + repetition_penalty = config.indextts.get("repetition_penalty", 10.0) # 解析参考音频文件 - reference_audio_path = parse_indextts2_voice(voice_name) + reference_audio_path = parse_indextts_voice(voice_name) if not reference_audio_path or not os.path.exists(reference_audio_path): - logger.error(f"IndexTTS2 参考音频文件不存在: {reference_audio_path}") + logger.error(f"IndexTTS-1.5 参考音频文件不存在: {reference_audio_path}") return None # 准备请求数据 @@ -2279,7 +2283,7 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1. # 重试机制 for attempt in range(3): try: - logger.info(f"第 {attempt + 1} 次调用 IndexTTS2 API") + logger.info(f"第 {attempt + 1} 次调用 IndexTTS-1.5 API") # 设置代理 proxies = {} @@ -2295,7 +2299,7 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1. files=files, data=data, proxies=proxies, - timeout=120 # IndexTTS2 推理可能需要较长时间 + timeout=120 # IndexTTS-1.5 推理可能需要较长时间 ) if response.status_code == 200: @@ -2303,9 +2307,9 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1. with open(voice_file, 'wb') as f: f.write(response.content) - logger.info(f"IndexTTS2 成功生成音频: {voice_file}, 大小: {len(response.content)} 字节") + logger.info(f"IndexTTS-1.5 成功生成音频: {voice_file}, 大小: {len(response.content)} 字节") - # IndexTTS2 不支持精确字幕生成,返回简单的 SubMaker 对象 + # IndexTTS-1.5 不支持精确字幕生成,返回简单的 SubMaker 对象 sub_maker = new_sub_maker() # 估算音频时长(基于文本长度) estimated_duration_ms = max(1000, int(len(text) * 200)) @@ -2314,14 +2318,14 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1. return sub_maker else: - logger.error(f"IndexTTS2 API 调用失败: {response.status_code} - {response.text}") + logger.error(f"IndexTTS-1.5 API 调用失败: {response.status_code} - {response.text}") except requests.exceptions.Timeout: - logger.error(f"IndexTTS2 API 调用超时 (尝试 {attempt + 1}/3)") + logger.error(f"IndexTTS-1.5 API 调用超时 (尝试 {attempt + 1}/3)") except requests.exceptions.RequestException as e: - logger.error(f"IndexTTS2 API 网络错误: {str(e)} (尝试 {attempt + 1}/3)") + logger.error(f"IndexTTS-1.5 API 网络错误: {str(e)} (尝试 {attempt + 1}/3)") except Exception as e: - logger.error(f"IndexTTS2 TTS 处理错误: {str(e)} (尝试 {attempt + 1}/3)") + logger.error(f"IndexTTS-1.5 TTS 处理错误: {str(e)} (尝试 {attempt + 1}/3)") finally: # 确保关闭文件 try: @@ -2338,5 +2342,5 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1. except: pass - logger.error("IndexTTS2 TTS 生成失败,已达到最大重试次数") + logger.error("IndexTTS-1.5 TTS 生成失败,已达到最大重试次数") return None diff --git a/config.example.toml b/config.example.toml index 3e81c08..652ffb0 100644 --- a/config.example.toml +++ b/config.example.toml @@ -114,8 +114,8 @@ api_key = "" model = "fun-asr" -[indextts2] - # IndexTTS2 语音克隆配置 +[indextts] + # IndexTTS-1.5 语音克隆配置 # 这是一个开源的零样本语音克隆项目,需要自行部署 # 项目地址:https://github.com/index-tts/index-tts # 默认 API 地址(本地部署) @@ -153,8 +153,8 @@ silence_duration = 0.125 [ui] - # TTS引擎选择 (indextts2, edge_tts, qwen3_tts, tencent_tts, doubaotts, azure_speech) - tts_engine = "indextts2" + # TTS引擎选择 (indextts, edge_tts, qwen3_tts, tencent_tts, doubaotts, azure_speech) + tts_engine = "indextts" # Edge TTS 配置 edge_voice_name = "zh-CN-XiaoyiNeural-Female" diff --git a/requirements.txt b/requirements.txt index 12def4d..c6011de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ requests>=2.32.0 moviepy==2.1.1 edge-tts==7.2.7 -streamlit>=1.45.0 +streamlit>=1.57.0 watchdog==6.0.0 loguru>=0.7.3 tomli>=2.2.1 diff --git a/webui.py b/webui.py index 7c70ad7..f57d240 100644 --- a/webui.py +++ b/webui.py @@ -2,6 +2,7 @@ import streamlit as st import os import sys import time +from html import escape from loguru import logger from app.config import config from webui.components import basic_settings, video_settings, audio_settings, subtitle_settings, script_settings, \ @@ -232,10 +233,10 @@ 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 == 'indextts2': - reference_audio = config.indextts2.get('reference_audio', '') + if config.normalize_tts_engine_name(tts_engine) == config.INDEXTTS_ENGINE: + reference_audio = config.indextts.get('reference_audio', '') if reference_audio: - return f"indextts2:{reference_audio}" + return f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}" return config.ui.get('voice_name', '') if tts_engine == 'doubaotts': return config.ui.get('doubaotts_voice_type', 'BV700_streaming') @@ -247,7 +248,7 @@ def get_voice_name_for_tts_engine(tts_engine: str) -> str: return config.ui.get('voice_name', config.ui.get('edge_voice_name', 'zh-CN-XiaoxiaoNeural-Female')) -def get_jianying_export_params() -> VideoClipParams: +def get_jianying_export_params(draft_name=None) -> VideoClipParams: """获取导出到剪映草稿的参数""" tts_engine = st.session_state.get('tts_engine', config.ui.get('tts_engine', 'edge_tts')) voice_name = get_voice_name_for_tts_engine(tts_engine) @@ -272,20 +273,178 @@ def get_jianying_export_params() -> VideoClipParams: tts_volume=st.session_state.get('tts_volume', 1.0), original_volume=st.session_state.get('original_volume', 0.7), bgm_volume=st.session_state.get('bgm_volume', 0.3), - draft_name=st.session_state.get('draft_name_input', f"NarratoAI_{int(time.time())}") + draft_name=( + draft_name + if draft_name is not None + else st.session_state.get('draft_name_input', f"NarratoAI_{int(time.time())}") + ) ) +def _render_jianying_export_status(): + """渲染剪映导出的结果提示。""" + result = st.session_state.get('jianying_export_result') + error = st.session_state.get('jianying_export_error') + + if result: + st.success(tr("Jianying draft exported successfully").format(name=result['draft_name'])) + st.info(tr("Draft saved to").format(path=result['draft_path'])) + elif error: + st.error(f"{tr('Failed to export Jianying draft')}: {error}") + + +def _render_jianying_export_dialog(): + """使用弹窗确认剪映草稿名称。""" + import uuid + from loguru import logger + + @st.dialog(tr("Export to Jianying Draft"), width="small") + def jianying_export_dialog(): + jianying_draft_path = config.ui.get("jianying_draft_path", "") + dialog_title = escape(tr("Jianying export dialog title")) + dialog_description = escape(tr("Jianying export dialog description")) + destination_label = escape(tr("Jianying export destination")) + destination_path = escape(jianying_draft_path or "-") + + st.markdown( + f""" + +
+
📤
+
+
{dialog_title}
+
{dialog_description}
+
+
+
+ {destination_label} + {destination_path} +
+ """, + unsafe_allow_html=True, + ) + + draft_name = st.text_input( + tr("Jianying draft name"), + key="draft_name_input", + placeholder="NarratoAI_", + ) + + error = st.session_state.get('jianying_export_error') + if error: + st.error(f"{tr('Failed to export Jianying draft')}: {error}") + + cancel_col, confirm_col = st.columns(2) + with cancel_col: + if st.button(tr("Cancel"), key="cancel_export", use_container_width=True): + st.session_state['jianying_export_error'] = None + st.rerun() + + with confirm_col: + if st.button(tr("Confirm Export"), key="confirm_export", type="primary", use_container_width=True): + draft_name = (draft_name or "").strip() + if not draft_name: + st.error(tr("Please enter draft name")) + return + + # 创建任务ID + task_id = str(uuid.uuid4()) + st.session_state['task_id'] = task_id + + # 构建参数 + try: + params = get_jianying_export_params(draft_name) + except Exception as e: + logger.error(f"构建参数失败: {e}") + st.session_state['jianying_export_error'] = f"{tr('Failed to build parameters')}: {e}" + st.error(st.session_state['jianying_export_error']) + return + + with st.spinner(tr("Exporting to Jianying draft...")): + try: + from app.services import jianying_task + + # 调用导出到剪映草稿的任务 + result = jianying_task.start_export_jianying_draft(task_id, params) + + # 记录日志 + logger.info(f"成功导出到剪映草稿: {result['draft_name']}") + logger.info(f"草稿已保存到: {result['draft_path']}") + + # 保存结果到session state + st.session_state['jianying_export_result'] = result + st.session_state['jianying_export_error'] = None + st.rerun() + except Exception as e: + logger.error(f"导出到剪映草稿失败: {e}") + import traceback + logger.error(f"错误详情: {traceback.format_exc()}") + st.session_state['jianying_export_error'] = str(e) + st.session_state['jianying_export_result'] = None + st.error(f"{tr('Failed to export Jianying draft')}: {e}") + + jianying_export_dialog() + + def render_export_jianying_button(): """渲染导出到剪映草稿按钮和处理逻辑""" import os import time - import uuid - from loguru import logger # 初始化session state - if 'show_jianying_export_form' not in st.session_state: - st.session_state['show_jianying_export_form'] = False if 'jianying_export_result' not in st.session_state: st.session_state['jianying_export_result'] = None if 'jianying_export_error' not in st.session_state: @@ -310,70 +469,12 @@ def render_export_jianying_button(): st.error(tr("Jianying draft folder does not exist").format(path=jianying_draft_path)) return - # 显示导出表单 - st.session_state['show_jianying_export_form'] = True st.session_state['jianying_export_result'] = None st.session_state['jianying_export_error'] = None + st.session_state['draft_name_input'] = f"NarratoAI_{int(time.time())}" + _render_jianying_export_dialog() - # 显示导出表单 - if st.session_state['show_jianying_export_form']: - st.markdown("---") - st.subheader(tr("Export to Jianying Draft")) - - draft_name = st.text_input( - tr("Please enter Jianying draft name"), - value=f"NarratoAI_{int(time.time())}", - key="draft_name_input" - ) - - if st.button(tr("Confirm Export"), key="confirm_export"): - if not draft_name: - st.error(tr("Please enter draft name")) - return - - # 创建任务ID - task_id = str(uuid.uuid4()) - st.session_state['task_id'] = task_id - - # 构建参数 - try: - params = get_jianying_export_params() - except Exception as e: - logger.error(f"构建参数失败: {e}") - st.error(f"{tr('Failed to build parameters')}: {e}") - return - - with st.spinner(tr("Exporting to Jianying draft...")): - try: - from app.services import jianying_task - - # 调用导出到剪映草稿的任务 - result = jianying_task.start_export_jianying_draft(task_id, params) - - # 记录日志 - logger.info(f"成功导出到剪映草稿: {result['draft_name']}") - logger.info(f"草稿已保存到: {result['draft_path']}") - - # 保存结果到session state - st.session_state['jianying_export_result'] = result - st.session_state['jianying_export_error'] = None - st.session_state['show_jianying_export_form'] = False - - st.success(tr("Jianying draft exported successfully").format(name=result['draft_name'])) - st.info(tr("Draft saved to").format(path=result['draft_path'])) - except Exception as e: - logger.error(f"导出到剪映草稿失败: {e}") - import traceback - logger.error(f"错误详情: {traceback.format_exc()}") - st.session_state['jianying_export_error'] = str(e) - st.session_state['jianying_export_result'] = None - st.error(f"{tr('Failed to export Jianying draft')}: {e}") - - if st.button(tr("Cancel"), key="cancel_export"): - st.session_state['show_jianying_export_form'] = False - st.session_state['jianying_export_result'] = None - st.session_state['jianying_export_error'] = None - st.rerun() + _render_jianying_export_status() diff --git a/webui/components/audio_settings.py b/webui/components/audio_settings.py index 43b48fd..5b12cdd 100644 --- a/webui/components/audio_settings.py +++ b/webui/components/audio_settings.py @@ -9,9 +9,9 @@ from app.models.schema import AudioVolumeDefaults from app.utils import utils -INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR = "/Users/viccy/Downloads/tts-mp3-clone/mp3" -INDEXTTS2_REFERENCE_AUDIO_COPY_SUBDIR = "indextts2_refs" -INDEXTTS2_REFERENCE_AUDIO_MAP = [ +INDEXTTS_REFERENCE_AUDIO_SOURCE_DIR = "/Users/viccy/Downloads/tts-mp3-clone/mp3" +INDEXTTS_REFERENCE_AUDIO_COPY_SUBDIR = "indextts_refs" +INDEXTTS_REFERENCE_AUDIO_MAP = [ ("yingshijieshuo-zh-male.mp3", "影视解说", "Film Narration"), ("maikeashe-zh-male.mp3", "麦克阿瑟", "Macintosh"), ("dong-yuhui-zh-male.mp3", "董宇辉", "Dong Yuhui"), @@ -35,7 +35,7 @@ INDEXTTS2_REFERENCE_AUDIO_MAP = [ ("meiqu-kelong-en-unknown.mp3", "美式男声", "US Clone"), ("sarah-en-female.mp3", "莎拉", "Sarah"), ] -INDEXTTS2_REFERENCE_AUDIO_EXTENSIONS = (".mp3", ".wav", ".flac", ".m4a", ".aac", ".ogg") +INDEXTTS_REFERENCE_AUDIO_EXTENSIONS = (".mp3", ".wav", ".flac", ".m4a", ".aac", ".ogg") BGM_RESOURCE_DIR = "/Users/viccy/Downloads/tts-mp3-clone/bgms-safe" BGM_TRACKS_JSON = os.path.join(BGM_RESOURCE_DIR, "tracks.json") BGM_UPLOAD_SUBDIR = "uploaded_bgms" @@ -56,7 +56,7 @@ def get_soulvoice_voices(): def get_tts_engine_options(tr=lambda key: key): """获取TTS引擎选项""" return { - "indextts2": "IndexTTS2", + config.INDEXTTS_ENGINE: config.INDEXTTS_DISPLAY_NAME, "edge_tts": "Edge TTS", "qwen3_tts": tr("Tongyi Qwen3 TTS"), "tencent_tts": tr("Tencent Cloud TTS"), @@ -92,10 +92,10 @@ def get_tts_engine_descriptions(tr=lambda key: key): "use_case": tr("High-quality Chinese speech synthesis use case"), "registration": "https://dashscope.aliyuncs.com/" }, - "indextts2": { - "title": "IndexTTS2", - "features": tr("IndexTTS2 features"), - "use_case": tr("IndexTTS2 use case"), + config.INDEXTTS_ENGINE: { + "title": config.INDEXTTS_DISPLAY_NAME, + "features": tr("IndexTTS features"), + "use_case": tr("IndexTTS use case"), "registration": None }, "doubaotts": { @@ -107,7 +107,7 @@ def get_tts_engine_descriptions(tr=lambda key: key): } -def infer_indextts2_reference_audio_language(filename): +def infer_indextts_reference_audio_language(filename): """根据文件名推断参考音频语言""" lower_filename = filename.lower() if "-zh-" in lower_filename: @@ -117,30 +117,30 @@ def infer_indextts2_reference_audio_language(filename): return "unknown" -def get_indextts2_reference_audio_options(): - """获取本地 IndexTTS2 参考音频选项""" +def get_indextts_reference_audio_options(): + """获取本地 IndexTTS-1.5 参考音频选项""" options = [] mapped_files = set() - for filename, zh_name, en_name in INDEXTTS2_REFERENCE_AUDIO_MAP: - audio_path = os.path.join(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR, filename) + for filename, zh_name, en_name in INDEXTTS_REFERENCE_AUDIO_MAP: + audio_path = os.path.join(INDEXTTS_REFERENCE_AUDIO_SOURCE_DIR, filename) if os.path.isfile(audio_path): options.append({ "filename": filename, "path": audio_path, "zh": zh_name, "en": en_name, - "language": infer_indextts2_reference_audio_language(filename), + "language": infer_indextts_reference_audio_language(filename), }) mapped_files.add(filename) - if os.path.isdir(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR): - for filename in sorted(os.listdir(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR)): + if os.path.isdir(INDEXTTS_REFERENCE_AUDIO_SOURCE_DIR): + for filename in sorted(os.listdir(INDEXTTS_REFERENCE_AUDIO_SOURCE_DIR)): if filename in mapped_files: continue - if not filename.lower().endswith(INDEXTTS2_REFERENCE_AUDIO_EXTENSIONS): + if not filename.lower().endswith(INDEXTTS_REFERENCE_AUDIO_EXTENSIONS): continue - audio_path = os.path.join(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR, filename) + audio_path = os.path.join(INDEXTTS_REFERENCE_AUDIO_SOURCE_DIR, filename) if not os.path.isfile(audio_path): continue fallback_name = os.path.splitext(filename)[0] @@ -149,14 +149,14 @@ def get_indextts2_reference_audio_options(): "path": audio_path, "zh": fallback_name, "en": fallback_name, - "language": infer_indextts2_reference_audio_language(filename), + "language": infer_indextts_reference_audio_language(filename), }) return options -def format_indextts2_reference_audio_option(option): - """格式化 IndexTTS2 参考音频下拉显示名""" +def format_indextts_reference_audio_option(option): + """格式化 IndexTTS-1.5 参考音频下拉显示名""" zh_name = option.get("zh", "") en_name = option.get("en", "") language = option.get("language", "unknown") @@ -182,7 +182,7 @@ def format_indextts2_reference_audio_option(option): return f"{display_name} ({language_label})" -def get_indextts2_reference_audio_index(options, saved_reference_audio): +def get_indextts_reference_audio_index(options, saved_reference_audio): """根据已保存的参考音频文件匹配下拉选项索引""" if not options: return 0 @@ -195,12 +195,12 @@ def get_indextts2_reference_audio_index(options, saved_reference_audio): return 0 -def copy_indextts2_reference_audio(source_path): +def copy_indextts_reference_audio(source_path): """复制一份参考音频到项目存储目录,并返回复制后的路径""" if not source_path or not os.path.isfile(source_path): return "" - target_dir = utils.storage_dir(INDEXTTS2_REFERENCE_AUDIO_COPY_SUBDIR, create=True) + target_dir = utils.storage_dir(INDEXTTS_REFERENCE_AUDIO_COPY_SUBDIR, create=True) target_path = os.path.join(target_dir, os.path.basename(source_path)) if os.path.abspath(source_path) == os.path.abspath(target_path): @@ -336,7 +336,7 @@ def render_reference_audio_preview_button(reference_audio, key, tr): disabled=not can_preview, use_container_width=True, ): - st.session_state["indextts2_reference_audio_preview_path"] = reference_audio + st.session_state["indextts_reference_audio_preview_path"] = reference_audio def render_bgm_preview_button(bgm_file, key, tr): @@ -395,11 +395,13 @@ def render_tts_settings(tr): engine_descriptions = get_tts_engine_descriptions(tr) # 获取保存的TTS引擎设置 - saved_tts_engine = config.ui.get("tts_engine", "indextts2") + saved_tts_engine = config.normalize_tts_engine_name( + config.ui.get("tts_engine", config.INDEXTTS_ENGINE) + ) # 确保保存的引擎在可用选项中 if saved_tts_engine not in engine_options: - saved_tts_engine = "indextts2" + saved_tts_engine = config.INDEXTTS_ENGINE # TTS引擎选择下拉框 selected_engine = st.selectbox( @@ -438,8 +440,8 @@ def render_tts_settings(tr): render_tencent_tts_settings(tr) elif selected_engine == "qwen3_tts": render_qwen3_tts_settings(tr) - elif selected_engine == "indextts2": - render_indextts2_tts_settings(tr) + elif selected_engine == config.INDEXTTS_ENGINE: + render_indextts_tts_settings(tr) elif selected_engine == "doubaotts": render_doubaotts_settings(tr) @@ -850,22 +852,22 @@ def render_qwen3_tts_settings(tr): config.ui["voice_name"] = voice_type #兼容性 -def render_indextts2_tts_settings(tr): - """渲染 IndexTTS2 TTS 设置""" +def render_indextts_tts_settings(tr): + """渲染 IndexTTS-1.5 TTS 设置""" # API 地址配置 api_url = st.text_input( tr("API URL"), - value=config.indextts2.get("api_url", "http://127.0.0.1:8081/tts"), - help=tr("IndexTTS2 API URL Help") + value=config.indextts.get("api_url", "http://127.0.0.1:8081/tts"), + help=tr("IndexTTS API URL Help") ) - saved_reference_audio = config.indextts2.get("reference_audio", "") + 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.indextts2.get("reference_audio_source", "resource") + 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( @@ -880,7 +882,7 @@ def render_indextts2_tts_settings(tr): options=reference_audio_source_labels, selection_mode="single", default=default_reference_audio_source_label, - key="indextts2_reference_audio_source_selection", + key="indextts_reference_audio_source_selection", help=tr("Reference Audio Source Help"), label_visibility="collapsed", width="stretch", @@ -890,24 +892,24 @@ def render_indextts2_tts_settings(tr): reference_audio_source = reference_audio_source_options[reference_audio_source_label] reference_audio = saved_reference_audio - reference_audio_options = get_indextts2_reference_audio_options() + reference_audio_options = get_indextts_reference_audio_options() if reference_audio_source == "resource" and reference_audio_options: - selected_audio_index = get_indextts2_reference_audio_index(reference_audio_options, saved_reference_audio) + 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_indextts2_reference_audio_option(reference_audio_options[x]), + 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_indextts2_reference_audio(selected_audio_option["path"]) + reference_audio = copy_indextts_reference_audio(selected_audio_option["path"]) with preview_col: render_reference_audio_preview_button( reference_audio, - "indextts2_resource_reference_audio_preview", + "indextts_resource_reference_audio_preview", tr, ) elif reference_audio_source == "resource": @@ -926,7 +928,7 @@ def render_indextts2_tts_settings(tr): ) if uploaded_file is not None: - target_dir = utils.storage_dir(INDEXTTS2_REFERENCE_AUDIO_COPY_SUBDIR, create=True) + 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()) @@ -935,11 +937,11 @@ def render_indextts2_tts_settings(tr): with preview_col: render_reference_audio_preview_button( reference_audio, - "indextts2_upload_reference_audio_preview", + "indextts_upload_reference_audio_preview", tr, ) - preview_audio_path = st.session_state.get("indextts2_reference_audio_preview_path", "") + 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)) @@ -949,7 +951,7 @@ def render_indextts2_tts_settings(tr): ("普通推理", tr("Standard Inference")), ("快速推理", tr("Fast Inference")), ] - infer_mode_index = 0 if config.indextts2.get("infer_mode", "普通推理") == "普通推理" else 1 + infer_mode_index = 0 if config.indextts.get("infer_mode", "普通推理") == "普通推理" else 1 infer_mode = infer_mode_options[st.selectbox( tr("Inference Mode"), options=range(len(infer_mode_options)), @@ -967,7 +969,7 @@ def render_indextts2_tts_settings(tr): tr("Sampling Temperature"), min_value=0.1, max_value=2.0, - value=float(config.indextts2.get("temperature", 1.0)), + value=float(config.indextts.get("temperature", 1.0)), step=0.1, help=tr("Sampling Temperature Help") ) @@ -976,7 +978,7 @@ def render_indextts2_tts_settings(tr): "Top P", min_value=0.0, max_value=1.0, - value=float(config.indextts2.get("top_p", 0.8)), + value=float(config.indextts.get("top_p", 0.8)), step=0.05, help=tr("Top P Help") ) @@ -985,7 +987,7 @@ def render_indextts2_tts_settings(tr): "Top K", min_value=0, max_value=100, - value=int(config.indextts2.get("top_k", 30)), + value=int(config.indextts.get("top_k", 30)), step=5, help=tr("Top K Help") ) @@ -995,7 +997,7 @@ def render_indextts2_tts_settings(tr): tr("Num Beams"), min_value=1, max_value=10, - value=int(config.indextts2.get("num_beams", 3)), + value=int(config.indextts.get("num_beams", 3)), step=1, help=tr("Num Beams Help") ) @@ -1004,36 +1006,36 @@ def render_indextts2_tts_settings(tr): tr("Repetition Penalty"), min_value=1.0, max_value=20.0, - value=float(config.indextts2.get("repetition_penalty", 10.0)), + value=float(config.indextts.get("repetition_penalty", 10.0)), step=0.5, help=tr("Repetition Penalty Help") ) do_sample = st.checkbox( tr("Enable Sampling"), - value=config.indextts2.get("do_sample", True), + value=config.indextts.get("do_sample", True), help=tr("Enable Sampling Help") ) # 显示使用说明 - with st.expander(tr("IndexTTS2 Usage Instructions Title"), expanded=False): - st.markdown(tr("IndexTTS2 Usage Instructions")) + with st.expander(tr("IndexTTS Usage Instructions Title"), expanded=False): + st.markdown(tr("IndexTTS Usage Instructions")) # 保存配置 - config.indextts2["api_url"] = api_url - config.indextts2["reference_audio_source"] = reference_audio_source - config.indextts2["reference_audio"] = reference_audio - config.indextts2["infer_mode"] = infer_mode - 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["do_sample"] = do_sample + config.indextts["api_url"] = api_url + config.indextts["reference_audio_source"] = reference_audio_source + config.indextts["reference_audio"] = reference_audio + config.indextts["infer_mode"] = infer_mode + config.indextts["temperature"] = temperature + config.indextts["top_p"] = top_p + config.indextts["top_k"] = top_k + config.indextts["num_beams"] = num_beams + config.indextts["repetition_penalty"] = repetition_penalty + config.indextts["do_sample"] = do_sample # 保存 voice_name 用于兼容性 if reference_audio: - config.ui["voice_name"] = f"indextts2:{reference_audio}" + config.ui["voice_name"] = f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}" def render_doubaotts_settings(tr): @@ -1317,12 +1319,12 @@ def render_voice_preview_new(tr, selected_engine): voice_name = f"qwen3:{vt}" voice_rate = config.ui.get("qwen3_rate", 1.0) voice_pitch = 1.0 # Qwen3 TTS 不支持音调调节 - elif selected_engine == "indextts2": - reference_audio = config.indextts2.get("reference_audio", "") + elif selected_engine == config.INDEXTTS_ENGINE: + reference_audio = config.indextts.get("reference_audio", "") if reference_audio: - voice_name = f"indextts2:{reference_audio}" - voice_rate = 1.0 # IndexTTS2 不支持速度调节 - voice_pitch = 1.0 # IndexTTS2 不支持音调调节 + 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 == "doubaotts": voice_type = config.ui.get("doubaotts_voice_type", "BV700_streaming") voice_name = voice_type @@ -1599,5 +1601,5 @@ def get_audio_params(): 'bgm_type': st.session_state.get('bgm_type', 'random'), 'bgm_file': st.session_state.get('bgm_file', ''), 'bgm_volume': st.session_state.get('bgm_volume', AudioVolumeDefaults.BGM_VOLUME), - 'tts_engine': st.session_state.get('tts_engine', "indextts2"), + 'tts_engine': st.session_state.get('tts_engine', config.INDEXTTS_ENGINE), } diff --git a/webui/i18n/en.json b/webui/i18n/en.json index 654185c..bf8ffe3 100644 --- a/webui/i18n/en.json +++ b/webui/i18n/en.json @@ -271,7 +271,7 @@ "Disabled subtitles help": "This TTS engine does not support subtitle generation. Please use another TTS engine.", "Tencent Cloud TTS": "Tencent Cloud TTS", "Tongyi Qwen3 TTS": "Tongyi Qwen3 TTS", - "IndexTTS2 Voice Clone": "IndexTTS2 Voice Clone", + "IndexTTS Voice Clone": "IndexTTS-1.5 Voice Clone", "Doubao TTS": "Doubao TTS", "Edge TTS features": "Completely free, but service stability can vary and voice cloning is not supported.", "Edge TTS use case": "Testing and lightweight use", @@ -281,9 +281,9 @@ "Tencent Cloud TTS use case": "Personal and enterprise users who need stable Chinese speech synthesis", "Tongyi Qwen3 TTS features": "Alibaba Cloud Tongyi Qwen speech synthesis with high-quality voices and multiple voice options.", "High-quality Chinese speech synthesis use case": "Users who need high-quality Chinese speech synthesis", - "IndexTTS2 features": "A locally or privately deployed voice-cloning engine. Choose a resource audio file or upload a reference audio file, then synthesize narration in that voice.", - "IndexTTS2 use case": "Best for fixed narrator voices, character dubbing, or generating multiple videos with the same voice. Start the IndexTTS2 API service before use. Deployment package: https://pan.quark.cn/s/0767c9bcefd5", - "IndexTTS2 download link": "Download link: https://pan.quark.cn/s/0767c9bcefd5", + "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", "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.", @@ -330,6 +330,10 @@ "Export to Jianying Draft": "📤 Export to Jianying Draft", "Please configure Jianying draft folder in basic settings": "Please configure the Jianying draft folder in Basic Settings", "Jianying draft folder does not exist": "Jianying draft folder does not exist: {path}", + "Jianying export dialog title": "Confirm draft name", + "Jianying export dialog description": "Confirm the Jianying draft name before exporting. Once complete, you can open it from the Jianying draft folder.", + "Jianying export destination": "Save location", + "Jianying draft name": "Draft name", "Please enter Jianying draft name": "Please enter the Jianying draft name", "Confirm Export": "Confirm Export", "Please enter draft name": "Please enter a draft name", @@ -435,7 +439,7 @@ "Qwen TTS Model Help": "Qwen TTS model name, for example qwen3-tts-flash", "Select Qwen3 TTS Voice": "Select a Qwen3 TTS voice", "API URL": "API URL", - "IndexTTS2 API URL Help": "IndexTTS2 API service URL", + "IndexTTS API URL Help": "IndexTTS-1.5 API service URL", "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", @@ -463,8 +467,8 @@ "Repetition Penalty Help": "Higher values reduce repetition, but overly high values may sound unnatural.", "Enable Sampling": "Enable Sampling", "Enable Sampling Help": "Enable sampling for more natural speech.", - "IndexTTS2 Usage Instructions Title": "💡 IndexTTS2 Usage Instructions", - "IndexTTS2 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 IndexTTS2 service is running\n3. **Start synthesis**: the system will use the reference voice to synthesize new speech\n\n**Notes**:\n- Reference audio quality directly affects synthesis quality\n- Use clean audio without background noise when possible\n- Keep text length within a reasonable range\n- The first synthesis may take longer", + "IndexTTS 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", "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 1fc60f0..84ea88e 100644 --- a/webui/i18n/zh.json +++ b/webui/i18n/zh.json @@ -252,7 +252,7 @@ "Disabled subtitles help": "当前 TTS 引擎不支持字幕生成,请使用其他 TTS 引擎", "Tencent Cloud TTS": "腾讯云 TTS", "Tongyi Qwen3 TTS": "通义千问 Qwen3 TTS", - "IndexTTS2 Voice Clone": "IndexTTS2 语音克隆", + "IndexTTS Voice Clone": "IndexTTS-1.5 语音克隆", "Doubao TTS": "豆包语音 TTS", "Edge TTS features": "完全免费,但服务稳定性一般,不支持语音克隆功能", "Edge TTS use case": "测试和轻量级使用", @@ -262,9 +262,9 @@ "Tencent Cloud TTS use case": "个人和企业用户,需要稳定的中文语音合成", "Tongyi Qwen3 TTS features": "阿里云通义千问语音合成,音质优秀,支持多种音色", "High-quality Chinese speech synthesis use case": "需要高质量中文语音合成的用户", - "IndexTTS2 features": "本地/私有部署的语音克隆引擎。选择资源目录音频或上传参考音频后,可按该音色合成旁白。", - "IndexTTS2 use case": "适合需要固定旁白音色、角色配音或批量生成同一音色视频的场景。使用前请先启动 IndexTTS2 API 服务;部署包下载:https://pan.quark.cn/s/0767c9bcefd5", - "IndexTTS2 download link": "下载地址:https://pan.quark.cn/s/0767c9bcefd5", + "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", "Doubao TTS features": "火山引擎豆包语音合成,支持多种音色和情感,国内访问速度快", "Select TTS Engine": "选择 TTS 引擎", "Select TTS Engine Help": "选择您要使用的文本转语音引擎", @@ -312,6 +312,10 @@ "Export to Jianying Draft": "📤 导出到剪映草稿", "Please configure Jianying draft folder in basic settings": "请在基础设置中配置剪映草稿地址", "Jianying draft folder does not exist": "剪映草稿文件夹不存在: {path}", + "Jianying export dialog title": "确认草稿名称", + "Jianying export dialog description": "导出前请确认剪映草稿名称,完成后可在剪映草稿目录中打开。", + "Jianying export destination": "保存目录", + "Jianying draft name": "草稿名称", "Please enter Jianying draft name": "请输入剪映草稿名称", "Confirm Export": "确认导出", "Please enter draft name": "请输入草稿名称", @@ -417,7 +421,7 @@ "Qwen TTS Model Help": "Qwen TTS 模型名,例如 qwen3-tts-flash", "Select Qwen3 TTS Voice": "选择 Qwen3 TTS 音色", "API URL": "API 地址", - "IndexTTS2 API URL Help": "IndexTTS2 API 服务地址", + "IndexTTS API URL Help": "IndexTTS-1.5 API 服务地址", "Reference Audio Source": "参考音频来源", "Reference Audio Source Help": "选择从资源目录选择参考音频,或上传新的参考音频", "Select from Resource Directory": "从资源目录选择", @@ -445,8 +449,8 @@ "Repetition Penalty Help": "值越大越能避免重复,但过大可能导致不自然", "Enable Sampling": "启用采样", "Enable Sampling Help": "启用采样可以获得更自然的语音", - "IndexTTS2 Usage Instructions Title": "💡 IndexTTS2 使用说明", - "IndexTTS2 Usage Instructions": "**零样本语音克隆**\n\n1. **准备参考音频**:上传或指定一段清晰的音频文件(建议 3-10 秒)\n2. **设置 API 地址**:确保 IndexTTS2 服务正常运行\n3. **开始合成**:系统会自动使用参考音频的音色合成新语音\n\n**注意事项**:\n- 参考音频质量直接影响合成效果\n- 建议使用无背景噪音的清晰音频\n- 文本长度建议控制在合理范围内\n- 首次合成可能需要较长时间", + "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- 首次合成可能需要较长时间", "Volcengine Access Key Help": "火山引擎 Access Key", "Volcengine Secret Key Help": "火山引擎 Secret Key", "Doubao AppID Help": "豆包语音应用 AppID",