joelsefanja's picture
Deploy Mplus Studio
993eeba verified
Raw
History Blame Contribute Delete
5.55 kB
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,
};
}