"""Voice clone tasks - process voice clone requests via CosyVoice API.""" import logging from celery import Task from celery.exceptions import Retry from worker_app.celery_app import celery_app from worker_app.db import SessionLocal from packages.adapters.sqlalchemy_impl.voice_clone_profile_repository import ( SQLAlchemyVoiceCloneProfileRepository, ) from packages.application.cosyvoice_service import ( CosyVoiceError, CosyVoiceService, CosyVoiceTimeoutError, ) from packages.application.voice_clone.workflow import VoiceCloneWorkflowService logger = logging.getLogger(__name__) @celery_app.task(bind=True, max_retries=2, name="worker.process_voice_clone") def process_voice_clone(self: Task, profile_id: str) -> dict: """处理音色克隆任务。 通过 VoiceCloneWorkflowService.poll_and_process_clone() 轮询 CosyVoice 克隆任务状态,更新 VoiceCloneProfile。 重试策略(Celery 5.x bind=True 模式): - max_retries=2:最多重试 2 次(共执行 3 次),超过后抛出 MaxRetriesExceededError - CosyVoiceTimeoutError:网络超时属于临时性故障,使用 countdown=30 延迟 30 秒后重试 - CosyVoiceError:API 业务错误(如任务失败),属于永久性故障,不重试直接标记 failed - Exception:未知错误,不重试直接标记 failed,避免无限重试掩盖 bug - Retry 异常:Celery 内部重试信号,必须向上传播不能被捕获 Args: profile_id: VoiceCloneProfile ID Returns: dict: {"ok": True, "profile_id": str, "voice_id": str} """ # P2-2 修复:session 初始化为 None,避免 SessionLocal() 抛异常时 # finally 块中 session.close() 触发 UnboundLocalError session = None try: session = SessionLocal() repo = SQLAlchemyVoiceCloneProfileRepository(session) workflow = VoiceCloneWorkflowService( repository=repo, cosyvoice_service=CosyVoiceService(), ) updated_profile = workflow.poll_and_process_clone(profile_id, timeout=300) session.commit() logger.info(f"Voice clone completed: profile_id={profile_id}, " f"voice_id={updated_profile.voice_id}") return { "ok": True, "profile_id": profile_id, "voice_id": updated_profile.voice_id, } except Retry: # Celery Retry 异常必须向上传播,不能被后续 except 捕获 raise except CosyVoiceTimeoutError as e: logger.error(f"Voice clone timeout for {profile_id}: {e}") if session is not None: session.rollback() # 超时属于临时性故障,延迟 30 秒后重试 raise self.retry(exc=e, countdown=30) except CosyVoiceError as e: logger.error(f"Voice clone failed for {profile_id}: {e}") if session is not None: session.rollback() # API 业务错误属于永久性故障,不重试,标记 profile 为 failed try: if session is not None: profile = repo.get(profile_id) if profile is not None: profile.mark_failed(str(e)) repo.update(profile) session.commit() except Exception as inner_e: logger.error(f"Failed to mark profile as failed: {inner_e}") if session is not None: session.rollback() return {"ok": False, "profile_id": profile_id, "error": str(e)} except Exception as e: logger.error(f"Voice clone unexpected error for {profile_id}: {e}") if session is not None: session.rollback() # 未知错误不重试,标记 profile 为 failed,避免无限重试掩盖 bug try: if session is not None: profile = repo.get(profile_id) if profile is not None: profile.mark_failed(str(e)) repo.update(profile) session.commit() except Exception as inner_e: logger.error(f"Failed to mark profile as failed: {inner_e}") if session is not None: session.rollback() return {"ok": False, "profile_id": profile_id, "error": str(e)} finally: if session is not None: session.close()