diff --git a/apps/worker/worker_app/tasks/viral_video.py b/apps/worker/worker_app/tasks/viral_video.py index 156a0554e..5f02a5150 100644 --- a/apps/worker/worker_app/tasks/viral_video.py +++ b/apps/worker/worker_app/tasks/viral_video.py @@ -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