From d138577a9d232bfe70a868610f4a00b947269df0 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Mon, 22 Jun 2026 12:14:31 +0200 Subject: [PATCH] feat: mockup generation API route using gpt-image-1 --- src/app/api/mockup/generate/route.ts | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/app/api/mockup/generate/route.ts diff --git a/src/app/api/mockup/generate/route.ts b/src/app/api/mockup/generate/route.ts new file mode 100644 index 0000000..f3172cd --- /dev/null +++ b/src/app/api/mockup/generate/route.ts @@ -0,0 +1,66 @@ +import { NextRequest } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { readFile } from 'fs/promises' +import { getTemplateImagePath, listTemplates } from '@/lib/mockupTemplates' +import type { MockupGenerateRequest } from '@/types/mockup' + +function buildPrompt(req: MockupGenerateRequest): string { + const familyContext: Record = { + 'Menuiserie': 'door installed in a residential entrance or hallway, visible wall and floor context', + 'Baies vitrées': 'bay window or sliding door installed in a living room with garden view', + 'Carrelage': 'tiles covering the floor or wall of a modern room', + } + const context = familyContext[req.family] ?? 'product installed in an appropriate room setting' + const dimClause = req.dimensions + ? ` The product dimensions are exactly ${req.dimensions} — preserve these exact proportions, do NOT resize or distort the product to fit the scene.` + : '' + const instructionClause = req.instruction ? ` Additional instruction: ${req.instruction}.` : '' + + return `Replace the product in the reference scene with the provided product image (${req.productName}). Show the ${context}.${dimClause} Keep the product's exact design, texture, colors and proportions identical to the provided image. Maintain realistic perspective and lighting consistent with the scene.${instructionClause}` +} + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions) + if (!session) return Response.json({ error: 'Non authentifié' }, { status: 401 }) + + const apiKey = process.env.OPENAI_API_KEY + if (!apiKey) return Response.json({ error: 'OPENAI_API_KEY manquant' }, { status: 500 }) + + const body = await req.json() as MockupGenerateRequest + + const templates = await listTemplates() + const template = templates.find(t => t.id === body.templateId) + if (!template) return Response.json({ error: 'Template introuvable' }, { status: 404 }) + + const templateBuffer = await readFile(getTemplateImagePath(template.filename)) + const templateExt = template.filename.split('.').pop() ?? 'jpg' + const templateMime = templateExt === 'png' ? 'image/png' : 'image/jpeg' + + const productBuffer = Buffer.from(body.productImageBase64, 'base64') + + const form = new FormData() + form.append('model', 'gpt-image-1') + form.append('prompt', buildPrompt(body)) + form.append('n', '1') + form.append('size', '1024x1024') + form.append('image[]', new Blob([templateBuffer], { type: templateMime }), `template.${templateExt}`) + form.append('image[]', new Blob([productBuffer], { type: 'image/png' }), 'product.png') + + const openaiRes = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${apiKey}` }, + body: form, + }) + + if (!openaiRes.ok) { + const err = await openaiRes.text() + return Response.json({ error: `OpenAI error: ${err}` }, { status: 502 }) + } + + const data = await openaiRes.json() as { data: Array<{ b64_json?: string }> } + const b64 = data.data?.[0]?.b64_json + if (!b64) return Response.json({ error: 'Pas de résultat OpenAI' }, { status: 502 }) + + return Response.json({ imageBase64: b64 }) +}