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.27 发布新版本 0.7.9,新增 **Fun-ASR一键转录字幕**
- 2026.04.03 发布新版本 0.7.8,重构纪录片逐帧分析链路,统一共享服务并优化抽帧、缓存、视觉并发与文案生成流程 - 2026.04.03 发布新版本 0.7.8,重构纪录片逐帧分析链路,统一共享服务并优化抽帧、缓存、视觉并发与文案生成流程
- 2026.03.27 发布新版本 0.7.7,出于安全考虑,已移除 LiteLLM 依赖,统一使用 OpenAI 兼容请求链路 - 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.10.15 发布新版本 0.7.3,升级大模型供应商管理能力
- 2025.09.10 发布新版本 0.7.2新增腾讯云tts - 2025.09.10 发布新版本 0.7.2新增腾讯云tts
- 2025.08.18 发布新版本 0.7.1,支持 **语音克隆** 和 最新大模型 - 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__)))) root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
config_file = f"{root_dir}/config.toml" config_file = f"{root_dir}/config.toml"
version_file = f"{root_dir}/project_version" 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(): def get_version_from_file():
@ -32,13 +62,13 @@ def load_config():
_config_ = build_default_config() _config_ = build_default_config()
write_config_file(_config_) write_config_file(_config_)
logger.info("create config.toml with shared defaults") logger.info("create config.toml with shared defaults")
return _config_ return migrate_indextts_config(_config_)
logger.info(f"load config from file: {config_file}") logger.info(f"load config from file: {config_file}")
_config_ = load_toml_file(config_file) _config_ = load_toml_file(config_file)
_config_["app"] = merge_missing_app_defaults(_config_.get("app", {})) _config_["app"] = merge_missing_app_defaults(_config_.get("app", {}))
return _config_ return migrate_indextts_config(_config_)
def load_toml_file(file_path): def load_toml_file(file_path):
@ -60,7 +90,7 @@ def build_default_config():
config_data = load_toml_file(example_file) config_data = load_toml_file(example_file)
config_data["app"] = build_default_app_config(config_data.get("app", {})) 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): def write_config_file(config_data):
@ -82,7 +112,8 @@ def save_config():
_cfg["ui"] = ui _cfg["ui"] = ui
_cfg["tts_qwen"] = tts_qwen _cfg["tts_qwen"] = tts_qwen
_cfg["fun_asr"] = fun_asr _cfg["fun_asr"] = fun_asr
_cfg["indextts2"] = indextts2 _cfg["indextts"] = indextts
_cfg.pop(INDEXTTS_LEGACY_ENGINE, None)
_cfg["doubaotts"] = doubaotts _cfg["doubaotts"] = doubaotts
f.write(toml.dumps(_cfg)) f.write(toml.dumps(_cfg))
@ -98,7 +129,7 @@ ui = _cfg.get("ui", {})
frames = _cfg.get("frames", {}) frames = _cfg.get("frames", {})
tts_qwen = _cfg.get("tts_qwen", {}) tts_qwen = _cfg.get("tts_qwen", {})
fun_asr = _cfg.get("fun_asr", {}) fun_asr = _cfg.get("fun_asr", {})
indextts2 = _cfg.get("indextts2", {}) indextts = _cfg.get("indextts", _cfg.get(INDEXTTS_LEGACY_ENGINE, {}))
doubaotts = _cfg.get("doubaotts", {}) doubaotts = _cfg.get("doubaotts", {})
hostname = socket.gethostname() 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.assertEqual("Pro/zai-org/GLM-5", saved_config["app"]["text_openai_model_name"])
self.assertTrue(saved_config["app"]["hide_config"]) 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): class OpenAICompatibleModelDefaultsTests(unittest.TestCase):
def test_ui_keeps_full_model_name_and_openai_provider(self): 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) return get_media_duration_ffprobe(audio_file)
def _strip_indextts2_prefix(voice_name: str) -> str: def _strip_indextts_prefix(voice_name: str) -> str:
prefix = "indextts2:" voice_name = config.normalize_indextts_voice_prefix(voice_name or "")
prefix = config.INDEXTTS_VOICE_PREFIX
if voice_name.startswith(prefix): if voice_name.startswith(prefix):
return voice_name[len(prefix):] return voice_name[len(prefix):]
return voice_name return voice_name
@ -99,24 +100,25 @@ def _clamp_duration_to_media(
return safe_duration return safe_duration
def _normalize_indextts2_reference_audio(params: VideoClipParams) -> None: def _normalize_indextts_reference_audio(params: VideoClipParams) -> None:
"""Ensure IndexTTS2 uses the configured reference audio instead of a stale UI voice.""" """Ensure IndexTTS-1.5 uses the configured reference audio instead of a stale UI voice."""
if params.tts_engine != "indextts2": params.tts_engine = config.normalize_tts_engine_name(params.tts_engine)
if params.tts_engine != config.INDEXTTS_ENGINE:
return 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): if candidate and os.path.isfile(candidate):
params.voice_name = f"indextts2:{candidate}" params.voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{candidate}"
logger.info(f"IndexTTS2 使用参考音频: {candidate}") logger.info(f"IndexTTS-1.5 使用参考音频: {candidate}")
return 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): if configured_ref and os.path.isfile(configured_ref):
params.voice_name = f"indextts2:{configured_ref}" params.voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{configured_ref}"
logger.info(f"IndexTTS2 使用配置中的参考音频: {configured_ref}") logger.info(f"IndexTTS-1.5 使用配置中的参考音频: {configured_ref}")
return return
raise ValueError("IndexTTS2 参考音频不存在,请在音频设置中上传或选择有效的参考音频") raise ValueError("IndexTTS-1.5 参考音频不存在,请在音频设置中上传或选择有效的参考音频")
def start_export_jianying_draft(task_id: str, params: VideoClipParams): 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 生成音频素材 2. 使用 TTS 生成音频素材
""" """
logger.info("\n\n## 2. 根据OST设置生成音频列表") logger.info("\n\n## 2. 根据OST设置生成音频列表")
_normalize_indextts2_reference_audio(params) _normalize_indextts_reference_audio(params)
tts_segments = [ tts_segments = [
segment for segment in list_script segment for segment in list_script
if segment['OST'] in [0, 2] if segment['OST'] in [0, 2]

View File

@ -12,31 +12,40 @@ DraftPathPlaceholder = "##_draftpath_placeholder_0E685133-18CE-45ED-8CB8-2904A21
class JianyingTaskTests(unittest.TestCase): 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: 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: with tempfile.TemporaryDirectory() as temp_dir:
ref_path = Path(temp_dir) / "reference.wav" ref_path = Path(temp_dir) / "reference.wav"
ref_path.write_bytes(b"fake 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): with patch.dict(jianying_task.config.indextts, {"reference_audio": str(ref_path)}, clear=False):
jianying_task._normalize_indextts2_reference_audio(params) 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): def test_normalize_indextts_accepts_legacy_engine_and_prefix(self):
params = VideoClipParams(tts_engine="indextts2", voice_name="zh-CN-YunjianNeural") 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): jianying_task._normalize_indextts_reference_audio(params)
with self.assertRaisesRegex(ValueError, "IndexTTS2 参考音频不存在"):
jianying_task._normalize_indextts2_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): def test_floor_duration_to_milliseconds(self):
self.assertAlmostEqual(6.997, jianying_task._floor_duration_to_milliseconds(6.997333)) 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( def tts(
text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str, tts_engine: str text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str, tts_engine: str
) -> Union[SubMaker, None]: ) -> 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}'") logger.info(f"使用 TTS 引擎: '{tts_engine}', 语音: '{voice_name}'")
if tts_engine == "tencent_tts": if tts_engine == "tencent_tts":
@ -1288,9 +1290,9 @@ def tts(
logger.info("分发到 Edge TTS") logger.info("分发到 Edge TTS")
return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file) return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file)
if tts_engine == "indextts2": if tts_engine == config.INDEXTTS_ENGINE:
logger.info("分发到 IndexTTS2") logger.info("分发到 IndexTTS-1.5")
return indextts2_tts(text, voice_name, voice_file, speed=voice_rate) return indextts_tts(text, voice_name, voice_file, speed=voice_rate)
if tts_engine == "doubaotts": if tts_engine == "doubaotts":
logger.info("分发到豆包语音 TTS") 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 引擎 :param tts_engine: TTS 引擎
:return: 生成的音频文件列表 :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) output_dir = utils.task_dir(task_id)
tts_results = [] tts_results = []
@ -1800,8 +1803,8 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f
f"或者使用其他 tts 引擎") f"或者使用其他 tts 引擎")
continue continue
else: else:
# SoulVoice、Qwen3、IndexTTS2、豆包语音 引擎不生成字幕文件 # SoulVoice、Qwen3、IndexTTS-1.5、豆包语音 引擎不生成字幕文件
if is_soulvoice_voice(voice_name) or is_qwen_engine(tts_engine) or tts_engine == "indextts2" or tts_engine == "doubaotts": 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) duration = get_audio_duration_from_file(audio_file)
if duration <= 0: if duration <= 0:
@ -2219,24 +2222,25 @@ def parse_soulvoice_voice(voice_name: str) -> str:
return voice_name return voice_name
def parse_indextts2_voice(voice_name: str) -> str: def parse_indextts_voice(voice_name: str) -> str:
""" """
解析 IndexTTS2 语音名称 解析 IndexTTS-1.5 语音名称
支持格式indextts2:reference_audio_path 支持格式indextts:reference_audio_path
返回参考音频文件路径 返回参考音频文件路径
""" """
if voice_name.startswith("indextts2:"): voice_name = config.normalize_indextts_voice_prefix(voice_name)
return voice_name[10:] # 移除 "indextts2:" 前缀 if voice_name.startswith(config.INDEXTTS_VOICE_PREFIX):
return voice_name[len(config.INDEXTTS_VOICE_PREFIX):]
return voice_name 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: Args:
text: 要转换的文本 text: 要转换的文本
voice_name: 参考音频文件格式indextts2:path/to/audio.wav voice_name: 参考音频文件格式indextts:path/to/audio.wav
voice_file: 输出音频文件路径 voice_file: 输出音频文件路径
speed: 语音速度此引擎暂不支持速度调节 speed: 语音速度此引擎暂不支持速度调节
@ -2244,20 +2248,20 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.
SubMaker: 包含时间戳信息的字幕制作器失败时返回 None SubMaker: 包含时间戳信息的字幕制作器失败时返回 None
""" """
# 获取配置 # 获取配置
api_url = config.indextts2.get("api_url", "http://192.168.3.6:8081/tts") api_url = config.indextts.get("api_url", "http://192.168.3.6:8081/tts")
infer_mode = config.indextts2.get("infer_mode", "普通推理") infer_mode = config.indextts.get("infer_mode", "普通推理")
temperature = config.indextts2.get("temperature", 1.0) temperature = config.indextts.get("temperature", 1.0)
top_p = config.indextts2.get("top_p", 0.8) top_p = config.indextts.get("top_p", 0.8)
top_k = config.indextts2.get("top_k", 30) top_k = config.indextts.get("top_k", 30)
do_sample = config.indextts2.get("do_sample", True) do_sample = config.indextts.get("do_sample", True)
num_beams = config.indextts2.get("num_beams", 3) num_beams = config.indextts.get("num_beams", 3)
repetition_penalty = config.indextts2.get("repetition_penalty", 10.0) 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): 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 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): for attempt in range(3):
try: try:
logger.info(f"{attempt + 1} 次调用 IndexTTS2 API") logger.info(f"{attempt + 1} 次调用 IndexTTS-1.5 API")
# 设置代理 # 设置代理
proxies = {} proxies = {}
@ -2295,7 +2299,7 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.
files=files, files=files,
data=data, data=data,
proxies=proxies, proxies=proxies,
timeout=120 # IndexTTS2 推理可能需要较长时间 timeout=120 # IndexTTS-1.5 推理可能需要较长时间
) )
if response.status_code == 200: 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: with open(voice_file, 'wb') as f:
f.write(response.content) 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() sub_maker = new_sub_maker()
# 估算音频时长(基于文本长度) # 估算音频时长(基于文本长度)
estimated_duration_ms = max(1000, int(len(text) * 200)) 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 return sub_maker
else: 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: 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: 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: 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: finally:
# 确保关闭文件 # 确保关闭文件
try: try:
@ -2338,5 +2342,5 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.
except: except:
pass pass
logger.error("IndexTTS2 TTS 生成失败,已达到最大重试次数") logger.error("IndexTTS-1.5 TTS 生成失败,已达到最大重试次数")
return None return None

