348 lines
12 KiB
TypeScript
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));
|
|
}
|