From 57950468d56877560caaabdec15aab95c7a70e6e Mon Sep 17 00:00:00 2001 From: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:57:00 +0800 Subject: [PATCH] feat: add Atlas Cloud image provider Signed-off-by: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> --- .env.local.example | 1 + app/lib/generate-image.ts | 97 ++++++++++++++++++++++++++++++++++++++- app/page.tsx | 7 ++- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/.env.local.example b/.env.local.example index 9ef9816..8ec38cf 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,2 +1,3 @@ # Get your API key from https://fal.ai/dashboard/keys FAL_KEY=your_fal_api_key_here +ATLASCLOUD_API_KEY=your_atlascloud_api_key_here diff --git a/app/lib/generate-image.ts b/app/lib/generate-image.ts index c2720ab..01cd537 100644 --- a/app/lib/generate-image.ts +++ b/app/lib/generate-image.ts @@ -1,6 +1,10 @@ import { fal } from "@fal-ai/client"; -export type ImageModel = "nano-banana-pro" | "nano-banana-lite" | "gpt-image-2"; +export type ImageModel = + | "nano-banana-pro" + | "nano-banana-lite" + | "gpt-image-2" + | "atlas-nano-banana-lite"; export type AspectRatio = "1:1" | "21:9" | "9:16" | "16:9"; @@ -8,7 +12,11 @@ export type GptImageQuality = "low" | "medium" | "high"; /** Coerce an untrusted request value into a valid ImageModel (defaults to nano-banana-pro). */ export function normalizeImageModel(value: unknown): ImageModel { - return value === "gpt-image-2" || value === "nano-banana-lite" ? value : "nano-banana-pro"; + return value === "gpt-image-2" || + value === "nano-banana-lite" || + value === "atlas-nano-banana-lite" + ? value + : "nano-banana-pro"; } interface GenerateImageInput { @@ -35,6 +43,10 @@ export async function generateImage({ }: GenerateImageInput): Promise { const isEdit = Array.isArray(imageUrls) && imageUrls.length > 0; + if (model === "atlas-nano-banana-lite") { + return generateWithAtlasCloud({ prompt, imageUrls, aspectRatio }); + } + if (model === "gpt-image-2") { const endpoint = isEdit ? "openai/gpt-image-2/edit" : "openai/gpt-image-2"; const input: Record = { @@ -97,6 +109,87 @@ export async function generateImage({ return data.images[0]; } +const ATLAS_API_BASE = "https://api.atlascloud.ai/api/v1"; +const ATLAS_POLL_INTERVAL_MS = 3_000; +const ATLAS_MAX_POLLS = 40; + +async function generateWithAtlasCloud({ + prompt, + imageUrls, + aspectRatio, +}: Pick): Promise { + const apiKey = process.env.ATLASCLOUD_API_KEY; + if (!apiKey) throw new Error("ATLASCLOUD_API_KEY is not configured"); + + const isEdit = Boolean(imageUrls?.length); + const model = isEdit + ? "google/nano-banana-2-lite/edit" + : "google/nano-banana-2-lite/text-to-image"; + const response = await atlasRequest<{ id: string }>("/model/generateImage", apiKey, { + method: "POST", + body: JSON.stringify({ + model, + prompt, + aspect_ratio: aspectRatio, + resolution: "1k", + ...(isEdit ? { images: imageUrls } : {}), + }), + }); + + for (let attempt = 0; attempt < ATLAS_MAX_POLLS; attempt += 1) { + const prediction = await atlasRequest<{ + status: string; + outputs?: string[] | null; + error?: string; + }>(`/model/prediction/${response.id}`, apiKey); + + if (prediction.status === "completed" || prediction.status === "succeeded") { + const url = prediction.outputs?.[0]; + if (!url) throw new Error("Atlas Cloud completed without an image URL"); + return { url, ...aspectRatioToDimensions(aspectRatio) }; + } + if (prediction.status === "failed" || prediction.status === "timeout") { + throw new Error(prediction.error || `Atlas Cloud generation ${prediction.status}`); + } + await new Promise((resolve) => setTimeout(resolve, ATLAS_POLL_INTERVAL_MS)); + } + + throw new Error("Atlas Cloud generation timed out"); +} + +async function atlasRequest(path: string, apiKey: string, init?: RequestInit): Promise { + const response = await fetch(`${ATLAS_API_BASE}${path}`, { + ...init, + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + ...init?.headers, + }, + }); + const payload = (await response.json()) as { + code?: number; + message?: string; + data?: T; + }; + if (!response.ok || !payload.data || (payload.code != null && payload.code !== 200)) { + throw new Error(payload.message || `Atlas Cloud request failed (${response.status})`); + } + return payload.data; +} + +function aspectRatioToDimensions(aspectRatio: AspectRatio): Pick { + switch (aspectRatio) { + case "9:16": + return { width: 576, height: 1024 }; + case "16:9": + return { width: 1024, height: 576 }; + case "21:9": + return { width: 1024, height: 439 }; + default: + return { width: 1024, height: 1024 }; + } +} + function aspectRatioToImageSize( ar: AspectRatio ): string | { width: number; height: number } { diff --git a/app/page.tsx b/app/page.tsx index 3908699..47f73fa 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -35,7 +35,11 @@ const FalSpinner = ({ size = 48 }: { size?: number }) => ( type Step = 1 | 2 | 3 | 4 | 5 | 6; type GameMode = "side-scroller" | "isometric"; -type ImageModel = "nano-banana-pro" | "nano-banana-lite" | "gpt-image-2"; +type ImageModel = + | "nano-banana-pro" + | "nano-banana-lite" + | "gpt-image-2" + | "atlas-nano-banana-lite"; type GptImageQuality = "low" | "medium" | "high"; interface BoundingBox { @@ -1405,6 +1409,7 @@ export default function Home() { ["nano-banana-pro", "Nano Banana Pro"], ["nano-banana-lite", "Nano Banana Lite"], ["gpt-image-2", "GPT-Image-2"], + ["atlas-nano-banana-lite", "Atlas Cloud Nano Banana Lite"], ] as const).map(([value, label], i) => (