feat: use RunPod Gemini proxy for image generation (Nano Banana)

Route Gemini image generation through RunPod US-based serverless
endpoint to bypass geo-restrictions in Germany. Uses same approach
as zine.jeffemmett.com (mycro-zine).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-23 01:38:39 -05:00
parent 6db1a2f016
commit d2ea2c975e
1 changed files with 63 additions and 45 deletions

View File

@ -14,43 +14,52 @@ function getGenAI(): GoogleGenerativeAI {
return _genAI; return _genAI;
} }
// RunPod Stable Diffusion configuration // RunPod Gemini Proxy configuration (US-based to bypass geo-restrictions)
const RUNPOD_API_KEY = process.env.RUNPOD_API_KEY; const RUNPOD_API_KEY = process.env.RUNPOD_API_KEY;
const RUNPOD_SD_ENDPOINT = "tzf1j3sc3zufsy"; // Automatic1111 endpoint const RUNPOD_GEMINI_ENDPOINT_ID = process.env.RUNPOD_GEMINI_ENDPOINT_ID || "ntqjz8cdsth42i";
interface RunPodResponse { /**
id: string; * Generate image using Gemini API via RunPod US proxy
status: "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED" | "FAILED"; * This bypasses the geo-restriction on Gemini image generation in Germany
output?: { */
images?: string[]; async function generateImageWithGeminiProxy(prompt: string): Promise<string> {
image?: string; const apiKey = process.env.GEMINI_API_KEY;
}; const runpodApiKey = RUNPOD_API_KEY;
error?: string;
}
async function generateImageWithRunPod(prompt: string): Promise<string> { if (!apiKey) {
if (!RUNPOD_API_KEY) { throw new Error("GEMINI_API_KEY environment variable is not set");
}
if (!runpodApiKey) {
throw new Error("RUNPOD_API_KEY environment variable is not set"); throw new Error("RUNPOD_API_KEY environment variable is not set");
} }
const runUrl = `https://api.runpod.ai/v2/${RUNPOD_SD_ENDPOINT}/runsync`; const runpodUrl = `https://api.runpod.ai/v2/${RUNPOD_GEMINI_ENDPOINT_ID}/runsync`;
// Call RunPod Automatic1111 endpoint console.log("Calling Gemini via RunPod proxy...");
const response = await fetch(runUrl, {
const response = await fetch(runpodUrl, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${RUNPOD_API_KEY}`, "Authorization": `Bearer ${runpodApiKey}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
input: { input: {
prompt: prompt, api_key: apiKey,
negative_prompt: "blurry, low quality, distorted text, watermark, signature", model: "gemini-2.0-flash-exp",
width: 512, contents: [
height: 768, // Portrait orientation for zine pages {
num_inference_steps: 25, parts: [
guidance_scale: 7.5, {
sampler_name: "DPM++ 2M Karras", text: `Generate an image: ${prompt}`,
},
],
},
],
generationConfig: {
responseModalities: ["TEXT", "IMAGE"],
},
}, },
}), }),
}); });
@ -61,20 +70,24 @@ async function generateImageWithRunPod(prompt: string): Promise<string> {
throw new Error(`RunPod API error: ${response.status}`); throw new Error(`RunPod API error: ${response.status}`);
} }
const result: RunPodResponse = await response.json(); const result = await response.json();
const data = result.output || result;
if (result.status === "FAILED") { if (data.error) {
throw new Error(`RunPod job failed: ${result.error || "Unknown error"}`); console.error("Gemini API error via RunPod:", JSON.stringify(data.error));
throw new Error(data.error.message || "Gemini API error");
} }
// Extract base64 image from response // Extract image from response
const imageData = result.output?.images?.[0] || result.output?.image; const parts = data.candidates?.[0]?.content?.parts || [];
if (!imageData) { for (const part of parts) {
throw new Error("No image data in RunPod response"); if (part.inlineData?.mimeType?.startsWith("image/")) {
console.log("Successfully generated image via RunPod proxy");
return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
}
} }
// RunPod returns base64 without prefix throw new Error("No image in Gemini response");
return `data:image/png;base64,${imageData}`;
} }
export interface PageOutline { export interface PageOutline {
@ -191,26 +204,31 @@ export async function generatePageImage(
tone: string, tone: string,
feedback?: string feedback?: string
): Promise<string> { ): Promise<string> {
// Use RunPod Stable Diffusion for image generation // Use Gemini image generation via RunPod US proxy (bypasses geo-restriction)
// (Gemini image gen is geo-blocked in Germany where the server is located)
const styleDesc = STYLE_PROMPTS[style] || STYLE_PROMPTS["mycelial"]; const styleDesc = STYLE_PROMPTS[style] || STYLE_PROMPTS["mycelial"];
const toneDesc = TONE_PROMPTS[tone] || TONE_PROMPTS["regenerative"]; const toneDesc = TONE_PROMPTS[tone] || TONE_PROMPTS["regenerative"];
// Build a Stable Diffusion optimized prompt let imagePrompt = `Create a single page for a mini-zine (approximately 825x1275 pixels aspect ratio, portrait orientation).
let imagePrompt = `${pageOutline.title}, ${pageOutline.keyPoints.join(", ")}, ${styleDesc}, ${toneDesc}, ${pageOutline.imagePrompt}, zine page design, printable art, high quality illustration`;
Page ${pageOutline.pageNumber}: ${pageOutline.title}
Type: ${pageOutline.type}
Key elements: ${pageOutline.keyPoints.join(", ")}
Visual style: ${styleDesc}
Mood/tone: ${toneDesc}
Specific requirements:
${pageOutline.imagePrompt}
The image should be a complete, self-contained page that could be printed. Include any text as part of the design in a ${style} typography style.`;
if (feedback) { if (feedback) {
imagePrompt += `, ${feedback}`; imagePrompt += `\n\nUser feedback for refinement: ${feedback}`;
}
// Truncate prompt if too long (SD has token limits)
if (imagePrompt.length > 500) {
imagePrompt = imagePrompt.substring(0, 500);
} }
try { try {
console.log(`Generating image for page ${pageOutline.pageNumber} with RunPod...`); console.log(`Generating image for page ${pageOutline.pageNumber} via Gemini/RunPod proxy...`);
const imageDataUrl = await generateImageWithRunPod(imagePrompt); const imageDataUrl = await generateImageWithGeminiProxy(imagePrompt);
console.log(`Successfully generated image for page ${pageOutline.pageNumber}`); console.log(`Successfully generated image for page ${pageOutline.pageNumber}`);
return imageDataUrl; return imageDataUrl;
} catch (error) { } catch (error) {