View File

@ -114,8 +114,8 @@
api_key = "" api_key = ""
model = "fun-asr" model = "fun-asr"
[indextts2] [indextts]
# IndexTTS2 语音克隆配置 # IndexTTS-1.5 语音克隆配置
# 这是一个开源的零样本语音克隆项目,需要自行部署 # 这是一个开源的零样本语音克隆项目,需要自行部署
# 项目地址https://github.com/index-tts/index-tts # 项目地址https://github.com/index-tts/index-tts
# 默认 API 地址(本地部署) # 默认 API 地址(本地部署)
@ -153,8 +153,8 @@
silence_duration = 0.125 silence_duration = 0.125
[ui] [ui]
# TTS引擎选择 (indextts2, edge_tts, qwen3_tts, tencent_tts, doubaotts, azure_speech) # TTS引擎选择 (indextts, edge_tts, qwen3_tts, tencent_tts, doubaotts, azure_speech)
tts_engine = "indextts2" tts_engine = "indextts"
# Edge TTS 配置 # Edge TTS 配置
edge_voice_name = "zh-CN-XiaoyiNeural-Female" edge_voice_name = "zh-CN-XiaoyiNeural-Female"

View File

@ -2,7 +2,7 @@
requests>=2.32.0 requests>=2.32.0
moviepy==2.1.1 moviepy==2.1.1
edge-tts==7.2.7 edge-tts==7.2.7
streamlit>=1.45.0 streamlit>=1.57.0
watchdog==6.0.0 watchdog==6.0.0
loguru>=0.7.3 loguru>=0.7.3
tomli>=2.2.1 tomli>=2.2.1

