Files
xiaoxia-saas/packages/application/tts_job/workflow.py
T
灵应 e539105256
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 163h53m1s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 163h53m6s
Deploy / Deploy Staging (push) Failing after 164h23m43s
CI/CD Pipeline / Frontend Lint (push) Failing after 164h23m43s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 164h23m50s
fix: 3.09 审计问题修复 — P1-1 TTS Celery 执行器 + P2-1~P2-4
P1-1 (阻塞性): TTS 合成完整异步链路
- CosyVoiceService 新增 submit_synthesize_task() + poll_synthesize_task()
- 新建 TTSWorkflowService 编排层 (packages/application/tts_job/workflow.py)
- 新建 Celery 任务 process_tts_synthesis (apps/worker/worker_app/tasks/tts_synthesis.py)
- 注册到 celery_app.conf.imports + tasks/__init__.py 懒加载
- TTS 路由 synthesize() 增加 CosyVoice 提交 + Celery 调度

P2-1: voice_clone.py 添加详细 Celery 重试策略注释
P2-2: 修复 voice_clone.py Session 泄漏 (session=None 安全模式)
P2-3: ListVoiceLibraryUseCase 返回 (items, count) 元组,消除重复 count_by_user()
P2-4: 新增 find_profile_ids_by_voice_ids() 批量方法,填充 voice_clone_profile_id

测试: 749 passed, 0 failed
2026-07-02 16:30:37 +08:00

215 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""TTS Job workflow orchestration — Phase 3.
编排 TTS 合成的完整流程:
1. 创建 TTSJobpending
2. 提交 CosyVoice 合成任务
3. 轮询处理合成结果(成功/失败)
4. 重试失败的合成
"""
from __future__ import annotations
import logging
from typing import Optional
from packages.application.cosyvoice_service import (
CosyVoiceAuthError,
CosyVoiceError,
CosyVoiceService,
)
from packages.domain.tts_job import TTSJob, TTSJobStatus
from packages.ports.tts_job_repository import TTSJobRepository
logger = logging.getLogger(__name__)
class TTSWorkflowError(Exception):
"""TTS 合成工作流异常。"""
pass
class TTSJobNotFoundError(Exception):
"""TTS 任务未找到。"""
pass
class TTSWorkflowService:
"""TTS 合成工作流编排服务。
协调 TTSJobRepository + CosyVoiceService
实现完整的 TTS 合成生命周期管理。
"""
def __init__(
self,
repository: TTSJobRepository,
cosyvoice_service: CosyVoiceService,
) -> None:
self.repository = repository
self.cosyvoice_service = cosyvoice_service
def start_synthesis(
self,
job_id: str,
) -> TTSJob:
"""启动 TTS 合成流程。
1. 获取 pending 状态的 TTSJob
2. 标记为 processing
3. 提交 CosyVoice 合成任务
4. 保存 task_id 到 metadata
5. 返回 jobCelery task 由调用方触发)
Args:
job_id: TTSJob ID
Returns:
TTSJob: 更新后的 job
Raises:
TTSJobNotFoundError: job 不存在
TTSWorkflowError: CosyVoice 提交失败
"""
job = self.repository.get(job_id)
if job is None:
raise TTSJobNotFoundError(f"TTS job {job_id} not found")
# 标记为 processing
job.mark_processing()
job = self.repository.update(job)
try:
submit_result = self.cosyvoice_service.submit_synthesize_task(
text=job.input_text,
voice_id=job.voice_id,
sample_rate=job.sample_rate,
format=job.format,
)
# 保存 task_id / request_id 到 metadata
job_metadata = dict(job.metadata)
job_metadata["cosyvoice_task_id"] = submit_result.get("task_id", "")
job_metadata["cosyvoice_request_id"] = submit_result.get("request_id", "")
# 如果 CosyVoice 同步返回了 audio_url,直接标记完成
audio_url = submit_result.get("audio_url", "")
if audio_url:
job.mark_completed(
output_audio_url=audio_url,
duration=submit_result.get("duration", 0.0),
file_size=submit_result.get("file_size", 0),
)
job.metadata = job_metadata
job = self.repository.update(job)
logger.info(
f"TTS 合成同步完成: job_id={job.id}, audio_url={audio_url}"
)
return job
job.metadata = job_metadata
job = self.repository.update(job)
logger.info(
f"TTS 合成任务已提交: job_id={job.id}, "
f"task_id={submit_result.get('task_id')}"
)
except (CosyVoiceError, CosyVoiceAuthError) as e:
job.mark_failed(str(e))
job = self.repository.update(job)
logger.error(f"TTS 合成提交失败: job_id={job.id}, error={e}")
except ValueError as e:
job.mark_failed(str(e))
job = self.repository.update(job)
logger.error(f"TTS 合成参数错误: job_id={job.id}, error={e}")
return job
def poll_and_process_synthesis(
self, job_id: str, timeout: float = 120.0
) -> TTSJob:
"""轮询 CosyVoice 合成任务并处理结果。
从 job.metadata 获取 task_id,调用 CosyVoiceService.poll_synthesize_task()
轮询状态,然后通过 process_synthesis_result / process_synthesis_failure 更新 job。
供 Celery 后台任务调用。
"""
job = self.repository.get(job_id)
if job is None:
raise TTSJobNotFoundError(f"TTS job {job_id} not found")
task_id = (job.metadata or {}).get("cosyvoice_task_id", "")
if not task_id:
raise ValueError(
f"TTSJob {job_id} has no cosyvoice_task_id in metadata"
)
result = self.cosyvoice_service.poll_synthesize_task(task_id, timeout=timeout)
return self.process_synthesis_result(
job_id,
audio_url=result["audio_url"],
duration=result.get("duration", 0.0),
file_size=result.get("file_size", 0),
)
def process_synthesis_result(
self,
job_id: str,
audio_url: str,
*,
duration: float = 0.0,
file_size: int = 0,
) -> TTSJob:
"""处理合成成功结果。
Args:
job_id: TTSJob ID
audio_url: 输出音频 URL
duration: 音频时长
file_size: 文件大小
Returns:
TTSJob: 更新后的 job
Raises:
TTSJobNotFoundError: job 不存在
"""
job = self.repository.get(job_id)
if job is None:
raise TTSJobNotFoundError(f"TTS job {job_id} not found")
job.mark_completed(
output_audio_url=audio_url,
duration=duration,
file_size=file_size,
)
job = self.repository.update(job)
logger.info(f"TTS 合成成功: job_id={job_id}, audio_url={audio_url}")
return job
def process_synthesis_failure(
self, job_id: str, error_message: str
) -> TTSJob:
"""处理合成失败结果。
Args:
job_id: TTSJob ID
error_message: 错误信息
Returns:
TTSJob: 更新后的 job
Raises:
TTSJobNotFoundError: job 不存在
"""
job = self.repository.get(job_id)
if job is None:
raise TTSJobNotFoundError(f"TTS job {job_id} not found")
job.mark_failed(error_message)
job = self.repository.update(job)
logger.error(f"TTS 合成失败: job_id={job_id}, error={error_message}")
return job