7fd5367577
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.
226 lines
6.0 KiB
TypeScript
226 lines
6.0 KiB
TypeScript
import { type NextRequest, NextResponse } from "next/server";
|
|
import Together from "together-ai";
|
|
import { auth } from "@clerk/nextjs/server";
|
|
import {
|
|
updatePage,
|
|
createStory,
|
|
createPage,
|
|
getNextPageNumber,
|
|
getStoryById,
|
|
} from "@/lib/db-actions";
|
|
import { freeTierRateLimit } from "@/lib/rate-limit";
|
|
import { COMIC_STYLES } from "@/lib/constants";
|
|
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,
|
|
apiKey,
|
|
style = "noir",
|
|
characterImages = [],
|
|
isContinuation = false,
|
|
previousContext = "",
|
|
} = await request.json();
|
|
|
|
if (!prompt) {
|
|
return NextResponse.json(
|
|
{ error: "Missing required fields" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Determine which API key to use
|
|
let finalApiKey = apiKey;
|
|
const isUsingFreeTier = !apiKey;
|
|
|
|
if (isUsingFreeTier) {
|
|
// Using free tier - apply rate limiting
|
|
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)
|
|
); // days
|
|
|
|
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 for unlimited access.`,
|
|
resetDate: resetDate.toISOString(),
|
|
isRateLimited: true,
|
|
},
|
|
{ status: 429 }
|
|
);
|
|
}
|
|
|
|
// Use default API key for free tier
|
|
finalApiKey = process.env.TOGETHER_API_KEY_DEFAULT;
|
|
if (!finalApiKey) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "Server configuration error - default API key not available",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
let page;
|
|
let story;
|
|
|
|
if (storyId) {
|
|
const story = await getStoryById(storyId);
|
|
if (!story) {
|
|
return NextResponse.json({ error: "Story not found" }, { status: 404 });
|
|
}
|
|
|
|
const nextPageNumber = await getNextPageNumber(storyId);
|
|
page = await createPage({
|
|
storyId,
|
|
pageNumber: nextPageNumber,
|
|
prompt,
|
|
characterImageUrls: characterImages,
|
|
});
|
|
} else {
|
|
story = await createStory({
|
|
title: prompt.length > 50 ? prompt.substring(0, 50) + "..." : prompt,
|
|
description: undefined,
|
|
userId: userId,
|
|
style,
|
|
});
|
|
|
|
page = await createPage({
|
|
storyId: story.id,
|
|
pageNumber: 1,
|
|
prompt,
|
|
characterImageUrls: characterImages,
|
|
});
|
|
}
|
|
|
|
const dimensions = FIXED_DIMENSIONS;
|
|
|
|
const fullPrompt = buildComicPrompt({
|
|
prompt,
|
|
style,
|
|
characterImages,
|
|
isContinuation,
|
|
previousContext,
|
|
});
|
|
|
|
const client = new Together({ apiKey: finalApiKey });
|
|
|
|
let response;
|
|
try {
|
|
response = await client.images.generate({
|
|
model: IMAGE_MODEL,
|
|
prompt: fullPrompt,
|
|
width: dimensions.width,
|
|
height: dimensions.height,
|
|
temperature: 0.1, // Lower temperature for more consistent face matching
|
|
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. Please add credits to your Together.ai account at https://api.together.ai/settings/billing or update your API key.",
|
|
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;
|
|
|
|
// Upload image to S3 for permanent storage
|
|
const s3Key = `${storyId || story!.id}/page-${page.pageNumber}-${Date.now()}.jpg`;
|
|
const s3ImageUrl = await uploadImageToS3(imageUrl, s3Key);
|
|
|
|
// Update page in database with S3 URL
|
|
try {
|
|
await updatePage(page.id, s3ImageUrl);
|
|
} catch (dbError) {
|
|
console.error("Error updating page in database:", dbError);
|
|
return NextResponse.json(
|
|
{ error: "Failed to save generated image" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
const responseData = storyId
|
|
? { imageUrl: s3ImageUrl, pageId: page.id, pageNumber: page.pageNumber }
|
|
: {
|
|
imageUrl: s3ImageUrl,
|
|
storyId: story!.id,
|
|
storySlug: story!.slug,
|
|
pageId: page.id,
|
|
pageNumber: page.pageNumber,
|
|
};
|
|
|
|
return NextResponse.json(responseData);
|
|
} catch (error) {
|
|
console.error("Error in generate-comic API:", error);
|
|
return NextResponse.json(
|
|
{
|
|
error: `Internal server error: ${
|
|
error instanceof Error ? error.message : "Unknown error"
|
|
}`,
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|