← back to Norma
app/api/social/ai-image/route.ts
146 lines
import { NextRequest, NextResponse } from 'next/server';
import { randomUUID } from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
import { query } from '@/lib/db';
export const dynamic = 'force-dynamic';
const GEMINI_IMAGE_KEY = process.env.GEMINI_IMAGE_API_KEY || process.env.GEMINI_API_KEY || '';
const GEMINI_IMAGE_MODEL = 'gemini-2.5-flash-image';
const GEMINI_IMAGE_URL =
`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_IMAGE_MODEL}:generateContent?key=${GEMINI_IMAGE_KEY}`;
const OUT_DIR = '/root/Projects/Norma/public/social-generated';
/**
* POST /api/social/ai-image
* Body: { prompt: string, post_id?: uuid }
*/
export async function POST(request: NextRequest) {
let body: { prompt?: string; post_id?: string };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
const { prompt, post_id } = body;
if (!prompt || typeof prompt !== 'string') {
return NextResponse.json({ error: 'prompt is required' }, { status: 400 });
}
const recordError = async (errMsg: string) => {
try {
const ins = await query(
`INSERT INTO social_ai_images (prompt, model, error, used_in_post)
VALUES ($1, $2, $3, $4)
RETURNING id`,
[prompt, GEMINI_IMAGE_MODEL, errMsg, post_id ?? null],
);
return ins.rows[0]?.id ?? null;
} catch (dbErr) {
console.error('[ai-image] failed to record error row:', (dbErr as Error).message);
return null;
}
};
let geminiResponse: Response;
try {
geminiResponse = await fetch(GEMINI_IMAGE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { responseModalities: ['TEXT', 'IMAGE'] },
}),
});
} catch (err) {
const msg = `Gemini fetch failed: ${(err as Error).message}`;
console.error('[ai-image]', msg);
const id = await recordError(msg);
return NextResponse.json(
{ error: 'Gemini image service unavailable', id },
{ status: 500 },
);
}
if (!geminiResponse.ok) {
const errText = await geminiResponse.text().catch(() => '');
const status = geminiResponse.status;
const msg = `Gemini API ${status}: ${errText.slice(0, 300)}`;
console.error('[ai-image]', msg);
const id = await recordError(msg);
if (status === 403 || status === 401) {
return NextResponse.json(
{
error: 'Gemini image key needs refresh',
detail: 'The image generation API key is revoked or unauthorized. Update GEMINI_IMAGE_KEY.',
id,
},
{ status: 500 },
);
}
return NextResponse.json(
{ error: `Gemini image generation failed (${status})`, id },
{ status: 500 },
);
}
let json: any;
try {
json = await geminiResponse.json();
} catch (err) {
const msg = `Invalid Gemini JSON: ${(err as Error).message}`;
const id = await recordError(msg);
return NextResponse.json({ error: msg, id }, { status: 500 });
}
// Find inlineData in parts
const parts: any[] = json?.candidates?.[0]?.content?.parts ?? [];
const imagePart = parts.find((p) => p?.inlineData?.data);
const b64: string | undefined = imagePart?.inlineData?.data;
if (!b64) {
const msg = 'Gemini response contained no image data';
const id = await recordError(msg);
return NextResponse.json({ error: msg, id }, { status: 500 });
}
const fileId = randomUUID();
const fileName = `${fileId}.png`;
const filePath = path.join(OUT_DIR, fileName);
const publicUrl = `/social-generated/${fileName}`;
try {
await fs.mkdir(OUT_DIR, { recursive: true });
await fs.writeFile(filePath, Buffer.from(b64, 'base64'));
} catch (err) {
const msg = `Failed to write image: ${(err as Error).message}`;
console.error('[ai-image]', msg);
const id = await recordError(msg);
return NextResponse.json({ error: msg, id }, { status: 500 });
}
try {
const inserted = await query(
`INSERT INTO social_ai_images (prompt, model, image_url, image_b64, used_in_post)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, prompt, image_url`,
[prompt, GEMINI_IMAGE_MODEL, publicUrl, b64, post_id ?? null],
);
const row = inserted.rows[0];
return NextResponse.json({
id: row.id,
image_url: row.image_url,
prompt: row.prompt,
});
} catch (err) {
const msg = `DB insert failed: ${(err as Error).message}`;
console.error('[ai-image]', msg);
return NextResponse.json({ error: msg, image_url: publicUrl }, { status: 500 });
}
}