better-chatbot / src /lib /ai /image /generate-image.ts
Bot
Initial commit for HF Spaces
05c5ed5
Raw
History Blame Contribute Delete
5.78 kB
"use server";
import {
GoogleGenAI,
Part as GeminiPart,
Content as GeminiMessage,
} from "@google/genai";
import { safe, watchError } from "ts-safe";
import { getBase64Data } from "lib/file-storage/storage-utils";
import { serverFileStorage } from "lib/file-storage";
import { openai } from "@ai-sdk/openai";
import { xai } from "@ai-sdk/xai";
import {
FilePart,
ImagePart,
ModelMessage,
TextPart,
experimental_generateImage,
} from "ai";
import { isString } from "lib/utils";
import logger from "logger";
type GenerateImageOptions = {
messages?: ModelMessage[];
prompt: string;
abortSignal?: AbortSignal;
};
type GeneratedImage = {
base64: string;
mimeType?: string;
};
export type GeneratedImageResult = {
images: GeneratedImage[];
};
export async function generateImageWithOpenAI(
options: GenerateImageOptions,
): Promise<GeneratedImageResult> {
return experimental_generateImage({
model: openai.image("gpt-image-1-mini"),
abortSignal: options.abortSignal,
prompt: options.prompt,
}).then((res) => {
return {
images: res.images.map((v) => {
const item: GeneratedImage = {
base64: Buffer.from(v.uint8Array).toString("base64"),
mimeType: v.mediaType,
};
return item;
}),
};
});
}
export async function generateImageWithXAI(
options: GenerateImageOptions,
): Promise<GeneratedImageResult> {
return experimental_generateImage({
model: xai.image("grok-2-image"),
abortSignal: options.abortSignal,
prompt: options.prompt,
}).then((res) => {
return {
images: res.images.map((v) => ({
base64: Buffer.from(v.uint8Array).toString("base64"),
mimeType: v.mediaType,
})),
};
});
}
export const generateImageWithNanoBanana = async (
options: GenerateImageOptions,
): Promise<GeneratedImageResult> => {
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
if (!apiKey) {
throw new Error("GOOGLE_GENERATIVE_AI_API_KEY is not set");
}
const ai = new GoogleGenAI({
apiKey: apiKey,
});
const geminiMessages: GeminiMessage[] = await safe(options.messages || [])
.map((messages) => Promise.all(messages.map(convertToGeminiMessage)))
.watch(watchError(logger.error))
.unwrap();
if (options.prompt) {
geminiMessages.push({
role: "user",
parts: [{ text: options.prompt }],
});
}
const response = await ai.models
.generateContent({
model: "gemini-2.5-flash-image",
config: {
abortSignal: options.abortSignal,
responseModalities: ["IMAGE"],
},
contents: geminiMessages,
})
.catch((err) => {
logger.error(err);
throw err;
});
return (
response.candidates?.reduce(
(acc, candidate) => {
const images =
candidate.content?.parts
?.filter((part) => part.inlineData)
.map((p) => ({
base64: p.inlineData!.data!,
mimeType: p.inlineData!.mimeType,
})) ?? [];
acc.images.push(...images);
return acc;
},
{ images: [] as GeneratedImage[] },
) || { images: [] as GeneratedImage[] }
);
};
async function convertToGeminiMessage(
message: ModelMessage,
): Promise<GeminiMessage> {
const getBase64DataSmart = async (input: {
data: string | Uint8Array | ArrayBuffer | Buffer | URL;
mimeType: string;
}): Promise<{ data: string; mimeType: string }> => {
if (
typeof input.data === "string" &&
(input.data.startsWith("http://") || input.data.startsWith("https://"))
) {
// Try fetching directly (public URLs)
try {
const resp = await fetch(input.data);
if (resp.ok) {
const buf = Buffer.from(await resp.arrayBuffer());
return { data: buf.toString("base64"), mimeType: input.mimeType };
}
} catch {
// fall through to storage fallback
}
// Fallback: derive key and download via storage backend (works for private buckets)
try {
const u = new URL(input.data as string);
const key = decodeURIComponent(u.pathname.replace(/^\//, ""));
const buf = await serverFileStorage.download(key);
return { data: buf.toString("base64"), mimeType: input.mimeType };
} catch {
// Ignore and fall back to generic helper below
}
}
// Default fallback: use generic helper (handles base64, buffers, blobs, etc.)
return getBase64Data(input);
};
const parts = isString(message.content)
? ([{ text: message.content }] as GeminiPart[])
: await Promise.all(
message.content.map(async (content) => {
if (content.type == "file") {
const part = content as FilePart;
const data = await getBase64DataSmart({
data: part.data,
mimeType: part.mediaType!,
});
return {
inlineData: data,
} as GeminiPart;
}
if (content.type == "text") {
const part = content as TextPart;
return {
text: part.text,
};
}
if (content.type == "image") {
const part = content as ImagePart;
const data = await getBase64DataSmart({
data: part.image,
mimeType: part.mediaType!,
});
return {
inlineData: data,
};
}
return null;
}),
)
.then((parts) => parts.filter(Boolean) as GeminiPart[])
.catch((err) => {
logger.withTag("convertToGeminiMessage").error(err);
throw err;
});
return {
role: message.role == "user" ? "user" : "model",
parts,
};
}