import type { VideoCanvasModelCatalogEntry } from "@/lib/video-canvas/video-model-catalog"; export type VideoCanvasNodeCreationConfig = { audio: { advancedSettingsOpen: boolean; }; image: { count: number; height: number; modelLabel: string; quality: string; ratio: string; size: string; width: number; }; upload: { accept: string; maxFileSizeMb: number; maxFiles: number; placementGap: number; }; video: { count: number; durationSeconds: number; generateAudio: boolean; height: number; modelCatalogId: string; ratio: string; resolution: string; width: number; }; }; export const defaultVideoCanvasNodeCreationConfig: VideoCanvasNodeCreationConfig = { audio: { advancedSettingsOpen: true, }, image: { count: 1, height: 350, modelLabel: "Lib Image", quality: "标准画质", ratio: "16:9", size: "2K", width: 622, }, upload: { accept: "image/*,video/*,audio/*", maxFileSizeMb: 50, maxFiles: 10, placementGap: 450, }, video: { count: 1, durationSeconds: 5, generateAudio: true, height: 350, modelCatalogId: "seedance-1-5-pro", ratio: "16:9", resolution: "1080P", width: 622, }, }; function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function normalizeInteger(value: unknown, fallback: number, minimum: number, maximum: number) { const parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; return Math.max(minimum, Math.min(maximum, Math.round(parsed))); } function normalizeLabel(value: unknown, fallback: string, maximumLength = 80) { return String(value || "").trim().slice(0, maximumLength) || fallback; } export function normalizeVideoCanvasNodeCreationConfig( value: unknown, ): VideoCanvasNodeCreationConfig { const fallback = defaultVideoCanvasNodeCreationConfig; const incoming = isRecord(value) ? value : {}; const incomingAudio = isRecord(incoming.audio) ? incoming.audio : {}; const incomingImage = isRecord(incoming.image) ? incoming.image : {}; const incomingUpload = isRecord(incoming.upload) ? incoming.upload : {}; const incomingVideo = isRecord(incoming.video) ? incoming.video : {}; return { audio: { advancedSettingsOpen: typeof incomingAudio.advancedSettingsOpen === "boolean" ? incomingAudio.advancedSettingsOpen : fallback.audio.advancedSettingsOpen, }, image: { count: normalizeInteger(incomingImage.count, fallback.image.count, 1, 4), height: normalizeInteger(incomingImage.height, fallback.image.height, 240, 1200), modelLabel: normalizeLabel(incomingImage.modelLabel, fallback.image.modelLabel), quality: normalizeLabel(incomingImage.quality, fallback.image.quality, 40), ratio: normalizeLabel(incomingImage.ratio, fallback.image.ratio, 30), size: normalizeLabel(incomingImage.size, fallback.image.size, 30), width: normalizeInteger(incomingImage.width, fallback.image.width, 240, 1600), }, upload: { accept: normalizeLabel(incomingUpload.accept, fallback.upload.accept, 240), maxFileSizeMb: normalizeInteger( incomingUpload.maxFileSizeMb, fallback.upload.maxFileSizeMb, 1, 500, ), maxFiles: normalizeInteger( incomingUpload.maxFiles, fallback.upload.maxFiles, 1, 50, ), placementGap: normalizeInteger( incomingUpload.placementGap, fallback.upload.placementGap, 40, 2000, ), }, video: { count: normalizeInteger(incomingVideo.count, fallback.video.count, 1, 4), durationSeconds: normalizeInteger( incomingVideo.durationSeconds, fallback.video.durationSeconds, 1, 60, ), generateAudio: typeof incomingVideo.generateAudio === "boolean" ? incomingVideo.generateAudio : fallback.video.generateAudio, height: normalizeInteger(incomingVideo.height, fallback.video.height, 240, 1200), modelCatalogId: normalizeLabel( incomingVideo.modelCatalogId, fallback.video.modelCatalogId, 120, ), ratio: normalizeLabel(incomingVideo.ratio, fallback.video.ratio, 30), resolution: normalizeLabel( incomingVideo.resolution, fallback.video.resolution, 30, ).toUpperCase(), width: normalizeInteger(incomingVideo.width, fallback.video.width, 240, 1600), }, }; } export function getInitialVideoCanvasNodeCreationOrdinal( nodes: Array<{ title?: string | null }>, ) { const highestSuffix = nodes.reduce((maximum, node) => { const match = String(node.title || "").trim().match(/(?:^|\s)(\d+)$/); return match ? Math.max(maximum, Number.parseInt(match[1], 10)) : maximum; }, 0); return Math.max(1, nodes.length, highestSuffix + 1); } export function resolveVideoCanvasCreationModel( config: VideoCanvasNodeCreationConfig, catalog: VideoCanvasModelCatalogEntry[], ) { const selected = catalog.find((entry) => entry.enabled && entry.id === config.video.modelCatalogId) || catalog.find((entry) => entry.enabled) || catalog[0]; return { catalogId: selected?.id || config.video.modelCatalogId, label: selected?.label || "自动模型", modelRef: selected?.modelRef || "auto", }; } export function buildVideoCanvasImageCreationParams( config: VideoCanvasNodeCreationConfig, ): Record { return { count: config.image.count, imageCount: config.image.count, imageQuality: config.image.quality, imageSize: config.image.size, modelLabel: config.image.modelLabel, ratio: config.image.ratio, settings: { count: config.image.count, imageQuality: config.image.quality, imageSize: config.image.size, modelLabel: config.image.modelLabel, ratio: config.image.ratio, }, }; } export function buildVideoCanvasVideoCreationParams( config: VideoCanvasNodeCreationConfig, catalog: VideoCanvasModelCatalogEntry[], ): Record { const model = resolveVideoCanvasCreationModel(config, catalog); const audio = config.video.generateAudio ? "开启" : "关闭"; return { audio: config.video.generateAudio, count: config.video.count, duration: config.video.durationSeconds, mode: "text-to-video", model: model.modelRef, ratio: config.video.ratio, resolution: config.video.resolution.toLowerCase(), settings: { audio, count: `${config.video.count}个`, duration: `${config.video.durationSeconds}s`, mode: "text-to-video", model: model.modelRef, modelCatalogId: model.catalogId, modelLabel: model.label, ratio: config.video.ratio, resolution: config.video.resolution, }, }; }