import crypto from 'node:crypto'; import cors from 'cors'; import express, { Request, Response } from 'express'; import multer from 'multer'; import { buildGenerationResponse, GenerationProvider, GenerationResponse, generationRequestSchema, UploadedImage } from './generation/generation.js'; import { KaggleProgress, generateKaggleImages } from './providers/kaggle.js'; const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024, files: 50 } }); type GenerationJob = { id: string; provider: GenerationProvider; status: 'queued' | 'running' | 'completed' | 'failed'; createdAt: string; updatedAt: string; result?: GenerationResponse; error?: string; progress: number; message: string; events: string[]; }; if (!globalThis.crypto) { globalThis.crypto = crypto.webcrypto; } export function createApp(): express.Express { const app = express(); const jobs = new Map(); app.use(cors({ origin: true })); app.use(express.json()); app.get('/health', (_request: Request, response: Response) => { response.json({ status: 'ok' }); }); app.post('/api/generations', upload.array('images'), async (request: Request, response: Response) => { const parsedRequest = generationRequestSchema.safeParse(request.body); if (!parsedRequest.success) { response.status(400).json({ error: 'Invalid generation request' }); return; } const files = request.files ?? []; const images = files .filter((file) => file.mimetype.startsWith('image/')) .map((file) => ({ originalName: file.originalname, mimeType: file.mimetype, size: file.size, buffer: file.buffer, })); if (images.length === 0) { response.status(400).json({ error: 'At least one image is required' }); return; } const provider = getGenerationProvider(); const now = new Date().toISOString(); const job: GenerationJob = { id: crypto.randomUUID(), provider, status: 'queued', createdAt: now, updatedAt: now, progress: 0, message: 'Taak aangemaakt.', events: ['Taak aangemaakt.'], }; jobs.set(job.id, job); runGenerationJob(job, normalizeGenerationInput(parsedRequest.data, images), images); response.status(202).json(toJobResponse(job)); }); app.get('/api/generations/:jobId', (request: Request, response: Response) => { const jobId = request.params.jobId; if (typeof jobId !== 'string') { response.status(404).json({ error: 'Generation job not found' }); return; } const job = jobs.get(jobId); if (job === undefined) { response.status(404).json({ error: 'Generation job not found' }); return; } response.json(toJobResponse(job)); }); function runGenerationJob(job: GenerationJob, input: ReturnType, images: UploadedImage[]): void { void (async () => { updateJob(job, { status: 'running', progress: 5, message: 'Beelden worden voorbereid.' }); try { const result = job.provider === 'kaggle' ? await generateKaggleImages(input, images, (progress) => updateProgress(job, progress)) : buildGenerationResponse(input, images); updateJob(job, { status: 'completed', result, progress: 100, message: 'Beeld is klaar.' }); } catch (error) { updateJob(job, { status: 'failed', progress: 100, message: 'Generatie mislukt.', error: getSafeGenerationError(error), }); } })(); } function updateJob(job: GenerationJob, updates: Partial): void { const nextMessage = updates.message; if (nextMessage !== undefined && job.events[job.events.length - 1] !== nextMessage) { job.events.push(nextMessage); } Object.assign(job, updates, { updatedAt: new Date().toISOString() }); } function updateProgress(job: GenerationJob, progress: KaggleProgress): void { updateJob(job, { progress: Math.max(job.progress, progress.progress), message: progress.message, }); } return app; } function normalizeGenerationInput(input: ReturnType, images: UploadedImage[]): ReturnType { if (images.length > 1) { return { ...input, mode: 'menu', ratio: '1:1' }; } return input; } function getSafeGenerationError(error: unknown): string { const message = error instanceof Error ? error.message : 'Generation provider failed'; if (/out of memory|cuda/i.test(message)) { return 'De beeldgenerator had onvoldoende GPU-geheugen. Probeer opnieuw; de app gebruikt nu de veilige menu-flow voor meerdere beelden.'; } if (/Kaggle kernel error/i.test(message)) { return 'De beeldgenerator kon het beeld niet maken. Probeer opnieuw met minder of duidelijkere referentiefoto’s.'; } return message.split('\n')[0] ?? 'Generation provider failed'; } function getGenerationProvider(): GenerationProvider { return process.env.GENERATION_PROVIDER === 'kaggle' ? 'kaggle' : 'mock'; } function toJobResponse(job: GenerationJob): Record { return { jobId: job.id, provider: job.provider, status: job.status, createdAt: job.createdAt, updatedAt: job.updatedAt, progress: job.progress, message: job.message, events: job.events, result: job.result, error: job.error, }; }