506 lines
14 KiB
TypeScript
506 lines
14 KiB
TypeScript
export const VIDEO_CANVAS_MODEL_ICON_KEYS = [
|
||
"seedance",
|
||
"happy-horse",
|
||
"kling",
|
||
"wan",
|
||
"hailuo",
|
||
"vidu",
|
||
"pixverse",
|
||
"omnihuman",
|
||
"generic",
|
||
] as const;
|
||
|
||
export type VideoCanvasModelIconKey =
|
||
(typeof VIDEO_CANVAS_MODEL_ICON_KEYS)[number];
|
||
|
||
export type VideoCanvasModelCatalogEntry = {
|
||
badge: string;
|
||
description: string;
|
||
discountRate: number;
|
||
durationLabel: string;
|
||
enabled: boolean;
|
||
icon: VideoCanvasModelIconKey;
|
||
id: string;
|
||
label: string;
|
||
membershipOnly: boolean;
|
||
modelRef: string;
|
||
pointsPerSecondByResolution: Record<string, number>;
|
||
shortLabel: string;
|
||
};
|
||
|
||
type CatalogSeed = Omit<
|
||
VideoCanvasModelCatalogEntry,
|
||
| "badge"
|
||
| "discountRate"
|
||
| "enabled"
|
||
| "membershipOnly"
|
||
| "modelRef"
|
||
| "pointsPerSecondByResolution"
|
||
> &
|
||
Partial<
|
||
Pick<
|
||
VideoCanvasModelCatalogEntry,
|
||
| "badge"
|
||
| "discountRate"
|
||
| "enabled"
|
||
| "membershipOnly"
|
||
| "modelRef"
|
||
| "pointsPerSecondByResolution"
|
||
>
|
||
>;
|
||
|
||
function createCatalogEntry(seed: CatalogSeed): VideoCanvasModelCatalogEntry {
|
||
return {
|
||
badge: seed.badge || "",
|
||
description: seed.description,
|
||
discountRate: seed.discountRate ?? 1,
|
||
durationLabel: seed.durationLabel,
|
||
enabled: seed.enabled !== false,
|
||
icon: seed.icon,
|
||
id: seed.id,
|
||
label: seed.label,
|
||
membershipOnly: seed.membershipOnly === true,
|
||
modelRef: seed.modelRef || "auto",
|
||
pointsPerSecondByResolution: seed.pointsPerSecondByResolution || {},
|
||
shortLabel: seed.shortLabel,
|
||
};
|
||
}
|
||
|
||
export const defaultVideoCanvasModelCatalog: VideoCanvasModelCatalogEntry[] = [
|
||
createCatalogEntry({
|
||
id: "seedance-2-vip",
|
||
label: "Seedance 2.0 VIP",
|
||
shortLabel: "2.0",
|
||
durationLabel: "2min",
|
||
description: "最强视频模型,会员专属通道,15s音画同步",
|
||
badge: "720P限时9折",
|
||
membershipOnly: true,
|
||
icon: "seedance",
|
||
discountRate: 0.9,
|
||
pointsPerSecondByResolution: { "720P": 27 },
|
||
}),
|
||
createCatalogEntry({
|
||
id: "seedance-2-fast-vip",
|
||
label: "Seedance 2.0 Fast VIP",
|
||
shortLabel: "2.0 Fast",
|
||
durationLabel: "2min",
|
||
description: "最强视频模型快速版,会员专属通道,15s音画同步",
|
||
badge: "720P限时9折",
|
||
membershipOnly: true,
|
||
icon: "seedance",
|
||
discountRate: 0.9,
|
||
pointsPerSecondByResolution: { "720P": 22 },
|
||
}),
|
||
createCatalogEntry({
|
||
id: "seedance-2-mini",
|
||
label: "Seedance 2.0 Mini",
|
||
shortLabel: "2.0 Mini",
|
||
durationLabel: "2min",
|
||
description: "最强视频模型mini版,高性价比生成,15s音画同步",
|
||
membershipOnly: true,
|
||
icon: "seedance",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "happy-horse-1-1",
|
||
label: "Happy Horse 1.1",
|
||
shortLabel: "Happy Horse 1.1",
|
||
durationLabel: "3min",
|
||
description: "阿里最新视频模型,一致性与视听质量更可控",
|
||
badge: "限时4折",
|
||
membershipOnly: true,
|
||
icon: "happy-horse",
|
||
discountRate: 0.4,
|
||
}),
|
||
createCatalogEntry({
|
||
id: "happy-horse-1-0",
|
||
label: "Happy Horse 1.0",
|
||
shortLabel: "Happy Horse 1.0",
|
||
durationLabel: "3min",
|
||
description: "阿里视频模型,支持多参生成",
|
||
membershipOnly: true,
|
||
icon: "happy-horse",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "kling-o3",
|
||
label: "Kling O3",
|
||
shortLabel: "Kling O3",
|
||
durationLabel: "3min",
|
||
description: "视频编辑模型、参考一致性、音画同出、多镜头",
|
||
membershipOnly: true,
|
||
icon: "kling",
|
||
pointsPerSecondByResolution: { "720P": 11 },
|
||
}),
|
||
createCatalogEntry({
|
||
id: "kling-3",
|
||
label: "Kling 3.0",
|
||
shortLabel: "Kling 3.0",
|
||
durationLabel: "3min",
|
||
description: "视频生成模型,高质感、支持多镜头",
|
||
membershipOnly: true,
|
||
icon: "kling",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "wan-2-7",
|
||
label: "Wan 2.7",
|
||
shortLabel: "Wan 2.7",
|
||
durationLabel: "3min",
|
||
description: "全能参考,支持修改视频画面、剧情、环境",
|
||
icon: "wan",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "kling-o1",
|
||
label: "Kling O1",
|
||
shortLabel: "Kling O1",
|
||
durationLabel: "3min",
|
||
description: "可灵一代编辑模型、支持多模态输入",
|
||
icon: "kling",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "kling-2-6",
|
||
label: "Kling 2.6",
|
||
shortLabel: "Kling 2.6",
|
||
durationLabel: "2min",
|
||
description: "视频生成模型、直出音画同步",
|
||
icon: "kling",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "kling-3-motion-transfer",
|
||
label: "Kling3.0 动作迁移",
|
||
shortLabel: "Kling3.0 动作迁移",
|
||
durationLabel: "8min",
|
||
description: "动作控制模型,需输入1张图片、1条视频",
|
||
icon: "kling",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "mj-video",
|
||
label: "MJ Video",
|
||
shortLabel: "MJ Video",
|
||
durationLabel: "2min",
|
||
description: "图生视频效果稳定,画面表现力强",
|
||
icon: "generic",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "hailuo-02",
|
||
label: "Hailuo 02",
|
||
shortLabel: "Hailuo 02",
|
||
durationLabel: "2min",
|
||
description: "画质稳定,适合打造运动特效场景",
|
||
icon: "hailuo",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "vidu-q2",
|
||
label: "Vidu Q2",
|
||
shortLabel: "Vidu Q2",
|
||
durationLabel: "3min",
|
||
description: "多图主体参考,精确控制效果佳",
|
||
icon: "vidu",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "vidu-q2-pro",
|
||
label: "Vidu Q2 Pro",
|
||
shortLabel: "Vidu Q2 Pro",
|
||
durationLabel: "",
|
||
description: "Vidu Q2 Pro",
|
||
icon: "vidu",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "vidu-q2-turbo",
|
||
label: "Vidu Q2 Turbo",
|
||
shortLabel: "Vidu Q2 Turbo",
|
||
durationLabel: "",
|
||
description: "Vidu Q2 Turbo",
|
||
icon: "vidu",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "kling-2-5",
|
||
label: "Kling 2.5",
|
||
shortLabel: "Kling 2.5",
|
||
durationLabel: "2min",
|
||
description: "速度快、效果稳定、性价比高",
|
||
icon: "kling",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "kling-2-1",
|
||
label: "Kling 2.1",
|
||
shortLabel: "Kling 2.1",
|
||
durationLabel: "3min",
|
||
description: "支持首尾帧,图生视频效果更出色",
|
||
icon: "kling",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "pixverse-v5-5",
|
||
label: "Pixverse V5.5",
|
||
shortLabel: "Pixverse V5.5",
|
||
durationLabel: "3min",
|
||
description: "支持特效、玩法丰富",
|
||
icon: "pixverse",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "pixverse-v5",
|
||
label: "Pixverse V5",
|
||
shortLabel: "Pixverse V5",
|
||
durationLabel: "3min",
|
||
description: "支持特效、玩法丰富",
|
||
icon: "pixverse",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "kling-3-turbo",
|
||
label: "Kling 3.0 Turbo",
|
||
shortLabel: "Kling 3.0 Turbo",
|
||
durationLabel: "3min",
|
||
description: "视频生成模型,高质感、支持多镜头",
|
||
icon: "kling",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "wan-2-6",
|
||
label: "Wan 2.6",
|
||
shortLabel: "Wan 2.6",
|
||
durationLabel: "3min",
|
||
description: "音画同步,支持多机位镜头,最长可生15秒视频",
|
||
icon: "wan",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "hailuo-2-3-fast",
|
||
label: "Hailuo 2.3 Fast",
|
||
shortLabel: "Hailuo 2.3 Fast",
|
||
durationLabel: "1min",
|
||
description: "善于表达动作、表情、镜头,更快速",
|
||
icon: "hailuo",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "hailuo-2-3",
|
||
label: "Hailuo 2.3",
|
||
shortLabel: "Hailuo 2.3",
|
||
durationLabel: "2min",
|
||
description: "善于表达动作、表情、镜头,更高质感",
|
||
icon: "hailuo",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "seedance-1-5-pro",
|
||
label: "Seedance1.5 Pro",
|
||
shortLabel: "Seedance1.5 Pro",
|
||
durationLabel: "2min",
|
||
description: "音画同步,支持多机位镜头,最长可生12秒视频",
|
||
icon: "seedance",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "seedance-1-pro",
|
||
label: "Seedance 1.0 Pro",
|
||
shortLabel: "1.0 Pro",
|
||
durationLabel: "2min",
|
||
description: "高精度提示词理解,40秒生成1080P视频",
|
||
membershipOnly: true,
|
||
icon: "seedance",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "seedance-1-lite",
|
||
label: "Seedance 1.0 Lite",
|
||
shortLabel: "1.0 Lite",
|
||
durationLabel: "1min",
|
||
description: "轻量快速,一键进行日常视频生成",
|
||
membershipOnly: true,
|
||
icon: "seedance",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "vidu-q3-pro",
|
||
label: "Vidu Q3 Pro",
|
||
shortLabel: "Vidu Q3 Pro",
|
||
durationLabel: "2min",
|
||
description: "支持主体参考,精确控制效果佳",
|
||
membershipOnly: true,
|
||
icon: "vidu",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "omnihuman-1-5",
|
||
label: "OmniHuman 1.5",
|
||
shortLabel: "OmniHuman 1.5",
|
||
durationLabel: "3min",
|
||
description: "多模态数字人视频生成",
|
||
membershipOnly: true,
|
||
icon: "omnihuman",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "wan-2-2",
|
||
label: "Wan 2.2",
|
||
shortLabel: "Wan 2.2",
|
||
durationLabel: "3min",
|
||
description: "支持特效、玩法千变万化",
|
||
icon: "wan",
|
||
}),
|
||
createCatalogEntry({
|
||
id: "wan-2-5",
|
||
label: "Wan 2.5",
|
||
shortLabel: "Wan 2.5",
|
||
durationLabel: "3min",
|
||
description: "支持特效、直出音画同步",
|
||
icon: "wan",
|
||
}),
|
||
];
|
||
|
||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||
}
|
||
|
||
function normalizeText(value: unknown, fallback: string, maxLength: number) {
|
||
return String(value ?? fallback).trim().slice(0, maxLength) || fallback;
|
||
}
|
||
|
||
function normalizeDiscountRate(value: unknown, fallback: number) {
|
||
const numeric = Number(value);
|
||
if (!Number.isFinite(numeric)) return fallback;
|
||
return Math.min(1, Math.max(0.01, Math.round(numeric * 100) / 100));
|
||
}
|
||
|
||
function normalizePricingMap(value: unknown) {
|
||
if (!isRecord(value)) return {};
|
||
return Object.fromEntries(
|
||
Object.entries(value)
|
||
.map(([resolution, points]) => [resolution.trim().slice(0, 24), Number(points)] as const)
|
||
.filter(
|
||
([resolution, points]) =>
|
||
Boolean(resolution) && Number.isFinite(points) && points >= 0,
|
||
)
|
||
.slice(0, 12)
|
||
.map(([resolution, points]) => [
|
||
resolution,
|
||
Math.min(100_000, Math.round(points * 100) / 100),
|
||
]),
|
||
);
|
||
}
|
||
|
||
export function normalizeVideoCanvasModelCatalog(
|
||
value: unknown,
|
||
): VideoCanvasModelCatalogEntry[] {
|
||
if (!Array.isArray(value) || value.length === 0) {
|
||
return defaultVideoCanvasModelCatalog.map((entry) => ({
|
||
...entry,
|
||
pointsPerSecondByResolution: { ...entry.pointsPerSecondByResolution },
|
||
}));
|
||
}
|
||
|
||
const usedIds = new Set<string>();
|
||
const normalized = value.flatMap((item, index) => {
|
||
if (!isRecord(item)) return [];
|
||
const fallback =
|
||
defaultVideoCanvasModelCatalog.find(
|
||
(entry) => entry.id === item.id || entry.label === item.label,
|
||
) || defaultVideoCanvasModelCatalog[index];
|
||
const label = normalizeText(item.label, fallback?.label || "", 120);
|
||
if (!label) return [];
|
||
const rawId = normalizeText(item.id, fallback?.id || `video-model-${index + 1}`, 120)
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9_-]+/g, "-")
|
||
.replace(/^-+|-+$/g, "") || `video-model-${index + 1}`;
|
||
let id = rawId;
|
||
let suffix = 2;
|
||
while (usedIds.has(id)) {
|
||
id = `${rawId}-${suffix}`;
|
||
suffix += 1;
|
||
}
|
||
usedIds.add(id);
|
||
const icon = VIDEO_CANVAS_MODEL_ICON_KEYS.includes(
|
||
item.icon as VideoCanvasModelIconKey,
|
||
)
|
||
? (item.icon as VideoCanvasModelIconKey)
|
||
: fallback?.icon || "generic";
|
||
return [
|
||
{
|
||
badge: normalizeText(item.badge, fallback?.badge || "", 60),
|
||
description: normalizeText(
|
||
item.description,
|
||
fallback?.description || label,
|
||
240,
|
||
),
|
||
discountRate: normalizeDiscountRate(
|
||
item.discountRate,
|
||
fallback?.discountRate ?? 1,
|
||
),
|
||
durationLabel: normalizeText(
|
||
item.durationLabel,
|
||
fallback?.durationLabel || "",
|
||
24,
|
||
),
|
||
enabled: item.enabled !== false,
|
||
icon,
|
||
id,
|
||
label,
|
||
membershipOnly:
|
||
typeof item.membershipOnly === "boolean"
|
||
? item.membershipOnly
|
||
: fallback?.membershipOnly === true,
|
||
modelRef: normalizeText(item.modelRef, fallback?.modelRef || "auto", 160),
|
||
pointsPerSecondByResolution: normalizePricingMap(
|
||
item.pointsPerSecondByResolution ?? fallback?.pointsPerSecondByResolution,
|
||
),
|
||
shortLabel: normalizeText(item.shortLabel, fallback?.shortLabel || label, 80),
|
||
},
|
||
];
|
||
});
|
||
|
||
return normalized.length > 0
|
||
? normalized.slice(0, 80)
|
||
: normalizeVideoCanvasModelCatalog(undefined);
|
||
}
|
||
|
||
export function getVideoCanvasModelCatalogEntry(
|
||
catalog: VideoCanvasModelCatalogEntry[],
|
||
selection: { label?: string; modelRef?: string },
|
||
) {
|
||
const enabledCatalog = catalog.filter((entry) => entry.enabled);
|
||
const label = String(selection.label || "").trim();
|
||
if (label) {
|
||
const labelMatch = enabledCatalog.find((entry) => entry.label === label);
|
||
if (labelMatch) return labelMatch;
|
||
}
|
||
const modelRef = String(selection.modelRef || "").trim();
|
||
if (modelRef && modelRef !== "auto") {
|
||
const modelMatch = enabledCatalog.find((entry) => entry.modelRef === modelRef);
|
||
if (modelMatch) return modelMatch;
|
||
}
|
||
return enabledCatalog[0] || catalog[0];
|
||
}
|
||
|
||
export function resolveVideoCanvasCatalogModelRef(
|
||
entry: VideoCanvasModelCatalogEntry,
|
||
fallbackModelRef: string,
|
||
) {
|
||
const configured = String(entry.modelRef || "").trim();
|
||
if (configured && configured !== "auto") return configured;
|
||
const fallback = String(fallbackModelRef || "").trim();
|
||
return fallback || "auto";
|
||
}
|
||
|
||
export function estimateVideoCanvasModelCatalogPoints(
|
||
entry: VideoCanvasModelCatalogEntry | undefined,
|
||
settings: {
|
||
count: number;
|
||
durationSeconds: number;
|
||
fallbackPointsPerSecond: number;
|
||
resolution: string;
|
||
},
|
||
) {
|
||
const configuredRate = Number(
|
||
entry?.pointsPerSecondByResolution[settings.resolution],
|
||
);
|
||
const fallbackRate = Number(settings.fallbackPointsPerSecond);
|
||
const pointsPerSecond = Number.isFinite(configuredRate)
|
||
? configuredRate
|
||
: Number.isFinite(fallbackRate)
|
||
? fallbackRate
|
||
: 0;
|
||
const originalCost = Math.max(
|
||
0,
|
||
Math.round(
|
||
Math.max(0, pointsPerSecond) *
|
||
Math.max(1, Math.round(settings.durationSeconds)) *
|
||
Math.max(1, Math.round(settings.count)),
|
||
),
|
||
);
|
||
const discountRate = entry?.discountRate ?? 1;
|
||
const cost = Math.max(0, Math.round(originalCost * discountRate));
|
||
return {
|
||
cost,
|
||
originalCost: cost === originalCost ? undefined : originalCost,
|
||
};
|
||
}
|