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 { 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) { 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 | 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 | 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(); }