ai-chatbot/lib/ai/video-model-config.ts
Your Name fd95b9547e
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: show generation point estimates
2026-04-18 11:58:37 +08:00

198 lines
6.6 KiB
TypeScript

export type VideoDurationCostMap = Record<string, number>;
export type VideoResolutionCostMap = Record<string, number>;
export const recommendedVideoDurationOptions = [5, 10, 15] as const;
export const recommendedVideoDurationReferenceCosts: VideoDurationCostMap = {
"5": 165,
"10": 350,
"15": 600,
};
export const recommendedVideoResolutionBaseCosts: VideoResolutionCostMap = {
"480p": 600,
"720p": 900,
"1080p": 1200,
};
export type VideoImageGenConfigLike = {
creditCostPerVideo?: number | null;
videoDurationOptions?: number[] | null;
creditCostPerVideoByDuration?: Record<string, number> | null;
creditCostPerVideoByResolution?: Record<string, number> | null;
};
function normalizePositiveInteger(value: unknown) {
const normalized = Number(value);
if (!Number.isFinite(normalized)) return null;
const rounded = Math.round(normalized);
if (rounded < 1) return null;
return rounded;
}
export function normalizeVideoDurationOptions(value: unknown): number[] {
if (!Array.isArray(value)) return [];
return Array.from(
new Set(
value
.map((item) => normalizePositiveInteger(item))
.filter((item): item is number => item !== null)
)
).sort((left, right) => left - right);
}
export function normalizeVideoDurationCostMap(
value: unknown,
durationOptions?: readonly number[] | null
): VideoDurationCostMap | undefined {
if (!value || typeof value !== "object") return undefined;
const allowedDurations = Array.isArray(durationOptions)
? new Set(durationOptions.map((item) => String(item)))
: null;
const entries = Object.entries(value as Record<string, unknown>)
.map(([durationKey, costValue]) => {
const normalizedDuration = normalizePositiveInteger(durationKey);
const normalizedCost = Number(costValue);
if (normalizedDuration === null || !Number.isFinite(normalizedCost)) {
return null;
}
const durationText = String(normalizedDuration);
if (allowedDurations && !allowedDurations.has(durationText)) {
return null;
}
return [durationText, Math.max(0, Math.round(normalizedCost))] as const;
})
.filter((entry): entry is readonly [string, number] => Boolean(entry));
if (entries.length === 0) return undefined;
return Object.fromEntries(entries);
}
export function normalizeVideoResolutionKey(value: unknown) {
const normalized = String(value || "").trim().toLowerCase();
if (!normalized) return null;
if (normalized === "480p" || normalized === "480") return "480p";
if (normalized === "720p" || normalized === "720") return "720p";
if (normalized === "1080p" || normalized === "1080") return "1080p";
if (normalized === "1k") return "1K";
if (normalized === "2k") return "2K";
if (normalized === "4k") return "4K";
return null;
}
export function normalizeVideoResolutionCostMap(
value: unknown
): VideoResolutionCostMap | undefined {
if (!value || typeof value !== "object") return undefined;
const entries: Array<readonly [string, number]> = [];
for (const [resolutionKey, costValue] of Object.entries(
value as Record<string, unknown>
)) {
const normalizedResolution = normalizeVideoResolutionKey(resolutionKey);
const normalizedCost = Number(costValue);
if (!normalizedResolution || !Number.isFinite(normalizedCost)) {
continue;
}
entries.push([
normalizedResolution,
Math.max(0, Math.round(normalizedCost)),
] as const);
}
if (entries.length === 0) return undefined;
return Object.fromEntries(entries);
}
export function getConfiguredVideoDurationOptions(
config: VideoImageGenConfigLike | null | undefined
): number[] {
return normalizeVideoDurationOptions(config?.videoDurationOptions);
}
export function buildVideoDurationCostMap(params: {
durationOptions: readonly number[];
costMap?: unknown;
fallbackCost?: number | null | undefined;
}): VideoDurationCostMap | undefined {
const fallbackCost = Number.isFinite(Number(params.fallbackCost))
? Math.max(0, Math.round(Number(params.fallbackCost)))
: 0;
const normalizedCostMap =
normalizeVideoDurationCostMap(params.costMap, params.durationOptions) || {};
if (params.durationOptions.length === 0 && Object.keys(normalizedCostMap).length === 0) {
return undefined;
}
return Object.fromEntries(
params.durationOptions.map((duration) => {
const key = String(duration);
return [key, normalizedCostMap[key] ?? fallbackCost];
})
);
}
export function getVideoCreditCostFromConfig(
config: VideoImageGenConfigLike | null | undefined,
duration?: number | null,
resolution?: string | null
): number {
const normalizedDuration = normalizePositiveInteger(duration);
const normalizedResolution = normalizeVideoResolutionKey(resolution);
const resolutionCostMap = normalizeVideoResolutionCostMap(
config?.creditCostPerVideoByResolution
);
const durationCostMap = normalizeVideoDurationCostMap(
config?.creditCostPerVideoByDuration
);
const durationCostEntries = durationCostMap
? Object.entries(durationCostMap)
.map(([durationKey, cost]) => ({
duration: normalizePositiveInteger(durationKey),
cost: Math.max(0, Math.round(Number(cost) || 0)),
}))
.filter((item): item is { duration: number; cost: number } =>
item.duration !== null
)
.sort((left, right) => left.duration - right.duration)
: [];
const getDurationCost = () => {
if (normalizedDuration === null || durationCostEntries.length === 0) return null;
const matched =
durationCostEntries.find((item) => item.duration === normalizedDuration) ||
durationCostEntries.find((item) => normalizedDuration <= item.duration) ||
durationCostEntries[durationCostEntries.length - 1] ||
null;
return matched?.cost ?? null;
};
if (normalizedResolution && resolutionCostMap) {
if (Object.prototype.hasOwnProperty.call(resolutionCostMap, normalizedResolution)) {
const resolutionBaseCost = resolutionCostMap[normalizedResolution] || 0;
const durationCost = getDurationCost();
const baseDurationCost =
[...durationCostEntries]
.reverse()
.find((item) => item.cost > 0)?.cost ?? null;
if (durationCost !== null && baseDurationCost && baseDurationCost > 0) {
return Math.max(
0,
Math.round((resolutionBaseCost * durationCost) / baseDurationCost)
);
}
return resolutionBaseCost;
}
}
const durationCost = getDurationCost();
if (durationCost !== null) {
return durationCost;
}
const fallbackCost = Number(config?.creditCostPerVideo ?? 0);
return Number.isFinite(fallbackCost) ? Math.max(0, Math.round(fallbackCost)) : 0;
}