323 lines
9.5 KiB
TypeScript
323 lines
9.5 KiB
TypeScript
import type { VideoModelCapabilities } from "@/types/ai";
|
|
import type {
|
|
VideoToolCapabilityContract,
|
|
VideoToolMode,
|
|
VideoToolModeOption,
|
|
VideoToolResolution,
|
|
} from "@/types/video-tool-capability";
|
|
|
|
const MODE_LABELS: Record<VideoToolMode, string> = {
|
|
"text-to-video": "文生视频",
|
|
"first-frame": "主体参考",
|
|
"first-last-frame": "首尾帧",
|
|
"reference-images": "智能多帧",
|
|
};
|
|
|
|
const MODE_ORDER: VideoToolMode[] = [
|
|
"text-to-video",
|
|
"first-last-frame",
|
|
"reference-images",
|
|
"first-frame",
|
|
];
|
|
|
|
const STANDARD_RATIOS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"];
|
|
const PORTRAIT_RATIOS = ["16:9", "9:16", "1:1"];
|
|
const STANDARD_RESOLUTIONS: VideoToolResolution[] = ["480p", "720p", "1080p"];
|
|
const STANDARD_DURATIONS = [5, 10];
|
|
|
|
function asObject(value: unknown) {
|
|
return value && typeof value === "object" ? (value as Record<string, any>) : null;
|
|
}
|
|
|
|
function normalizeDurations(min?: number, max?: number) {
|
|
const safeMin = Number.isFinite(min) ? Math.max(1, Math.round(Number(min))) : undefined;
|
|
const safeMax = Number.isFinite(max) ? Math.max(1, Math.round(Number(max))) : undefined;
|
|
if (!safeMin || !safeMax || safeMax < safeMin) {
|
|
return [...STANDARD_DURATIONS];
|
|
}
|
|
|
|
if (safeMax - safeMin <= 12) {
|
|
return Array.from({ length: safeMax - safeMin + 1 }, (_, index) => safeMin + index);
|
|
}
|
|
return [safeMin, safeMax];
|
|
}
|
|
|
|
function normalizeModeOptions(
|
|
modes: VideoToolMode[],
|
|
maxReferenceImages = 4
|
|
): VideoToolModeOption[] {
|
|
return MODE_ORDER.filter((mode) => modes.includes(mode)).map((mode) => {
|
|
if (mode === "text-to-video") {
|
|
return { mode, label: MODE_LABELS[mode], imageMin: 0, imageMax: 0 };
|
|
}
|
|
if (mode === "first-frame") {
|
|
return { mode, label: MODE_LABELS[mode], imageMin: 1, imageMax: 1 };
|
|
}
|
|
if (mode === "first-last-frame") {
|
|
return { mode, label: MODE_LABELS[mode], imageMin: 2, imageMax: 2 };
|
|
}
|
|
return {
|
|
mode,
|
|
label: MODE_LABELS[mode],
|
|
imageMin: 1,
|
|
imageMax: Math.max(1, Math.min(8, maxReferenceImages)),
|
|
};
|
|
});
|
|
}
|
|
|
|
function normalizeVideoCapabilities(rawCapabilities: unknown): Partial<VideoModelCapabilities> {
|
|
const raw = asObject(rawCapabilities);
|
|
if (!raw) return {};
|
|
|
|
const supportedModes = Array.isArray(raw.supportedModes)
|
|
? raw.supportedModes.filter((item): item is VideoToolMode =>
|
|
item === "text-to-video" ||
|
|
item === "first-frame" ||
|
|
item === "first-last-frame" ||
|
|
item === "reference-images"
|
|
)
|
|
: undefined;
|
|
|
|
const supportedRatios = Array.isArray(raw.supportedRatios)
|
|
? raw.supportedRatios
|
|
.map((item) => String(item || "").trim())
|
|
.filter(Boolean)
|
|
: undefined;
|
|
|
|
const supportedResolutions = Array.isArray(raw.supportedResolutions)
|
|
? raw.supportedResolutions.filter(
|
|
(item): item is VideoToolResolution => item === "480p" || item === "720p" || item === "1080p"
|
|
)
|
|
: undefined;
|
|
|
|
const durationRange = asObject(raw.durationRange);
|
|
const min = Number(durationRange?.min);
|
|
const max = Number(durationRange?.max);
|
|
|
|
return {
|
|
supportedModes,
|
|
supportedRatios: supportedRatios as any,
|
|
supportedResolutions: supportedResolutions as any,
|
|
durationRange:
|
|
Number.isFinite(min) && Number.isFinite(max)
|
|
? { min: Math.round(min), max: Math.round(max) }
|
|
: undefined,
|
|
maxReferenceImages: Number.isFinite(Number(raw.maxReferenceImages))
|
|
? Math.round(Number(raw.maxReferenceImages))
|
|
: undefined,
|
|
supportReturnLastFrame:
|
|
typeof raw.supportReturnLastFrame === "boolean" ? raw.supportReturnLastFrame : undefined,
|
|
supportSeed: typeof raw.supportSeed === "boolean" ? raw.supportSeed : undefined,
|
|
};
|
|
}
|
|
|
|
function createProviderDefaultCapability(providerType: string): VideoToolCapabilityContract {
|
|
if (providerType === "fal-video") {
|
|
return {
|
|
schemaVersion: "v1",
|
|
source: "provider-default",
|
|
providerType,
|
|
modeOptions: normalizeModeOptions(["first-frame", "first-last-frame"]),
|
|
ratioOptions: [...PORTRAIT_RATIOS],
|
|
durationOptions: Array.from({ length: 13 }, (_, index) => index + 3),
|
|
resolutionOptions: [],
|
|
ui: {
|
|
showResolution: false,
|
|
showFps: false,
|
|
showCameraMove: false,
|
|
showGenerateAudio: true,
|
|
showReturnLastFrame: true,
|
|
showSeed: true,
|
|
},
|
|
defaults: {
|
|
mode: "first-frame",
|
|
ratio: "16:9",
|
|
duration: 5,
|
|
generateAudio: true,
|
|
fps: 24,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (providerType === "ruxa-video") {
|
|
return {
|
|
schemaVersion: "v1",
|
|
source: "provider-default",
|
|
providerType,
|
|
modeOptions: normalizeModeOptions(["first-frame"]),
|
|
ratioOptions: [...PORTRAIT_RATIOS],
|
|
durationOptions: [5, 10],
|
|
resolutionOptions: [],
|
|
ui: {
|
|
showResolution: false,
|
|
showFps: false,
|
|
showCameraMove: false,
|
|
showGenerateAudio: false,
|
|
showReturnLastFrame: false,
|
|
showSeed: false,
|
|
},
|
|
defaults: {
|
|
mode: "first-frame",
|
|
ratio: "16:9",
|
|
duration: 10,
|
|
generateAudio: false,
|
|
fps: 24,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (providerType === "qianwen-video") {
|
|
return {
|
|
schemaVersion: "v1",
|
|
source: "provider-default",
|
|
providerType,
|
|
modeOptions: normalizeModeOptions(["first-frame"]),
|
|
ratioOptions: ["16:9"],
|
|
durationOptions: [5],
|
|
resolutionOptions: [],
|
|
ui: {
|
|
showResolution: false,
|
|
showFps: false,
|
|
showCameraMove: false,
|
|
showGenerateAudio: false,
|
|
showReturnLastFrame: false,
|
|
showSeed: false,
|
|
},
|
|
defaults: {
|
|
mode: "first-frame",
|
|
ratio: "16:9",
|
|
duration: 5,
|
|
generateAudio: false,
|
|
fps: 24,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
schemaVersion: "v1",
|
|
source: "provider-default",
|
|
providerType,
|
|
modeOptions: normalizeModeOptions([
|
|
"text-to-video",
|
|
"first-last-frame",
|
|
"reference-images",
|
|
"first-frame",
|
|
]),
|
|
ratioOptions: [...STANDARD_RATIOS],
|
|
durationOptions: [...STANDARD_DURATIONS],
|
|
resolutionOptions: [...STANDARD_RESOLUTIONS],
|
|
ui: {
|
|
showResolution: true,
|
|
showFps: true,
|
|
showCameraMove: true,
|
|
showGenerateAudio: false,
|
|
showReturnLastFrame: true,
|
|
showSeed: true,
|
|
},
|
|
defaults: {
|
|
mode: "text-to-video",
|
|
ratio: "16:9",
|
|
duration: 5,
|
|
resolution: "720p",
|
|
generateAudio: false,
|
|
fps: 24,
|
|
},
|
|
};
|
|
}
|
|
|
|
function pickPreferredRatio(ratioOptions: string[]) {
|
|
if (ratioOptions.includes("16:9")) return "16:9";
|
|
return ratioOptions[0] || "16:9";
|
|
}
|
|
|
|
function pickPreferredDuration(durationOptions: number[]) {
|
|
if (durationOptions.includes(5)) return 5;
|
|
if (durationOptions.includes(10)) return 10;
|
|
return durationOptions[0] || 5;
|
|
}
|
|
|
|
function pickPreferredMode(modeOptions: VideoToolModeOption[], fallback: VideoToolMode) {
|
|
const modeSet = new Set(modeOptions.map((item) => item.mode));
|
|
if (modeSet.has(fallback)) return fallback;
|
|
if (modeSet.has("first-frame")) return "first-frame";
|
|
if (modeSet.has("text-to-video")) return "text-to-video";
|
|
return modeOptions[0]?.mode || fallback;
|
|
}
|
|
|
|
export function buildVideoToolCapability(params: {
|
|
providerType?: string | null;
|
|
modelId?: string | null;
|
|
rawCapabilities?: unknown;
|
|
}): VideoToolCapabilityContract | null {
|
|
const providerType = String(params.providerType || "").trim();
|
|
if (!providerType) return null;
|
|
const base = createProviderDefaultCapability(providerType);
|
|
|
|
if (providerType === "fal-video" || providerType === "qianwen-video" || providerType === "ruxa-video") {
|
|
return {
|
|
...base,
|
|
modelId: params.modelId || undefined,
|
|
};
|
|
}
|
|
|
|
const parsed = normalizeVideoCapabilities(params.rawCapabilities);
|
|
const hasModelCapability =
|
|
Boolean(parsed.supportedModes?.length) ||
|
|
Boolean(parsed.supportedRatios?.length) ||
|
|
Boolean(parsed.supportedResolutions?.length) ||
|
|
Boolean(parsed.durationRange);
|
|
|
|
if (!hasModelCapability) {
|
|
return {
|
|
...base,
|
|
modelId: params.modelId || undefined,
|
|
};
|
|
}
|
|
|
|
const supportedModes =
|
|
parsed.supportedModes && parsed.supportedModes.length > 0
|
|
? parsed.supportedModes
|
|
: base.modeOptions.map((item) => item.mode);
|
|
|
|
const modeOptions = normalizeModeOptions(supportedModes, parsed.maxReferenceImages || 4);
|
|
const ratioOptions =
|
|
parsed.supportedRatios && parsed.supportedRatios.length > 0
|
|
? Array.from(new Set(parsed.supportedRatios))
|
|
: base.ratioOptions;
|
|
const resolutionOptions =
|
|
parsed.supportedResolutions && parsed.supportedResolutions.length > 0
|
|
? parsed.supportedResolutions
|
|
: base.resolutionOptions;
|
|
const durationOptions = parsed.durationRange
|
|
? normalizeDurations(parsed.durationRange.min, parsed.durationRange.max)
|
|
: base.durationOptions;
|
|
|
|
return {
|
|
...base,
|
|
source: "model-capabilities",
|
|
modelId: params.modelId || undefined,
|
|
modeOptions,
|
|
ratioOptions,
|
|
resolutionOptions,
|
|
durationOptions,
|
|
ui: {
|
|
...base.ui,
|
|
showReturnLastFrame:
|
|
typeof parsed.supportReturnLastFrame === "boolean"
|
|
? parsed.supportReturnLastFrame
|
|
: base.ui.showReturnLastFrame,
|
|
showSeed:
|
|
typeof parsed.supportSeed === "boolean" ? parsed.supportSeed : base.ui.showSeed,
|
|
},
|
|
defaults: {
|
|
...base.defaults,
|
|
mode: pickPreferredMode(modeOptions, base.defaults.mode),
|
|
ratio: pickPreferredRatio(ratioOptions),
|
|
duration: pickPreferredDuration(durationOptions),
|
|
resolution: resolutionOptions.includes("720p")
|
|
? "720p"
|
|
: resolutionOptions[0] || undefined,
|
|
},
|
|
};
|
|
}
|