ai-chatbot/lib/ai/atlascloud-models.ts

905 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export const ATLASCLOUD_BASE_URL = "https://api.atlascloud.ai";
export const ATLASCLOUD_CATALOG_URL =
"https://console.atlascloud.ai/api/v1/models";
const COMMON_RATIO_ORDER = [
"auto",
"1:1",
"16:9",
"9:16",
"4:3",
"3:4",
"3:2",
"2:3",
"5:4",
"4:5",
"21:9",
];
const IMAGE_INPUT_KEYS = [
"images",
"image_urls",
"input_images",
"reference_images",
"reference_image_urls",
"image",
"Image",
"image_url",
"input_image",
"reference_image",
];
const RESOLUTION_KEYS = ["resolution", "image_size", "output_resolution"];
const VIDEO_RESOLUTION_KEYS = [
"resolution",
"output_resolution",
"target_resolution",
];
const ASPECT_RATIO_KEYS = ["aspect_ratio", "aspectRatio", "ratio"];
const OUTPUT_COUNT_KEYS = ["num_images", "number_of_images", "n", "output_count"];
const VIDEO_DURATION_KEYS = ["duration", "video_duration", "seconds"];
const VIDEO_REQUEST_KEYS = new Set([
"model",
"prompt",
...VIDEO_RESOLUTION_KEYS,
...ASPECT_RATIO_KEYS,
...VIDEO_DURATION_KEYS,
"seed",
"watermark",
"camera_fixed",
"return_last_frame",
]);
export interface AtlasCloudCatalogModel {
uuid?: string;
model: string;
type?: string;
displayName?: string;
profile?: string;
logo?: string;
avatar?: string;
schema?: string;
priority?: number;
categories?: string[];
tags?: string[];
display_console?: boolean;
}
export interface AtlasCloudRuntimeConfig {
submitEndpoint: string;
pollEndpoint: string;
propertyNames: string[];
requiredFields: string[];
imageInputs: Array<{
key: string;
mode: "array" | "single";
maxItems: number;
required: boolean;
}>;
imageInputKey?: string;
imageInputMode?: "array" | "single";
videoClipInput?: {
key: string;
maxItems: number;
required: boolean;
itemDefaults: Record<string, string | number | boolean>;
};
resolutionKey?: string;
durationKey?: string;
aspectRatioKey?: string;
outputCountKey?: string;
aspectRatioValueMap: Record<string, string>;
resolutionValueMap?: Record<string, string>;
maxReferenceImages: number;
maxVideoReferences: number;
maxImages: number;
defaults: Record<string, string | number | boolean>;
enums: Record<string, Array<string | number>>;
numericRanges?: Record<string, { min?: number; max?: number }>;
sizeValueMap: Record<string, Record<string, string>>;
}
export interface AtlasCloudImageModelPreset {
modelId: string;
displayName: string;
avatarUrl?: string;
description: string;
supportsVision: boolean;
supportsImageGen: boolean;
sortOrder: number;
capabilities: Record<string, unknown>;
defaultParams: Record<string, unknown>;
imageGenConfig: Record<string, unknown>;
}
type JsonRecord = Record<string, any>;
export type AtlasCloudMediaKind = "image" | "video" | "3d";
export function resolveAtlasCloudDisplayName(
existingDisplayName: unknown,
catalogDisplayName: unknown,
modelId: string
) {
const existing = String(existingDisplayName || "").trim();
if (existing) return existing;
return String(catalogDisplayName || "").trim() || modelId;
}
export function shouldInsertMissingAtlasCloudPreset(
existingMediaKinds: ReadonlySet<string>,
mediaKind: string
) {
return !existingMediaKinds.has(mediaKind);
}
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value)
? (value as JsonRecord)
: {};
}
function uniqueStrings(values: unknown[]) {
return Array.from(
new Set(values.map((value) => String(value || "").trim()).filter(Boolean))
);
}
function normalizeIntentToken(value: unknown) {
return String(value || "")
.trim()
.toLowerCase()
.replaceAll("_", "-");
}
export function inferAtlasCloudMediaKind(
catalogModel: AtlasCloudCatalogModel
): AtlasCloudMediaKind {
const catalogType = normalizeIntentToken(catalogModel.type);
const intentTokens = uniqueStrings([
catalogModel.model,
catalogModel.displayName,
...(catalogModel.categories || []),
...(catalogModel.tags || []),
]).map(normalizeIntentToken);
const intentText = intentTokens.join(" ");
if (
/(?:^|[\s/.-])(?:text|image)-to-3d(?:$|[\s/.-])/.test(intentText) ||
/(?:^|[\s/.-])(?:hunyuan3d|seed3d|tripo-h3(?:\.\d+)?)(?:$|[\s/.-])/.test(
intentText
)
) {
return "3d";
}
if (
catalogType === "video" ||
intentTokens.some((token) =>
[
"text-to-video",
"image-to-video",
"video-to-video",
"audio-to-video",
].includes(token)
)
) {
return "video";
}
return "image";
}
function derive3DSupportedModes(catalogModel: AtlasCloudCatalogModel) {
const intentText = uniqueStrings([
catalogModel.model,
catalogModel.displayName,
...(catalogModel.categories || []),
...(catalogModel.tags || []),
])
.map(normalizeIntentToken)
.join(" ");
const modes = [
...(intentText.includes("text-to-3d") ? ["text-to-3d"] : []),
...(intentText.includes("image-to-3d") ? ["image-to-3d"] : []),
];
return modes.length > 0 ? modes : ["3d"];
}
function findFirstKey(properties: JsonRecord, candidates: string[]) {
return candidates.find((key) => key in properties);
}
function getImageInputs(properties: JsonRecord, requiredFields: string[]) {
return Object.entries(properties)
.filter(([key, property]) => {
const normalizedKey = key.toLowerCase();
const record = asRecord(property);
const acceptsMediaValue = record.type === "string" || record.type === "array";
return (
IMAGE_INPUT_KEYS.includes(key) ||
(acceptsMediaValue &&
normalizedKey.includes("image") &&
!/(?:size|count|format|resolution|width|height)/.test(normalizedKey)) ||
(acceptsMediaValue &&
String(record["x-format"] || "").toLowerCase() === "image")
);
})
.map(([key, property]) => {
const record = asRecord(property);
const mode = record.type === "array" ? "array" : "single";
return {
key,
mode,
maxItems:
mode === "array"
? Math.max(1, Math.min(14, Number(record.maxItems || 14)))
: 1,
required: requiredFields.includes(key),
} as const;
})
.sort((a, b) => Number(b.required) - Number(a.required));
}
function getVideoClipInput(properties: JsonRecord, requiredFields: string[]) {
const entry = Object.entries(properties).find(([key, property]) => {
const normalizedKey = key.toLowerCase();
const record = asRecord(property);
const itemProperties = asRecord(asRecord(record.items).properties);
return (
record.type === "array" &&
normalizedKey.includes("video") &&
("url" in itemProperties || normalizedKey.includes("clip"))
);
});
if (!entry) return undefined;
const [key, property] = entry;
const record = asRecord(property);
const itemProperties = asRecord(asRecord(record.items).properties);
const itemDefaults: Record<string, string | number | boolean> = {};
for (const [itemKey, itemProperty] of Object.entries(itemProperties)) {
if (itemKey === "url") continue;
const value = asRecord(itemProperty).default;
if (["string", "number", "boolean"].includes(typeof value)) {
itemDefaults[itemKey] = value;
}
}
return {
key,
maxItems: Math.max(1, Math.min(4, Number(record.maxItems || 1))),
required: requiredFields.includes(key),
itemDefaults,
};
}
function getEnumValues(property: unknown) {
const record = asRecord(property);
const values = Array.isArray(record.enum) ? record.enum : [];
return values.filter(
(value) => typeof value === "string" || typeof value === "number"
) as Array<string | number>;
}
function getPostEndpoint(schema: JsonRecord) {
const paths = asRecord(schema.paths);
return (
Object.entries(paths).find(([, value]) => asRecord(value).post)?.[0] ||
"/api/v1/model/generateImage"
);
}
function getPollEndpoint(schema: JsonRecord) {
const paths = asRecord(schema.paths);
return (
Object.entries(paths).find(
([path, value]) =>
path.includes("{") && Boolean(asRecord(value).get)
)?.[0] || "/api/v1/model/prediction/{request_id}"
);
}
function getInputSchema(schema: JsonRecord) {
const components = asRecord(schema.components);
const schemas = asRecord(components.schemas);
return asRecord(schemas.Input || schemas.input);
}
function normalizeKSize(value: unknown) {
const match = String(value || "")
.trim()
.match(/^(1|2|4)\s*k$/i);
return match?.[1] ? `${match[1]}K` : "";
}
function gcd(a: number, b: number): number {
return b ? gcd(b, a % b) : a;
}
function parsePixelSize(value: unknown) {
const match = String(value || "")
.trim()
.match(/^(\d+)\s*[x*×]\s*(\d+)$/i);
if (!match) return null;
const width = Number(match[1]);
const height = Number(match[2]);
if (!width || !height) return null;
const divisor = gcd(width, height);
return {
value: String(value),
width,
height,
ratio: `${Math.round(width / divisor)}:${Math.round(height / divisor)}`,
};
}
function getPixelTier(width: number, height: number) {
const maxSide = Math.max(width, height);
if (maxSide <= 1400) return "1K";
if (maxSide <= 2800) return "2K";
return "4K";
}
function parseRatio(value: string) {
const match = value.match(/^(\d+):(\d+)$/);
if (!match) return null;
const width = Number(match[1]);
const height = Number(match[2]);
return width && height ? width / height : null;
}
function closestCommonRatio(ratio: string) {
const target = parseRatio(ratio);
if (!target) return ratio;
let winner = ratio;
let score = Number.POSITIVE_INFINITY;
for (const candidate of COMMON_RATIO_ORDER) {
const value = parseRatio(candidate);
if (!value) continue;
const nextScore = Math.abs(Math.log(target / value));
if (nextScore < score) {
winner = candidate;
score = nextScore;
}
}
return winner;
}
function normalizeAspectRatioEnum(value: unknown) {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "auto") return "auto";
if (normalized === "adaptive") return "adaptive";
if (/^\d+:\d+$/.test(normalized)) return normalized;
if (normalized.startsWith("square")) return "1:1";
const namedRatio = normalized.match(
/^(landscape|portrait)[_-](\d+)[_x:](\d+)$/
);
if (!namedRatio) return "";
const [, orientation, first, second] = namedRatio;
return orientation === "portrait" ? `${second}:${first}` : `${first}:${second}`;
}
function normalizeVideoResolution(value: unknown) {
const normalized = String(value || "").trim().toLowerCase();
const match = normalized.match(/(?:^|[^0-9])(480|720|1080)p(?:$|[^0-9])/);
return match?.[1] ? `${match[1]}p` : "";
}
function buildVideoResolutionValueMap(
properties: JsonRecord,
resolutionKey?: string
) {
if (!resolutionKey) return {};
const map: Record<string, string> = {};
for (const value of getEnumValues(properties[resolutionKey])) {
const normalized = normalizeVideoResolution(value);
if (!normalized) continue;
const raw = String(value);
const current = map[normalized];
if (!current || raw.toLowerCase() === normalized) {
map[normalized] = raw;
}
}
return map;
}
function buildNumericRanges(properties: JsonRecord) {
const ranges: Record<string, { min?: number; max?: number }> = {};
for (const [key, property] of Object.entries(properties)) {
const record = asRecord(property);
const min = Number(record.minimum ?? record.min);
const max = Number(record.maximum ?? record.max);
const range = {
...(Number.isFinite(min) ? { min } : {}),
...(Number.isFinite(max) ? { max } : {}),
};
if (Object.keys(range).length > 0) ranges[key] = range;
}
return ranges;
}
function deriveVideoDurationOptions(
properties: JsonRecord,
durationKey?: string
) {
if (!durationKey) return [];
return Array.from(
new Set(
getEnumValues(properties[durationKey])
.map(Number)
.filter((value) => Number.isFinite(value) && value > 0)
.map((value) => Math.round(value))
)
).sort((a, b) => a - b);
}
function deriveAtlasCloudVideoModes(params: {
catalogModel: AtlasCloudCatalogModel;
properties: JsonRecord;
requiredFields: string[];
imageInputs: AtlasCloudRuntimeConfig["imageInputs"];
}) {
const { catalogModel, properties, requiredFields, imageInputs } = params;
const intentText = uniqueStrings([
catalogModel.model,
catalogModel.displayName,
...(catalogModel.categories || []),
...(catalogModel.tags || []),
])
.map(normalizeIntentToken)
.join(" ");
const imageKeys = new Set(imageInputs.map((input) => input.key));
const hasUnsupportedRequiredInput = requiredFields.some((key) => {
if (imageKeys.has(key) || VIDEO_REQUEST_KEYS.has(key)) return false;
const defaultValue = asRecord(properties[key]).default;
return !["string", "number", "boolean"].includes(typeof defaultValue);
});
const requiredImages = imageInputs.filter((input) => input.required);
const singleImageInputs = imageInputs.filter((input) => input.mode === "single");
const arrayImageInput = imageInputs.find(
(input) => input.mode === "array" && input.maxItems > 1
);
const modes: string[] = [];
if (
!hasUnsupportedRequiredInput &&
requiredImages.length === 0 &&
("prompt" in properties || intentText.includes("text-to-video"))
) {
modes.push("text-to-video");
}
if (!hasUnsupportedRequiredInput && imageInputs.length > 0) {
if (requiredImages.length <= 1) modes.push("first-frame");
if (
singleImageInputs.length >= 2 ||
/(?:first|start).*(?:last|end)|(?:last|end).*(?:first|start)/.test(intentText)
) {
modes.push("first-last-frame");
}
if (
arrayImageInput ||
imageInputs.length > 2 ||
/reference-to-video|reference-images/.test(intentText)
) {
modes.push("reference-images");
}
}
return uniqueStrings(modes);
}
function buildAspectRatioValueMap(properties: JsonRecord) {
for (const key of [...ASPECT_RATIO_KEYS, "image_size"]) {
if (!(key in properties)) continue;
const map: Record<string, string> = {};
for (const value of getEnumValues(properties[key])) {
const ratio = normalizeAspectRatioEnum(value);
if (ratio && !map[ratio]) map[ratio] = String(value);
}
if (Object.keys(map).length > 0) return { key, map };
}
return { key: undefined, map: {} };
}
function buildSizeValueMap(sizeValues: Array<string | number>) {
const map: Record<string, Record<string, string>> = {};
for (const rawValue of sizeValues) {
const parsed = parsePixelSize(rawValue);
if (!parsed) continue;
const tier = getPixelTier(parsed.width, parsed.height);
const ratio = closestCommonRatio(parsed.ratio);
map[tier] ||= {};
map[tier][ratio] ||= parsed.value;
}
return map;
}
function deriveSupportedImageSizes(
properties: JsonRecord,
resolutionKey: string | undefined,
sizeValueMap: Record<string, Record<string, string>>
) {
const direct = resolutionKey
? getEnumValues(properties[resolutionKey]).map(normalizeKSize).filter(Boolean)
: [];
return uniqueStrings([...direct, ...Object.keys(sizeValueMap)]).filter((size) =>
["1K", "2K", "4K"].includes(size)
);
}
function deriveAspectRatios(
aspectRatioValueMap: Record<string, string>,
sizeValueMap: Record<string, Record<string, string>>
) {
const fromSizes = Object.values(sizeValueMap).flatMap((entry) =>
Object.keys(entry)
);
const ratios = uniqueStrings([...Object.keys(aspectRatioValueMap), ...fromSizes]);
return ratios.sort((a, b) => {
const ai = COMMON_RATIO_ORDER.indexOf(a);
const bi = COMMON_RATIO_ORDER.indexOf(b);
return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi);
});
}
function buildScalarDefaults(properties: JsonRecord) {
const defaults: Record<string, string | number | boolean> = {};
for (const [key, property] of Object.entries(properties)) {
if (["model", "prompt", ...IMAGE_INPUT_KEYS].includes(key)) continue;
const value = asRecord(property).default;
if (["string", "number", "boolean"].includes(typeof value)) {
defaults[key] = value;
}
}
return defaults;
}
function buildEnums(properties: JsonRecord) {
const enums: Record<string, Array<string | number>> = {};
for (const [key, property] of Object.entries(properties)) {
const values = getEnumValues(property);
if (values.length > 0) enums[key] = values;
}
return enums;
}
function deriveMaxImages(properties: JsonRecord, outputCountKey?: string) {
if (!outputCountKey) return 1;
const property = asRecord(properties[outputCountKey]);
const enumValues = getEnumValues(property)
.map(Number)
.filter((value) => Number.isFinite(value) && value > 0);
const configuredMax = Number(property.maximum || property.max || 0);
return Math.max(1, Math.min(15, configuredMax || Math.max(1, ...enumValues)));
}
export function createAtlasCloudImagePreset(
catalogModel: AtlasCloudCatalogModel,
schemaValue: unknown,
index = 0
): AtlasCloudImageModelPreset {
const schema = asRecord(schemaValue);
const input = getInputSchema(schema);
const properties = asRecord(input.properties);
const requiredFields = uniqueStrings(Array.isArray(input.required) ? input.required : []);
const imageInputs = getImageInputs(properties, requiredFields);
const videoClipInput = getVideoClipInput(properties, requiredFields);
const imageInputKey = imageInputs[0]?.key;
const mediaKind = inferAtlasCloudMediaKind(catalogModel);
const resolutionCandidates =
mediaKind === "video" ? VIDEO_RESOLUTION_KEYS : RESOLUTION_KEYS;
const resolutionKey = resolutionCandidates.find((key) => {
if (!(key in properties)) return false;
const values = getEnumValues(properties[key]);
return (
values.length === 0 ||
values.some((value) =>
mediaKind === "video"
? Boolean(normalizeVideoResolution(value))
: Boolean(normalizeKSize(value))
)
);
});
const durationKey = findFirstKey(properties, VIDEO_DURATION_KEYS);
const aspectRatioConfig = buildAspectRatioValueMap(properties);
const aspectRatioKey = aspectRatioConfig.key;
const outputCountKey = findFirstKey(properties, OUTPUT_COUNT_KEYS);
const sizeValueMap = buildSizeValueMap(getEnumValues(properties.size));
const supportedImageSizes =
mediaKind === "video"
? []
: deriveSupportedImageSizes(properties, resolutionKey, sizeValueMap);
const resolutionValueMap = buildVideoResolutionValueMap(
properties,
mediaKind === "video" ? resolutionKey : undefined
);
const supportedVideoResolutions = Object.keys(resolutionValueMap);
const durationOptions = deriveVideoDurationOptions(properties, durationKey);
const numericRanges = buildNumericRanges(properties);
const supportedAspectRatios = deriveAspectRatios(
aspectRatioConfig.map,
sizeValueMap
);
const imageProperty = imageInputKey ? asRecord(properties[imageInputKey]) : {};
const maxReferenceImages = Math.min(
14,
imageInputs.reduce((total, input) => total + input.maxItems, 0)
);
const maxVideoReferences = videoClipInput?.maxItems || 0;
const maxImages = deriveMaxImages(properties, outputCountKey);
const categories = uniqueStrings(catalogModel.categories || []).map((value) =>
value.toLowerCase()
);
const supportedModes =
mediaKind === "3d"
? derive3DSupportedModes(catalogModel)
: mediaKind === "video"
? deriveAtlasCloudVideoModes({
catalogModel,
properties,
requiredFields,
imageInputs,
})
: [
...(categories.some((value) => value.includes("text-to-image"))
? ["text-to-image"]
: []),
...(imageInputKey ||
categories.some((value) => value.includes("image-to-image"))
? ["image-edit", "reference-images"]
: []),
...(videoClipInput ? ["reference-video"] : []),
];
if (mediaKind === "image" && supportedModes.length === 0) {
supportedModes.push("text-to-image");
}
const runtimeConfig: AtlasCloudRuntimeConfig = {
submitEndpoint: getPostEndpoint(schema),
pollEndpoint: getPollEndpoint(schema),
propertyNames: Object.keys(properties),
requiredFields,
imageInputs,
imageInputKey,
imageInputMode: imageProperty.type === "array" ? "array" : "single",
videoClipInput,
resolutionKey,
durationKey,
aspectRatioKey,
outputCountKey,
aspectRatioValueMap: aspectRatioConfig.map,
resolutionValueMap,
maxReferenceImages,
maxVideoReferences,
maxImages,
defaults: buildScalarDefaults(properties),
enums: buildEnums(properties),
numericRanges,
sizeValueMap,
};
const supportsSize = Boolean(
resolutionKey || Object.keys(sizeValueMap).length > 0
);
const defaultImageSize =
supportedImageSizes.find((value) => value === "2K") ||
supportedImageSizes[0] ||
"1K";
const defaultAspectRatio =
supportedAspectRatios.find((value) =>
mediaKind === "video" ? value === "adaptive" : value === "auto"
) ||
supportedAspectRatios.find((value) => value === "1:1") ||
supportedAspectRatios[0] ||
(mediaKind === "video" ? "16:9" : "1:1");
const defaultVideoResolution =
supportedVideoResolutions.find((value) => value === "720p") ||
supportedVideoResolutions[0] ||
"720p";
const durationRange = numericRanges[durationKey || ""] || {};
const defaultDuration =
Number(runtimeConfig.defaults[durationKey || ""]) ||
durationOptions.find((value) => value === 5) ||
durationOptions[0] ||
durationRange.min ||
5;
return {
modelId: catalogModel.model,
displayName: catalogModel.displayName || catalogModel.model,
avatarUrl: catalogModel.avatar || catalogModel.logo || undefined,
description:
catalogModel.profile ||
`Atlas Cloud ${mediaKind === "3d" ? "3D" : mediaKind === "video" ? "视频" : "图片"}模型 ${catalogModel.displayName || catalogModel.model}`,
supportsVision: Boolean(imageInputKey),
supportsImageGen: mediaKind === "image",
sortOrder: Math.max(0, 20_000 - index),
capabilities: {
kind:
mediaKind === "3d"
? "3d"
: mediaKind === "video"
? "video"
: imageInputKey
? "image-edit"
: "text-to-image",
mediaType: mediaKind,
supportedModes,
...(mediaKind === "video"
? {
supportedResolutions: supportedVideoResolutions,
supportedRatios: supportedAspectRatios,
durationOptions,
durationRange: {
min:
durationOptions[0] ||
durationRange.min ||
defaultDuration,
max:
durationOptions[durationOptions.length - 1] ||
durationRange.max ||
defaultDuration,
},
supportWatermark: "watermark" in properties,
supportCameraFixed: "camera_fixed" in properties,
supportSeed: "seed" in properties,
supportReturnLastFrame: "return_last_frame" in properties,
}
: {}),
maxReferenceImages,
maxVideoReferences,
supportsMultiImageReference: maxReferenceImages > 1,
supportsVideoReference: maxVideoReferences > 0,
requiresVideoReference: videoClipInput?.required === true,
provider: "atlascloud",
schemaUrl: catalogModel.schema || null,
},
defaultParams: {
...(mediaKind === "video"
? {
resolution: defaultVideoResolution,
ratio: defaultAspectRatio,
duration: defaultDuration,
}
: {
imageSize: defaultImageSize,
aspectRatio: defaultAspectRatio,
imageCount: 1,
}),
runtimeModelId: catalogModel.model,
},
imageGenConfig: {
apiFormat:
mediaKind === "video"
? "atlascloud-unified-video"
: "atlascloud-unified-image",
maxImages,
maxReferenceImages,
maxVideoReferences,
supportsSize,
supports1K: supportedImageSizes.includes("1K"),
supports2K: supportedImageSizes.includes("2K"),
supports4K: supportedImageSizes.includes("4K"),
supportedImageSizes,
supportedAspectRatios,
videoDurationOptions: durationOptions,
supportsSeed: "seed" in properties,
supportsNegativePrompt: "negative_prompt" in properties,
supportsWatermark: "watermark" in properties,
enableLoadingPlaceholder: true,
atlasCloud: runtimeConfig,
},
};
}
async function mapWithConcurrency<T, R>(
values: T[],
limit: number,
mapper: (value: T, index: number) => Promise<R>
) {
const results = new Array<R>(values.length);
let cursor = 0;
const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
while (cursor < values.length) {
const index = cursor;
cursor += 1;
results[index] = await mapper(values[index], index);
}
});
await Promise.all(workers);
return results;
}
async function fetchAtlasCloudPresets(mediaTypes: Array<"image" | "video">) {
const catalogResponse = await fetch(ATLASCLOUD_CATALOG_URL, {
cache: "no-store",
headers: { Accept: "application/json", "X-Source-From": "website" },
signal: AbortSignal.timeout(60_000),
});
if (!catalogResponse.ok) {
throw new Error(`Atlas Cloud 模型目录请求失败(HTTP ${catalogResponse.status})`);
}
const catalogPayload = await catalogResponse.json();
const catalogModels: AtlasCloudCatalogModel[] = Array.isArray(catalogPayload?.data)
? catalogPayload.data
: [];
const models = catalogModels
.filter(
(model: AtlasCloudCatalogModel) =>
mediaTypes.includes(
String(model?.type || "").toLowerCase() as "image" | "video"
) &&
model?.display_console !== false &&
Boolean(model?.model) &&
Boolean(model?.schema)
)
.sort(
(a: AtlasCloudCatalogModel, b: AtlasCloudCatalogModel) =>
Number(b.priority || 0) - Number(a.priority || 0)
);
const settled = await mapWithConcurrency(models, 10, async (model, index) => {
try {
const schemaResponse = await fetch(String(model.schema), {
cache: "no-store",
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(30_000),
});
if (!schemaResponse.ok) {
throw new Error(`HTTP ${schemaResponse.status}`);
}
const preset = createAtlasCloudImagePreset(
model,
await schemaResponse.json(),
index
);
const supportedModes = Array.isArray(preset.capabilities.supportedModes)
? preset.capabilities.supportedModes
: [];
if (
preset.capabilities.mediaType === "video" &&
supportedModes.length === 0
) {
return {
preset: null,
error: null,
skipped: {
modelId: model.model,
reason: "当前视频工具不支持该模型要求的视频或音频输入",
},
};
}
return { preset, error: null, skipped: null };
} catch (error) {
return {
preset: null,
error: {
modelId: model.model,
error: error instanceof Error ? error.message : String(error),
},
skipped: null,
};
}
});
return {
presets: settled.flatMap((result) => (result.preset ? [result.preset] : [])),
errors: settled.flatMap((result) => (result.error ? [result.error] : [])),
skipped: settled.flatMap((result) => (result.skipped ? [result.skipped] : [])),
catalogCount: models.length,
};
}
export function fetchAtlasCloudImagePresets() {
return fetchAtlasCloudPresets(["image"]);
}
export function fetchAtlasCloudVideoPresets() {
return fetchAtlasCloudPresets(["video"]);
}
export function fetchAtlasCloudModelPresets() {
return fetchAtlasCloudPresets(["image", "video"]);
}