Refactor comic page generation and add add-page API
Introduces a new /api/add-page endpoint for adding pages to existing comics, refactors prompt construction into a shared utility (lib/prompt.ts), and updates the editor and modal components to use the new API and prompt builder. Removes manual style and character selection in the modal, streamlining the page generation workflow and ensuring previous story context is referenced automatically.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import Together from "together-ai";
|
||||
import {
|
||||
updatePage,
|
||||
createPage,
|
||||
getNextPageNumber,
|
||||
getStoryWithPagesBySlug,
|
||||
} from "@/lib/db-actions";
|
||||
import { freeTierRateLimit } from "@/lib/rate-limit";
|
||||
import { uploadImageToS3 } from "@/lib/s3-upload";
|
||||
import { buildComicPrompt } from "@/lib/prompt";
|
||||
|
||||
const NEW_MODEL = false;
|
||||
|
||||
const IMAGE_MODEL = NEW_MODEL
|
||||
? "google/gemini-3-pro-image"
|
||||
: "google/flash-image-2.5";
|
||||
|
||||
const FIXED_DIMENSIONS = NEW_MODEL
|
||||
? { width: 896, height: 1200 }
|
||||
: { width: 864, height: 1184 };
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Authentication required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { storyId, prompt, characterImages = [] } = await request.json();
|
||||
|
||||
if (!storyId || !prompt) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields: storyId and prompt" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get the story and all its pages
|
||||
const storyData = await getStoryWithPagesBySlug(storyId);
|
||||
if (!storyData) {
|
||||
return NextResponse.json({ error: "Story not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const { story, pages } = storyData;
|
||||
|
||||
// Check ownership
|
||||
if (story.userId !== userId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Apply rate limiting for free tier
|
||||
const hasApiKey = request.headers.get('x-api-key');
|
||||
if (!hasApiKey) {
|
||||
const { success, reset } = await freeTierRateLimit.limit(userId);
|
||||
if (!success) {
|
||||
const resetDate = new Date(reset);
|
||||
const timeUntilReset = Math.ceil(
|
||||
(reset - Date.now()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Free tier limit reached. You can generate 1 comic per week. Try again in ${timeUntilReset} day(s), or provide your own API key.`,
|
||||
resetDate: resetDate.toISOString(),
|
||||
isRateLimited: true,
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const nextPageNumber = await getNextPageNumber(story.id);
|
||||
const page = await createPage({
|
||||
storyId: story.id,
|
||||
pageNumber: nextPageNumber,
|
||||
prompt,
|
||||
characterImageUrls: characterImages,
|
||||
});
|
||||
|
||||
const dimensions = FIXED_DIMENSIONS;
|
||||
|
||||
// Build the prompt with continuation context
|
||||
const previousPages = pages.map(p => ({
|
||||
prompt: p.prompt,
|
||||
characterImages: p.characterImageUrls,
|
||||
}));
|
||||
|
||||
const fullPrompt = buildComicPrompt({
|
||||
prompt,
|
||||
style: story.style,
|
||||
characterImages,
|
||||
isAddPage: true,
|
||||
previousPages,
|
||||
});
|
||||
|
||||
const client = new Together({ apiKey: process.env.TOGETHER_API_KEY_DEFAULT });
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await client.images.generate({
|
||||
model: IMAGE_MODEL,
|
||||
prompt: fullPrompt,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
temperature: 0.1,
|
||||
reference_images: characterImages.length > 0 ? characterImages : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Together AI API error:", error);
|
||||
|
||||
if (error instanceof Error && "status" in error) {
|
||||
const status = (error as any).status;
|
||||
if (status === 402) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Insufficient API credits.",
|
||||
errorType: "credit_limit",
|
||||
},
|
||||
{ status: 402 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.message || `Failed to generate image: ${status}`,
|
||||
errorType: "api_error",
|
||||
},
|
||||
{ status: status || 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Internal server error: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.data || !response.data[0] || !response.data[0].url) {
|
||||
return NextResponse.json(
|
||||
{ error: "No image URL in response" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const imageUrl = response.data[0].url;
|
||||
const s3Key = `${story.id}/page-${page.pageNumber}-${Date.now()}.jpg`;
|
||||
const s3ImageUrl = await uploadImageToS3(imageUrl, s3Key);
|
||||
|
||||
await updatePage(page.id, s3ImageUrl);
|
||||
|
||||
return NextResponse.json({
|
||||
imageUrl: s3ImageUrl,
|
||||
pageId: page.id,
|
||||
pageNumber: page.pageNumber,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in add-page API:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Internal server error: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user