feat(webui): 优化剪映草稿导出的用户体验

- 更新streamlit依赖至1.57.0以支持原生弹窗组件
- 重构剪映导出逻辑,使用原生弹窗替代旧的内联表单
- 新增带样式的导出确认面板并补充多语言翻译
- 简化导出状态渲染与会话状态管理逻辑
This commit is contained in:
viccy 2026-06-06 12:43:57 +08:00
parent 5a9775d62d
commit a2645aebd3
12 changed files with 398 additions and 226 deletions

View File

@ -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,支持 **语音克隆** 和 最新大模型

View File

@ -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()

View File

@ -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):

View File

@ -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]

View File

@ -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))

View File

@ -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

View File

@ -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"

View File

@ -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

241
webui.py
View File

@ -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"""
<style>
.jianying-export-panel {{
display: flex;
gap: 12px;
align-items: flex-start;
padding: 14px;
margin: 2px 0 18px;
border: 1px solid rgba(255, 75, 75, 0.24);
border-radius: 8px;
background: linear-gradient(135deg, rgba(255, 75, 75, 0.10), rgba(255, 255, 255, 0.96));
}}
.jianying-export-icon {{
width: 38px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
border-radius: 8px;
color: #ffffff;
background: #ff4b4b;
font-size: 20px;
line-height: 1;
}}
.jianying-export-title {{
color: #202534;
font-size: 17px;
font-weight: 700;
line-height: 1.35;
margin-bottom: 4px;
}}
.jianying-export-description {{
color: #5f6575;
font-size: 13px;
line-height: 1.55;
}}
.jianying-export-path {{
padding: 10px 12px;
margin: 2px 0 16px;
border: 1px solid #e4e7ef;
border-radius: 8px;
background: #f8f9fc;
color: #323846;
font-size: 13px;
line-height: 1.45;
word-break: break-all;
}}
.jianying-export-path-label {{
display: block;
color: #7a8192;
font-size: 12px;
margin-bottom: 4px;
}}
</style>
<div class="jianying-export-panel">
<div class="jianying-export-icon">📤</div>
<div>
<div class="jianying-export-title">{dialog_title}</div>
<div class="jianying-export-description">{dialog_description}</div>
</div>
</div>
<div class="jianying-export-path">
<span class="jianying-export-path-label">{destination_label}</span>
{destination_path}
</div>
""",
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()

View File

@ -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),
}

View File

@ -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",

View File

@ -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",