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