241
webui.py
View File

@ -2,6 +2,7 @@ import streamlit as st
import os import os
import sys import sys
import time import time
from html import escape
from loguru import logger from loguru import logger
from app.config import config from app.config import config
from webui.components import basic_settings, video_settings, audio_settings, subtitle_settings, script_settings, \ 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')}" return f"tencent:{config.ui.get('tencent_voice_type', '101001')}"
if tts_engine == 'qwen3_tts': if tts_engine == 'qwen3_tts':
return f"qwen3:{config.ui.get('qwen_voice_type', 'Cherry')}" return f"qwen3:{config.ui.get('qwen_voice_type', 'Cherry')}"
if tts_engine == 'indextts2': if config.normalize_tts_engine_name(tts_engine) == config.INDEXTTS_ENGINE:
reference_audio = config.indextts2.get('reference_audio', '') reference_audio = config.indextts.get('reference_audio', '')
if reference_audio: if reference_audio:
return f"indextts2:{reference_audio}" return f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}"
return config.ui.get('voice_name', '') return config.ui.get('voice_name', '')
if tts_engine == 'doubaotts': if tts_engine == 'doubaotts':
return config.ui.get('doubaotts_voice_type', 'BV700_streaming') 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')) 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')) 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) 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), tts_volume=st.session_state.get('tts_volume', 1.0),
original_volume=st.session_state.get('original_volume', 0.7), original_volume=st.session_state.get('original_volume', 0.7),
bgm_volume=st.session_state.get('bgm_volume', 0.3), 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(): def render_export_jianying_button():
"""渲染导出到剪映草稿按钮和处理逻辑""" """渲染导出到剪映草稿按钮和处理逻辑"""
import os import os
import time import time
import uuid
from loguru import logger
# 初始化session state # 初始化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: if 'jianying_export_result' not in st.session_state:
st.session_state['jianying_export_result'] = None st.session_state['jianying_export_result'] = None
if 'jianying_export_error' not in st.session_state: 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)) st.error(tr("Jianying draft folder does not exist").format(path=jianying_draft_path))
return return
# 显示导出表单
st.session_state['show_jianying_export_form'] = True
st.session_state['jianying_export_result'] = None st.session_state['jianying_export_result'] = None
st.session_state['jianying_export_error'] = None st.session_state['jianying_export_error'] = None
st.session_state['draft_name_input'] = f"NarratoAI_{int(time.time())}"
_render_jianying_export_dialog()
# 显示导出表单 _render_jianying_export_status()
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()

