Spaces:
Sleeping
Sleeping
File size: 5,782 Bytes
05c5ed5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | "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,
};
}
|