import "server-only"; import type { PreparedStoryboardShot, StoryboardModelCandidate, StoryboardProgressEvent, } from "@/lib/video-agent/types"; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); function normalizeErrorMessage(error: unknown) { if (!error) return "unknown_error"; if (typeof error === "string") return error; if (error instanceof Error) return error.message || String(error); try { return JSON.stringify(error); } catch { return String(error); } } export type SequencerShotResult = { shotIndex: number; success: boolean; videoUrl?: string; modelId?: string; text?: string; error?: string; }; export async function runVideoSequencerSkill(params: { provider: string; taskId: string; shots: PreparedStoryboardShot[]; modelCandidates: StoryboardModelCandidate[]; maxRetry: number; dispatchIntervalSec: number; enableFallback: boolean; onProgress?: (event: StoryboardProgressEvent) => void | Promise; generateOneShot: (input: { shot: PreparedStoryboardShot; model: StoryboardModelCandidate; }) => Promise<{ videoUrl: string; text: string }>; }): Promise<{ results: SequencerShotResult[]; successCount: number; failedCount: number; }> { const { provider, taskId, shots, modelCandidates, maxRetry, dispatchIntervalSec, enableFallback, onProgress, generateOneShot, } = params; const candidateList = enableFallback && modelCandidates.length > 0 ? modelCandidates : modelCandidates.slice(0, 1); const results: SequencerShotResult[] = []; for (let index = 0; index < shots.length; index += 1) { const shot = shots[index]!; let shotSuccess = false; let lastError = ""; let resultText = ""; let resultUrl = ""; let usedModelId = ""; for (const model of candidateList) { const retries = Math.max(0, Math.round(maxRetry)); for (let attempt = 1; attempt <= retries + 1; attempt += 1) { try { const output = await generateOneShot({ shot, model }); resultText = output.text; resultUrl = output.videoUrl; usedModelId = model.modelId; shotSuccess = true; break; } catch (error) { lastError = normalizeErrorMessage(error); if (attempt <= retries) { await onProgress?.({ stage: "shot_retry", provider, taskId, shotIndex: shot.shotIndex, attempt, modelId: model.modelId, error: lastError, }); const backoffSec = Math.min(40, Math.pow(2, attempt - 1) * 10); await sleep(backoffSec * 1000); } } } if (shotSuccess) { break; } } if (shotSuccess) { results.push({ shotIndex: shot.shotIndex, success: true, modelId: usedModelId, videoUrl: resultUrl, text: resultText, }); await onProgress?.({ stage: "shot_completed", provider, taskId, shotIndex: shot.shotIndex, modelId: usedModelId, videoUrl: resultUrl, text: resultText, }); } else { results.push({ shotIndex: shot.shotIndex, success: false, error: lastError || "video_generation_failed", }); await onProgress?.({ stage: "shot_failed", provider, taskId, shotIndex: shot.shotIndex, error: lastError || "video_generation_failed", }); } if (index < shots.length - 1 && dispatchIntervalSec > 0) { await sleep(Math.round(dispatchIntervalSec * 1000)); } } const successCount = results.filter((item) => item.success).length; const failedCount = results.length - successCount; return { results, successCount, failedCount }; }