feat(viral_video): voice_source=my_voice/clone 克隆音色TTS支持 (#2188) #2191

Merged
xiaoxia merged 2 commits from feature/voice-source-clone-support into develop 2026-10-05 12:26:44 +08:00
+69 -7
View File
@@ -1398,15 +1398,79 @@ def _step_review(job: ViralVideoJob, copy_result: dict) -> dict:
return {"passed": True, "score": 75, "details": {}, "issues": []}
def _resolve_tts_voice_id(job: ViralVideoJob) -> str:
"""#2188: 根据 voice_source + voice_id 解析真正传给 CosyVoice 的 voice_id。
- voice_source 在 ("my_voice", "clone"):voice_id 是 VoiceCloneProfile.id,
需要从 DB 查 profile.voice_id(CosyVoice 返回的音色 ID)。
- 其他/空:voice_id 直接视为 CosyVoice preset 音色名(longxiaochun_v3 等)。
- 任何解析失败都回退默认 longxiaochun_v3,保证任务不崩。
"""
default_voice = "longxiaochun_v3"
raw_voice_id = (getattr(job, "voice_id", "") or "").strip()
voice_source = (getattr(job, "voice_source", "") or "").strip().lower()
if not raw_voice_id:
return default_voice
# 克隆音色:前端传 profile.id,需查 DB 取 cosyvoice_voice_id
if voice_source in ("my_voice", "clone"):
session = None
try:
from packages.adapters.sqlalchemy_impl.voice_clone_profile_repository import (
SQLAlchemyVoiceCloneProfileRepository,
)
session = SessionLocal()
_repo = SQLAlchemyVoiceCloneProfileRepository(session)
_profile = _repo.get(raw_voice_id)
if _profile and _profile.status.value == "ready" and (_profile.voice_id or "").strip():
logger.info(
"[爆款视频] 克隆音色解析: profile_id=%s → cosyvoice_voice_id=%s",
raw_voice_id,
_profile.voice_id,
)
return _profile.voice_id.strip()
# 找不到/未就绪/无voice_id
if _profile is None:
logger.warning("[爆款视频] 克隆音色 profile_id=%s 不存在,回退默认音色", raw_voice_id)
elif _profile.status.value != "ready":
logger.warning(
"[爆款视频] 克隆音色 profile_id=%s 状态=%s 未就绪,回退默认音色",
raw_voice_id,
_profile.status.value,
)
else:
logger.warning("[爆款视频] 克隆音色 profile_id=%s 就绪但 voice_id 为空,回退默认音色", raw_voice_id)
return default_voice
except Exception as _e:
logger.warning("[爆款视频] 克隆音色解析异常 profile_id=%s err=%s,回退默认音色", raw_voice_id, _e)
return default_voice
finally:
if session is not None:
try:
session.close()
except Exception:
pass
# preset/my-voice 直传/空 source:voice_id 就是 CosyVoice 音色名
return raw_voice_id or default_voice
def _step_tts(job: ViralVideoJob, voiceover_script: str):
"""步骤 5: CosyVoice 整段配音 → 返回本地 MP3 Path;失败返回 None。"""
"""步骤 5: CosyVoice 整段配音 → 返回本地 MP3 Path;失败返回 None。
#2188: 支持 voice_source='my_voice'/'clone',先把前端传的 profile.id
解析成 CosyVoice 真正的克隆音色 voice_id,再走同一条 synthesize 链路。
"""
try:
from pathlib import Path as _Path
from apps.worker.services.tts_service_factory import get_tts_service
tts_service = get_tts_service()
voice_id = (getattr(job, "voice_id", "") or "").strip()
# #2188: 根据 voice_source 解析实际传给 CosyVoice 的 voice_id
voice_id = _resolve_tts_voice_id(job)
text = (voiceover_script or "").strip()
if not text:
logger.warning("[爆款视频] voiceover_script 为空,跳过 TTS")
@@ -1414,21 +1478,19 @@ def _step_tts(job: ViralVideoJob, voiceover_script: str):
try:
result = tts_service.synthesize(
text=text,
voice_id=voice_id or "longxiaochun_v3",
voice_id=voice_id,
format="mp3",
)
except TypeError:
try:
result = tts_service.synthesize(text=text, voice_id=voice_id or "longxiaochun_v3")
result = tts_service.synthesize(text=text, voice_id=voice_id)
except TypeError:
result = tts_service.synthesize(text=text)
if result is None:
return None
p = _Path(result) if not isinstance(result, _Path) else result
if p.exists() and p.stat().st_size > 0:
logger.info(
"[爆款视频] TTS 合成完成: voice=%s path=%s size=%d", voice_id or "longxiaochun_v3", p, p.stat().st_size
)
logger.info("[爆款视频] TTS 合成完成: voice=%s path=%s size=%d", voice_id, p, p.stat().st_size)
return p
logger.warning("[爆款视频] TTS 返回路径不存在或空文件: %s", p)
return None