"""TTS synthesis tasks - process TTS synthesis 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.tts_job_repository import ( SQLAlchemyTTSJobRepository, ) from packages.application.cosyvoice_service import ( CosyVoiceError, CosyVoiceService, CosyVoiceTimeoutError, ) from packages.application.tts_job.workflow import TTSWorkflowService logger = logging.getLogger(__name__) @celery_app.task(bind=True, max_retries=3, name="worker.process_tts_synthesis") def process_tts_synthesis(self: Task, job_id: str) -> dict: """处理 TTS 合成任务。 通过 TTSWorkflowService.poll_and_process_synthesis() 轮询 CosyVoice 合成任务状态,更新 TTSJob。 超时自动重试(最多 3 次),其他错误标记 job 为 failed。 Args: job_id: TTSJob ID Returns: dict: {"ok": True, "job_id": str, "audio_url": str} 或 {"ok": False, "job_id": str, "error": str} """ session = None try: session = SessionLocal() repo = SQLAlchemyTTSJobRepository(session) workflow = TTSWorkflowService( repository=repo, cosyvoice_service=CosyVoiceService(), ) updated_job = workflow.poll_and_process_synthesis(job_id, timeout=120) session.commit() logger.info(f"TTS synthesis completed: job_id={job_id}, " f"audio_url={updated_job.output_audio_url}") return { "ok": True, "job_id": job_id, "audio_url": updated_job.output_audio_url, } except Retry: # Celery Retry 异常必须向上传播,不能被后续 except 捕获 raise except CosyVoiceTimeoutError as e: logger.warning(f"TTS synthesis timeout for {job_id}: {e}") if session is not None: session.rollback() # 超时重试,指数退避 raise self.retry(exc=e, countdown=30) except CosyVoiceError as e: logger.error(f"TTS synthesis failed for {job_id}: {e}") if session is not None: session.rollback() # 标记 job 为 failed try: if session is not None: job = repo.get(job_id) if job is not None: job.mark_failed(str(e)) repo.update(job) session.commit() except Exception as inner_e: logger.error(f"Failed to mark job as failed: {inner_e}") if session is not None: session.rollback() return {"ok": False, "job_id": job_id, "error": str(e)} except Exception as e: logger.error(f"TTS synthesis unexpected error for {job_id}: {e}") if session is not None: session.rollback() # 标记 job 为 failed try: if session is not None: job = repo.get(job_id) if job is not None: job.mark_failed(str(e)) repo.update(job) session.commit() except Exception as inner_e: logger.error(f"Failed to mark job as failed: {inner_e}") if session is not None: session.rollback() return {"ok": False, "job_id": job_id, "error": str(e)} finally: if session is not None: session.close() @celery_app.task(bind=True, max_retries=2, name="worker.process_tts_segment_synthesis") def process_tts_segment_synthesis(self: Task, job_id: str) -> dict: """分段合成轮询任务 — 轮询多个 CosyVoice 子任务并合并音频。 与 process_tts_synthesis 类似,但超时更长(300s), 因为分段任务需要等待所有子任务完成。 """ session = None try: session = SessionLocal() repo = SQLAlchemyTTSJobRepository(session) workflow = TTSWorkflowService( repository=repo, cosyvoice_service=CosyVoiceService(), ) updated_job = workflow.poll_and_process_synthesis(job_id, timeout=300) session.commit() logger.info(f"TTS segment synthesis completed: job_id={job_id}, " f"audio_url={updated_job.output_audio_url}") return { "ok": True, "job_id": job_id, "audio_url": updated_job.output_audio_url, } except Retry: raise except CosyVoiceTimeoutError as e: logger.warning(f"TTS segment synthesis timeout for {job_id}: {e}") if session is not None: session.rollback() raise self.retry(exc=e, countdown=60) except CosyVoiceError as e: logger.error(f"TTS segment synthesis failed for {job_id}: {e}") if session is not None: session.rollback() try: if session is not None: job = repo.get(job_id) if job is not None: job.mark_failed(str(e)) repo.update(job) session.commit() except Exception as inner_e: logger.error(f"Failed to mark job as failed: {inner_e}") if session is not None: session.rollback() return {"ok": False, "job_id": job_id, "error": str(e)} except Exception as e: logger.error(f"TTS segment synthesis unexpected error for {job_id}: {e}") if session is not None: session.rollback() try: if session is not None: job = repo.get(job_id) if job is not None: job.mark_failed(str(e)) repo.update(job) session.commit() except Exception as inner_e: logger.error(f"Failed to mark job as failed: {inner_e}") if session is not None: session.rollback() return {"ok": False, "job_id": job_id, "error": str(e)} finally: if session is not None: session.close()