Spaces:
Sleeping
Sleeping
| 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>crypto.webcrypto; | |
| } | |
| export function createApp(): express.Express { | |
| const app = express(); | |
| const jobs = new Map<string, GenerationJob>(); | |
| 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 = <Express.Multer.File[]>request.files ?? []; | |
| const images = files | |
| .filter((file) => file.mimetype.startsWith('image/')) | |
| .map<UploadedImage>((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<typeof generationRequestSchema.parse>, 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<GenerationJob>): 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<typeof generationRequestSchema.parse>, images: UploadedImage[]): ReturnType<typeof generationRequestSchema.parse> { | |
| 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<string, unknown> { | |
| 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, | |
| }; | |
| } | |