ai-chatbot/lib/video-canvas/video-generation-parameters.ts

348 lines
12 KiB
TypeScript

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<string, number>;
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<string, unknown> {
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<T extends string>(
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<VideoCanvasGenerationParameterSettings>,
configValue: VideoCanvasGenerationParametersConfig,
): Record<string, unknown> {
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));
}