View File

@ -9,9 +9,9 @@ from app.models.schema import AudioVolumeDefaults
from app.utils import utils from app.utils import utils
INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR = "/Users/viccy/Downloads/tts-mp3-clone/mp3" INDEXTTS_REFERENCE_AUDIO_SOURCE_DIR = "/Users/viccy/Downloads/tts-mp3-clone/mp3"
INDEXTTS2_REFERENCE_AUDIO_COPY_SUBDIR = "indextts2_refs" INDEXTTS_REFERENCE_AUDIO_COPY_SUBDIR = "indextts_refs"
INDEXTTS2_REFERENCE_AUDIO_MAP = [ INDEXTTS_REFERENCE_AUDIO_MAP = [
("yingshijieshuo-zh-male.mp3", "影视解说", "Film Narration"), ("yingshijieshuo-zh-male.mp3", "影视解说", "Film Narration"),
("maikeashe-zh-male.mp3", "麦克阿瑟", "Macintosh"), ("maikeashe-zh-male.mp3", "麦克阿瑟", "Macintosh"),
("dong-yuhui-zh-male.mp3", "董宇辉", "Dong Yuhui"), ("dong-yuhui-zh-male.mp3", "董宇辉", "Dong Yuhui"),
@ -35,7 +35,7 @@ INDEXTTS2_REFERENCE_AUDIO_MAP = [
("meiqu-kelong-en-unknown.mp3", "美式男声", "US Clone"), ("meiqu-kelong-en-unknown.mp3", "美式男声", "US Clone"),
("sarah-en-female.mp3", "莎拉", "Sarah"), ("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_RESOURCE_DIR = "/Users/viccy/Downloads/tts-mp3-clone/bgms-safe"
BGM_TRACKS_JSON = os.path.join(BGM_RESOURCE_DIR, "tracks.json") BGM_TRACKS_JSON = os.path.join(BGM_RESOURCE_DIR, "tracks.json")
BGM_UPLOAD_SUBDIR = "uploaded_bgms" BGM_UPLOAD_SUBDIR = "uploaded_bgms"
@ -56,7 +56,7 @@ def get_soulvoice_voices():
def get_tts_engine_options(tr=lambda key: key): def get_tts_engine_options(tr=lambda key: key):
"""获取TTS引擎选项""" """获取TTS引擎选项"""
return { return {
"indextts2": "IndexTTS2", config.INDEXTTS_ENGINE: config.INDEXTTS_DISPLAY_NAME,
"edge_tts": "Edge TTS", "edge_tts": "Edge TTS",
"qwen3_tts": tr("Tongyi Qwen3 TTS"), "qwen3_tts": tr("Tongyi Qwen3 TTS"),
"tencent_tts": tr("Tencent Cloud 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"), "use_case": tr("High-quality Chinese speech synthesis use case"),
"registration": "https://dashscope.aliyuncs.com/" "registration": "https://dashscope.aliyuncs.com/"
}, },
"indextts2": { config.INDEXTTS_ENGINE: {
"title": "IndexTTS2", "title": config.INDEXTTS_DISPLAY_NAME,
"features": tr("IndexTTS2 features"), "features": tr("IndexTTS features"),
"use_case": tr("IndexTTS2 use case"), "use_case": tr("IndexTTS use case"),
"registration": None "registration": None
}, },
"doubaotts": { "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() lower_filename = filename.lower()
if "-zh-" in lower_filename: if "-zh-" in lower_filename:
@ -117,30 +117,30 @@ def infer_indextts2_reference_audio_language(filename):
return "unknown" return "unknown"
def get_indextts2_reference_audio_options(): def get_indextts_reference_audio_options():
"""获取本地 IndexTTS2 参考音频选项""" """获取本地 IndexTTS-1.5 参考音频选项"""
options = [] options = []
mapped_files = set() mapped_files = set()
for filename, zh_name, en_name in INDEXTTS2_REFERENCE_AUDIO_MAP: for filename, zh_name, en_name in INDEXTTS_REFERENCE_AUDIO_MAP:
audio_path = os.path.join(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR, filename) audio_path = os.path.join(INDEXTTS_REFERENCE_AUDIO_SOURCE_DIR, filename)
if os.path.isfile(audio_path): if os.path.isfile(audio_path):
options.append({ options.append({
"filename": filename, "filename": filename,
"path": audio_path, "path": audio_path,
"zh": zh_name, "zh": zh_name,
"en": en_name, "en": en_name,
"language": infer_indextts2_reference_audio_language(filename), "language": infer_indextts_reference_audio_language(filename),
}) })
mapped_files.add(filename) mapped_files.add(filename)
if os.path.isdir(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR): if os.path.isdir(INDEXTTS_REFERENCE_AUDIO_SOURCE_DIR):
for filename in sorted(os.listdir(INDEXTTS2_REFERENCE_AUDIO_SOURCE_DIR)): for filename in sorted(os.listdir(INDEXTTS_REFERENCE_AUDIO_SOURCE_DIR)):
if filename in mapped_files: if filename in mapped_files:
continue continue
if not filename.lower().endswith(INDEXTTS2_REFERENCE_AUDIO_EXTENSIONS): if not filename.lower().endswith(INDEXTTS_REFERENCE_AUDIO_EXTENSIONS):
continue 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): if not os.path.isfile(audio_path):
continue continue
fallback_name = os.path.splitext(filename)[0] fallback_name = os.path.splitext(filename)[0]
@ -149,14 +149,14 @@ def get_indextts2_reference_audio_options():
"path": audio_path, "path": audio_path,
"zh": fallback_name, "zh": fallback_name,
"en": fallback_name, "en": fallback_name,
"language": infer_indextts2_reference_audio_language(filename), "language": infer_indextts_reference_audio_language(filename),
}) })
return options return options
def format_indextts2_reference_audio_option(option): def format_indextts_reference_audio_option(option):
"""格式化 IndexTTS2 参考音频下拉显示名""" """格式化 IndexTTS-1.5 参考音频下拉显示名"""
zh_name = option.get("zh", "") zh_name = option.get("zh", "")
en_name = option.get("en", "") en_name = option.get("en", "")
language = option.get("language", "unknown") language = option.get("language", "unknown")
@ -182,7 +182,7 @@ def format_indextts2_reference_audio_option(option):
return f"{display_name} ({language_label})" 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: if not options:
return 0 return 0
@ -195,12 +195,12 @@ def get_indextts2_reference_audio_index(options, saved_reference_audio):
return 0 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): if not source_path or not os.path.isfile(source_path):
return "" 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)) target_path = os.path.join(target_dir, os.path.basename(source_path))
if os.path.abspath(source_path) == os.path.abspath(target_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, disabled=not can_preview,
use_container_width=True, 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): 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) engine_descriptions = get_tts_engine_descriptions(tr)
# 获取保存的TTS引擎设置 # 获取保存的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: if saved_tts_engine not in engine_options:
saved_tts_engine = "indextts2" saved_tts_engine = config.INDEXTTS_ENGINE
# TTS引擎选择下拉框 # TTS引擎选择下拉框
selected_engine = st.selectbox( selected_engine = st.selectbox(
@ -438,8 +440,8 @@ def render_tts_settings(tr):
render_tencent_tts_settings(tr) render_tencent_tts_settings(tr)
elif selected_engine == "qwen3_tts": elif selected_engine == "qwen3_tts":
render_qwen3_tts_settings(tr) render_qwen3_tts_settings(tr)
elif selected_engine == "indextts2": elif selected_engine == config.INDEXTTS_ENGINE:
render_indextts2_tts_settings(tr) render_indextts_tts_settings(tr)
elif selected_engine == "doubaotts": elif selected_engine == "doubaotts":
render_doubaotts_settings(tr) render_doubaotts_settings(tr)
@ -850,22 +852,22 @@ def render_qwen3_tts_settings(tr):
config.ui["voice_name"] = voice_type #兼容性 config.ui["voice_name"] = voice_type #兼容性
def render_indextts2_tts_settings(tr): def render_indextts_tts_settings(tr):
"""渲染 IndexTTS2 TTS 设置""" """渲染 IndexTTS-1.5 TTS 设置"""
# API 地址配置 # API 地址配置
api_url = st.text_input( api_url = st.text_input(
tr("API URL"), tr("API URL"),
value=config.indextts2.get("api_url", "http://127.0.0.1:8081/tts"), value=config.indextts.get("api_url", "http://127.0.0.1:8081/tts"),
help=tr("IndexTTS2 API URL Help") 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 = { reference_audio_source_options = {
tr("Select from Resource Directory"): "resource", tr("Select from Resource Directory"): "resource",
tr("Upload Reference Audio"): "upload", tr("Upload Reference Audio"): "upload",
} }
reference_audio_source_labels = list(reference_audio_source_options.keys()) 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(): if saved_reference_audio_source not in reference_audio_source_options.values():
saved_reference_audio_source = "resource" saved_reference_audio_source = "resource"
default_reference_audio_source_label = next( default_reference_audio_source_label = next(
@ -880,7 +882,7 @@ def render_indextts2_tts_settings(tr):
options=reference_audio_source_labels, options=reference_audio_source_labels,
selection_mode="single", selection_mode="single",
default=default_reference_audio_source_label, default=default_reference_audio_source_label,
key="indextts2_reference_audio_source_selection", key="indextts_reference_audio_source_selection",
help=tr("Reference Audio Source Help"), help=tr("Reference Audio Source Help"),
label_visibility="collapsed", label_visibility="collapsed",
width="stretch", 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_source = reference_audio_source_options[reference_audio_source_label]
reference_audio = saved_reference_audio 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: 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]) select_col, preview_col = st.columns([5, 1])
with select_col: with select_col:
selected_audio_option = reference_audio_options[st.selectbox( selected_audio_option = reference_audio_options[st.selectbox(
tr("Reference Audio Path"), tr("Reference Audio Path"),
options=range(len(reference_audio_options)), options=range(len(reference_audio_options)),
index=selected_audio_index, 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"), help=tr("Reference Audio Path Help"),
label_visibility="collapsed" 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: with preview_col:
render_reference_audio_preview_button( render_reference_audio_preview_button(
reference_audio, reference_audio,
"indextts2_resource_reference_audio_preview", "indextts_resource_reference_audio_preview",
tr, tr,
) )
elif reference_audio_source == "resource": elif reference_audio_source == "resource":
@ -926,7 +928,7 @@ def render_indextts2_tts_settings(tr):
) )
if uploaded_file is not None: 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}") audio_path = os.path.join(target_dir, f"uploaded_{uploaded_file.name}")
with open(audio_path, "wb") as f: with open(audio_path, "wb") as f:
f.write(uploaded_file.getbuffer()) f.write(uploaded_file.getbuffer())
@ -935,11 +937,11 @@ def render_indextts2_tts_settings(tr):
with preview_col: with preview_col:
render_reference_audio_preview_button( render_reference_audio_preview_button(
reference_audio, reference_audio,
"indextts2_upload_reference_audio_preview", "indextts_upload_reference_audio_preview",
tr, 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): if preview_audio_path == reference_audio and os.path.isfile(preview_audio_path):
with open(preview_audio_path, "rb") as audio_file: with open(preview_audio_path, "rb") as audio_file:
st.audio(audio_file.read(), format=get_audio_mime_type(preview_audio_path)) 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("Standard Inference")),
("快速推理", tr("Fast 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( infer_mode = infer_mode_options[st.selectbox(
tr("Inference Mode"), tr("Inference Mode"),
options=range(len(infer_mode_options)), options=range(len(infer_mode_options)),
@ -967,7 +969,7 @@ def render_indextts2_tts_settings(tr):
tr("Sampling Temperature"), tr("Sampling Temperature"),
min_value=0.1, min_value=0.1,
max_value=2.0, max_value=2.0,
value=float(config.indextts2.get("temperature", 1.0)), value=float(config.indextts.get("temperature", 1.0)),
step=0.1, step=0.1,
help=tr("Sampling Temperature Help") help=tr("Sampling Temperature Help")
) )
@ -976,7 +978,7 @@ def render_indextts2_tts_settings(tr):
"Top P", "Top P",
min_value=0.0, min_value=0.0,
max_value=1.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, step=0.05,
help=tr("Top P Help") help=tr("Top P Help")
) )
@ -985,7 +987,7 @@ def render_indextts2_tts_settings(tr):
"Top K", "Top K",
min_value=0, min_value=0,
max_value=100, max_value=100,
value=int(config.indextts2.get("top_k", 30)), value=int(config.indextts.get("top_k", 30)),
step=5, step=5,
help=tr("Top K Help") help=tr("Top K Help")
) )
@ -995,7 +997,7 @@ def render_indextts2_tts_settings(tr):
tr("Num Beams"), tr("Num Beams"),
min_value=1, min_value=1,
max_value=10, max_value=10,
value=int(config.indextts2.get("num_beams", 3)), value=int(config.indextts.get("num_beams", 3)),
step=1, step=1,
help=tr("Num Beams Help") help=tr("Num Beams Help")
) )
@ -1004,36 +1006,36 @@ def render_indextts2_tts_settings(tr):
tr("Repetition Penalty"), tr("Repetition Penalty"),
min_value=1.0, min_value=1.0,
max_value=20.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, step=0.5,
help=tr("Repetition Penalty Help") help=tr("Repetition Penalty Help")
) )
do_sample = st.checkbox( do_sample = st.checkbox(
tr("Enable Sampling"), tr("Enable Sampling"),
value=config.indextts2.get("do_sample", True), value=config.indextts.get("do_sample", True),
help=tr("Enable Sampling Help") help=tr("Enable Sampling Help")
) )
# 显示使用说明 # 显示使用说明
with st.expander(tr("IndexTTS2 Usage Instructions Title"), expanded=False): with st.expander(tr("IndexTTS Usage Instructions Title"), expanded=False):
st.markdown(tr("IndexTTS2 Usage Instructions")) st.markdown(tr("IndexTTS Usage Instructions"))
# 保存配置 # 保存配置
config.indextts2["api_url"] = api_url config.indextts["api_url"] = api_url
config.indextts2["reference_audio_source"] = reference_audio_source config.indextts["reference_audio_source"] = reference_audio_source
config.indextts2["reference_audio"] = reference_audio config.indextts["reference_audio"] = reference_audio
config.indextts2["infer_mode"] = infer_mode config.indextts["infer_mode"] = infer_mode
config.indextts2["temperature"] = temperature config.indextts["temperature"] = temperature
config.indextts2["top_p"] = top_p config.indextts["top_p"] = top_p
config.indextts2["top_k"] = top_k config.indextts["top_k"] = top_k
config.indextts2["num_beams"] = num_beams config.indextts["num_beams"] = num_beams
config.indextts2["repetition_penalty"] = repetition_penalty config.indextts["repetition_penalty"] = repetition_penalty
config.indextts2["do_sample"] = do_sample config.indextts["do_sample"] = do_sample
# 保存 voice_name 用于兼容性 # 保存 voice_name 用于兼容性
if reference_audio: 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): def render_doubaotts_settings(tr):
@ -1317,12 +1319,12 @@ def render_voice_preview_new(tr, selected_engine):
voice_name = f"qwen3:{vt}" voice_name = f"qwen3:{vt}"
voice_rate = config.ui.get("qwen3_rate", 1.0) voice_rate = config.ui.get("qwen3_rate", 1.0)
voice_pitch = 1.0 # Qwen3 TTS 不支持音调调节 voice_pitch = 1.0 # Qwen3 TTS 不支持音调调节
elif selected_engine == "indextts2": elif selected_engine == config.INDEXTTS_ENGINE:
reference_audio = config.indextts2.get("reference_audio", "") reference_audio = config.indextts.get("reference_audio", "")
if reference_audio: if reference_audio:
voice_name = f"indextts2:{reference_audio}" voice_name = f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}"
voice_rate = 1.0 # IndexTTS2 不支持速度调节 voice_rate = 1.0 # IndexTTS-1.5 不支持速度调节
voice_pitch = 1.0 # IndexTTS2 不支持音调调节 voice_pitch = 1.0 # IndexTTS-1.5 不支持音调调节
elif selected_engine == "doubaotts": elif selected_engine == "doubaotts":
voice_type = config.ui.get("doubaotts_voice_type", "BV700_streaming") voice_type = config.ui.get("doubaotts_voice_type", "BV700_streaming")
voice_name = voice_type voice_name = voice_type
@ -1599,5 +1601,5 @@ def get_audio_params():
'bgm_type': st.session_state.get('bgm_type', 'random'), 'bgm_type': st.session_state.get('bgm_type', 'random'),
'bgm_file': st.session_state.get('bgm_file', ''), 'bgm_file': st.session_state.get('bgm_file', ''),
'bgm_volume': st.session_state.get('bgm_volume', AudioVolumeDefaults.BGM_VOLUME), '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.", "Disabled subtitles help": "This TTS engine does not support subtitle generation. Please use another TTS engine.",
"Tencent Cloud TTS": "Tencent Cloud TTS", "Tencent Cloud TTS": "Tencent Cloud TTS",
"Tongyi Qwen3 TTS": "Tongyi Qwen3 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", "Doubao TTS": "Doubao TTS",
"Edge TTS features": "Completely free, but service stability can vary and voice cloning is not supported.", "Edge TTS features": "Completely free, but service stability can vary and voice cloning is not supported.",
"Edge TTS use case": "Testing and lightweight use", "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", "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.", "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", "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.", "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.",
"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", "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",
"IndexTTS2 download link": "Download link: 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.", "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": "Select TTS Engine",
"Select TTS Engine Help": "Choose the text-to-speech engine you want to use.", "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", "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", "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 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", "Please enter Jianying draft name": "Please enter the Jianying draft name",
"Confirm Export": "Confirm Export", "Confirm Export": "Confirm Export",
"Please enter draft name": "Please enter a draft name", "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", "Qwen TTS Model Help": "Qwen TTS model name, for example qwen3-tts-flash",
"Select Qwen3 TTS Voice": "Select a Qwen3 TTS voice", "Select Qwen3 TTS Voice": "Select a Qwen3 TTS voice",
"API URL": "API URL", "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": "Reference Audio Source",
"Reference Audio Source Help": "Choose a reference audio from the resource directory or upload a new one.", "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", "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.", "Repetition Penalty Help": "Higher values reduce repetition, but overly high values may sound unnatural.",
"Enable Sampling": "Enable Sampling", "Enable Sampling": "Enable Sampling",
"Enable Sampling Help": "Enable sampling for more natural speech.", "Enable Sampling Help": "Enable sampling for more natural speech.",
"IndexTTS2 Usage Instructions Title": "💡 IndexTTS2 Usage Instructions", "IndexTTS Usage Instructions Title": "💡 IndexTTS-1.5 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": "**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 Access Key Help": "Volcengine Access Key",
"Volcengine Secret Key Help": "Volcengine Secret Key", "Volcengine Secret Key Help": "Volcengine Secret Key",
"Doubao AppID Help": "Doubao TTS application AppID", "Doubao AppID Help": "Doubao TTS application AppID",

