ai-chatbot/lib/video-agent/skills/video-sequencer.ts
Your Name 1967992de7
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat(video): storyboard task tables and delayed archive flow
2026-03-05 15:58:43 +08:00

150 lines
3.8 KiB
TypeScript

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<void>;
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 };
}