diff --git a/backend/routes.ts b/backend/routes.ts index 4355bc6e..a9ea410f 100644 --- a/backend/routes.ts +++ b/backend/routes.ts @@ -4,7 +4,7 @@ import { storage } from "./storage"; import bcrypt from "bcrypt"; import { insertUserSchema, insertProjectSchema, insertLabelSchema, insertImageSchema, insertProjectImagesSchema, insertAnnotationSchema, insertLabelClassSchema, insertProjectImageSchema } from "@shared/schema"; import multer from 'multer'; -import { uploadFile, deleteFile, initializeMinio } from './services/minio'; +import { uploadFile, deleteFile, initializeMinio, getFileStream } from './services/minio'; import { extractImagesFromZip } from './services/zip'; import jwt from "jsonwebtoken"; @@ -1908,6 +1908,292 @@ app.get('/api/annotation/project/:projectId/stats', authenticateToken, requireRo } }); + + + +// ML Engineer Endpoints + /** + * @swagger + * /api/ML-Engineer/images: + * get: + * summary: Get all images (ML Engineer only) + * tags: [ML Engineer] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of all images + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Image' + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * description: Access denied + * 500: + * description: Server error + */ + +app.get("/api/ML-Engineer/images", authenticateToken, requireRole(["ml_engineer"]), async (req, res) => { + try { + const allImages = await storage.getAllImages(); + res.json({ + success: true, + count: allImages?.length || 0, + data: allImages + }); + } catch (error: any) { + console.error("ML: Get all images error:", error); + res.status(500).json({ error: "Failed to get image manifest" }); + } + }); + + +/** + * @swagger + * /api/ML-Engineer/{id}/download: + * get: + * summary: Download the actual binary image file + * tags: [ML Engineer] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * format: uuid + * description: The UUID of the image to download + * responses: + * 200: + * description: The binary image file + * content: + * image/*: + * schema: + * type: string + * format: binary + * 404: + * description: Image not found + */ +app.get("/api/ML-Engineer/:id/download", authenticateToken, requireRole(["ml_engineer"]), async (req, res) => { + try { + const { id } = req.params; + + const image = await storage.getImage(id); + + if (!image) { + return res.status(404).json({ error: "Image not found" }); + } + + const urlParts = image.url.split('/'); + const filename = urlParts[urlParts.length - 1]; + + if (!filename) { + return res.status(400).json({ error: "Invalid file path" }); + } + + const ext = filename.split('.').pop()?.toLowerCase(); + let contentType = 'application/octet-stream'; + if (ext === 'png') contentType = 'image/png'; + if (ext === 'jpg' || ext === 'jpeg') contentType = 'image/jpeg'; + + res.setHeader('Content-Type', contentType); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + + const fileStream = await getFileStream(filename); + fileStream.pipe(res); + + fileStream.on('error', (err) => { + console.error("Stream error:", err); + res.end(); + }); + + } catch (error: any) { + console.error("ML: Download image error:", error); + if (!res.headersSent) { + res.status(500).json({ error: "Failed to download image" }); + } + } +}); + + +/** + * @swagger + * /api/ML-Engineer/label-types: + * get: + * summary: Get all label definitions (Schema/Ontology) + * tags: [ML Engineer] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of all label types available + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SuccessResponse' + */ +app.get("/api/ML-Engineer/label-types", authenticateToken, requireRole(["ml_engineer"]), async (req, res) => { + try { + const labelTypes = await storage.getAllLabelTypes(); + res.json({ + success: true, + count: labelTypes.length, + data: labelTypes + }); + } catch (error: any) { + console.error("ML: Get label types error:", error); + res.status(500).json({ error: "Failed to fetch label types" }); + } +}); + + +/** + * @swagger + * /api/ML-Engineer/images/labels: + * post: + * summary: Get ground truth labels for a list of images (Bulk) + * tags: [ML Engineer] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - imageIds + * properties: + * imageIds: + * type: array + * items: + * type: string + * format: uuid + * example: ["550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440001"] + * responses: + * 200: + * description: List of annotations for the requested images + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SuccessResponse' + */ +app.post("/api/ML-Engineer/images/labels/", authenticateToken, requireRole(["ml_engineer"]), async (req, res) => { + try { + const { imageIds } = req.body; + + if (!Array.isArray(imageIds)) { + return res.status(400).json({ error: "imageIds must be an array" }); + } + + const annotations = await storage.getEnrichedAnnotationsByImageIds(imageIds); + + res.json({ + success: true, + count: annotations.length, + data: annotations + }); + } catch (error: any) { + console.error("ML: Bulk label fetch error:", error); + res.status(500).json({ error: "Failed to fetch bulk labels" }); + } +}); + + +/** + * @swagger + * /api/ML-Engineer/images/labels/{imageId}: + * get: + * summary: Get ground truth labels for a specific image + * tags: [ML Engineer] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: imageId + * required: true + * schema: + * type: string + * format: uuid + * responses: + * 200: + * description: Annotations for the specific image + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SuccessResponse' + */ +app.get("/api/ML-Engineer/images/labels/:imageId", authenticateToken, requireRole(["ml_engineer"]), async (req, res) => { + try { + const annotations = await storage.getEnrichedAnnotationsByImageIds([req.params.imageId]); + + res.json({ + success: true, + imageId: req.params.imageId, + annotations: annotations + }); + } catch (error: any) { + res.status(500).json({ error: "Failed to fetch labels" }); + } +}); + + +/** + * @swagger + * /api/ML-Engineer/projects: + * get: + * summary: Get full project manifest (Projects + Images + Label Types) + * tags: [ML Engineer] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: A complex manifest object useful for setting up training jobs + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * format: uuid + * name: + * type: string + * labelType: + * $ref: '#/components/schemas/Label' + * images: + * type: array + * items: + * $ref: '#/components/schemas/Image' + */ +app.get("/api/ML-Engineer/projects", authenticateToken, requireRole(["ml_engineer"]), async (req, res) => { + try { + const projectManifest = await storage.getAllProjectsWithManifest(); + + res.json({ + success: true, + count: projectManifest.length, + data: projectManifest + }); + } catch (error: any) { + console.error("ML: Get project manifest error:", error); + res.status(500).json({ error: "Failed to generate project manifest" }); + } +}); + const httpServer = createServer(app); return httpServer; } \ No newline at end of file diff --git a/backend/services/minio.ts b/backend/services/minio.ts index 5e1f8525..d741b599 100644 --- a/backend/services/minio.ts +++ b/backend/services/minio.ts @@ -87,4 +87,8 @@ export async function getFileUrl(filename: string): Promise { } } +export async function getFileStream(filename: string) { + return await minioClient.getObject(BUCKET_NAME, filename); +} + export { minioClient, BUCKET_NAME }; \ No newline at end of file diff --git a/backend/storage.ts b/backend/storage.ts index d55805d1..24a9cc05 100644 --- a/backend/storage.ts +++ b/backend/storage.ts @@ -103,6 +103,11 @@ export interface IStorage { // Project assignment methods assignUserToProject(assignment: InsertProjectAssignment): Promise; getProjectAssignments(projectId: string): Promise; + + + // ML Engineer Methods + getEnrichedAnnotationsByImageIds(imageIds: string[]): Promise + getAllProjectsWithManifest(): Promise } export class DbStorage implements IStorage { @@ -651,6 +656,82 @@ async createAnnotation(insertAnnotation: InsertAnnotation): Promise } }; } + + + /* + ML Engineer endpoints + */ + + // Get "Enriched" labels for ONE OR MANY images + async getEnrichedAnnotationsByImageIds(imageIds: string[]): Promise { + if (!imageIds || imageIds.length === 0) { + return []; + } + + const result = await db + .select({ + annotationId: annotations.id, + imageId: annotations.imageId, + labelClass: labelClasses.name, + labelType: labels.name, + annotatedAt: annotations.annotatedAt, + annotatorId: annotations.userId + }) + .from(annotations) + .innerJoin(labelClasses, eq(annotations.labelClassesId, labelClasses.id)) + .innerJoin(labels, eq(labelClasses.labelTypeId, labels.id)) + .where(inArray(annotations.imageId, imageIds)); + + return result; + } + + +// Get All Projects with their Images and Label Type +async getAllProjectsWithManifest(): Promise { + + // Get the base project info + Label Type info + const projectsList = await db + .select({ + id: projects.id, + name: projects.name, + description: projects.description, + status: projects.status, + createdAt: projects.createdAt, + labelType: { + id: labels.id, + name: labels.name, + description: labels.description + }, + createdBy: { + id: users.id, + email: users.email + } + }) + .from(projects) + .leftJoin(labels, eq(projects.labelTypeId, labels.id)) + .leftJoin(users, eq(projects.createdBy, users.id)); + + // For each project, fetch the list of assigned images -- IN PARALLEL for effeciency + const fullManifest = await Promise.all( + projectsList.map(async (project) => { + // Get all images assigned to this specific project + const imagesInProject = await this.getImagesByProject(project.id); + + return { + ...project, + // The requirement says "including images", so we attach the list here + images: imagesInProject.map(img => ({ + id: img.id, + filename: img.filename, + url: img.url + })) + }; + }) + ); + + return fullManifest; +} + } export const storage = new DbStorage(); \ No newline at end of file diff --git a/run.sh b/run.sh index 93f84eb3..9bfb2db7 100755 --- a/run.sh +++ b/run.sh @@ -15,8 +15,11 @@ npm install # 2. Set Up Environment Variables echo "Setting up environment variables..." -echo "DATABASE_URL=\"postgres://user:password@localhost:5434/ml-data-platform\"" > .env -echo "JWT_SECRET=ThisIsJustAJWT_SECRET" > .env +cat < .env +DATABASE_URL="postgres://user:password@localhost:5434/ml-data-platform" +JWT_SECRET=ThisIsJustAJWT_SECRET +EOF + source .env # 3. Start the Database