import type { AtlasCloudRuntimeConfig } from "@/lib/ai/atlascloud-models"; import type { VideoGenerationOptions } from "@/types/ai"; export interface AtlasCloudImageParams { aspectRatio?: string; imageSize?: string; imageCount?: number; negativePrompt?: string; promptExtend?: boolean; quality?: string; seed?: number; watermark?: boolean; } export interface AtlasCloudModelConfig { maxReferenceImages?: number; maxVideoReferences?: number; supportedAspectRatios?: string[]; supportedImageSizes?: string[]; atlasCloud?: AtlasCloudRuntimeConfig; } export interface AtlasCloudVideoModelConfig extends AtlasCloudModelConfig { videoDurationOptions?: number[]; } function normalizeAspectRatio( requested: string | undefined, supported: string[] | undefined ) { const values = (supported || []).map(String); const normalized = String(requested || "auto").trim().toLowerCase(); const exact = values.find((value) => value.toLowerCase() === normalized); if (exact) return exact; return ( values.find((value) => value === "auto") || values.find((value) => value === "1:1") || values[0] || "1:1" ); } function normalizeImageSize(requested: string | undefined, supported?: string[]) { const values = (supported || []).map((value) => String(value).trim().toUpperCase()); const normalized = String(requested || "2K").trim().toUpperCase(); if (values.includes(normalized)) return normalized; return values.includes("2K") ? "2K" : values[0] || normalized; } function matchEnumValue(values: Array | undefined, value: unknown) { if (!values?.length) return value; const normalized = String(value || "").trim().toLowerCase(); return values.find( (item) => String(item).trim().toLowerCase() === normalized ); } function parseRatio(value: string) { const match = value.match(/^(\d+):(\d+)$/); if (!match) return 1; return Number(match[1]) / Number(match[2]); } function pickClosestSizeValue( map: Record>, imageSize: string, aspectRatio: string ) { const tiers = [imageSize, "2K", "1K", "4K"]; const values = tiers.map((tier) => map[tier]).find(Boolean); if (!values) return undefined; if (values[aspectRatio]) return values[aspectRatio]; const target = parseRatio(aspectRatio); return Object.entries(values).sort( ([a], [b]) => Math.abs(Math.log(target / parseRatio(a))) - Math.abs(Math.log(target / parseRatio(b))) )[0]?.[1]; } export function createAtlasCloudRequestBody(params: { modelId: string; prompt: string; referenceImages: string[]; referenceVideos?: string[]; imageParams?: AtlasCloudImageParams; modelConfig: AtlasCloudModelConfig; }) { const runtime = params.modelConfig.atlasCloud; if (!runtime) { throw new Error("Atlas Cloud 模型运行参数缺失,请重新同步模型"); } const body: Record = { ...runtime.defaults }; if ( runtime.propertyNames.includes("model") || runtime.requiredFields.includes("model") ) { body.model = params.modelId; } if ( runtime.propertyNames.includes("prompt") || runtime.requiredFields.includes("prompt") ) { body.prompt = params.prompt; } const aspectRatio = normalizeAspectRatio( params.imageParams?.aspectRatio, params.modelConfig.supportedAspectRatios ); const imageSize = normalizeImageSize( params.imageParams?.imageSize, params.modelConfig.supportedImageSizes ); if (runtime.aspectRatioKey) { body[runtime.aspectRatioKey] = runtime.aspectRatioValueMap?.[aspectRatio] || matchEnumValue(runtime.enums[runtime.aspectRatioKey], aspectRatio) || aspectRatio; } if (runtime.resolutionKey) { body[runtime.resolutionKey] = matchEnumValue(runtime.enums[runtime.resolutionKey], imageSize.toLowerCase()) || matchEnumValue(runtime.enums[runtime.resolutionKey], imageSize) || imageSize.toLowerCase(); } else { const sizeValue = pickClosestSizeValue( runtime.sizeValueMap, imageSize, aspectRatio === "auto" ? "1:1" : aspectRatio ); if (sizeValue) body.size = sizeValue; } const imageInputs = runtime.imageInputs?.length ? runtime.imageInputs : runtime.imageInputKey ? [ { key: runtime.imageInputKey, mode: runtime.imageInputMode || ("single" as const), maxItems: Math.max(1, runtime.maxReferenceImages || 1), required: runtime.requiredFields.includes(runtime.imageInputKey), }, ] : []; let referenceCursor = 0; for (const input of imageInputs) { const values = params.referenceImages.slice( referenceCursor, referenceCursor + input.maxItems ); if (values.length > 0) { body[input.key] = input.mode === "single" ? values[0] : values; referenceCursor += values.length; } else if (input.required) { throw new Error( `该 Atlas Cloud 模型至少需要 ${referenceCursor + 1} 张参考图` ); } } if (imageInputs.length === 0 && params.referenceImages.length > 0) { throw new Error("当前 Atlas Cloud 模型不支持参考图,请选择图片编辑模型"); } const referenceVideos = params.referenceVideos || []; if (runtime.videoClipInput) { const values = referenceVideos.slice(0, runtime.videoClipInput.maxItems); if (values.length > 0) { body[runtime.videoClipInput.key] = values.map((url) => ({ ...runtime.videoClipInput?.itemDefaults, url, })); } else if (runtime.videoClipInput.required) { throw new Error("该 Atlas Cloud 模型至少需要 1 个参考视频"); } } else if (referenceVideos.length > 0) { throw new Error("当前 Atlas Cloud 模型不支持参考视频"); } if (runtime.outputCountKey) { const requestedCount = Math.max(1, params.imageParams?.imageCount || 1); body[runtime.outputCountKey] = Math.min(runtime.maxImages, requestedCount); } const optionalParams: Array<[string, unknown]> = [ ["negative_prompt", params.imageParams?.negativePrompt], ["prompt_extend", params.imageParams?.promptExtend], ["quality", params.imageParams?.quality], ["seed", params.imageParams?.seed], ["watermark", params.imageParams?.watermark], ]; for (const [key, value] of optionalParams) { if (value !== undefined && runtime.propertyNames.includes(key)) { body[key] = matchEnumValue(runtime.enums[key], value) ?? value; } } if (runtime.propertyNames.includes("enable_sync_mode")) { body.enable_sync_mode = false; } if (runtime.propertyNames.includes("enable_base64_output")) { body.enable_base64_output = false; } const unsupportedRequiredFields = runtime.requiredFields.filter( (key) => body[key] === undefined ); if (unsupportedRequiredFields.length > 0) { throw new Error( `当前界面暂不支持该 Atlas Cloud 模型的必填参数:${unsupportedRequiredFields.join(", ")}` ); } return { body, imageSize }; } function setAtlasCloudImages(params: { body: Record; runtime: AtlasCloudRuntimeConfig; images: string[]; }) { const imageInputs = params.runtime.imageInputs?.length ? params.runtime.imageInputs : params.runtime.imageInputKey ? [ { key: params.runtime.imageInputKey, mode: params.runtime.imageInputMode || ("single" as const), maxItems: Math.max(1, params.runtime.maxReferenceImages || 1), required: params.runtime.requiredFields.includes( params.runtime.imageInputKey ), }, ] : []; let cursor = 0; for (const input of imageInputs) { const values = params.images.slice(cursor, cursor + input.maxItems); if (values.length > 0) { params.body[input.key] = input.mode === "single" ? values[0] : values; cursor += values.length; } else if (input.required) { throw new Error(`该 Atlas Cloud 视频模型至少需要 ${cursor + 1} 张参考图`); } } if (imageInputs.length === 0 && params.images.length > 0) { throw new Error("当前 Atlas Cloud 视频模型不支持参考图"); } } export function createAtlasCloudVideoRequestBody(params: { modelId: string; options: VideoGenerationOptions; referenceImages: string[]; modelConfig: AtlasCloudVideoModelConfig; }) { const runtime = params.modelConfig.atlasCloud; if (!runtime) { throw new Error("Atlas Cloud 视频模型运行参数缺失,请重新同步模型"); } const body: Record = { ...runtime.defaults }; if ( runtime.propertyNames.includes("model") || runtime.requiredFields.includes("model") ) { body.model = params.modelId; } if ( runtime.propertyNames.includes("prompt") || runtime.requiredFields.includes("prompt") ) { body.prompt = params.options.prompt; } if (runtime.resolutionKey && params.options.resolution) { body[runtime.resolutionKey] = runtime.resolutionValueMap?.[params.options.resolution] || matchEnumValue( runtime.enums[runtime.resolutionKey], params.options.resolution ) || params.options.resolution; } if (runtime.aspectRatioKey && params.options.ratio) { const requestedRatio = params.options.ratio; body[runtime.aspectRatioKey] = runtime.aspectRatioValueMap?.[requestedRatio] || runtime.aspectRatioValueMap?.[ requestedRatio === "adaptive" ? "auto" : requestedRatio ] || matchEnumValue(runtime.enums[runtime.aspectRatioKey], requestedRatio) || requestedRatio; } if (runtime.durationKey && params.options.duration !== undefined) { body[runtime.durationKey] = matchEnumValue(runtime.enums[runtime.durationKey], params.options.duration) ?? params.options.duration; } setAtlasCloudImages({ body, runtime, images: params.referenceImages, }); const optionalParams: Array<[string, unknown]> = [ ["seed", params.options.seed], ["watermark", params.options.watermark], ["camera_fixed", params.options.cameraFixed], ["return_last_frame", params.options.returnLastFrame], ]; for (const [key, value] of optionalParams) { if (value !== undefined && runtime.propertyNames.includes(key)) { body[key] = matchEnumValue(runtime.enums[key], value) ?? value; } } if (runtime.propertyNames.includes("enable_sync_mode")) { body.enable_sync_mode = false; } if (runtime.propertyNames.includes("enable_base64_output")) { body.enable_base64_output = false; } const unsupportedRequiredFields = runtime.requiredFields.filter( (key) => body[key] === undefined ); if (unsupportedRequiredFields.length > 0) { throw new Error( `当前视频界面暂不支持该 Atlas Cloud 模型的必填参数:${unsupportedRequiredFields.join(", ")}` ); } return { body }; }