Files
meijiaka-zy/python-api/app/scheduler/handlers/subtitle_handler.py
T
小鱼开发 e58159fc42 refactor: 第三方平台架构改造(Adapter Protocol + Gateway)
Phase 1: 异常体系统一
- 新增 PlatformError / PlatformErrorType 标准定义
- 改造所有 Provider 异常抛出为 PlatformError
- 注册全局 PlatformError exception handler

Phase 2: Adapter Protocol
- 新增 app/ai/adapters/base.py(PlatformAdapter + SyncCapable + TaskCapable + CallbackCapable)
- 新增 app/ai/adapters/constants.py(Method 常量)
- 新增 PlatformConfigLoader(config/platform-config.yaml)

Phase 3: HTTP Client 统一
- ViduProvider 从 aiohttp 迁移到 httpx(注入方式)
- VolcengineCaptionService 改为注入 http_client
- lifespan 统一管理所有 Client 创建和关闭

Phase 4: Gateway 骨架 + Adapter 实现
- 新增 ViduAdapter / VolcengineArkAdapter / VolcengineCaptionAdapter
- 新增 PlatformGateway(call_sync / submit_task / query_task / handle_webhook)
- 新增 LLMGateway(带 Fallback 降级链)
- lifespan 注册所有 Adapter 和 Gateway

Phase 6: 清理与验证
- 从 Settings 移除 VIDU_BASE_URL / VOLCENGINE_BASE_URL
- Provider 改为从 PlatformConfigLoader 读取 base_url
- 清理 volcengine_caption_service 全局单例
- config_loader 默认路径改为 platform-config.yaml
- Scheduler 注入共享 HTTP client
- vidu.py 回调路由使用 Adapter 验签和解析
- ruff 全量通过,应用启动测试通过
2026-05-04 16:07:16 +08:00

150 lines
6.1 KiB
Python

"""
Subtitle 任务处理器
==================
管理火山引擎字幕生成与自动打轴的提交与轮询。
支持两种模式:
- caption: 字幕识别(从音频/视频提取带时间轴的字幕)
- auto_align: 自动打轴(为已有字幕文本配上时间轴)
"""
import logging
from typing import Any
from app.scheduler.handlers.base import AsyncHandler
from app.scheduler.models import StateChange
from app.scheduler.registry import JobRegistry
from app.scheduler.slot_manager import SlotManager
from app.services.volcengine_caption_service import VolcengineCaptionService
logger = logging.getLogger(__name__)
SLOT_KEY = "volc:subtitle_slots"
MAX_SLOTS = 5
class SubtitleHandler(AsyncHandler):
name = "subtitle"
slot_key = SLOT_KEY
max_slots = MAX_SLOTS
def __init__(self, service: VolcengineCaptionService | None = None):
self.service = service
def _get_service(self) -> VolcengineCaptionService:
if self.service is None:
self.service = VolcengineCaptionService()
return self.service
async def tick(
self, jobs: list[Any], registry: JobRegistry, slots: SlotManager
) -> list[StateChange]:
changes: list[StateChange] = []
for job in jobs:
params = job.params or {}
mode = params.get("mode", "caption")
volc_task_id = params.get("volc_task_id")
project_id = params.get("project_id", "")
video_path = params.get("video", params.get("video_path", ""))
language = params.get("language", "zh")
audio_text = params.get("audio_text", "")
if volc_task_id:
# 轮询
try:
service = self._get_service()
if mode == "auto_align":
result = await service.query_auto_align_task(volc_task_id, blocking=False)
else:
result = await service.query_caption_task(volc_task_id, blocking=False)
if result.code == 0:
utterances = result.utterances or []
result_data = {
"project_id": project_id,
"video_path": video_path,
"language": language,
"mode": mode,
"duration": result.duration,
"utterances": [
{
"text": u.text,
"start_time": u.start_time,
"end_time": u.end_time,
}
for u in utterances
],
}
await slots.release(SLOT_KEY, job.job_id)
changes.append(
StateChange(job_id=job.job_id, field_path="status", value="completed")
)
changes.append(
StateChange(
job_id=job.job_id, field_path="message", value="字幕生成完成"
)
)
changes.append(
StateChange(job_id=job.job_id, field_path="completed", value=1)
)
changes.append(StateChange(job_id=job.job_id, field_path="total", value=1))
changes.append(
StateChange(job_id=job.job_id, field_path="result", value=result_data)
)
elif result.code != 2000:
await slots.release(SLOT_KEY, job.job_id)
changes.append(
StateChange(job_id=job.job_id, field_path="status", value="failed")
)
changes.append(
StateChange(
job_id=job.job_id,
field_path="message",
value=f"字幕识别失败: {result.message}",
)
)
changes.append(
StateChange(job_id=job.job_id, field_path="error", value=result.message)
)
except Exception as e:
logger.error(f"[Subtitle {job.job_id}] poll error: {e}")
continue
# 提交
acquired = await slots.acquire(SLOT_KEY, job.job_id, MAX_SLOTS)
if not acquired:
continue
try:
service = VolcengineCaptionService()
if mode == "auto_align":
if not audio_text:
raise ValueError("auto_align 模式需要提供 audio_text")
volc_task_id = await service.submit_auto_align_task(
audio_url=video_path,
audio_text=audio_text,
)
else:
volc_task_id = await service.submit_caption_task(
audio_url=video_path, language=language
)
if not volc_task_id:
raise ValueError("未返回任务ID")
params["volc_task_id"] = volc_task_id
changes.append(StateChange(job_id=job.job_id, field_path="params", value=params))
changes.append(
StateChange(job_id=job.job_id, field_path="message", value="字幕任务已提交")
)
except Exception as e:
await slots.release(SLOT_KEY, job.job_id)
changes.append(StateChange(job_id=job.job_id, field_path="status", value="failed"))
changes.append(
StateChange(job_id=job.job_id, field_path="message", value=str(e)[:200])
)
changes.append(
StateChange(job_id=job.job_id, field_path="error", value=str(e)[:500])
)
return changes