Refactor image model selection and dimensions

This commit is contained in:
Riccardo Giorato
2025-12-23 21:41:38 +01:00
parent ff25be7c85
commit bbe8c8caf8
4 changed files with 132 additions and 41 deletions
+50 -17
View File
@@ -1,8 +1,19 @@
import { type NextRequest, NextResponse } from "next/server";
import Together from "together-ai";
import { updatePage, createStory, createPage, getNextPageNumber } from "@/lib/db-actions";
import {
updatePage,
createStory,
createPage,
getNextPageNumber,
} from "@/lib/db-actions";
const FIXED_DIMENSIONS = { width: 864, height: 1184 };
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 };
async function analyzeCharacterImage(
imageBase64: string,
@@ -101,7 +112,11 @@ export async function POST(request: NextRequest) {
previousContext = "",
} = await request.json();
console.log("Received request:", { storyId, prompt: prompt?.substring(0, 50), characterImagesCount: characterImages.length });
console.log("Received request:", {
storyId,
prompt: prompt?.substring(0, 50),
characterImagesCount: characterImages.length,
});
if (!prompt || !apiKey) {
return NextResponse.json(
@@ -157,24 +172,36 @@ export async function POST(request: NextRequest) {
if (characterImages.length > 0) {
if (characterImages.length === 1) {
characterSection = `
CRITICAL INSTRUCTIONS FOR CHARACTER REFERENCE:
- Use the uploaded character image as reference for the main character
- This character must appear in ALL 5 panels as the protagonist
- Maintain exact appearance from the reference image
- Draw them in ${style} comic art style but preserve their features, clothing, and pose from the image`;
CRITICAL FACE CONSISTENCY INSTRUCTIONS:
- REFERENCE CHARACTER: Use the uploaded image as EXACT reference for the protagonist's face and appearance
- FACE MATCHING: The character's face must be IDENTICAL to the reference image - same eyes, nose, mouth, hair, facial structure
- APPEARANCE PRESERVATION: Maintain exact skin tone, hair color/style, eye color, and distinctive facial features
- CHARACTER CONSISTENCY: This exact same character must appear in ALL 5 panels with the same face throughout
- STYLE APPLICATION: Apply ${style} comic art style to the body/pose/action but KEEP THE FACE EXACTLY AS IN THE REFERENCE IMAGE
- NO VARIATION: Do not alter, modify, or change the character's face in any way from the reference`;
} else if (characterImages.length === 2) {
characterSection = `
CRITICAL INSTRUCTIONS FOR CHARACTER REFERENCES:
- Use both uploaded character images as references for the two main characters
- CHARACTER 1 (first image) and CHARACTER 2 (second image) must appear together in at least 4 of the 5 panels
- Keep them VISUALLY DISTINCT - preserve exact appearances from their respective reference images
- Draw both in ${style} comic art style but maintain their individual features and clothing
- They are the two protagonists interacting with each other throughout the story`;
CRITICAL DUAL CHARACTER FACE CONSISTENCY INSTRUCTIONS:
- CHARACTER 1 REFERENCE: Use the FIRST uploaded image as EXACT reference for Character 1's face and appearance
- CHARACTER 2 REFERENCE: Use the SECOND uploaded image as EXACT reference for Character 2's face and appearance
- FACE MATCHING: Both characters' faces must be IDENTICAL to their respective reference images
- VISUAL DISTINCTION: Keep both characters clearly visually distinct with their unique faces, hair, and features
- CONSISTENT PRESENCE: Both characters must appear together in at least 4 of the 5 panels
- STYLE APPLICATION: Apply ${style} comic art style while maintaining EXACT facial features from references
- NO FACE VARIATION: Never alter or modify either character's face from their reference images`;
}
}
const systemPrompt = `Professional comic book page illustration.
${continuationContext}
${characterSection}
CHARACTER CONSISTENCY RULES (HIGHEST PRIORITY):
- If reference images are provided, the characters' FACES must be 100% identical to the reference images
- Never change hair color, eye color, facial structure, or distinctive features
- Apply comic style to body/pose/action but preserve exact facial appearance
- Same character must look identical across all panels they appear in
TEXT AND LETTERING (CRITICAL):
- All text in speech bubbles must be PERFECTLY CLEAR, LEGIBLE, and correctly spelled
- Use bold clean comic book lettering, large and easy to read
@@ -209,11 +236,11 @@ COMPOSITION:
let response;
try {
response = await client.images.generate({
model: "google/flash-image-2.5",
model: IMAGE_MODEL,
prompt: fullPrompt,
width: dimensions.width,
height: dimensions.height,
n: 1,
temperature: 0.1, // Lower temperature for more consistent face matching
reference_images:
characterImages.length > 0 ? characterImages : undefined,
});
@@ -274,7 +301,13 @@ COMPOSITION:
const responseData = storyId
? { imageUrl, pageId: page.id, pageNumber: page.pageNumber }
: { imageUrl, storyId: story!.id, storySlug: story!.slug, pageId: page.id, pageNumber: page.pageNumber };
: {
imageUrl,
storyId: story!.id,
storySlug: story!.slug,
pageId: page.id,
pageNumber: page.pageNumber,
};
return NextResponse.json(responseData);
} catch (error) {
+36 -15
View File
@@ -4,6 +4,8 @@ import { useState, useRef, useEffect } from "react"
import { Upload, X, Loader2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useToast } from "@/hooks/use-toast"
import { validateFileForUpload, generateFilePreview } from "@/lib/file-utils"
const COMIC_STYLES = [
{ id: "american-modern", name: "American Modern" },
@@ -47,6 +49,7 @@ export function GeneratePageModal({
const [isGenerating, setIsGenerating] = useState(false)
const [isContinuing, setIsContinuing] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const { toast } = useToast()
const selectedStyle = previousPageStyle || "noir"
@@ -66,25 +69,43 @@ export function GeneratePageModal({
}
}, [previousCharacters])
const handleFiles = (newFiles: FileList | null) => {
const handleFiles = async (newFiles: FileList | null) => {
if (!newFiles) return
const validFiles = Array.from(newFiles).filter((file) => file.type.startsWith("image/"))
const totalFiles = [...uploadedFiles, ...validFiles].slice(0, 2) // Max 2 files
const filesArray = Array.from(newFiles)
// Validate files (including WebP rejection)
const validationResults = filesArray.map(file => ({
file,
validation: validateFileForUpload(file, true)
}))
// Show errors for invalid files
validationResults.forEach(({ validation }) => {
if (!validation.valid && validation.error) {
toast({
title: "Invalid file",
description: validation.error,
variant: "destructive",
duration: 4000,
})
}
})
const validFiles = validationResults
.filter(({ validation }) => validation.valid)
.map(({ file }) => file)
if (validFiles.length === 0) return
const totalFiles = [...uploadedFiles, ...validFiles].slice(0, 2) // Max 2 files
setUploadedFiles(totalFiles)
const newPreviews: string[] = []
totalFiles.forEach((file, index) => {
const reader = new FileReader()
reader.onload = (e) => {
newPreviews[index] = e.target?.result as string
if (newPreviews.filter(Boolean).length === totalFiles.length) {
setPreviews([...newPreviews])
}
}
reader.readAsDataURL(file)
})
// Generate previews for all files
const newPreviews = await Promise.all(
totalFiles.map((file) => generateFilePreview(file))
)
setPreviews(newPreviews)
}
const removeFile = (index: number) => {
@@ -257,7 +278,7 @@ export function GeneratePageModal({
<input
ref={fileInputRef}
type="file"
accept="image/*"
accept="image/png,image/jpeg,image/jpg"
multiple
className="hidden"
onChange={(e) => handleFiles(e.target.files)}
+17 -9
View File
@@ -4,19 +4,27 @@ import type React from "react"
import { useState, useRef } from "react"
import { X, Upload } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useToast } from "@/hooks/use-toast"
import { validateFileForUpload, generateFilePreview } from "@/lib/file-utils"
export function CharacterUploader() {
const [preview, setPreview] = useState<string | null>(null)
const [isDragging, setIsDragging] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const { toast } = useToast()
const handleFile = (file: File) => {
if (file && file.type.startsWith("image/")) {
const reader = new FileReader()
reader.onload = (e) => {
setPreview(e.target?.result as string)
}
reader.readAsDataURL(file)
const handleFile = async (file: File) => {
const validation = validateFileForUpload(file, true)
if (validation.valid) {
const previewUrl = await generateFilePreview(file)
setPreview(previewUrl)
} else if (validation.error) {
toast({
title: "Invalid file",
description: validation.error,
variant: "destructive",
duration: 4000,
})
}
}
@@ -24,7 +32,7 @@ export function CharacterUploader() {
e.preventDefault()
setIsDragging(false)
const file = e.dataTransfer.files[0]
handleFile(file)
if (file) handleFile(file)
}
const handleDragOver = (e: React.DragEvent) => {
@@ -77,7 +85,7 @@ export function CharacterUploader() {
<input
ref={fileInputRef}
type="file"
accept="image/*"
accept="image/png,image/jpeg,image/jpg"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0]
+29
View File
@@ -0,0 +1,29 @@
export function validateFileForUpload(file: File, rejectWebP: boolean = true): { valid: boolean; error?: string } {
// Reject WebP files
if (rejectWebP && (file.type === "image/webp" || file.name.toLowerCase().endsWith('.webp'))) {
return {
valid: false,
error: "WebP files not supported. Please upload PNG, JPG, or JPEG files instead of WebP."
}
}
// Check if file is an image
if (!file.type.startsWith("image/")) {
return {
valid: false,
error: "Only image files are supported."
}
}
return { valid: true }
}
export function generateFilePreview(file: File): Promise<string> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
resolve(e.target?.result as string)
}
reader.readAsDataURL(file)
})
}