fix: use Nano Banana directly and enforce 750x1200px zine dimensions

- Switch from RunPod proxy to direct Gemini API (no geo-blocking on gemini-2.5-flash-image)
- Add sharp post-processing to resize all images to exact 750x1200px
- Dimensions: 1/8 letter (2.5" x 4.0" content area with 0.125" margins) at 300 DPI
- Update regenerate-page FLUX img2img to use same dimensions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-01-21 13:41:31 +01:00
parent 23ace0a7f3
commit 8dc3145e5c
2 changed files with 67 additions and 44 deletions

View File

@ -2,6 +2,12 @@ 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 (8.5" x 11") at 300 DPI
// Page size: 2.75" x 4.25", with 0.125" margin on each side
// Content area: 2.5" x 4.0"
const ZINE_PAGE_WIDTH = 750; // 2.5 inches * 300 DPI (2.75" - 0.25" margins)
const ZINE_PAGE_HEIGHT = 1200; // 4.0 inches * 300 DPI (4.25" - 0.25" margins)
// Style-specific image generation prompts
const STYLE_PROMPTS: Record<string, string> = {
"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",
@ -74,7 +80,10 @@ export async function POST(request: NextRequest) {
}
function buildImagePrompt(outline: PageOutline, stylePrompt: string, tonePrompt: string): string {
return `Create a single zine page image (portrait orientation, 825x1275 pixels aspect ratio).
return `Create a single zine page image for print.
EXACT DIMENSIONS: ${ZINE_PAGE_WIDTH}x${ZINE_PAGE_HEIGHT} pixels (portrait orientation, 2.5" x 4.0" at 300 DPI)
This is 1/8 of a US Letter page with 0.125" margins for an 8-page mini-zine fold.
PAGE ${outline.pageNumber}: "${outline.title}"
Type: ${outline.type}
@ -88,12 +97,12 @@ 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
CRITICAL REQUIREMENTS:
- Portrait orientation ONLY (taller than wide)
- Fill the entire page - no blank margins
- Make it visually striking and cohesive
- The design should work in print (high contrast, clear details)`;
- 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(
@ -102,17 +111,11 @@ async function generateImageWithGemini(
style: string
): Promise<string> {
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}
@ -122,15 +125,17 @@ CRITICAL TEXT RENDERING INSTRUCTIONS:
- 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)
// Call Nano Banana (Gemini 2.5 Flash Image) directly - no geo-blocking from EU
try {
const result = await generateWithNanoBananaProViaRunPod(enhancedPrompt, apiKey, runpodApiKey, runpodEndpointId);
const result = await generateWithNanoBanana(enhancedPrompt, apiKey);
if (result) {
console.log("✅ Generated image with Nano Banana Pro via RunPod US");
return 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 Pro via RunPod error:", error);
console.error("Nano Banana error:", error);
}
// Final fallback: Create styled placeholder
@ -138,50 +143,62 @@ CRITICAL TEXT RENDERING INSTRUCTIONS:
return createStyledPlaceholder(outline, style);
}
// Nano Banana Pro via RunPod US proxy (bypasses geo-blocking in Germany)
async function generateWithNanoBananaProViaRunPod(
// Resize image to exact zine page dimensions (1/8 letter)
async function resizeToZineDimensions(base64Image: string): Promise<string> {
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,
runpodApiKey: string,
endpointId: string
apiKey: string
): Promise<string | null> {
const runpodUrl = `https://api.runpod.ai/v2/${endpointId}/runsync`;
const model = "gemini-2.5-flash-image";
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
console.log("Calling Nano Banana Pro via RunPod US proxy...");
console.log("Calling Nano Banana (gemini-2.5-flash-image)...");
const response = await fetch(runpodUrl, {
const response = await fetch(url, {
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"],
contents: [
{
parts: [{ text: prompt }],
},
],
generationConfig: {
responseModalities: ["image", "text"],
},
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error("RunPod API error:", response.status, errorText);
console.error("Gemini API error:", response.status, errorText);
return null;
}
const result = await response.json();
const data = result.output || result;
const data = await response.json();
if (data.error) {
console.error("Gemini API error via RunPod:", JSON.stringify(data.error));
console.error("Gemini API error:", JSON.stringify(data.error));
return null;
}
@ -189,7 +206,7 @@ async function generateWithNanoBananaProViaRunPod(
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");
console.log("✅ Successfully received image from Nano Banana");
return part.inlineData.data;
}
}
@ -261,7 +278,7 @@ async function createStyledPlaceholder(
const pageType = escapeXml(outline.type.toUpperCase());
const svg = `
<svg width="825" height="1275" xmlns="http://www.w3.org/2000/svg">
<svg width="${ZINE_PAGE_WIDTH}" height="${ZINE_PAGE_HEIGHT}" xmlns="http://www.w3.org/2000/svg">
<defs>
${s.pattern}
<style>

View File

@ -3,6 +3,12 @@ import { GoogleGenerativeAI } from "@google/generative-ai";
import { getZine, saveZine, getPageImagePath, readFileAsBase64, savePageImage } from "@/lib/storage";
import type { PageOutline } from "@/lib/gemini";
// Zine page dimensions: 1/8 of US Letter (8.5" x 11") at 300 DPI
// Page size: 2.75" x 4.25", with 0.125" margin on each side
// Content area: 2.5" x 4.0"
const ZINE_PAGE_WIDTH = 750; // 2.5 inches * 300 DPI (2.75" - 0.25" margins)
const ZINE_PAGE_HEIGHT = 1200; // 4.0 inches * 300 DPI (4.25" - 0.25" margins)
// Regeneration modes with denoising strengths
const MODE_STRENGTHS: Record<string, number> = {
refine: 0.25, // Keep most of image, minor tweaks
@ -183,8 +189,8 @@ async function generateWithFluxImg2Img(
num_inference_steps: 28,
guidance_scale: 3.5,
image_size: {
width: 768,
height: 1024 // Portrait for zine pages
width: ZINE_PAGE_WIDTH,
height: ZINE_PAGE_HEIGHT // 1/8 letter page (2.75" x 4.25" at 300 DPI)
},
output_format: "png"
}),