feat(audio): 新增voxcpm-0.5b和voxcpm-2b本地语音合成支持

新增完整的voxcpm系列本地语音合成功能,包括:
- 新增配置常量、加载逻辑与示例配置项
- 集成webui界面:引擎选型、参数配置与语音预览
- 实现核心调用逻辑,包含API地址标准化、重试机制与文件处理
- 新增两套单元测试覆盖核心流程
- 在字幕转录设置页面补充转录工具包的下载提示文本
This commit is contained in:
linyq-laien 2026-07-16 19:11:51 +08:00
parent 15af56cbd3
commit 7639c9d289
9 changed files with 557 additions and 0 deletions

View File

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

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

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

View File

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

View File

@ -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 配置仍兼容

View File

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

View File

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

View File

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

View File

@ -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-PackmacOS 双击 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-PackmacOS 双击 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": "生成模式",