export type VideoCanvasGenerationParametersConfig = { countOptions: number[]; defaultCount: number; defaultDurationSeconds: number; defaultGenerateAudio: boolean; defaultRatio: string; defaultResolution: string; durationStepSeconds: number; maxDurationSeconds: number; minDurationSeconds: number; parameterPricingEnabled: boolean; pointsPerSecondByResolution: Record; ratioOptions: string[]; resolutionOptions: string[]; }; export type VideoCanvasGenerationParameterSettings = { audio: "开启" | "关闭"; count: string; duration: string; ratio: string; resolution: string; }; export type VideoCanvasGenerationCount = 1 | 2 | 4; const SUPPORTED_RATIO_OPTIONS = [ "Auto", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9", ] as const; const SUPPORTED_RESOLUTION_OPTIONS = ["480P", "720P", "1080P", "4K"] as const; const SUPPORTED_COUNT_OPTIONS: readonly VideoCanvasGenerationCount[] = [1, 2, 4]; export const defaultVideoCanvasGenerationParametersConfig: VideoCanvasGenerationParametersConfig = { countOptions: [1, 2, 4], defaultCount: 1, defaultDurationSeconds: 5, defaultGenerateAudio: true, defaultRatio: "16:9", defaultResolution: "720P", durationStepSeconds: 1, maxDurationSeconds: 15, minDurationSeconds: 4, parameterPricingEnabled: true, pointsPerSecondByResolution: { "480P": 13, "720P": 27, "1080P": 68, "4K": 140, }, ratioOptions: [...SUPPORTED_RATIO_OPTIONS], resolutionOptions: [...SUPPORTED_RESOLUTION_OPTIONS], }; function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function normalizeInteger(value: unknown, fallback: number, min: number, max: number) { const incoming = Number(value); if (!Number.isFinite(incoming)) return fallback; return Math.min(max, Math.max(min, Math.round(incoming))); } function normalizeRatioLabel(value: unknown) { const normalized = String(value || "").trim(); if (normalized.toLowerCase() === "adaptive") return "Auto"; return SUPPORTED_RATIO_OPTIONS.find( (option) => option.toLowerCase() === normalized.toLowerCase(), ); } function normalizeResolutionLabel(value: unknown) { const normalized = String(value || "") .trim() .replace(/\s+/g, "") .toUpperCase(); return SUPPORTED_RESOLUTION_OPTIONS.find((option) => option.toUpperCase() === normalized); } function normalizeOptionList( value: unknown, supported: readonly T[], fallback: readonly T[], ) { if (!Array.isArray(value)) return [...fallback]; const supportedByKey = new Map(supported.map((item) => [item.toLowerCase(), item])); const normalized = Array.from( new Set( value.flatMap((item) => { const match = supportedByKey.get(String(item || "").trim().toLowerCase()); return match ? [match] : []; }), ), ); return normalized.length > 0 ? normalized : [...fallback]; } function parseDuration(value: unknown, fallback: number) { const matched = String(value || "").match(/\d+/); const parsed = matched ? Number(matched[0]) : Number(value); return Number.isFinite(parsed) ? Math.round(parsed) : fallback; } export function normalizeVideoCanvasGenerationRequestCount( value: unknown, fallback: VideoCanvasGenerationCount = 1, ): VideoCanvasGenerationCount | null { const count = Number(value ?? fallback); return SUPPORTED_COUNT_OPTIONS.includes(count as VideoCanvasGenerationCount) ? (count as VideoCanvasGenerationCount) : null; } export function normalizeVideoCanvasGenerationParametersConfig( value: unknown, ): VideoCanvasGenerationParametersConfig { const fallback = defaultVideoCanvasGenerationParametersConfig; const incoming = isRecord(value) ? value : {}; const ratioOptions: string[] = normalizeOptionList( Array.isArray(incoming.ratioOptions) ? incoming.ratioOptions.map((item) => normalizeRatioLabel(item) || item) : incoming.ratioOptions, SUPPORTED_RATIO_OPTIONS, fallback.ratioOptions as (typeof SUPPORTED_RATIO_OPTIONS)[number][], ); const resolutionOptions: string[] = normalizeOptionList( incoming.resolutionOptions, SUPPORTED_RESOLUTION_OPTIONS, fallback.resolutionOptions as (typeof SUPPORTED_RESOLUTION_OPTIONS)[number][], ); const minDurationSeconds = normalizeInteger( incoming.minDurationSeconds, fallback.minDurationSeconds, 1, 60, ); const maxDurationSeconds = normalizeInteger( incoming.maxDurationSeconds, fallback.maxDurationSeconds, minDurationSeconds, 180, ); const durationStepSeconds = normalizeInteger( incoming.durationStepSeconds, fallback.durationStepSeconds, 1, Math.max(1, maxDurationSeconds - minDurationSeconds), ); const countOptions = Array.isArray(incoming.countOptions) ? Array.from( new Set( incoming.countOptions .map((item) => normalizeInteger(item, 0, 1, 4)) .filter((item): item is (typeof SUPPORTED_COUNT_OPTIONS)[number] => SUPPORTED_COUNT_OPTIONS.includes( item as (typeof SUPPORTED_COUNT_OPTIONS)[number], ), ), ), ).sort((left, right) => left - right) : fallback.countOptions; const safeCountOptions = countOptions.length > 0 ? countOptions : fallback.countOptions; const defaultRatioCandidate = normalizeRatioLabel(incoming.defaultRatio); const defaultResolutionCandidate = normalizeResolutionLabel(incoming.defaultResolution); const pointsInput = isRecord(incoming.pointsPerSecondByResolution) ? incoming.pointsPerSecondByResolution : {}; const pointsPerSecondByResolution = Object.fromEntries( resolutionOptions.map((resolution) => { const incomingEntry = Object.entries(pointsInput).find( ([key]) => normalizeResolutionLabel(key) === resolution, )?.[1]; const fallbackRate = fallback.pointsPerSecondByResolution[resolution] || 0; const rate = Number(incomingEntry); return [ resolution, Number.isFinite(rate) ? Math.min( 100_000, Math.max(0, Math.round((rate + Number.EPSILON) * 100) / 100), ) : fallbackRate, ]; }), ); return { countOptions: safeCountOptions, defaultCount: safeCountOptions.includes(Number(incoming.defaultCount)) ? Number(incoming.defaultCount) : safeCountOptions.includes(fallback.defaultCount) ? fallback.defaultCount : safeCountOptions[0], defaultDurationSeconds: normalizeInteger( incoming.defaultDurationSeconds, fallback.defaultDurationSeconds, minDurationSeconds, maxDurationSeconds, ), defaultGenerateAudio: typeof incoming.defaultGenerateAudio === "boolean" ? incoming.defaultGenerateAudio : fallback.defaultGenerateAudio, defaultRatio: defaultRatioCandidate && ratioOptions.includes(defaultRatioCandidate) ? defaultRatioCandidate : ratioOptions.includes(fallback.defaultRatio) ? fallback.defaultRatio : ratioOptions[0], defaultResolution: defaultResolutionCandidate && resolutionOptions.includes(defaultResolutionCandidate) ? defaultResolutionCandidate : resolutionOptions.includes(fallback.defaultResolution) ? fallback.defaultResolution : resolutionOptions[0], durationStepSeconds, maxDurationSeconds, minDurationSeconds, parameterPricingEnabled: typeof incoming.parameterPricingEnabled === "boolean" ? incoming.parameterPricingEnabled : fallback.parameterPricingEnabled, pointsPerSecondByResolution, ratioOptions, resolutionOptions, }; } function normalizeDurationForConfig( value: unknown, config: VideoCanvasGenerationParametersConfig, ) { const parsed = parseDuration(value, config.defaultDurationSeconds); const clamped = Math.min(config.maxDurationSeconds, Math.max(config.minDurationSeconds, parsed)); const stepped = config.minDurationSeconds + Math.round((clamped - config.minDurationSeconds) / config.durationStepSeconds) * config.durationStepSeconds; return Math.min(config.maxDurationSeconds, Math.max(config.minDurationSeconds, stepped)); } export function getVideoCanvasGenerationCount(value: unknown, fallback = 1) { const matched = String(value || "").match(/\d+/); const parsed = matched ? Number(matched[0]) : Number(value); return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : fallback; } export function resolveVideoCanvasGenerationParameterSettings( paramsValue: unknown, configValue: VideoCanvasGenerationParametersConfig, ): VideoCanvasGenerationParameterSettings { const config = normalizeVideoCanvasGenerationParametersConfig(configValue); const params = isRecord(paramsValue) ? paramsValue : {}; const settings = isRecord(params.settings) ? params.settings : {}; const ratioCandidate = normalizeRatioLabel(params.ratio ?? settings.ratio); const resolutionCandidate = normalizeResolutionLabel( params.resolution ?? settings.resolution, ); const audioValue = params.audio ?? settings.audio; const audio = typeof audioValue === "boolean" ? audioValue ? "开启" : "关闭" : String(audioValue || "").trim() === "关闭" ? "关闭" : String(audioValue || "").trim() === "开启" ? "开启" : config.defaultGenerateAudio ? "开启" : "关闭"; const requestedCount = getVideoCanvasGenerationCount( params.count ?? settings.count, config.defaultCount, ); const count = config.countOptions.includes(requestedCount) ? requestedCount : config.defaultCount; return { audio, count: `${count}个`, duration: `${normalizeDurationForConfig(params.duration ?? settings.duration, config)}s`, ratio: ratioCandidate && config.ratioOptions.includes(ratioCandidate) ? ratioCandidate : config.defaultRatio, resolution: resolutionCandidate && config.resolutionOptions.includes(resolutionCandidate) ? resolutionCandidate : config.defaultResolution, }; } export function mergeVideoCanvasGenerationParameterSettings( paramsValue: unknown, updates: Partial, configValue: VideoCanvasGenerationParametersConfig, ): Record { const config = normalizeVideoCanvasGenerationParametersConfig(configValue); const params = isRecord(paramsValue) ? paramsValue : {}; const settings = isRecord(params.settings) ? params.settings : {}; const current = resolveVideoCanvasGenerationParameterSettings(params, config); const next = resolveVideoCanvasGenerationParameterSettings( { settings: { ...current, ...updates } }, config, ); return { ...params, audio: next.audio === "开启", count: getVideoCanvasGenerationCount(next.count, config.defaultCount), duration: parseDuration(next.duration, config.defaultDurationSeconds), ratio: next.ratio === "Auto" ? "adaptive" : next.ratio, resolution: next.resolution.toLowerCase(), settings: { ...settings, ...next, }, }; } export function estimateVideoCanvasGenerationParameterPoints( settings: Pick< VideoCanvasGenerationParameterSettings, "count" | "duration" | "resolution" >, configValue: VideoCanvasGenerationParametersConfig, ) { const config = normalizeVideoCanvasGenerationParametersConfig(configValue); if (!config.parameterPricingEnabled) return 0; const resolution = normalizeResolutionLabel(settings.resolution) || config.defaultResolution; const pointsPerSecond = Number(config.pointsPerSecondByResolution[resolution] || 0); if (!Number.isFinite(pointsPerSecond) || pointsPerSecond <= 0) return 0; const duration = normalizeDurationForConfig(settings.duration, config); const requestedCount = getVideoCanvasGenerationCount(settings.count, config.defaultCount); const count = config.countOptions.includes(requestedCount) ? requestedCount : config.defaultCount; return Math.max(0, Math.round(pointsPerSecond * duration * count)); }