106 lines
4.0 KiB
TypeScript

export type VideoHdResolution = "1080P" | "2K" | "4K";
export type VideoHdInterpolationMode = "none" | "high-quality";
export type VideoHdResolutionConfig = {
width: number;
height: number;
baseCost: number;
interpolationExtraCost: number;
};
export type VideoCanvasVideoHdConfig = {
enabled: boolean;
model: string;
setupCost: number;
frameRates: number[];
slowMultipliers: number[];
resolutions: Record<VideoHdResolution, VideoHdResolutionConfig>;
};
export const defaultVideoCanvasVideoHdConfig: VideoCanvasVideoHdConfig = {
enabled: true,
model: "Topazlabs",
setupCost: 6,
frameRates: [24, 30, 60, 90, 120],
slowMultipliers: [1, 2, 3, 5],
resolutions: {
"1080P": { width: 1920, height: 1080, baseCost: 24, interpolationExtraCost: 12 },
"2K": { width: 2560, height: 1440, baseCost: 42, interpolationExtraCost: 24 },
"4K": { width: 3840, height: 2160, baseCost: 90, interpolationExtraCost: 48 },
},
};
function positiveInteger(value: unknown, fallback: number) {
const parsed = Math.round(Number(value));
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function nonnegativeNumber(value: unknown, fallback: number) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}
function numberList(value: unknown, fallback: number[]) {
if (!Array.isArray(value)) return fallback;
const normalized = Array.from(
new Set(value.map((item) => positiveInteger(item, 0)).filter((item) => item > 0)),
).sort((a, b) => a - b);
return normalized.length > 0 ? normalized : fallback;
}
export function normalizeVideoCanvasVideoHdConfig(value: unknown): VideoCanvasVideoHdConfig {
const candidate = value && typeof value === "object" ? value as Partial<VideoCanvasVideoHdConfig> : {};
const candidateResolutions =
candidate.resolutions && typeof candidate.resolutions === "object"
? candidate.resolutions as Partial<Record<VideoHdResolution, Partial<VideoHdResolutionConfig>>>
: {};
const resolutions = Object.fromEntries(
(Object.keys(defaultVideoCanvasVideoHdConfig.resolutions) as VideoHdResolution[]).map((key) => {
const fallback = defaultVideoCanvasVideoHdConfig.resolutions[key];
const item = candidateResolutions[key] || {};
return [key, {
width: positiveInteger(item.width, fallback.width),
height: positiveInteger(item.height, fallback.height),
baseCost: nonnegativeNumber(item.baseCost, fallback.baseCost),
interpolationExtraCost: nonnegativeNumber(
item.interpolationExtraCost,
fallback.interpolationExtraCost,
),
}];
}),
) as Record<VideoHdResolution, VideoHdResolutionConfig>;
return {
enabled: typeof candidate.enabled === "boolean" ? candidate.enabled : true,
model: String(candidate.model || defaultVideoCanvasVideoHdConfig.model).trim() || "Topazlabs",
setupCost: nonnegativeNumber(candidate.setupCost, defaultVideoCanvasVideoHdConfig.setupCost),
frameRates: numberList(candidate.frameRates, defaultVideoCanvasVideoHdConfig.frameRates),
slowMultipliers: numberList(
candidate.slowMultipliers,
defaultVideoCanvasVideoHdConfig.slowMultipliers,
),
resolutions,
};
}
export function getVideoHdCost(
config: VideoCanvasVideoHdConfig,
resolution: VideoHdResolution,
interpolationMode: VideoHdInterpolationMode,
slowMultiplier: number,
) {
const definition = config.resolutions[resolution];
const singleDurationCost = definition.baseCost +
(interpolationMode === "high-quality" ? definition.interpolationExtraCost : 0);
const multiplier = Math.max(1, positiveInteger(slowMultiplier, 1));
return Math.round(config.setupCost + (singleDurationCost - config.setupCost) * multiplier);
}
export function formatVideoHdFrameRate(frameRate: number) {
if (frameRate === 24 || frameRate === 30) return `标准(${frameRate}fps)`;
if (frameRate === 60) return "流畅(60fps)";
if (frameRate === 90) return "超流畅(90fps)";
if (frameRate === 120) return "极致流畅(120fps)";
return `${frameRate}fps`;
}