From 7493f6d2b87d6c98d5b39055ecb3de10f035fee4 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 19 Jul 2026 14:43:36 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(P0):=20=E9=A2=84=E7=BD=AE=E9=9F=B3?= =?UTF-8?q?=E8=89=B2=E8=AF=95=E5=90=AC=20-=20=E6=96=B0=E5=A2=9E=E5=AE=9E?= =?UTF-8?q?=E6=97=B6TTS=E5=90=88=E6=88=90=E8=AF=95=E5=90=AC=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GET /voices/presets/{voice_id}/preview 端点 - 支持自定义试听文本,默认使用预置音色名称的问候语 - 默认试听音频7天内存缓存,减少重复TTS调用 - 修复预置音色全没声音的问题 Issue: #578 --- apps/api/app/api/routes/voices.py | 64 ++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/apps/api/app/api/routes/voices.py b/apps/api/app/api/routes/voices.py index b6db583b3..9bbb6f4d2 100644 --- a/apps/api/app/api/routes/voices.py +++ b/apps/api/app/api/routes/voices.py @@ -9,7 +9,7 @@ from typing import Literal, Optional from app.api.routes._helpers import get_user_plan from app.auth import AuthenticatedUser, get_current_user -from app.dependencies import get_audio_url_signer, get_db_session, get_user_repository +from app.dependencies import get_audio_url_signer, get_cosyvoice_service, get_db_session, get_user_repository from app.schemas.voice import ( PresetVoiceItemResponse, PresetVoiceListResponse, @@ -37,11 +37,19 @@ from packages.application.voice_library.use_cases import ( QuotaExceededError, UpdateVoiceLibraryUseCase, ) -from packages.domain.preset_voices import PRESET_VOICES +from packages.application.cosyvoice_service import CosyVoiceService, CosyVoiceError +from packages.domain.preset_voices import PRESET_VOICES, get_preset_voice_by_id from packages.ports.user_repository import UserRepository router = APIRouter() +# 预置音色试听音频缓存(内存缓存,减少重复TTS调用) +# key: voice_id, value: (audio_url, timestamp) +_preset_preview_cache: dict[str, tuple[str, float]] = {} +PREVIEW_CACHE_TTL = 7 * 24 * 3600 # 7天TTL +# 每个预置音色的默认试听文本 +PREVIEW_TEMPLATE = "你好,我是{name},很高兴认识你。" + def _get_voice_repository(session: Session = Depends(get_db_session)) -> SQLAlchemyVoiceLibraryRepository: return SQLAlchemyVoiceLibraryRepository(session) @@ -216,6 +224,58 @@ def list_preset_voices() -> PresetVoiceListResponse: return PresetVoiceListResponse(items=items, total=len(items)) +@router.get("/presets/{voice_id}/preview") +def get_preset_voice_preview( + voice_id: str, + text: str = Query("", description="自定义试听文本,为空则使用默认示例"), + cosyvoice: CosyVoiceService = Depends(get_cosyvoice_service), +) -> dict: + """获取预置音色试听音频(实时 TTS 合成)。 + + - 首次调用会合成并缓存7天 + - 相同 voice_id 重复调用直接返回缓存的音频URL + - 可传入自定义 text 参数试听不同文本 + """ + import time + + preset = get_preset_voice_by_id(voice_id) + if preset is None: + raise HTTPException(status_code=404, detail=f"预置音色不存在: {voice_id}") + + # 有自定义文本时不缓存 + use_cache = not text.strip() + + if use_cache and voice_id in _preset_preview_cache: + audio_url, cached_at = _preset_preview_cache[voice_id] + if time.time() - cached_at < PREVIEW_CACHE_TTL: + return {"voice_id": voice_id, "audio_url": audio_url, "cached": True} + + # 合成试听音频 + preview_text = text.strip() or PREVIEW_TEMPLATE.format(name=preset.name) + try: + result = cosyvoice.synthesize_speech( + text=preview_text, + voice_id=preset.voice_id, + format="mp3", + speed=1.0, + ) + except CosyVoiceError as e: + raise HTTPException(status_code=502, detail=f"TTS 合成失败: {e}") from e + + audio_url = result.audio_url + + # 缓存(仅默认试听文本) + if use_cache: + _preset_preview_cache[voice_id] = (audio_url, time.time()) + + return { + "voice_id": voice_id, + "audio_url": audio_url, + "text": preview_text, + "cached": False, + } + + # ==================== 原有 CRUD 端点(保持向后兼容)==================== -- 2.54.0 From 25d3fdd087b99566585fbf1d9033e8dd20fad223 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 19 Jul 2026 14:52:16 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dimport=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E9=97=AE=E9=A2=98=EF=BC=88isort=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/app/api/routes/voices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/app/api/routes/voices.py b/apps/api/app/api/routes/voices.py index 9bbb6f4d2..ccf67d5ab 100644 --- a/apps/api/app/api/routes/voices.py +++ b/apps/api/app/api/routes/voices.py @@ -27,6 +27,7 @@ from sqlalchemy.orm import Session from packages.adapters.sqlalchemy_impl.voice_clone_profile_repository import SQLAlchemyVoiceCloneProfileRepository from packages.adapters.sqlalchemy_impl.voice_library_repository import SQLAlchemyVoiceLibraryRepository +from packages.application.cosyvoice_service import CosyVoiceError, CosyVoiceService from packages.application.voice_library.commands import CreateVoiceLibraryCommand, UpdateVoiceLibraryCommand from packages.application.voice_library.use_cases import ( CreateVoiceLibraryUseCase, @@ -37,7 +38,6 @@ from packages.application.voice_library.use_cases import ( QuotaExceededError, UpdateVoiceLibraryUseCase, ) -from packages.application.cosyvoice_service import CosyVoiceService, CosyVoiceError from packages.domain.preset_voices import PRESET_VOICES, get_preset_voice_by_id from packages.ports.user_repository import UserRepository -- 2.54.0