150 lines
3.8 KiB
TypeScript
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 };
|
|
}
|