From 3143e8e338a5e5b79bb2ec74e1d64840b93dc155 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 18 Dec 2025 23:16:48 -0500 Subject: [PATCH] feat: add simple image generation API for canvas integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New /api/generate-image endpoint that returns base64 directly - Uses RunPod proxy to bypass geo-restrictions - Enables canvas-website MycroZine tool to generate images - Includes CORS headers for cross-origin requests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- web/app/api/generate-image/route.ts | 171 ++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 web/app/api/generate-image/route.ts diff --git a/web/app/api/generate-image/route.ts b/web/app/api/generate-image/route.ts new file mode 100644 index 0000000..3dcc015 --- /dev/null +++ b/web/app/api/generate-image/route.ts @@ -0,0 +1,171 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * Simple image generation API that proxies to RunPod/Gemini + * Returns base64 image data directly (no file storage) + * Used by canvas-website MycroZine tool + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { prompt } = body; + + if (!prompt) { + return NextResponse.json( + { error: "Missing required field: prompt" }, + { status: 400 } + ); + } + + // Generate image via RunPod proxy + const imageBase64 = await generateImageWithRunPod(prompt); + + if (!imageBase64) { + return NextResponse.json( + { error: "Image generation failed" }, + { status: 500 } + ); + } + + return NextResponse.json( + { + success: true, + imageData: imageBase64, + mimeType: "image/png", + }, + { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + } + ); + } catch (error) { + console.error("Image generation error:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Failed to generate image" }, + { status: 500 } + ); + } +} + +async function generateImageWithRunPod(prompt: string): Promise { + const apiKey = process.env.GEMINI_API_KEY; + const runpodApiKey = process.env.RUNPOD_API_KEY; + const runpodEndpointId = process.env.RUNPOD_GEMINI_ENDPOINT_ID || "ntqjz8cdsth42i"; + + if (!apiKey) { + console.error("GEMINI_API_KEY not configured"); + return null; + } + + if (!runpodApiKey) { + console.error("RUNPOD_API_KEY not configured, trying direct API"); + return generateDirectGeminiImage(prompt, apiKey); + } + + const runpodUrl = `https://api.runpod.ai/v2/${runpodEndpointId}/runsync`; + + try { + const response = await fetch(runpodUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${runpodApiKey}`, + }, + body: JSON.stringify({ + input: { + api_key: apiKey, + model: "gemini-2.0-flash-exp", + contents: [ + { + parts: [ + { + text: `Generate an image: ${prompt}`, + }, + ], + }, + ], + generationConfig: { + responseModalities: ["TEXT", "IMAGE"], + }, + }, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("RunPod API error:", response.status, errorText); + return null; + } + + const result = await response.json(); + const data = result.output || result; + + if (data.error) { + console.error("Gemini API error via RunPod:", data.error); + return null; + } + + // Extract image from response + const parts = data.candidates?.[0]?.content?.parts || []; + for (const part of parts) { + if (part.inlineData?.mimeType?.startsWith("image/")) { + console.log("Generated image via RunPod proxy"); + return part.inlineData.data; + } + } + + console.error("No image in response"); + return null; + } catch (error) { + console.error("RunPod request error:", error); + return null; + } +} + +async function generateDirectGeminiImage(prompt: string, apiKey: string): Promise { + const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`; + + try { + const response = await fetch(geminiUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + contents: [{ parts: [{ text: `Generate an image: ${prompt}` }] }], + generationConfig: { responseModalities: ["TEXT", "IMAGE"] }, + }), + }); + + if (!response.ok) { + console.error("Direct Gemini API error:", response.status); + return null; + } + + const data = await response.json(); + const parts = data.candidates?.[0]?.content?.parts || []; + for (const part of parts) { + if (part.inlineData?.mimeType?.startsWith("image/")) { + return part.inlineData.data; + } + } + + return null; + } catch (error) { + console.error("Direct Gemini error:", error); + return null; + } +} + +// Allow CORS for canvas-website +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }); +}