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:
parent
23ace0a7f3
commit
8dc3145e5c
|
|
@ -2,6 +2,12 @@ import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getZine, saveZine, savePageImage } from "@/lib/storage";
|
import { getZine, saveZine, savePageImage } from "@/lib/storage";
|
||||||
import type { PageOutline } from "@/lib/gemini";
|
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
|
// Style-specific image generation prompts
|
||||||
const STYLE_PROMPTS: Record<string, string> = {
|
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",
|
"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 {
|
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}"
|
PAGE ${outline.pageNumber}: "${outline.title}"
|
||||||
Type: ${outline.type}
|
Type: ${outline.type}
|
||||||
|
|
@ -88,12 +97,12 @@ Mood/Tone: ${tonePrompt}
|
||||||
Detailed requirements:
|
Detailed requirements:
|
||||||
${outline.imagePrompt}
|
${outline.imagePrompt}
|
||||||
|
|
||||||
IMPORTANT:
|
CRITICAL REQUIREMENTS:
|
||||||
- This is a SINGLE page that will be printed
|
- Portrait orientation ONLY (taller than wide)
|
||||||
- Include any text/typography as part of the graphic design
|
|
||||||
- Fill the entire page - no blank margins
|
- Fill the entire page - no blank margins
|
||||||
- Make it visually striking and cohesive
|
- Design must work at small print size (high contrast, clear details)
|
||||||
- The design should work in print (high contrast, clear details)`;
|
- Include any text/typography as part of the graphic design
|
||||||
|
- Make it visually striking and cohesive`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateImageWithGemini(
|
async function generateImageWithGemini(
|
||||||
|
|
@ -102,17 +111,11 @@ async function generateImageWithGemini(
|
||||||
style: string
|
style: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const apiKey = process.env.GEMINI_API_KEY;
|
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) {
|
if (!apiKey) {
|
||||||
throw new Error("GEMINI_API_KEY not configured");
|
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
|
// Enhanced prompt for better text rendering and image quality
|
||||||
const enhancedPrompt = `${prompt}
|
const enhancedPrompt = `${prompt}
|
||||||
|
|
||||||
|
|
@ -122,15 +125,17 @@ CRITICAL TEXT RENDERING INSTRUCTIONS:
|
||||||
- Avoid distorted or warped letters
|
- Avoid distorted or warped letters
|
||||||
- Text should be integrated naturally into the design`;
|
- 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 {
|
try {
|
||||||
const result = await generateWithNanoBananaProViaRunPod(enhancedPrompt, apiKey, runpodApiKey, runpodEndpointId);
|
const result = await generateWithNanoBanana(enhancedPrompt, apiKey);
|
||||||
if (result) {
|
if (result) {
|
||||||
console.log("✅ Generated image with Nano Banana Pro via RunPod US");
|
console.log("✅ Generated image with Nano Banana (gemini-2.5-flash-image)");
|
||||||
return result;
|
// Resize to exact zine dimensions
|
||||||
|
const resizedImage = await resizeToZineDimensions(result);
|
||||||
|
return resizedImage;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Nano Banana Pro via RunPod error:", error);
|
console.error("Nano Banana error:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final fallback: Create styled placeholder
|
// Final fallback: Create styled placeholder
|
||||||
|
|
@ -138,50 +143,62 @@ CRITICAL TEXT RENDERING INSTRUCTIONS:
|
||||||
return createStyledPlaceholder(outline, style);
|
return createStyledPlaceholder(outline, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nano Banana Pro via RunPod US proxy (bypasses geo-blocking in Germany)
|
// Resize image to exact zine page dimensions (1/8 letter)
|
||||||
async function generateWithNanoBananaProViaRunPod(
|
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,
|
prompt: string,
|
||||||
apiKey: string,
|
apiKey: string
|
||||||
runpodApiKey: string,
|
|
||||||
endpointId: string
|
|
||||||
): Promise<string | null> {
|
): 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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": `Bearer ${runpodApiKey}`,
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
input: {
|
contents: [
|
||||||
api_key: apiKey,
|
{
|
||||||
model: "gemini-2.0-flash-exp-image-generation",
|
parts: [{ text: prompt }],
|
||||||
contents: [
|
|
||||||
{
|
|
||||||
parts: [{ text: prompt }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
generationConfig: {
|
|
||||||
responseModalities: ["IMAGE"],
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
generationConfig: {
|
||||||
|
responseModalities: ["image", "text"],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error("RunPod API error:", response.status, errorText);
|
console.error("Gemini API error:", response.status, errorText);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const data = await response.json();
|
||||||
const data = result.output || result;
|
|
||||||
|
|
||||||
if (data.error) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,7 +206,7 @@ async function generateWithNanoBananaProViaRunPod(
|
||||||
const parts = data.candidates?.[0]?.content?.parts || [];
|
const parts = data.candidates?.[0]?.content?.parts || [];
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (part.inlineData?.mimeType?.startsWith("image/")) {
|
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;
|
return part.inlineData.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +278,7 @@ async function createStyledPlaceholder(
|
||||||
const pageType = escapeXml(outline.type.toUpperCase());
|
const pageType = escapeXml(outline.type.toUpperCase());
|
||||||
|
|
||||||
const svg = `
|
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>
|
<defs>
|
||||||
${s.pattern}
|
${s.pattern}
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ import { GoogleGenerativeAI } from "@google/generative-ai";
|
||||||
import { getZine, saveZine, getPageImagePath, readFileAsBase64, savePageImage } from "@/lib/storage";
|
import { getZine, saveZine, getPageImagePath, readFileAsBase64, savePageImage } from "@/lib/storage";
|
||||||
import type { PageOutline } from "@/lib/gemini";
|
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
|
// Regeneration modes with denoising strengths
|
||||||
const MODE_STRENGTHS: Record<string, number> = {
|
const MODE_STRENGTHS: Record<string, number> = {
|
||||||
refine: 0.25, // Keep most of image, minor tweaks
|
refine: 0.25, // Keep most of image, minor tweaks
|
||||||
|
|
@ -183,8 +189,8 @@ async function generateWithFluxImg2Img(
|
||||||
num_inference_steps: 28,
|
num_inference_steps: 28,
|
||||||
guidance_scale: 3.5,
|
guidance_scale: 3.5,
|
||||||
image_size: {
|
image_size: {
|
||||||
width: 768,
|
width: ZINE_PAGE_WIDTH,
|
||||||
height: 1024 // Portrait for zine pages
|
height: ZINE_PAGE_HEIGHT // 1/8 letter page (2.75" x 4.25" at 300 DPI)
|
||||||
},
|
},
|
||||||
output_format: "png"
|
output_format: "png"
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue