""" 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 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 = VolcengineCaptionService() 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