import { NextRequest, NextResponse } from "next/server"; import { getZine, saveZine, savePageImage } from "@/lib/storage"; import type { PageOutline } from "@/lib/gemini"; // Zine page dimensions: 1/8 of US Letter at 300 DPI // Aspect ratio: 1.55 (height / width) const ZINE_PAGE_WIDTH = 750; const ZINE_PAGE_HEIGHT = 1163; // 750 * 1.55 = 1162.5 ≈ 1163 // 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 for print. EXACT DIMENSIONS: ${ZINE_PAGE_WIDTH}x${ZINE_PAGE_HEIGHT} pixels (portrait, 1.55 aspect ratio) This is 1/8 of a US Letter page for an 8-page mini-zine fold. 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} CRITICAL REQUIREMENTS: - Portrait orientation ONLY (taller than wide) - Fill the entire page - no blank margins - Design must work at small print size (high contrast, clear details) - Include any text/typography as part of the graphic design - Make it visually striking and cohesive`; } async function generateImageWithGemini( prompt: string, outline: PageOutline, style: string ): Promise { const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) { throw new Error("GEMINI_API_KEY not configured"); } // 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`; // Call Nano Banana (Gemini 2.5 Flash Image) directly - no geo-blocking from EU try { const result = await generateWithNanoBanana(enhancedPrompt, apiKey); if (result) { console.log("✅ Generated image with Nano Banana (gemini-2.5-flash-image)"); // Resize to exact zine dimensions const resizedImage = await resizeToZineDimensions(result); return resizedImage; } } catch (error) { console.error("Nano Banana error:", error); } // Final fallback: Create styled placeholder console.log("Using styled placeholder image for page", outline.pageNumber); return createStyledPlaceholder(outline, style); } // Resize image to exact zine page dimensions (1/8 letter) async function resizeToZineDimensions(base64Image: string): Promise { const sharp = (await import("sharp")).default; const inputBuffer = Buffer.from(base64Image, "base64"); // Resize to exact dimensions, covering the area and cropping excess const resizedBuffer = await sharp(inputBuffer) .resize(ZINE_PAGE_WIDTH, ZINE_PAGE_HEIGHT, { fit: "cover", // Cover the dimensions, crop if needed position: "center" // Center the crop }) .png() .toBuffer(); console.log(`✅ Resized image to ${ZINE_PAGE_WIDTH}x${ZINE_PAGE_HEIGHT}px`); return resizedBuffer.toString("base64"); } // Direct Nano Banana API call (Gemini 2.5 Flash Image) async function generateWithNanoBanana( prompt: string, apiKey: string ): Promise { const model = "gemini-2.5-flash-image"; const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; console.log("Calling Nano Banana (gemini-2.5-flash-image)..."); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ contents: [ { parts: [{ text: prompt }], }, ], generationConfig: { responseModalities: ["image", "text"], }, }), }); if (!response.ok) { const errorText = await response.text(); console.error("Gemini API error:", response.status, errorText); return null; } const data = await response.json(); if (data.error) { console.error("Gemini API error:", 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"); 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"); }