210 lines
5.8 KiB
TypeScript
210 lines
5.8 KiB
TypeScript
import type { VideoCanvasSmartStoryboardConfig } from "@/lib/video-canvas/config";
|
|
|
|
export type VideoCanvasSmartStoryboardShot = {
|
|
description: string;
|
|
durationSeconds: number;
|
|
id: string;
|
|
};
|
|
|
|
export type VideoCanvasSmartStoryboard = {
|
|
enabled: boolean;
|
|
shots: VideoCanvasSmartStoryboardShot[];
|
|
totalDurationSeconds: number;
|
|
};
|
|
|
|
export type VideoCanvasSmartStoryboardValidation = {
|
|
emptyShotIndexes: number[];
|
|
status: "exact" | "over" | "under";
|
|
totalShotDurationSeconds: number;
|
|
valid: boolean;
|
|
};
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function clampInteger(value: unknown, fallback: number, min: number, max: number) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed)
|
|
? Math.max(min, Math.min(max, Math.round(parsed)))
|
|
: fallback;
|
|
}
|
|
|
|
function normalizeDescription(value: unknown, maximum: number) {
|
|
return String(value || "").replace(/\u00a0/g, " ").trim().slice(0, maximum);
|
|
}
|
|
|
|
function interpolate(template: string, variables: Record<string, string | number>) {
|
|
return Object.entries(variables).reduce(
|
|
(result, [key, value]) =>
|
|
result.replaceAll(`{{${key}}}`, String(value)),
|
|
template,
|
|
);
|
|
}
|
|
|
|
export function supportsVideoCanvasSmartStoryboard(
|
|
config: VideoCanvasSmartStoryboardConfig,
|
|
modelDescriptor: string,
|
|
) {
|
|
if (!config.enabled) return false;
|
|
const normalizedModel = modelDescriptor.trim().toLowerCase();
|
|
return (
|
|
normalizedModel.length > 0 &&
|
|
config.modelPatterns.some((pattern) =>
|
|
normalizedModel.includes(pattern.trim().toLowerCase()),
|
|
)
|
|
);
|
|
}
|
|
|
|
export function normalizeVideoCanvasSmartStoryboard(
|
|
value: unknown,
|
|
options: {
|
|
config: VideoCanvasSmartStoryboardConfig;
|
|
fallbackPrompt?: string;
|
|
fallbackTotalDurationSeconds?: number;
|
|
},
|
|
): VideoCanvasSmartStoryboard {
|
|
const { config } = options;
|
|
const incoming = isRecord(value) ? value : {};
|
|
const fallbackTotalDurationSeconds = clampInteger(
|
|
options.fallbackTotalDurationSeconds,
|
|
config.minTotalDurationSeconds,
|
|
config.minTotalDurationSeconds,
|
|
config.maxTotalDurationSeconds,
|
|
);
|
|
const totalDurationSeconds = clampInteger(
|
|
incoming.totalDurationSeconds,
|
|
fallbackTotalDurationSeconds,
|
|
config.minTotalDurationSeconds,
|
|
config.maxTotalDurationSeconds,
|
|
);
|
|
const incomingShots = Array.isArray(incoming.shots)
|
|
? incoming.shots.slice(0, config.maxShots)
|
|
: [];
|
|
const shots = incomingShots.flatMap((item, index) => {
|
|
const shot = isRecord(item) ? item : {};
|
|
const description = normalizeDescription(
|
|
shot.description,
|
|
config.maxDescriptionLength,
|
|
);
|
|
const durationSeconds = clampInteger(
|
|
shot.durationSeconds,
|
|
index === 0 ? totalDurationSeconds : config.defaultShotDurationSeconds,
|
|
config.minShotDurationSeconds,
|
|
config.maxTotalDurationSeconds,
|
|
);
|
|
return [
|
|
{
|
|
description,
|
|
durationSeconds,
|
|
id:
|
|
String(shot.id || "").trim().slice(0, 100) ||
|
|
`shot-${index + 1}`,
|
|
},
|
|
];
|
|
});
|
|
|
|
return {
|
|
enabled: incoming.enabled === true,
|
|
shots:
|
|
shots.length > 0
|
|
? shots
|
|
: [
|
|
{
|
|
description: normalizeDescription(
|
|
options.fallbackPrompt,
|
|
config.maxDescriptionLength,
|
|
),
|
|
durationSeconds: totalDurationSeconds,
|
|
id: "shot-1",
|
|
},
|
|
],
|
|
totalDurationSeconds,
|
|
};
|
|
}
|
|
|
|
export function getVideoCanvasSmartStoryboard(
|
|
draftValues: Record<string, unknown> | undefined,
|
|
options: {
|
|
config: VideoCanvasSmartStoryboardConfig;
|
|
fallbackPrompt?: string;
|
|
fallbackTotalDurationSeconds?: number;
|
|
},
|
|
) {
|
|
const params = isRecord(draftValues?.params) ? draftValues.params : {};
|
|
return normalizeVideoCanvasSmartStoryboard(
|
|
draftValues?.smartStoryboard ?? params.smartStoryboard,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export function setVideoCanvasSmartStoryboard(
|
|
draftValues: Record<string, unknown> | undefined,
|
|
storyboard: VideoCanvasSmartStoryboard,
|
|
) {
|
|
const params = isRecord(draftValues?.params) ? draftValues.params : {};
|
|
return {
|
|
...(draftValues || {}),
|
|
params: {
|
|
...params,
|
|
smartStoryboard: storyboard,
|
|
},
|
|
smartStoryboard: storyboard,
|
|
};
|
|
}
|
|
|
|
export function validateVideoCanvasSmartStoryboard(
|
|
storyboard: VideoCanvasSmartStoryboard,
|
|
): VideoCanvasSmartStoryboardValidation {
|
|
const emptyShotIndexes = storyboard.shots.flatMap((shot, index) =>
|
|
shot.description.trim() ? [] : [index],
|
|
);
|
|
const totalShotDurationSeconds = storyboard.shots.reduce(
|
|
(total, shot) => total + shot.durationSeconds,
|
|
0,
|
|
);
|
|
const status =
|
|
totalShotDurationSeconds === storyboard.totalDurationSeconds
|
|
? "exact"
|
|
: totalShotDurationSeconds > storyboard.totalDurationSeconds
|
|
? "over"
|
|
: "under";
|
|
return {
|
|
emptyShotIndexes,
|
|
status,
|
|
totalShotDurationSeconds,
|
|
valid:
|
|
!storyboard.enabled ||
|
|
(emptyShotIndexes.length === 0 && status === "exact"),
|
|
};
|
|
}
|
|
|
|
export function composeVideoCanvasSmartStoryboardPrompt(
|
|
prompt: string,
|
|
storyboard: VideoCanvasSmartStoryboard,
|
|
config: VideoCanvasSmartStoryboardConfig,
|
|
) {
|
|
const normalizedPrompt = prompt.trim();
|
|
if (!storyboard.enabled) return normalizedPrompt;
|
|
const validation = validateVideoCanvasSmartStoryboard(storyboard);
|
|
if (!validation.valid) return normalizedPrompt;
|
|
|
|
const shots = storyboard.shots
|
|
.map((shot, index) =>
|
|
interpolate(config.shotPromptTemplate, {
|
|
description: shot.description,
|
|
duration: shot.durationSeconds,
|
|
index: index + 1,
|
|
}).trim(),
|
|
)
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
|
|
return interpolate(config.promptTemplate, {
|
|
prompt: normalizedPrompt,
|
|
shotCount: storyboard.shots.length,
|
|
shots,
|
|
totalDuration: storyboard.totalDurationSeconds,
|
|
}).trim();
|
|
}
|