651 lines
20 KiB
TypeScript
651 lines
20 KiB
TypeScript
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 ASPECT_RATIO_KEYS = ["aspect_ratio", "aspectRatio"];
|
||
const OUTPUT_COUNT_KEYS = ["num_images", "number_of_images", "n", "output_count"];
|
||
|
||
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;
|
||
aspectRatioKey?: string;
|
||
outputCountKey?: string;
|
||
aspectRatioValueMap: Record<string, string>;
|
||
maxReferenceImages: number;
|
||
maxVideoReferences: number;
|
||
maxImages: number;
|
||
defaults: Record<string, string | number | boolean>;
|
||
enums: Record<string, Array<string | 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";
|
||
|
||
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 (/^\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 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 resolutionKey = RESOLUTION_KEYS.find((key) => {
|
||
if (!(key in properties)) return false;
|
||
const values = getEnumValues(properties[key]);
|
||
return values.length === 0 || values.some((value) => Boolean(normalizeKSize(value)));
|
||
});
|
||
const aspectRatioConfig = buildAspectRatioValueMap(properties);
|
||
const aspectRatioKey = aspectRatioConfig.key;
|
||
const outputCountKey = findFirstKey(properties, OUTPUT_COUNT_KEYS);
|
||
const sizeValueMap = buildSizeValueMap(getEnumValues(properties.size));
|
||
const supportedImageSizes = deriveSupportedImageSizes(
|
||
properties,
|
||
resolutionKey,
|
||
sizeValueMap
|
||
);
|
||
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 mediaKind = inferAtlasCloudMediaKind(catalogModel);
|
||
const supportedModes =
|
||
mediaKind === "3d"
|
||
? derive3DSupportedModes(catalogModel)
|
||
: [
|
||
...(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,
|
||
aspectRatioKey,
|
||
outputCountKey,
|
||
aspectRatioValueMap: aspectRatioConfig.map,
|
||
maxReferenceImages,
|
||
maxVideoReferences,
|
||
maxImages,
|
||
defaults: buildScalarDefaults(properties),
|
||
enums: buildEnums(properties),
|
||
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) => value === "auto") ||
|
||
supportedAspectRatios.find((value) => value === "1:1") ||
|
||
supportedAspectRatios[0] ||
|
||
"1:1";
|
||
|
||
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,
|
||
maxReferenceImages,
|
||
maxVideoReferences,
|
||
supportsMultiImageReference: maxReferenceImages > 1,
|
||
supportsVideoReference: maxVideoReferences > 0,
|
||
requiresVideoReference: videoClipInput?.required === true,
|
||
provider: "atlascloud",
|
||
schemaUrl: catalogModel.schema || null,
|
||
},
|
||
defaultParams: {
|
||
imageSize: defaultImageSize,
|
||
aspectRatio: defaultAspectRatio,
|
||
imageCount: 1,
|
||
runtimeModelId: catalogModel.model,
|
||
},
|
||
imageGenConfig: {
|
||
apiFormat: "atlascloud-unified-image",
|
||
maxImages,
|
||
maxReferenceImages,
|
||
maxVideoReferences,
|
||
supportsSize,
|
||
supports1K: supportedImageSizes.includes("1K"),
|
||
supports2K: supportedImageSizes.includes("2K"),
|
||
supports4K: supportedImageSizes.includes("4K"),
|
||
supportedImageSizes,
|
||
supportedAspectRatios,
|
||
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;
|
||
}
|
||
|
||
export async function fetchAtlasCloudImagePresets() {
|
||
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) =>
|
||
String(model?.type || "").toLowerCase() === "image" &&
|
||
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}`);
|
||
}
|
||
return {
|
||
preset: createAtlasCloudImagePreset(model, await schemaResponse.json(), index),
|
||
error: null,
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
preset: null,
|
||
error: {
|
||
modelId: model.model,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
},
|
||
};
|
||
}
|
||
});
|
||
|
||
return {
|
||
presets: settled.flatMap((result) => (result.preset ? [result.preset] : [])),
|
||
errors: settled.flatMap((result) => (result.error ? [result.error] : [])),
|
||
catalogCount: models.length,
|
||
};
|
||
}
|