mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-07-17 11:38:09 +00:00
feat(audio): 新增voxcpm-0.5b和voxcpm-2b本地语音合成支持
新增完整的voxcpm系列本地语音合成功能,包括: - 新增配置常量、加载逻辑与示例配置项 - 集成webui界面:引擎选型、参数配置与语音预览 - 实现核心调用逻辑,包含API地址标准化、重试机制与文件处理 - 新增两套单元测试覆盖核心流程 - 在字幕转录设置页面补充转录工具包的下载提示文本
This commit is contained in:
parent
15af56cbd3
commit
7639c9d289
@ -17,10 +17,16 @@ INDEXTTS2_ENGINE = "indextts2"
|
||||
INDEXTTS2_DISPLAY_NAME = "IndexTTS-2"
|
||||
OMNIVOICE_ENGINE = "omnivoice"
|
||||
OMNIVOICE_DISPLAY_NAME = "OmniVoice"
|
||||
VOXCPM_ENGINE = "voxcpm_05b"
|
||||
VOXCPM_DISPLAY_NAME = "VoxCPM-0.5B"
|
||||
VOXCPM2_ENGINE = "voxcpm_2b"
|
||||
VOXCPM2_DISPLAY_NAME = "VoxCPM-2B"
|
||||
INDEXTTS_VOICE_PREFIX = f"{INDEXTTS_ENGINE}:"
|
||||
INDEXTTS_MACOS_VOICE_PREFIX = f"{INDEXTTS_MACOS_ENGINE}:"
|
||||
INDEXTTS2_VOICE_PREFIX = f"{INDEXTTS2_ENGINE}:"
|
||||
OMNIVOICE_VOICE_PREFIX = f"{OMNIVOICE_ENGINE}:"
|
||||
VOXCPM_VOICE_PREFIX = f"{VOXCPM_ENGINE}:"
|
||||
VOXCPM2_VOICE_PREFIX = f"{VOXCPM2_ENGINE}:"
|
||||
INDEXTTS2_EMOTION_VECTOR_FIELDS = (
|
||||
("happy", "vec_happy"),
|
||||
("angry", "vec_angry"),
|
||||
@ -175,6 +181,8 @@ def save_config():
|
||||
_cfg["indextts_macos"] = indextts_macos
|
||||
_cfg["indextts2"] = indextts2
|
||||
_cfg["omnivoice"] = omnivoice
|
||||
_cfg["voxcpm_05b"] = voxcpm_05b
|
||||
_cfg["voxcpm_2b"] = voxcpm_2b
|
||||
_cfg["doubaotts"] = doubaotts
|
||||
f.write(toml.dumps(_cfg))
|
||||
|
||||
@ -194,6 +202,8 @@ indextts = _cfg.get("indextts", {})
|
||||
indextts_macos = _cfg.get("indextts_macos", {})
|
||||
indextts2 = _cfg.get("indextts2", {})
|
||||
omnivoice = _cfg.get("omnivoice", {})
|
||||
voxcpm_05b = _cfg.get("voxcpm_05b", {})
|
||||
voxcpm_2b = _cfg.get("voxcpm_2b", {})
|
||||
doubaotts = _cfg.get("doubaotts", {})
|
||||
|
||||
hostname = socket.gethostname()
|
||||
|
||||
81
app/services/test_voxcpm2_tts_unittest.py
Normal file
81
app/services/test_voxcpm2_tts_unittest.py
Normal file
@ -0,0 +1,81 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.services import voice
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, *, status_code=200, content=b"", payload=None):
|
||||
self.status_code = status_code
|
||||
self.content = content
|
||||
self._payload = payload or {}
|
||||
self.text = "OK"
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
|
||||
class VoxCPM2TtsTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.original_config = dict(voice.config.voxcpm_2b)
|
||||
self.original_proxy = dict(voice.config.proxy)
|
||||
|
||||
def tearDown(self):
|
||||
voice.config.voxcpm_2b.clear()
|
||||
voice.config.voxcpm_2b.update(self.original_config)
|
||||
voice.config.proxy.clear()
|
||||
voice.config.proxy.update(self.original_proxy)
|
||||
|
||||
def test_voice_design_sends_control_and_downloads_wav(self):
|
||||
voice.config.voxcpm_2b.clear()
|
||||
voice.config.voxcpm_2b.update({
|
||||
"api_url": "http://127.0.0.1:7863/v1/audio/speech",
|
||||
"mode": "design", "control": "温暖自然的年轻女声",
|
||||
"cfg_value": 2.0, "inference_timesteps": 10,
|
||||
"normalize": True, "denoise": False, "output_48k": True,
|
||||
"context_aware": True, "streaming": False,
|
||||
})
|
||||
voice.config.proxy.clear()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
output = Path(temp_dir) / "output.wav"
|
||||
with (
|
||||
patch("app.services.voice.requests.post", return_value=FakeResponse(payload={"downloads": {"wav": "/download/result.wav"}})) as post,
|
||||
patch("app.services.voice.requests.get", return_value=FakeResponse(content=b"wav-2b")) as get,
|
||||
patch("app.services.voice.get_audio_duration_from_file", return_value=2.0),
|
||||
):
|
||||
result = voice.voxcpm2_tts(" 高质量旁白。 ", "voxcpm_2b:design", str(output))
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(output.read_bytes(), b"wav-2b")
|
||||
self.assertEqual(post.call_args.args[0], "http://127.0.0.1:7863/tts")
|
||||
self.assertIsNone(post.call_args.kwargs["files"])
|
||||
self.assertEqual(post.call_args.kwargs["data"]["control"], "温暖自然的年轻女声")
|
||||
self.assertEqual(post.call_args.kwargs["data"]["output_48k"], "true")
|
||||
self.assertEqual(get.call_args.args[0], "http://127.0.0.1:7863/download/result.wav")
|
||||
|
||||
def test_clone_uploads_reference_audio(self):
|
||||
voice.config.voxcpm_2b.clear()
|
||||
voice.config.voxcpm_2b.update({
|
||||
"api_url": "http://127.0.0.1:7863/tts/batch",
|
||||
"mode": "clone", "prompt_text": "参考音频文本",
|
||||
})
|
||||
voice.config.proxy.clear()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
reference = Path(temp_dir) / "reference.wav"
|
||||
output = Path(temp_dir) / "output.wav"
|
||||
reference.write_bytes(b"reference")
|
||||
with (
|
||||
patch("app.services.voice.requests.post", return_value=FakeResponse(payload={"downloads": {"wav": "/download/result.wav"}})) as post,
|
||||
patch("app.services.voice.requests.get", return_value=FakeResponse(content=b"wav")),
|
||||
patch("app.services.voice.get_audio_duration_from_file", return_value=1.0),
|
||||
):
|
||||
result = voice.voxcpm2_tts("克隆测试", f"voxcpm_2b:{reference}", str(output))
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(post.call_args.args[0], "http://127.0.0.1:7863/tts")
|
||||
self.assertIn("reference_audio", post.call_args.kwargs["files"])
|
||||
self.assertEqual(post.call_args.kwargs["data"]["prompt_text"], "参考音频文本")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
83
app/services/test_voxcpm_tts_unittest.py
Normal file
83
app/services/test_voxcpm_tts_unittest.py
Normal file
@ -0,0 +1,83 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.services import voice
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, *, status_code=200, content=b"", payload=None):
|
||||
self.status_code = status_code
|
||||
self.content = content
|
||||
self._payload = payload or {}
|
||||
self.text = "OK"
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
|
||||
class VoxCPMTtsTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.original_config = dict(voice.config.voxcpm_05b)
|
||||
self.original_proxy = dict(voice.config.proxy)
|
||||
|
||||
def tearDown(self):
|
||||
voice.config.voxcpm_05b.clear()
|
||||
voice.config.voxcpm_05b.update(self.original_config)
|
||||
voice.config.proxy.clear()
|
||||
voice.config.proxy.update(self.original_proxy)
|
||||
|
||||
def test_uploads_prompt_audio_and_downloads_generated_wav(self):
|
||||
voice.config.voxcpm_05b.clear()
|
||||
voice.config.voxcpm_05b.update({
|
||||
"api_url": "http://127.0.0.1:7864/v1/audio/speech",
|
||||
"prompt_text": "参考文本",
|
||||
"cfg_value": 2.1,
|
||||
"inference_timesteps": 12,
|
||||
"max_length": 2048,
|
||||
"normalize": True,
|
||||
"denoise": False,
|
||||
})
|
||||
voice.config.proxy.clear()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
reference = Path(temp_dir) / "reference.wav"
|
||||
output = Path(temp_dir) / "output.wav"
|
||||
reference.write_bytes(b"reference")
|
||||
with (
|
||||
patch("app.services.voice.requests.post", return_value=FakeResponse(payload={"audio_url": "/outputs/result.wav"})) as post,
|
||||
patch("app.services.voice.requests.get", return_value=FakeResponse(content=b"wav-bytes")) as get,
|
||||
patch("app.services.voice.get_audio_duration_from_file", return_value=1.5),
|
||||
):
|
||||
result = voice.voxcpm_tts(" 测试文本。 ", f"voxcpm_05b:{reference}", str(output))
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(output.read_bytes(), b"wav-bytes")
|
||||
self.assertEqual(post.call_args.args[0], "http://127.0.0.1:7864/tts")
|
||||
self.assertEqual(post.call_args.kwargs["data"], {
|
||||
"text": "测试文本。", "prompt_text": "参考文本", "cfg_value": 2.1,
|
||||
"inference_timesteps": 12, "max_length": 2048,
|
||||
"normalize": "true", "denoise": "false",
|
||||
})
|
||||
self.assertIn("prompt_audio", post.call_args.kwargs["files"])
|
||||
self.assertEqual(get.call_args.args[0], "http://127.0.0.1:7864/outputs/result.wav")
|
||||
|
||||
def test_default_voice_does_not_upload_audio(self):
|
||||
voice.config.voxcpm_05b.clear()
|
||||
voice.config.voxcpm_05b.update({"api_url": "http://127.0.0.1:7864"})
|
||||
voice.config.proxy.clear()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
output = Path(temp_dir) / "output.wav"
|
||||
with (
|
||||
patch("app.services.voice.requests.post", return_value=FakeResponse(payload={"audio_url": "/outputs/result.wav"})) as post,
|
||||
patch("app.services.voice.requests.get", return_value=FakeResponse(content=b"wav")),
|
||||
patch("app.services.voice.get_audio_duration_from_file", return_value=1.0),
|
||||
):
|
||||
result = voice.voxcpm_tts("默认音色", "voxcpm_05b:default", str(output))
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIsNone(post.call_args.kwargs["files"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1313,6 +1313,14 @@ def tts(
|
||||
if tts_engine == config.OMNIVOICE_ENGINE:
|
||||
logger.info("分发到 OmniVoice")
|
||||
return omnivoice_tts(text, voice_name, voice_file, speed=voice_rate)
|
||||
|
||||
if tts_engine == config.VOXCPM_ENGINE:
|
||||
logger.info("分发到 VoxCPM-0.5B")
|
||||
return voxcpm_tts(text, voice_name, voice_file)
|
||||
|
||||
if tts_engine == config.VOXCPM2_ENGINE:
|
||||
logger.info("分发到 VoxCPM-2B")
|
||||
return voxcpm2_tts(text, voice_name, voice_file)
|
||||
|
||||
if tts_engine == "doubaotts":
|
||||
logger.info("分发到豆包语音 TTS")
|
||||
@ -1803,6 +1811,8 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f
|
||||
config.INDEXTTS_MACOS_ENGINE,
|
||||
config.INDEXTTS2_ENGINE,
|
||||
config.OMNIVOICE_ENGINE,
|
||||
config.VOXCPM_ENGINE,
|
||||
config.VOXCPM2_ENGINE,
|
||||
) else ".mp3"
|
||||
|
||||
for item in list_script:
|
||||
@ -1838,6 +1848,8 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f
|
||||
config.INDEXTTS_MACOS_ENGINE,
|
||||
config.INDEXTTS2_ENGINE,
|
||||
config.OMNIVOICE_ENGINE,
|
||||
config.VOXCPM_ENGINE,
|
||||
config.VOXCPM2_ENGINE,
|
||||
)
|
||||
or tts_engine == "doubaotts"
|
||||
):
|
||||
@ -2299,6 +2311,20 @@ def parse_omnivoice_voice(voice_name: str) -> str:
|
||||
return voice_name
|
||||
|
||||
|
||||
def parse_voxcpm_voice(voice_name: str) -> str:
|
||||
"""解析 VoxCPM-0.5B 的可选参考音频路径。"""
|
||||
if isinstance(voice_name, str) and voice_name.startswith(config.VOXCPM_VOICE_PREFIX):
|
||||
return voice_name[len(config.VOXCPM_VOICE_PREFIX):]
|
||||
return voice_name
|
||||
|
||||
|
||||
def parse_voxcpm2_voice(voice_name: str) -> str:
|
||||
"""解析 VoxCPM-2B 的模式或参考音频路径。"""
|
||||
if isinstance(voice_name, str) and voice_name.startswith(config.VOXCPM2_VOICE_PREFIX):
|
||||
return voice_name[len(config.VOXCPM2_VOICE_PREFIX):]
|
||||
return voice_name
|
||||
|
||||
|
||||
def indextts_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0) -> Union[SubMaker, None]:
|
||||
"""
|
||||
使用 IndexTTS-1.5 API 进行零样本语音克隆
|
||||
@ -2742,6 +2768,144 @@ def indextts2_tts(text: str, voice_name: str, voice_file: str) -> Union[SubMaker
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_voxcpm_api_url(api_url: str) -> str:
|
||||
api_url = (api_url or "http://127.0.0.1:7864").strip().rstrip("/")
|
||||
if api_url.endswith("/v1/audio/speech"):
|
||||
api_url = api_url[:-len("/v1/audio/speech")]
|
||||
if api_url.endswith("/tts"):
|
||||
return api_url
|
||||
return f"{api_url}/tts"
|
||||
|
||||
|
||||
def voxcpm_tts(text: str, voice_name: str, voice_file: str) -> Union[SubMaker, None]:
|
||||
"""使用 VoxCPM-0.5B-Pack 的 multipart /tts 接口生成 WAV。"""
|
||||
voxcpm_config = getattr(config, "voxcpm_05b", {}) or {}
|
||||
api_url = _normalize_voxcpm_api_url(voxcpm_config.get("api_url", "http://127.0.0.1:7864"))
|
||||
reference_audio = parse_voxcpm_voice(voice_name)
|
||||
if reference_audio in ("", "default") or not os.path.isfile(reference_audio):
|
||||
reference_audio = voxcpm_config.get("reference_audio", "") or ""
|
||||
if reference_audio and not os.path.isfile(reference_audio):
|
||||
logger.error(f"VoxCPM-0.5B 参考音频文件不存在: {reference_audio}")
|
||||
return None
|
||||
|
||||
data = {"text": text.strip()}
|
||||
optional_fields = {
|
||||
"prompt_text": voxcpm_config.get("prompt_text"),
|
||||
"cfg_value": voxcpm_config.get("cfg_value"),
|
||||
"inference_timesteps": voxcpm_config.get("inference_timesteps"),
|
||||
"max_length": voxcpm_config.get("max_length"),
|
||||
}
|
||||
for key, value in optional_fields.items():
|
||||
if value not in (None, ""):
|
||||
data[key] = value
|
||||
for key in ("normalize", "denoise"):
|
||||
if key in voxcpm_config:
|
||||
data[key] = str(bool(voxcpm_config[key])).lower()
|
||||
|
||||
proxies = _get_configured_proxies()
|
||||
for attempt in range(3):
|
||||
files = {}
|
||||
try:
|
||||
if reference_audio:
|
||||
files["prompt_audio"] = open(reference_audio, "rb")
|
||||
response = requests.post(api_url, data=data, files=files or None, proxies=proxies, timeout=300)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
audio_url = result.get("audio_url", "") if isinstance(result, dict) else ""
|
||||
if audio_url:
|
||||
audio_response = requests.get(urljoin(api_url, audio_url), proxies=proxies, timeout=180)
|
||||
if audio_response.status_code == 200:
|
||||
with open(voice_file, "wb") as output:
|
||||
output.write(audio_response.content)
|
||||
if os.path.getsize(voice_file) > 0:
|
||||
sub_maker = new_sub_maker()
|
||||
duration = get_audio_duration_from_file(voice_file)
|
||||
duration_ms = int(duration * 1000) if duration > 0 else max(1000, len(text) * 200)
|
||||
add_subtitle_event(sub_maker, 0, duration_ms * 10000, text)
|
||||
return sub_maker
|
||||
logger.error(f"VoxCPM-0.5B API 响应中没有有效音频地址: {result}")
|
||||
else:
|
||||
logger.error(f"VoxCPM-0.5B API 调用失败: {response.status_code} - {response.text}")
|
||||
except (ValueError, requests.exceptions.RequestException) as exc:
|
||||
logger.error(f"VoxCPM-0.5B TTS 请求失败 (尝试 {attempt + 1}/3): {exc}")
|
||||
finally:
|
||||
for file_obj in files.values():
|
||||
file_obj.close()
|
||||
if attempt < 2:
|
||||
time.sleep(2)
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_voxcpm2_api_url(api_url: str) -> str:
|
||||
api_url = (api_url or "http://127.0.0.1:7863").strip().rstrip("/")
|
||||
for suffix in ("/v1/audio/speech", "/tts/batch"):
|
||||
if api_url.endswith(suffix):
|
||||
api_url = api_url[:-len(suffix)]
|
||||
return api_url if api_url.endswith("/tts") else f"{api_url}/tts"
|
||||
|
||||
|
||||
def voxcpm2_tts(text: str, voice_name: str, voice_file: str) -> Union[SubMaker, None]:
|
||||
"""使用 VoxCPM-2B-Pack 的 /tts 接口进行音色设计或参考音频克隆。"""
|
||||
pack_config = getattr(config, "voxcpm_2b", {}) or {}
|
||||
api_url = _normalize_voxcpm2_api_url(pack_config.get("api_url", "http://127.0.0.1:7863"))
|
||||
mode = str(pack_config.get("mode", "design"))
|
||||
parsed_voice = parse_voxcpm2_voice(voice_name)
|
||||
reference_audio = ""
|
||||
if mode == "clone":
|
||||
reference_audio = parsed_voice if parsed_voice and os.path.isfile(parsed_voice) else pack_config.get("reference_audio", "")
|
||||
if not reference_audio or not os.path.isfile(reference_audio):
|
||||
logger.error(f"VoxCPM-2B 参考音频文件不存在: {reference_audio}")
|
||||
return None
|
||||
|
||||
data = {"text": text.strip()}
|
||||
optional_fields = {
|
||||
"control": pack_config.get("control"),
|
||||
"prompt_text": pack_config.get("prompt_text") if mode == "clone" else None,
|
||||
"cfg_value": pack_config.get("cfg_value"),
|
||||
"inference_timesteps": pack_config.get("inference_timesteps"),
|
||||
}
|
||||
for key, value in optional_fields.items():
|
||||
if value not in (None, ""):
|
||||
data[key] = value
|
||||
for key in ("normalize", "denoise", "output_48k", "context_aware", "streaming"):
|
||||
if key in pack_config:
|
||||
data[key] = str(bool(pack_config[key])).lower()
|
||||
|
||||
proxies = _get_configured_proxies()
|
||||
for attempt in range(3):
|
||||
files = {}
|
||||
try:
|
||||
if reference_audio:
|
||||
files["reference_audio"] = open(reference_audio, "rb")
|
||||
response = requests.post(api_url, data=data, files=files or None, proxies=proxies, timeout=600)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
downloads = result.get("downloads", {}) if isinstance(result, dict) else {}
|
||||
audio_url = downloads.get("wav", "") if isinstance(downloads, dict) else ""
|
||||
if audio_url:
|
||||
audio_response = requests.get(urljoin(api_url, audio_url), proxies=proxies, timeout=180)
|
||||
if audio_response.status_code == 200:
|
||||
with open(voice_file, "wb") as output:
|
||||
output.write(audio_response.content)
|
||||
if os.path.getsize(voice_file) > 0:
|
||||
sub_maker = new_sub_maker()
|
||||
duration = get_audio_duration_from_file(voice_file)
|
||||
duration_ms = int(duration * 1000) if duration > 0 else max(1000, len(text) * 200)
|
||||
add_subtitle_event(sub_maker, 0, duration_ms * 10000, text)
|
||||
return sub_maker
|
||||
logger.error(f"VoxCPM-2B API 响应中没有有效 WAV 下载地址: {result}")
|
||||
else:
|
||||
logger.error(f"VoxCPM-2B API 调用失败: {response.status_code} - {response.text}")
|
||||
except (ValueError, requests.exceptions.RequestException) as exc:
|
||||
logger.error(f"VoxCPM-2B TTS 请求失败 (尝试 {attempt + 1}/3): {exc}")
|
||||
finally:
|
||||
for file_obj in files.values():
|
||||
file_obj.close()
|
||||
if attempt < 2:
|
||||
time.sleep(2)
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_omnivoice_api_url(api_url: str) -> str:
|
||||
api_url = (api_url or "http://127.0.0.1:7866/tts").strip()
|
||||
if api_url.endswith("/tts"):
|
||||
|
||||
@ -239,6 +239,35 @@
|
||||
postprocess_output = true
|
||||
preprocess_prompt = true
|
||||
|
||||
[voxcpm_05b]
|
||||
# VoxCPM-0.5B-Pack 本地语音合成/参考音频克隆配置
|
||||
# 启动整合包后,可填写服务根地址或完整 /tts 地址
|
||||
api_url = "http://127.0.0.1:7864"
|
||||
reference_audio_source = "resource"
|
||||
reference_audio = ""
|
||||
prompt_text = ""
|
||||
cfg_value = 2.0
|
||||
inference_timesteps = 10
|
||||
max_length = 4096
|
||||
normalize = true
|
||||
denoise = false
|
||||
|
||||
[voxcpm_2b]
|
||||
# VoxCPM-2B-Pack 本地语音合成、音色设计与参考音频克隆
|
||||
api_url = "http://127.0.0.1:7863"
|
||||
mode = "design"
|
||||
control = ""
|
||||
reference_audio_source = "resource"
|
||||
reference_audio = ""
|
||||
prompt_text = ""
|
||||
cfg_value = 2.0
|
||||
inference_timesteps = 10
|
||||
normalize = true
|
||||
denoise = false
|
||||
output_48k = true
|
||||
context_aware = true
|
||||
streaming = false
|
||||
|
||||
[doubaotts]
|
||||
# 豆包语音 TTS 配置
|
||||
# 新版配置优先填写 API Key;旧版 appid/token 配置仍兼容
|
||||
|
||||
@ -46,6 +46,8 @@ LOCAL_TTS_ENGINES = {
|
||||
config.INDEXTTS_MACOS_ENGINE,
|
||||
config.INDEXTTS2_ENGINE,
|
||||
config.OMNIVOICE_ENGINE,
|
||||
config.VOXCPM_ENGINE,
|
||||
config.VOXCPM2_ENGINE,
|
||||
}
|
||||
|
||||
|
||||
@ -79,6 +81,8 @@ def get_tts_engine_options(tr=lambda key: key):
|
||||
config.INDEXTTS_MACOS_ENGINE: config.INDEXTTS_MACOS_DISPLAY_NAME,
|
||||
config.INDEXTTS2_ENGINE: config.INDEXTTS2_DISPLAY_NAME,
|
||||
config.OMNIVOICE_ENGINE: config.OMNIVOICE_DISPLAY_NAME,
|
||||
config.VOXCPM_ENGINE: config.VOXCPM_DISPLAY_NAME,
|
||||
config.VOXCPM2_ENGINE: config.VOXCPM2_DISPLAY_NAME,
|
||||
"edge_tts": "Edge TTS",
|
||||
"qwen3_tts": tr("Tongyi Qwen3 TTS"),
|
||||
"tencent_tts": tr("Tencent Cloud TTS"),
|
||||
@ -157,6 +161,18 @@ def get_tts_engine_descriptions(tr=lambda key: key):
|
||||
"use_case": tr("OmniVoice use case"),
|
||||
"registration": None
|
||||
},
|
||||
config.VOXCPM_ENGINE: {
|
||||
"title": config.VOXCPM_DISPLAY_NAME,
|
||||
"features": tr("VoxCPM features"),
|
||||
"use_case": tr("VoxCPM use case"),
|
||||
"registration": None,
|
||||
},
|
||||
config.VOXCPM2_ENGINE: {
|
||||
"title": config.VOXCPM2_DISPLAY_NAME,
|
||||
"features": tr("VoxCPM2 features"),
|
||||
"use_case": tr("VoxCPM2 use case"),
|
||||
"registration": None,
|
||||
},
|
||||
"doubaotts": {
|
||||
"title": tr("Doubao TTS"),
|
||||
"features": tr("Doubao TTS features"),
|
||||
@ -603,6 +619,10 @@ def render_tts_settings(tr):
|
||||
render_indextts2_tts_settings(tr)
|
||||
elif selected_engine == config.OMNIVOICE_ENGINE:
|
||||
render_omnivoice_tts_settings(tr)
|
||||
elif selected_engine == config.VOXCPM_ENGINE:
|
||||
render_voxcpm_tts_settings(tr)
|
||||
elif selected_engine == config.VOXCPM2_ENGINE:
|
||||
render_voxcpm2_tts_settings(tr)
|
||||
elif selected_engine == "doubaotts":
|
||||
render_doubaotts_settings(tr)
|
||||
|
||||
@ -1564,6 +1584,109 @@ def render_omnivoice_tts_settings(tr):
|
||||
st.session_state["voice_pitch"] = 1.0
|
||||
|
||||
|
||||
def render_voxcpm_tts_settings(tr):
|
||||
"""渲染 VoxCPM-0.5B-Pack 设置。"""
|
||||
pack_config = config.voxcpm_05b
|
||||
api_url = st.text_input(
|
||||
tr("API URL"),
|
||||
value=pack_config.get("api_url", "http://127.0.0.1:7864"),
|
||||
help=tr("VoxCPM API URL Help"),
|
||||
)
|
||||
use_reference = st.checkbox(
|
||||
tr("VoxCPM Use Reference Audio"),
|
||||
value=bool(pack_config.get("reference_audio", "")),
|
||||
help=tr("VoxCPM Use Reference Audio Help"),
|
||||
)
|
||||
source = pack_config.get("reference_audio_source", "resource")
|
||||
reference_audio = pack_config.get("reference_audio", "")
|
||||
prompt_text = pack_config.get("prompt_text", "")
|
||||
if use_reference:
|
||||
source, reference_audio = render_indextts_reference_audio_selector(tr, pack_config, "voxcpm_05b")
|
||||
prompt_text = st.text_area(
|
||||
tr("VoxCPM Prompt Text"), value=prompt_text,
|
||||
help=tr("VoxCPM Prompt Text Help"), height=90,
|
||||
)
|
||||
else:
|
||||
reference_audio = ""
|
||||
|
||||
with st.expander(tr("Advanced Parameters"), expanded=False):
|
||||
cfg_value = st.slider("CFG Value", 1.0, 3.0, float(pack_config.get("cfg_value", 2.0)), 0.1)
|
||||
inference_timesteps = st.slider("Inference Timesteps", 1, 50, int(pack_config.get("inference_timesteps", 10)), 1)
|
||||
max_length = st.number_input("Max Length", 128, 8192, int(pack_config.get("max_length", 4096)), 128)
|
||||
normalize = st.checkbox(tr("VoxCPM Normalize"), value=bool(pack_config.get("normalize", True)))
|
||||
denoise = st.checkbox(tr("VoxCPM Denoise"), value=bool(pack_config.get("denoise", False)))
|
||||
|
||||
with st.expander(tr("VoxCPM Usage Instructions Title"), expanded=False):
|
||||
st.markdown(tr("VoxCPM Usage Instructions"))
|
||||
|
||||
pack_config.update({
|
||||
"api_url": api_url, "reference_audio_source": source,
|
||||
"reference_audio": reference_audio, "prompt_text": prompt_text,
|
||||
"cfg_value": cfg_value, "inference_timesteps": inference_timesteps,
|
||||
"max_length": max_length, "normalize": normalize, "denoise": denoise,
|
||||
})
|
||||
config.ui["voice_name"] = f"{config.VOXCPM_VOICE_PREFIX}{reference_audio or 'default'}"
|
||||
st.session_state["voice_rate"] = 1.0
|
||||
st.session_state["voice_pitch"] = 1.0
|
||||
|
||||
|
||||
def render_voxcpm2_tts_settings(tr):
|
||||
"""渲染 VoxCPM-2B-Pack 设置。"""
|
||||
pack_config = config.voxcpm_2b
|
||||
api_url = st.text_input(
|
||||
tr("API URL"), value=pack_config.get("api_url", "http://127.0.0.1:7863"),
|
||||
help=tr("VoxCPM2 API URL Help"),
|
||||
)
|
||||
mode_options = [("design", tr("VoxCPM2 Mode Design")), ("clone", tr("VoxCPM2 Mode Clone"))]
|
||||
mode_values = [item[0] for item in mode_options]
|
||||
saved_mode = pack_config.get("mode", "design")
|
||||
if saved_mode not in mode_values:
|
||||
saved_mode = "design"
|
||||
mode = mode_options[st.selectbox(
|
||||
tr("VoxCPM2 Generation Mode"), options=range(len(mode_options)),
|
||||
index=mode_values.index(saved_mode), format_func=lambda index: mode_options[index][1],
|
||||
help=tr("VoxCPM2 Generation Mode Help"),
|
||||
)][0]
|
||||
control = st.text_area(
|
||||
tr("VoxCPM2 Voice Control"), value=pack_config.get("control", ""),
|
||||
help=tr("VoxCPM2 Voice Control Help"), height=80,
|
||||
)
|
||||
source = pack_config.get("reference_audio_source", "resource")
|
||||
reference_audio = pack_config.get("reference_audio", "")
|
||||
prompt_text = pack_config.get("prompt_text", "")
|
||||
if mode == "clone":
|
||||
source, reference_audio = render_indextts_reference_audio_selector(tr, pack_config, "voxcpm_2b")
|
||||
prompt_text = st.text_area(
|
||||
tr("VoxCPM Prompt Text"), value=prompt_text,
|
||||
help=tr("VoxCPM Prompt Text Help"), height=90,
|
||||
)
|
||||
else:
|
||||
reference_audio = ""
|
||||
|
||||
with st.expander(tr("Advanced Parameters"), expanded=False):
|
||||
cfg_value = st.slider("CFG Value", 1.0, 3.0, float(pack_config.get("cfg_value", 2.0)), 0.1, key="voxcpm2_cfg")
|
||||
inference_timesteps = st.slider("Inference Timesteps", 1, 50, int(pack_config.get("inference_timesteps", 10)), 1, key="voxcpm2_steps")
|
||||
normalize = st.checkbox(tr("VoxCPM Normalize"), value=bool(pack_config.get("normalize", True)), key="voxcpm2_normalize")
|
||||
denoise = st.checkbox(tr("VoxCPM Denoise"), value=bool(pack_config.get("denoise", False)), key="voxcpm2_denoise")
|
||||
output_48k = st.checkbox(tr("VoxCPM2 Output 48k"), value=bool(pack_config.get("output_48k", True)))
|
||||
context_aware = st.checkbox(tr("VoxCPM2 Context Aware"), value=bool(pack_config.get("context_aware", True)))
|
||||
streaming = st.checkbox(tr("VoxCPM2 Streaming"), value=bool(pack_config.get("streaming", False)))
|
||||
|
||||
with st.expander(tr("VoxCPM2 Usage Instructions Title"), expanded=False):
|
||||
st.markdown(tr("VoxCPM2 Usage Instructions"))
|
||||
pack_config.update({
|
||||
"api_url": api_url, "mode": mode, "control": control,
|
||||
"reference_audio_source": source, "reference_audio": reference_audio,
|
||||
"prompt_text": prompt_text, "cfg_value": cfg_value,
|
||||
"inference_timesteps": inference_timesteps, "normalize": normalize,
|
||||
"denoise": denoise, "output_48k": output_48k,
|
||||
"context_aware": context_aware, "streaming": streaming,
|
||||
})
|
||||
config.ui["voice_name"] = f"{config.VOXCPM2_VOICE_PREFIX}{reference_audio if mode == 'clone' else mode}"
|
||||
st.session_state["voice_rate"] = 1.0
|
||||
st.session_state["voice_pitch"] = 1.0
|
||||
|
||||
|
||||
def render_doubaotts_settings(tr):
|
||||
"""渲染豆包语音 TTS 设置"""
|
||||
api_key = st.text_input(
|
||||
@ -1876,6 +1999,17 @@ def render_voice_preview_new(tr, selected_engine):
|
||||
voice_name = f"{config.OMNIVOICE_VOICE_PREFIX}{mode}"
|
||||
voice_rate = config.omnivoice.get("speed", 1.0)
|
||||
voice_pitch = 1.0
|
||||
elif selected_engine == config.VOXCPM_ENGINE:
|
||||
reference_audio = config.voxcpm_05b.get("reference_audio", "")
|
||||
voice_name = f"{config.VOXCPM_VOICE_PREFIX}{reference_audio or 'default'}"
|
||||
voice_rate = 1.0
|
||||
voice_pitch = 1.0
|
||||
elif selected_engine == config.VOXCPM2_ENGINE:
|
||||
mode = config.voxcpm_2b.get("mode", "design")
|
||||
reference_audio = config.voxcpm_2b.get("reference_audio", "")
|
||||
voice_name = f"{config.VOXCPM2_VOICE_PREFIX}{reference_audio if mode == 'clone' else mode}"
|
||||
voice_rate = 1.0
|
||||
voice_pitch = 1.0
|
||||
elif selected_engine == "doubaotts":
|
||||
voice_type = config.ui.get("doubaotts_voice_type", "BV700_streaming")
|
||||
voice_name = voice_type
|
||||
@ -1893,6 +2027,8 @@ def render_voice_preview_new(tr, selected_engine):
|
||||
config.INDEXTTS_MACOS_ENGINE,
|
||||
config.INDEXTTS2_ENGINE,
|
||||
config.OMNIVOICE_ENGINE,
|
||||
config.VOXCPM_ENGINE,
|
||||
config.VOXCPM2_ENGINE,
|
||||
) else "audio/mp3"
|
||||
audio_extension = ".wav" if audio_format == "audio/wav" else ".mp3"
|
||||
audio_file = os.path.join(temp_dir, f"tmp-voice-{str(uuid4())}{audio_extension}")
|
||||
|
||||
@ -1196,6 +1196,8 @@ def render_fun_asr_transcription(tr):
|
||||
)
|
||||
backend = backend_options[backend_label]
|
||||
|
||||
st.markdown(tr("Subtitle transcription package downloads"))
|
||||
|
||||
if backend == "upload":
|
||||
render_subtitle_upload(tr)
|
||||
elif backend == "local":
|
||||
|
||||
@ -390,6 +390,10 @@
|
||||
"IndexTTS2 use case": "Best for fixed voices, emotional narration, and local speech synthesis workflows that need finer sampling controls. Start the IndexTTS-2 MLX Pack service before use. Deployment packages:\n\n- **macOS**: [Download](https://cutagent.online/resources/indextts2-full-macos)\n- **Windows**: Coming soon",
|
||||
"OmniVoice features": "A locally or privately deployed OmniVoice-Pack multilingual TTS engine with automatic voice generation, voice design, and reference-audio cloning.",
|
||||
"OmniVoice use case": "Best for local controllable multilingual narration, voice design, or reference-audio cloning. Start the OmniVoice-Pack API service before use. Deployment packages:\n\n- **macOS**: [Download](https://cutagent.online/resources/omnivoice-macos)\n- **Windows**: [Download](https://cutagent.online/resources/omnivoice-windows)",
|
||||
"VoxCPM features": "A locally deployed VoxCPM-0.5B speech engine supporting its default voice and zero-shot cloning from reference audio.",
|
||||
"VoxCPM use case": "Best for private local Chinese narration and reusable voices on Apple Silicon Macs. Start VoxCPM-0.5B-Pack before use; [download the deployment package](https://cutagent.online/resources/voxcpm-05b-macos).",
|
||||
"VoxCPM2 features": "A locally deployed high-quality VoxCPM-2B engine with voice-description design, reference-audio cloning, 48k output, and context awareness.",
|
||||
"VoxCPM2 use case": "Best for higher-quality local narration, describable voices, style control, and cloned voices on Apple Silicon Macs. Start VoxCPM-2B-Pack before use; [download the deployment package](https://cutagent.online/resources/voxcpm-2b-macos).",
|
||||
"Doubao TTS features": "Volcengine Doubao speech synthesis with multiple voices and emotions, plus fast access in mainland China.",
|
||||
"Local Deployment": "Local Deployment",
|
||||
"Cloud Service": "Cloud Service",
|
||||
@ -482,6 +486,7 @@
|
||||
"Characters": "characters",
|
||||
"Ali Bailian Fun-ASR Subtitle Transcription": "Subtitle Processing",
|
||||
"Subtitle Processing Method": "Subtitle Processing Method",
|
||||
"Subtitle transcription package downloads": "**Subtitle transcription package downloads**\n\n- **Fun-ASR-Nano**: [macOS](https://cutagent.online/resources/fun-asr-nano-macos) · [Windows](https://cutagent.online/resources/fun-asr-nano-windows)\n- **FunASR v1.1.0**: [macOS](https://cutagent.online/resources/funasr-v110-macos) · [Windows](https://cutagent.online/resources/funasr-v110-windows)\n- **FireRedASR2-AED**: [macOS](https://cutagent.online/resources/fireredasr2-aed-macos) · [Windows](https://cutagent.online/resources/fireredasr2-aed-windows)",
|
||||
"Fun-ASR Backend": "Fun-ASR Backend",
|
||||
"Local FunASR-Pack API": "FunASR (Local)",
|
||||
"Local FireRedASR API": "FireRedASR2 (Local)",
|
||||
@ -577,6 +582,27 @@
|
||||
"IndexTTS macOS API URL Help": "IndexTTS-MLX-1.5-Pack service URL. Enter the service root or the full /v1/audio/speech/upload endpoint.",
|
||||
"IndexTTS2 API URL Help": "IndexTTS-2 MLX Pack service URL. Enter the service root or the full /v1/audio/speech/upload endpoint.",
|
||||
"OmniVoice API URL Help": "OmniVoice-Pack API service URL. You can enter the service root or the full /tts endpoint.",
|
||||
"VoxCPM API URL Help": "VoxCPM-0.5B-Pack service URL. Enter the service root, /tts, or /v1/audio/speech endpoint.",
|
||||
"VoxCPM Use Reference Audio": "Clone from Reference Audio",
|
||||
"VoxCPM Use Reference Audio Help": "Uploads prompt_audio through /tts when enabled; otherwise uses the model's default voice.",
|
||||
"VoxCPM Prompt Text": "Reference Audio Text",
|
||||
"VoxCPM Prompt Text Help": "Optional transcript of the speech in the reference audio.",
|
||||
"VoxCPM Normalize": "Normalize Text",
|
||||
"VoxCPM Denoise": "Denoise Reference Audio",
|
||||
"VoxCPM Usage Instructions Title": "VoxCPM-0.5B Usage Instructions",
|
||||
"VoxCPM Usage Instructions": "1. Start VoxCPM-0.5B-Pack (double-click start.command on macOS).\n2. The default service URL is http://127.0.0.1:7864.\n3. Use the default voice without reference audio, or enable reference audio to clone a voice.\n4. The first request may take longer while the model loads.",
|
||||
"VoxCPM2 API URL Help": "VoxCPM-2B-Pack service URL. Enter the service root, /tts, /tts/batch, or /v1/audio/speech endpoint.",
|
||||
"VoxCPM2 Generation Mode": "Generation Mode",
|
||||
"VoxCPM2 Generation Mode Help": "Voice design generates a voice from a text description; cloning uploads reference_audio.",
|
||||
"VoxCPM2 Mode Design": "Voice Design",
|
||||
"VoxCPM2 Mode Clone": "Reference Audio Clone",
|
||||
"VoxCPM2 Voice Control": "Voice Description / Style Control",
|
||||
"VoxCPM2 Voice Control Help": "Describe voice, emotion, pace, and expression, for example: warm, natural, slightly fast young female voice.",
|
||||
"VoxCPM2 Output 48k": "Output 48kHz Audio",
|
||||
"VoxCPM2 Context Aware": "Enable Context Awareness",
|
||||
"VoxCPM2 Streaming": "Enable Streaming Inference",
|
||||
"VoxCPM2 Usage Instructions Title": "VoxCPM-2B Usage Instructions",
|
||||
"VoxCPM2 Usage Instructions": "1. Start VoxCPM-2B-Pack (double-click start.command on macOS).\n2. The default service URL is http://127.0.0.1:7863.\n3. Describe the desired voice in design mode, or choose reference audio in clone mode.\n4. Initial model loading and 2B generation may take longer.",
|
||||
"OmniVoice Language Code": "Synthesis Language",
|
||||
"OmniVoice Language Code Help": "The language parameter sent to OmniVoice-Pack, such as zh or en.",
|
||||
"OmniVoice Generation Mode": "Generation Mode",
|
||||
|
||||
@ -328,6 +328,10 @@
|
||||
"IndexTTS2 use case": "适合需要固定音色、情绪化旁白或更细致采样控制的本地语音合成场景。使用前请先启动 IndexTTS-2 MLX Pack 服务;部署包下载:\n\n- **macOS**:[下载地址](https://cutagent.online/resources/indextts2-full-macos)\n- **Windows**:待更新",
|
||||
"OmniVoice features": "本地/私有部署的 OmniVoice-Pack 多语种语音合成引擎,支持自动音色、指令音色和参考音频克隆。",
|
||||
"OmniVoice use case": "适合需要本地可控、多语言旁白、音色设计或参考音频克隆的场景。使用前请先启动 OmniVoice-Pack API 服务;部署包下载:\n\n- **macOS**:[下载地址](https://cutagent.online/resources/omnivoice-macos)\n- **Windows**:[下载地址](https://cutagent.online/resources/omnivoice-windows)",
|
||||
"VoxCPM features": "本地部署的 VoxCPM-0.5B 语音合成引擎,支持默认音色和参考音频零样本音色克隆。",
|
||||
"VoxCPM use case": "适合 Apple Silicon Mac 上的本地中文旁白、固定音色与隐私敏感型语音合成。使用前请启动 VoxCPM-0.5B-Pack;[下载部署包](https://cutagent.online/resources/voxcpm-05b-macos)。",
|
||||
"VoxCPM2 features": "本地部署的 VoxCPM-2B 高质量语音引擎,支持音色描述设计、参考音频克隆、48k 输出和上下文感知。",
|
||||
"VoxCPM2 use case": "适合 Apple Silicon Mac 上需要更高质量、可描述音色、情绪风格控制或固定克隆音色的本地旁白。使用前请启动 VoxCPM-2B-Pack;[下载部署包](https://cutagent.online/resources/voxcpm-2b-macos)。",
|
||||
"Doubao TTS features": "火山引擎豆包语音合成,支持多种音色和情感,国内访问速度快",
|
||||
"Local Deployment": "本地部署",
|
||||
"Cloud Service": "云端服务",
|
||||
@ -421,6 +425,7 @@
|
||||
"Characters": "字符",
|
||||
"Ali Bailian Fun-ASR Subtitle Transcription": "字幕处理",
|
||||
"Subtitle Processing Method": "字幕处理方式",
|
||||
"Subtitle transcription package downloads": "**字幕转录整合包下载**\n\n- **Fun-ASR-Nano**:[macOS](https://cutagent.online/resources/fun-asr-nano-macos) · [Windows](https://cutagent.online/resources/fun-asr-nano-windows)\n- **FunASR v1.1.0**:[macOS](https://cutagent.online/resources/funasr-v110-macos) · [Windows](https://cutagent.online/resources/funasr-v110-windows)\n- **FireRedASR2-AED**:[macOS](https://cutagent.online/resources/fireredasr2-aed-macos) · [Windows](https://cutagent.online/resources/fireredasr2-aed-windows)",
|
||||
"Fun-ASR Backend": "Fun-ASR 后端",
|
||||
"Local FunASR-Pack API": "FunASR(本地部署)",
|
||||
"Local FireRedASR API": "FireRedASR2(本地部署)",
|
||||
@ -516,6 +521,27 @@
|
||||
"IndexTTS macOS API URL Help": "IndexTTS-MLX-1.5-Pack 服务地址,可填写服务根地址或完整 /v1/audio/speech/upload 地址",
|
||||
"IndexTTS2 API URL Help": "IndexTTS-2 MLX Pack 服务地址,可填写服务根地址或完整 /v1/audio/speech/upload 地址",
|
||||
"OmniVoice API URL Help": "OmniVoice-Pack API 服务地址,可填写服务根地址或完整 /tts 地址",
|
||||
"VoxCPM API URL Help": "VoxCPM-0.5B-Pack 服务地址,可填写服务根地址、/tts 或 /v1/audio/speech 地址。",
|
||||
"VoxCPM Use Reference Audio": "使用参考音频克隆音色",
|
||||
"VoxCPM Use Reference Audio Help": "启用后通过 /tts 上传 prompt_audio;关闭时使用模型默认音色。",
|
||||
"VoxCPM Prompt Text": "参考音频文本",
|
||||
"VoxCPM Prompt Text Help": "可选,填写参考音频中实际朗读的文字。",
|
||||
"VoxCPM Normalize": "文本规范化",
|
||||
"VoxCPM Denoise": "参考音频降噪",
|
||||
"VoxCPM Usage Instructions Title": "VoxCPM-0.5B 使用说明",
|
||||
"VoxCPM Usage Instructions": "1. 启动 VoxCPM-0.5B-Pack(macOS 双击 start.command)。\n2. 默认服务地址为 http://127.0.0.1:7864。\n3. 不选择参考音频时使用默认音色;启用参考音频后可克隆音色。\n4. 首次合成需要加载模型,可能耗时较长。",
|
||||
"VoxCPM2 API URL Help": "VoxCPM-2B-Pack 服务地址,可填写服务根地址、/tts、/tts/batch 或 /v1/audio/speech 地址。",
|
||||
"VoxCPM2 Generation Mode": "生成模式",
|
||||
"VoxCPM2 Generation Mode Help": "音色设计使用文字描述生成声音;参考音频克隆会上传 reference_audio。",
|
||||
"VoxCPM2 Mode Design": "音色设计",
|
||||
"VoxCPM2 Mode Clone": "参考音频克隆",
|
||||
"VoxCPM2 Voice Control": "音色描述 / 风格控制",
|
||||
"VoxCPM2 Voice Control Help": "描述音色、情绪、语速和表达,例如:温暖、自然、稍快的年轻女声。",
|
||||
"VoxCPM2 Output 48k": "输出 48kHz 音频",
|
||||
"VoxCPM2 Context Aware": "启用上下文感知",
|
||||
"VoxCPM2 Streaming": "启用流式推理",
|
||||
"VoxCPM2 Usage Instructions Title": "VoxCPM-2B 使用说明",
|
||||
"VoxCPM2 Usage Instructions": "1. 启动 VoxCPM-2B-Pack(macOS 双击 start.command)。\n2. 默认服务地址为 http://127.0.0.1:7863。\n3. 音色设计模式填写声音与风格描述;克隆模式选择参考音频,可补充其文本。\n4. 2B 模型首次加载和生成可能耗时较长。",
|
||||
"OmniVoice Language Code": "合成语言",
|
||||
"OmniVoice Language Code Help": "传给 OmniVoice-Pack 的 language 参数,例如 zh、en。",
|
||||
"OmniVoice Generation Mode": "生成模式",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user