Files
meijiaka-zy/python-api/app/ai/adapters/volcengine_caption_adapter.py
T
小鱼开发 30536276ba refactor(scheduler): 统一异步任务调度架构
核心变更:
- 统一第三方接口架构:所有服务走 PlatformGateway(call_sync/submit_task/query_task/handle_webhook)
- 视频生成(Vidu 对口型)纳入 Async Engine,与 script/subtitle/tts 统一为 POST /tasks/{task_type} 模式
- 新增 VideoHandler、TTSHandler,完善 ScriptHandler/SubtitleHandler
- PlatformGateway 生成 internal_task_id,建立 Redis 双向映射,callback 场景传入 Async Engine task_id 保证映射一致
- SlotManager 新增 acquire_ctx 上下文管理器,所有 Handler 统一使用
- ViduAdapter 状态映射归一化(normalize_state/denormalize_state)
- 移除 ViduService Semaphore 和 tenacity 重试,并发控制完全交予 SlotManager
- nonce 防重放下沉到 CallbackCapable 协议
- Service 层错误统一为 PlatformError,路由层错误信息脱敏
- 废弃 /voice/lip-sync,清理 vidu.py 遗留路由

Bug 修复:
- VideoHandler 轮询阶段后添加 continue,防止已提交任务重复创建
- voice.py synthesize_to_file 变量名冲突(request vs request_body)
- PlatformGateway.submit_task 空 data 防护
- ScriptHandler 动态导入 asyncio 改为模块级导入
- SubtitleHandler 完成时补充 progress=100

文档:
- 更新 AGENTS.md 核心功能、运行时架构、异步调度描述
2026-05-05 20:53:18 +08:00

145 lines
5.3 KiB
Python

"""
火山引擎字幕 Adapter
====================
实现 PlatformAdapter + TaskCapable。
直接接入 VolcengineCaptionProvider,提供标准 Protocol 接口。
"""
from __future__ import annotations
import logging
from typing import Any
from app.ai.adapters.base import AdapterResponse, PlatformAdapter, TaskCapable, TaskStatus
from app.ai.adapters.constants import Method
from app.ai.providers.volcengine_caption_provider import VolcengineCaptionProvider
from app.core.exceptions import PlatformError, PlatformErrorType
logger = logging.getLogger(__name__)
class VolcengineCaptionAdapter(PlatformAdapter, TaskCapable):
"""火山引擎字幕平台标准 Adapter"""
platform_id = "volcengine_caption"
def __init__(self, provider: VolcengineCaptionProvider):
self.provider = provider
# ── PlatformAdapter ──
async def health(self) -> AdapterResponse:
try:
# 火山字幕没有专门的健康检查,用提交一个无效任务测试连通性
# 401/403 说明网络通但认证问题,也算"可用"
await self.provider.submit_caption_task(audio_url="https://example.com/test.mp3")
return AdapterResponse(success=True)
except PlatformError as e:
if e.error_type in (PlatformErrorType.AUTH_FAILED, PlatformErrorType.BAD_REQUEST):
return AdapterResponse(success=True)
return AdapterResponse(
success=False,
error_message=str(e),
retryable=e.retryable,
)
except Exception as e:
return AdapterResponse(
success=False,
error_message=str(e),
retryable=False,
)
async def close(self) -> None:
await self.provider.close()
# ── TaskCapable ──
async def submit(
self,
task_type: str,
payload: dict[str, Any],
callback_url: str | None = None,
) -> AdapterResponse:
try:
if task_type == Method.CAPTION:
result = await self.provider.submit_caption_task(
audio_url=payload["audio_url"],
language=payload.get("language", "zh-CN"),
caption_type=payload.get("caption_type", "auto"),
use_punc=payload.get("use_punc", True),
use_itn=payload.get("use_itn", True),
words_per_line=payload.get("words_per_line", 46),
max_lines=payload.get("max_lines", 1),
)
return AdapterResponse(success=True, data={"task_id": result["id"]})
elif task_type == Method.AUTO_ALIGN:
result = await self.provider.submit_auto_align_task(
audio_url=payload["audio_url"],
audio_text=payload["audio_text"],
caption_type=payload.get("caption_type", "speech"),
sta_punc_mode=payload.get("sta_punc_mode", 3),
)
return AdapterResponse(success=True, data={"task_id": result["id"]})
else:
return AdapterResponse(
success=False,
error_message=f"不支持的任务类型: {task_type}",
retryable=False,
)
except PlatformError:
raise
except Exception as e:
raise PlatformError(
f"火山字幕 {task_type} 提交失败: {e}",
platform="volcengine_caption",
retryable=False,
error_type=PlatformErrorType.UNKNOWN,
) from e
async def query(self, platform_task_id: str) -> TaskStatus:
"""查询字幕任务状态(caption 类型)"""
try:
data = await self.provider.query_caption_task(platform_task_id, blocking=False)
return self._parse_status(data)
except Exception:
raise
async def query_auto_align(self, platform_task_id: str) -> TaskStatus:
"""查询打轴任务状态(auto_align 类型)"""
try:
data = await self.provider.query_auto_align_task(platform_task_id, blocking=False)
return self._parse_status(data)
except Exception:
raise
def _parse_status(self, data: dict) -> TaskStatus:
"""解析火山字幕原始响应为统一 TaskStatus"""
code = data.get("code", -1)
if code == 0:
utterances = data.get("utterances", [])
return TaskStatus(
state="completed",
result={
"duration": data.get("duration", 0.0),
"utterances": [
{
"text": u.get("text", ""),
"start_time": u.get("start_time", 0) or u.get("startTime", 0),
"end_time": u.get("end_time", 0) or u.get("endTime", 0),
}
for u in utterances
],
},
)
elif code == 2000:
return TaskStatus(state="processing")
else:
return TaskStatus(
state="failed",
error_message=data.get("message", f"未知错误: {code}"),
)