227 lines
6.8 KiB
TypeScript
227 lines
6.8 KiB
TypeScript
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<string, unknown> {
|
|
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<string, unknown> {
|
|
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<string, unknown> {
|
|
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,
|
|
},
|
|
};
|
|
}
|