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", "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", "academic": "clean infographic style, annotated diagrams, data visualization, technical illustration, educational layout, clear hierarchy", }; const TONE_PROMPTS: Record = { "rebellious": "defiant anti-establishment energy, provocative bold statements, raw and unfiltered, urgent", "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["punk-zine"]; const tonePrompt = TONE_PROMPTS[tone] || TONE_PROMPTS["rebellious"]; // Build the full image generation prompt const fullPrompt = buildImagePrompt(pageOutline, stylePrompt, tonePrompt); // Generate image using Gemini Imagen API // Note: This uses the MCP-style generation - in production, we'd call the Gemini API directly const imageBase64 = await generateImageWithGemini(fullPrompt); // 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}.png`, 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): Promise { const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) { throw new Error("GEMINI_API_KEY not configured"); } // Use Gemini's Imagen 3 API for image generation // API endpoint for image generation const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=${apiKey}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ instances: [{ prompt }], parameters: { sampleCount: 1, aspectRatio: "3:4", // Portrait for zine pages safetyFilterLevel: "BLOCK_ONLY_HIGH", personGeneration: "ALLOW_ADULT", }, }), } ); if (!response.ok) { const errorText = await response.text(); console.error("Imagen API error:", errorText); // Fallback to Gemini 2.0 Flash experimental image generation return await generateImageWithGemini2(prompt); } const data = await response.json(); if (data.predictions && data.predictions[0]?.bytesBase64Encoded) { return data.predictions[0].bytesBase64Encoded; } throw new Error("No image data in response"); } async function generateImageWithGemini2(prompt: string): Promise { const apiKey = process.env.GEMINI_API_KEY; // Try Gemini 2.0 Flash with image generation capability const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ contents: [ { parts: [ { text: `Generate an image: ${prompt}`, }, ], }, ], generationConfig: { responseModalities: ["IMAGE", "TEXT"], responseMimeType: "image/png", }, }), } ); if (!response.ok) { const errorText = await response.text(); console.error("Gemini 2.0 image error:", errorText); // Return a placeholder for development return createPlaceholderImage(prompt); } const data = await response.json(); // Extract image from response const parts = data.candidates?.[0]?.content?.parts || []; for (const part of parts) { if (part.inlineData?.mimeType?.startsWith("image/")) { return part.inlineData.data; } } // If no image, create placeholder return createPlaceholderImage(prompt); } async function createPlaceholderImage(prompt: string): Promise { // Create a simple placeholder image using sharp const sharp = (await import("sharp")).default; const svg = ` [IMAGE PLACEHOLDER] ${prompt.slice(0, 50)}... Image generation in progress `; const buffer = await sharp(Buffer.from(svg)).png().toBuffer(); return buffer.toString("base64"); }