160 lines
5.1 KiB
TypeScript
160 lines
5.1 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getZine, saveZine, getPageImagePath, readFileAsBase64, savePageImage } from "@/lib/storage";
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json();
|
|
const { zineId, pageNumber, maskBase64, newText, style, tone } = body;
|
|
|
|
// Validate required fields
|
|
if (!zineId || !pageNumber || !maskBase64 || !newText) {
|
|
return NextResponse.json(
|
|
{ error: "Missing required fields: zineId, pageNumber, maskBase64, newText" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate Fal.ai API key
|
|
const falKey = process.env.FAL_KEY;
|
|
if (!falKey) {
|
|
return NextResponse.json(
|
|
{ error: "FAL_KEY not configured" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
// Verify zine exists
|
|
const zine = await getZine(zineId);
|
|
if (!zine) {
|
|
return NextResponse.json(
|
|
{ error: "Zine not found" },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Get existing page image
|
|
const existingImagePath = await getPageImagePath(zineId, pageNumber);
|
|
if (!existingImagePath) {
|
|
return NextResponse.json(
|
|
{ error: "Page image not found" },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
const existingImageBase64 = await readFileAsBase64(existingImagePath);
|
|
|
|
// Build the text inpainting prompt
|
|
const textPrompt = buildTextPrompt(newText, style, tone);
|
|
console.log(`Inpainting text on page ${pageNumber}: "${newText.slice(0, 50)}..."`);
|
|
|
|
// Call Fal.ai FLUX Pro Fill for inpainting
|
|
const newImageBase64 = await inpaintWithFluxFill(
|
|
existingImageBase64,
|
|
maskBase64,
|
|
textPrompt,
|
|
falKey
|
|
);
|
|
|
|
// Save the inpainted image
|
|
await savePageImage(zineId, pageNumber, newImageBase64);
|
|
|
|
// Update zine metadata
|
|
zine.updatedAt = new Date().toISOString();
|
|
await saveZine(zine);
|
|
|
|
// Return success with cache-busted image URL
|
|
const imageUrl = `/api/zine/${zineId}?image=p${pageNumber}&t=${Date.now()}`;
|
|
|
|
return NextResponse.json({
|
|
pageNumber,
|
|
imageUrl,
|
|
success: true,
|
|
});
|
|
} catch (error) {
|
|
console.error("Text inpainting error:", error);
|
|
return NextResponse.json(
|
|
{ error: error instanceof Error ? error.message : "Failed to inpaint text" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
function buildTextPrompt(newText: string, style: string, tone: string): string {
|
|
// Build a contextual prompt for text generation
|
|
const styleDescriptions: Record<string, string> = {
|
|
"punk-zine": "punk zine aesthetic, bold hand-drawn lettering, screen-printed style",
|
|
"collage": "collage art style, cut-out letters, mixed media typography",
|
|
"minimal": "clean minimal design, modern sans-serif typography",
|
|
"vintage": "vintage retro style, distressed typography, aged paper texture",
|
|
"psychedelic": "psychedelic art style, flowing organic letterforms, vibrant colors",
|
|
};
|
|
|
|
const toneDescriptions: Record<string, string> = {
|
|
"rebellious": "bold, confrontational, high-contrast",
|
|
"playful": "fun, whimsical, energetic",
|
|
"thoughtful": "contemplative, balanced, readable",
|
|
"informative": "clear, educational, professional",
|
|
"poetic": "artistic, expressive, lyrical",
|
|
};
|
|
|
|
const styleDesc = styleDescriptions[style] || "creative zine typography";
|
|
const toneDesc = toneDescriptions[tone] || "expressive";
|
|
|
|
return `${styleDesc}. The text clearly reads: "${newText}". ${toneDesc} aesthetic. Bold, clear lettering that integrates seamlessly with the surrounding design. High contrast for readability.`;
|
|
}
|
|
|
|
async function inpaintWithFluxFill(
|
|
imageBase64: string,
|
|
maskBase64: string,
|
|
prompt: string,
|
|
falKey: string
|
|
): Promise<string> {
|
|
console.log("Calling Fal.ai FLUX Pro Fill for inpainting...");
|
|
|
|
// Use fal.run for synchronous execution
|
|
const response = await fetch("https://fal.run/fal-ai/flux-pro/v1/fill", {
|
|
method: "POST",
|
|
headers: {
|
|
"Authorization": `Key ${falKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
image_url: `data:image/png;base64,${imageBase64}`,
|
|
mask_url: `data:image/png;base64,${maskBase64}`,
|
|
prompt: prompt,
|
|
num_inference_steps: 40,
|
|
guidance_scale: 7.0,
|
|
output_format: "png",
|
|
safety_tolerance: 3,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error("Fal.ai error:", response.status, errorText);
|
|
throw new Error(`Fal.ai API error: ${response.status} - ${errorText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log("Fal.ai inpainting response received");
|
|
|
|
// Extract the image URL from response
|
|
if (result.images && result.images.length > 0) {
|
|
const imageUrl = result.images[0].url;
|
|
console.log("Downloading inpainted image...");
|
|
return await fetchImageAsBase64(imageUrl);
|
|
}
|
|
|
|
console.error("Fal.ai response:", JSON.stringify(result).slice(0, 500));
|
|
throw new Error("No image in Fal.ai response");
|
|
}
|
|
|
|
async function fetchImageAsBase64(url: string): Promise<string> {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch generated image");
|
|
}
|
|
const buffer = await response.arrayBuffer();
|
|
return Buffer.from(buffer).toString("base64");
|
|
}
|