import { NextRequest, NextResponse } from "next/server"; import { getZine, saveZine, savePageImage } from "@/lib/storage"; import type { PageOutline } from "@/lib/gemini"; // Style-specific image generation prompts const STYLE_PROMPTS: Record = { "punk-zine": "xerox-style high contrast black and white, DIY cut-and-paste collage aesthetic, hand-drawn typography, punk rock zine style, grainy texture, photocopied look, bold graphic elements", "mycelial": "organic mycelial network patterns, spore prints, earth tones with green accents, fungal textures, underground root systems, natural decomposition aesthetic, interconnected web designs", "minimal": "clean minimalist design, lots of white space, modern sans-serif typography, simple geometric shapes, subtle gradients, elegant composition", "collage": "layered mixed media collage, vintage photographs, torn paper edges, overlapping textures, eclectic composition, found imagery", "retro": "1970s aesthetic, earth tones, groovy psychedelic typography, halftone dot patterns, vintage illustration style, warm colors", }; const TONE_PROMPTS: Record = { "rebellious": "defiant anti-establishment energy, provocative bold statements, raw and unfiltered, urgent", "regenerative": "hopeful and healing, nature-inspired wisdom, interconnected systems thinking, restoration and renewal, growth from decay", "playful": "whimsical fun light-hearted energy, humor and wit, bright positive vibes, joyful", "informative": "educational and factual, clear explanations, structured information, accessible", "poetic": "lyrical and metaphorical, evocative imagery, emotional depth, contemplative", }; export async function POST(request: NextRequest) { try { const body = await request.json(); const { zineId, pageNumber, outline, style, tone } = body; if (!zineId || !pageNumber || !outline) { return NextResponse.json( { error: "Missing required fields: zineId, pageNumber, outline" }, { status: 400 } ); } // Verify zine exists const zine = await getZine(zineId); if (!zine) { return NextResponse.json( { error: "Zine not found" }, { status: 404 } ); } const pageOutline = outline as PageOutline; const stylePrompt = STYLE_PROMPTS[style] || STYLE_PROMPTS["mycelial"]; const tonePrompt = TONE_PROMPTS[tone] || TONE_PROMPTS["regenerative"]; // Build the full image generation prompt const fullPrompt = buildImagePrompt(pageOutline, stylePrompt, tonePrompt); // Generate image using Gemini Imagen API // Pass outline and style for styled fallback const imageBase64 = await generateImageWithGemini(fullPrompt, pageOutline, style); // Save the page image const imagePath = await savePageImage(zineId, pageNumber, imageBase64); // Update zine metadata zine.pages[pageNumber - 1] = imagePath; zine.updatedAt = new Date().toISOString(); await saveZine(zine); return NextResponse.json({ pageNumber, imageUrl: `/api/zine/${zineId}?image=p${pageNumber}`, success: true, }); } catch (error) { console.error("Page generation error:", error); return NextResponse.json( { error: error instanceof Error ? error.message : "Failed to generate page" }, { status: 500 } ); } } function buildImagePrompt(outline: PageOutline, stylePrompt: string, tonePrompt: string): string { return `Create a single zine page image (portrait orientation, 825x1275 pixels aspect ratio). PAGE ${outline.pageNumber}: "${outline.title}" Type: ${outline.type} Content to visualize: ${outline.keyPoints.map((p, i) => `${i + 1}. ${p}`).join("\n")} Visual Style: ${stylePrompt} Mood/Tone: ${tonePrompt} Detailed requirements: ${outline.imagePrompt} IMPORTANT: - This is a SINGLE page that will be printed - Include any text/typography as part of the graphic design - Fill the entire page - no blank margins - Make it visually striking and cohesive - The design should work in print (high contrast, clear details)`; } async function generateImageWithGemini( prompt: string, outline: PageOutline, style: 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) { throw new Error("GEMINI_API_KEY not configured"); } if (!runpodApiKey) { throw new Error("RUNPOD_API_KEY not configured - required for US proxy"); } // Enhanced prompt for better text rendering and image quality const enhancedPrompt = `${prompt} CRITICAL TEXT RENDERING INSTRUCTIONS: - Any text in the image must be spelled correctly and legibly - Use clean, readable typography appropriate to the style - Avoid distorted or warped letters - Text should be integrated naturally into the design`; // Use RunPod US proxy to call Nano Banana Pro (bypasses geo-blocking) try { const result = await generateWithNanoBananaProViaRunPod(enhancedPrompt, apiKey, runpodApiKey, runpodEndpointId); if (result) { console.log("✅ Generated image with Nano Banana Pro via RunPod US"); return result; } } catch (error) { console.error("Nano Banana Pro via RunPod error:", error); } // Final fallback: Create styled placeholder console.log("Using styled placeholder image for page", outline.pageNumber); return createStyledPlaceholder(outline, style); } // Nano Banana Pro via RunPod US proxy (bypasses geo-blocking in Germany) async function generateWithNanoBananaProViaRunPod( prompt: string, apiKey: string, runpodApiKey: string, endpointId: string ): Promise { const runpodUrl = `https://api.runpod.ai/v2/${endpointId}/runsync`; console.log("Calling Nano Banana Pro via RunPod US proxy..."); 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-image-generation", contents: [ { parts: [{ text: prompt }], }, ], generationConfig: { responseModalities: ["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:", JSON.stringify(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("✅ Successfully received image from Nano Banana Pro"); return part.inlineData.data; } } console.error("No image in Gemini response"); return null; } // Create styled placeholder images with actual page content async function createStyledPlaceholder( outline: PageOutline, style: string ): Promise { const sharp = (await import("sharp")).default; // Escape XML special characters const escapeXml = (str: string) => str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); const title = escapeXml(outline.title.slice(0, 40)); const keyPoints = outline.keyPoints.slice(0, 3).map((p) => escapeXml(p.slice(0, 50))); // Style-specific colors and patterns const styles: Record = { "punk-zine": { bg: "#ffffff", fg: "#000000", accent: "#ff0066", pattern: ` `, }, "mycelial": { bg: "#f5f0e8", fg: "#2d3a2d", accent: "#4a7c4f", pattern: ` `, }, minimal: { bg: "#fafafa", fg: "#333333", accent: "#0066ff", pattern: "", }, collage: { bg: "#f5e6d3", fg: "#2d2d2d", accent: "#8b4513", pattern: ` `, }, retro: { bg: "#fff8dc", fg: "#8b4513", accent: "#ff6347", pattern: ` `, }, }; const s = styles[style] || styles["mycelial"]; const pageNum = outline.pageNumber; const pageType = escapeXml(outline.type.toUpperCase()); const svg = ` ${s.pattern} ${s.pattern ? `` : ""} P${pageNum} ${pageType} ${title} ${keyPoints .map( (point, i) => ` ${point}${point.length >= 50 ? "..." : ""} ` ) .join("")} MycroZine - AI Generated Styled placeholder - image gen pending `; const buffer = await sharp(Buffer.from(svg)).png().toBuffer(); return buffer.toString("base64"); }