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
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
215 lines
6.3 KiB
Python
215 lines
6.3 KiB
Python
"""TTS Job workflow orchestration — Phase 3.
|
||
|
||
编排 TTS 合成的完整流程:
|
||
1. 创建 TTSJob(pending)
|
||
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. 返回 job(Celery 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
|