mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-07-28 17:05:57 +00:00
feat(webui): 优化剪映草稿导出的用户体验
- 更新streamlit依赖至1.57.0以支持原生弹窗组件 - 重构剪映导出逻辑,使用原生弹窗替代旧的内联表单 - 新增带样式的导出确认面板并补充多语言翻译 - 简化导出状态渲染与会话状态管理逻辑
This commit is contained in:
parent
5a9775d62d
commit
a2645aebd3
@ -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,支持 **语音克隆** 和 最新大模型
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
241
webui.py
@ -2,6 +2,7 @@ import streamlit as st
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from html import escape
|
||||
from loguru import logger
|
||||
from app.config import config
|
||||
from webui.components import basic_settings, video_settings, audio_settings, subtitle_settings, script_settings, \
|
||||
@ -232,10 +233,10 @@ def get_voice_name_for_tts_engine(tts_engine: str) -> str:
|
||||
return f"tencent:{config.ui.get('tencent_voice_type', '101001')}"
|
||||
if tts_engine == 'qwen3_tts':
|
||||
return f"qwen3:{config.ui.get('qwen_voice_type', 'Cherry')}"
|
||||
if tts_engine == 'indextts2':
|
||||
reference_audio = config.indextts2.get('reference_audio', '')
|
||||
if config.normalize_tts_engine_name(tts_engine) == config.INDEXTTS_ENGINE:
|
||||
reference_audio = config.indextts.get('reference_audio', '')
|
||||
if reference_audio:
|
||||
return f"indextts2:{reference_audio}"
|
||||
return f"{config.INDEXTTS_VOICE_PREFIX}{reference_audio}"
|
||||
return config.ui.get('voice_name', '')
|
||||
if tts_engine == 'doubaotts':
|
||||
return config.ui.get('doubaotts_voice_type', 'BV700_streaming')
|
||||
@ -247,7 +248,7 @@ def get_voice_name_for_tts_engine(tts_engine: str) -> str:
|
||||
return config.ui.get('voice_name', config.ui.get('edge_voice_name', 'zh-CN-XiaoxiaoNeural-Female'))
|
||||
|
||||
|
||||
def get_jianying_export_params() -> VideoClipParams:
|
||||
def get_jianying_export_params(draft_name=None) -> VideoClipParams:
|
||||
"""获取导出到剪映草稿的参数"""
|
||||
tts_engine = st.session_state.get('tts_engine', config.ui.get('tts_engine', 'edge_tts'))
|
||||
voice_name = get_voice_name_for_tts_engine(tts_engine)
|
||||
@ -272,20 +273,178 @@ def get_jianying_export_params() -> VideoClipParams:
|
||||
tts_volume=st.session_state.get('tts_volume', 1.0),
|
||||
original_volume=st.session_state.get('original_volume', 0.7),
|
||||
bgm_volume=st.session_state.get('bgm_volume', 0.3),
|
||||
draft_name=st.session_state.get('draft_name_input', f"NarratoAI_{int(time.time())}")
|
||||
draft_name=(
|
||||
draft_name
|
||||
if draft_name is not None
|
||||
else st.session_state.get('draft_name_input', f"NarratoAI_{int(time.time())}")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _render_jianying_export_status():
|
||||
"""渲染剪映导出的结果提示。"""
|
||||
result = st.session_state.get('jianying_export_result')
|
||||
error = st.session_state.get('jianying_export_error')
|
||||
|
||||
if result:
|
||||
st.success(tr("Jianying draft exported successfully").format(name=result['draft_name']))
|
||||
st.info(tr("Draft saved to").format(path=result['draft_path']))
|
||||
elif error:
|
||||
st.error(f"{tr('Failed to export Jianying draft')}: {error}")
|
||||
|
||||
|
||||
def _render_jianying_export_dialog():
|
||||
"""使用弹窗确认剪映草稿名称。"""
|
||||
import uuid
|
||||
from loguru import logger
|
||||
|
||||
@st.dialog(tr("Export to Jianying Draft"), width="small")
|
||||
def jianying_export_dialog():
|
||||
jianying_draft_path = config.ui.get("jianying_draft_path", "")
|
||||
dialog_title = escape(tr("Jianying export dialog title"))
|
||||
dialog_description = escape(tr("Jianying export dialog description"))
|
||||
destination_label = escape(tr("Jianying export destination"))
|
||||
destination_path = escape(jianying_draft_path or "-")
|
||||
|
||||
st.markdown(
|
||||
f"""
|
||||
<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()
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user