View File

@ -252,7 +252,7 @@
"Disabled subtitles help": "当前 TTS 引擎不支持字幕生成,请使用其他 TTS 引擎", "Disabled subtitles help": "当前 TTS 引擎不支持字幕生成,请使用其他 TTS 引擎",
"Tencent Cloud TTS": "腾讯云 TTS", "Tencent Cloud TTS": "腾讯云 TTS",
"Tongyi Qwen3 TTS": "通义千问 Qwen3 TTS", "Tongyi Qwen3 TTS": "通义千问 Qwen3 TTS",
"IndexTTS2 Voice Clone": "IndexTTS2 语音克隆", "IndexTTS Voice Clone": "IndexTTS-1.5 语音克隆",
"Doubao TTS": "豆包语音 TTS", "Doubao TTS": "豆包语音 TTS",
"Edge TTS features": "完全免费,但服务稳定性一般,不支持语音克隆功能", "Edge TTS features": "完全免费,但服务稳定性一般,不支持语音克隆功能",
"Edge TTS use case": "测试和轻量级使用", "Edge TTS use case": "测试和轻量级使用",
@ -262,9 +262,9 @@
"Tencent Cloud TTS use case": "个人和企业用户,需要稳定的中文语音合成", "Tencent Cloud TTS use case": "个人和企业用户,需要稳定的中文语音合成",
"Tongyi Qwen3 TTS features": "阿里云通义千问语音合成,音质优秀,支持多种音色", "Tongyi Qwen3 TTS features": "阿里云通义千问语音合成,音质优秀,支持多种音色",
"High-quality Chinese speech synthesis use case": "需要高质量中文语音合成的用户", "High-quality Chinese speech synthesis use case": "需要高质量中文语音合成的用户",
"IndexTTS2 features": "本地/私有部署的语音克隆引擎。选择资源目录音频或上传参考音频后,可按该音色合成旁白。", "IndexTTS features": "本地/私有部署的 IndexTTS-1.5 语音克隆引擎。选择资源目录音频或上传参考音频后,可按该音色合成旁白。",
"IndexTTS2 use case": "适合需要固定旁白音色、角色配音或批量生成同一音色视频的场景。使用前请先启动 IndexTTS2 API 服务部署包下载https://pan.quark.cn/s/0767c9bcefd5", "IndexTTS use case": "适合需要固定旁白音色、角色配音或批量生成同一音色视频的场景。使用前请先启动 IndexTTS-1.5 API 服务部署包下载https://pan.quark.cn/s/0767c9bcefd5",
"IndexTTS2 download link": "下载地址https://pan.quark.cn/s/0767c9bcefd5", "IndexTTS download link": "下载地址https://pan.quark.cn/s/0767c9bcefd5",
"Doubao TTS features": "火山引擎豆包语音合成,支持多种音色和情感,国内访问速度快", "Doubao TTS features": "火山引擎豆包语音合成,支持多种音色和情感,国内访问速度快",
"Select TTS Engine": "选择 TTS 引擎", "Select TTS Engine": "选择 TTS 引擎",
"Select TTS Engine Help": "选择您要使用的文本转语音引擎", "Select TTS Engine Help": "选择您要使用的文本转语音引擎",
@ -312,6 +312,10 @@
"Export to Jianying Draft": "📤 导出到剪映草稿", "Export to Jianying Draft": "📤 导出到剪映草稿",
"Please configure Jianying draft folder in basic settings": "请在基础设置中配置剪映草稿地址", "Please configure Jianying draft folder in basic settings": "请在基础设置中配置剪映草稿地址",
"Jianying draft folder does not exist": "剪映草稿文件夹不存在: {path}", "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": "请输入剪映草稿名称", "Please enter Jianying draft name": "请输入剪映草稿名称",
"Confirm Export": "确认导出", "Confirm Export": "确认导出",
"Please enter draft name": "请输入草稿名称", "Please enter draft name": "请输入草稿名称",
@ -417,7 +421,7 @@
"Qwen TTS Model Help": "Qwen TTS 模型名,例如 qwen3-tts-flash", "Qwen TTS Model Help": "Qwen TTS 模型名,例如 qwen3-tts-flash",
"Select Qwen3 TTS Voice": "选择 Qwen3 TTS 音色", "Select Qwen3 TTS Voice": "选择 Qwen3 TTS 音色",
"API URL": "API 地址", "API URL": "API 地址",
"IndexTTS2 API URL Help": "IndexTTS2 API 服务地址", "IndexTTS API URL Help": "IndexTTS-1.5 API 服务地址",
"Reference Audio Source": "参考音频来源", "Reference Audio Source": "参考音频来源",
"Reference Audio Source Help": "选择从资源目录选择参考音频,或上传新的参考音频", "Reference Audio Source Help": "选择从资源目录选择参考音频,或上传新的参考音频",
"Select from Resource Directory": "从资源目录选择", "Select from Resource Directory": "从资源目录选择",
@ -445,8 +449,8 @@
"Repetition Penalty Help": "值越大越能避免重复,但过大可能导致不自然", "Repetition Penalty Help": "值越大越能避免重复,但过大可能导致不自然",
"Enable Sampling": "启用采样", "Enable Sampling": "启用采样",
"Enable Sampling Help": "启用采样可以获得更自然的语音", "Enable Sampling Help": "启用采样可以获得更自然的语音",
"IndexTTS2 Usage Instructions Title": "💡 IndexTTS2 使用说明", "IndexTTS Usage Instructions Title": "💡 IndexTTS-1.5 使用说明",
"IndexTTS2 Usage Instructions": "**零样本语音克隆**\n\n1. **准备参考音频**:上传或指定一段清晰的音频文件(建议 3-10 秒)\n2. **设置 API 地址**:确保 IndexTTS2 服务正常运行\n3. **开始合成**:系统会自动使用参考音频的音色合成新语音\n\n**注意事项**\n- 参考音频质量直接影响合成效果\n- 建议使用无背景噪音的清晰音频\n- 文本长度建议控制在合理范围内\n- 首次合成可能需要较长时间", "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 Access Key Help": "火山引擎 Access Key",
"Volcengine Secret Key Help": "火山引擎 Secret Key", "Volcengine Secret Key Help": "火山引擎 Secret Key",
"Doubao AppID Help": "豆包语音应用 AppID", "Doubao AppID Help": "豆包语音应用 AppID",