feat: add simple image generation API for canvas integration

- 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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-18 23:16:48 -05:00
parent 44c30fa1a1
commit 3143e8e338
1 changed files with 171 additions and 0 deletions

View File

@ -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<string | null> {
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<string | null> {
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",
},
});
}