feat(tts): P0 音频转存 OSS — 合成完成后下载临时音频上传 OSS 存储永久 URL
CI/CD Pipeline / Deploy Staging (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Failing after 48h27m21s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 48h27m21s
CI/CD Pipeline / Deploy Staging (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Failing after 48h27m21s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 48h27m21s
- TTSWorkflowService 新增 _transfer_audio_to_oss 方法 - start_synthesis 同步路径和 process_synthesis_result 均集成 OSS 转存 - OSS 转存失败时优雅降级,回退到 CosyVoice 临时 URL - 支持 mp3/wav/pcm/opus 格式 content-type 映射 - 新增 9 个单元测试覆盖转存成功/失败/降级场景 - 向后兼容:storage_service 为可选参数,现有调用方无需修改 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from packages.application.cosyvoice_service import (
|
||||
CosyVoiceAuthError,
|
||||
CosyVoiceError,
|
||||
@@ -19,6 +22,7 @@ from packages.application.cosyvoice_service import (
|
||||
)
|
||||
from packages.domain.tts_job import TTSJob, TTSJobStatus
|
||||
from packages.ports.tts_job_repository import TTSJobRepository
|
||||
from packages.shared.storage import SharedStorageService, get_shared_storage_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,9 +50,55 @@ class TTSWorkflowService:
|
||||
self,
|
||||
repository: TTSJobRepository,
|
||||
cosyvoice_service: CosyVoiceService,
|
||||
storage_service: Optional[SharedStorageService] = None,
|
||||
) -> None:
|
||||
self.repository = repository
|
||||
self.cosyvoice_service = cosyvoice_service
|
||||
self._storage_service = storage_service
|
||||
|
||||
@property
|
||||
def _storage(self) -> SharedStorageService:
|
||||
if self._storage_service is None:
|
||||
self._storage_service = get_shared_storage_service()
|
||||
return self._storage_service
|
||||
|
||||
def _transfer_audio_to_oss(
|
||||
self,
|
||||
temp_url: str,
|
||||
user_id: str,
|
||||
job_id: str,
|
||||
audio_format: str = "mp3",
|
||||
) -> tuple[str, str]:
|
||||
"""下载 CosyVoice 临时音频并转存到 OSS。
|
||||
|
||||
Returns:
|
||||
(permanent_url, storage_key) 元组。
|
||||
转存失败时回退到原始临时 URL,storage_key 为空字符串。
|
||||
"""
|
||||
storage_key = f"tts-outputs/{user_id}/{job_id}.{audio_format}"
|
||||
content_type_map = {
|
||||
"mp3": "audio/mpeg",
|
||||
"wav": "audio/wav",
|
||||
"pcm": "audio/pcm",
|
||||
"opus": "audio/opus",
|
||||
}
|
||||
content_type = content_type_map.get(audio_format, "application/octet-stream")
|
||||
|
||||
try:
|
||||
# 下载临时音频
|
||||
resp = httpx.get(temp_url, timeout=60.0, follow_redirects=True)
|
||||
resp.raise_for_status()
|
||||
audio_data = resp.content
|
||||
|
||||
# 上传到 OSS
|
||||
file_obj = io.BytesIO(audio_data)
|
||||
permanent_url = self._storage.upload_file(file_obj, storage_key, content_type=content_type)
|
||||
logger.info(f"音频转存 OSS 成功: job_id={job_id}, " f"storage_key={storage_key}, size={len(audio_data)}")
|
||||
return permanent_url, storage_key
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"音频转存 OSS 失败,使用临时 URL: " f"job_id={job_id}, error={e}")
|
||||
return temp_url, ""
|
||||
|
||||
def start_synthesis(
|
||||
self,
|
||||
@@ -93,17 +143,19 @@ class TTSWorkflowService:
|
||||
job_metadata["cosyvoice_task_id"] = submit_result.get("task_id", "")
|
||||
job_metadata["cosyvoice_request_id"] = submit_result.get("request_id", "")
|
||||
|
||||
# 如果 CosyVoice 同步返回了 audio_url,直接标记完成
|
||||
# 如果 CosyVoice 同步返回了 audio_url,转存 OSS 后标记完成
|
||||
audio_url = submit_result.get("audio_url", "")
|
||||
if audio_url:
|
||||
permanent_url, storage_key = self._transfer_audio_to_oss(audio_url, job.user_id, job.id, job.format)
|
||||
job.mark_completed(
|
||||
output_audio_url=audio_url,
|
||||
output_audio_url=permanent_url,
|
||||
output_audio_key=storage_key,
|
||||
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}")
|
||||
logger.info(f"TTS 合成同步完成: job_id={job.id}, audio_url={permanent_url}")
|
||||
return job
|
||||
|
||||
job.metadata = job_metadata
|
||||
@@ -171,13 +223,17 @@ class TTSWorkflowService:
|
||||
if job is None:
|
||||
raise TTSJobNotFoundError(f"TTS job {job_id} not found")
|
||||
|
||||
# 转存音频到 OSS,获取永久 URL
|
||||
permanent_url, storage_key = self._transfer_audio_to_oss(audio_url, job.user_id, job.id, job.format)
|
||||
|
||||
job.mark_completed(
|
||||
output_audio_url=audio_url,
|
||||
output_audio_url=permanent_url,
|
||||
output_audio_key=storage_key,
|
||||
duration=duration,
|
||||
file_size=file_size,
|
||||
)
|
||||
job = self.repository.update(job)
|
||||
logger.info(f"TTS 合成成功: job_id={job_id}, audio_url={audio_url}")
|
||||
logger.info(f"TTS 合成成功: job_id={job_id}, audio_url={permanent_url}")
|
||||
return job
|
||||
|
||||
def process_synthesis_failure(self, job_id: str, error_message: str) -> TTSJob:
|
||||
|
||||
Reference in New Issue
Block a user