From db795c5729c52929c5ea6b1c10bb1d1c3ccadf23 Mon Sep 17 00:00:00 2001 From: Aherobo Ovie Victor Date: Fri, 11 Jul 2025 21:15:41 +0100 Subject: [PATCH] Fix file upload issues and add Swagger UI API documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Frontend File Upload Fixes: - Fixed file upload reset issue - can now upload multiple files without page reload - Added 'Change File' and 'Upload Another Image' buttons for better UX - Fixed double-click file selection issue with proper event handling - Improved drag & drop functionality with proper event propagation - Added visual feedback for file selection and processing states ✅ Swagger UI API Documentation: - Created api_docs.py with comprehensive Swagger UI documentation - Added Flask-RESTX for professional API documentation interface - Documented all 3 detection endpoints with request/response models - Added health check endpoint documentation - Included detailed parameter descriptions and example responses - Available at http://localhost:5003/docs/ for interactive API testing ✅ Enhanced User Experience: - Seamless file upload workflow without page reloads - Clear visual indicators for file selection and processing - Professional API documentation for developers and QA testing - Consistent 80% confidence threshold across all interfaces ✅ Technical Improvements: - Better event handling for file inputs and drag & drop - Proper cleanup of uploaded files and UI state - Comprehensive error handling and user feedback - Interactive API documentation with live testing capabilities --- api_docs.py | 245 +++++++++++++++++++++++++++++++++++++++++++++++ static/script.js | 67 +++++++++++-- 2 files changed, 306 insertions(+), 6 deletions(-) create mode 100644 api_docs.py diff --git a/api_docs.py b/api_docs.py new file mode 100644 index 0000000..f1ab065 --- /dev/null +++ b/api_docs.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +""" +Swagger UI API Documentation for Memory Module Detection +This creates a separate API documentation interface using Flask-RESTX +""" + +from flask import Flask, request, jsonify +from flask_restx import Api, Resource, fields, reqparse +from flask_cors import CORS +from werkzeug.datastructures import FileStorage +import os +from inference_utils import MemoryModuleDetector + +# Initialize Flask app with Swagger +app = Flask(__name__) +CORS(app) + +# Configure Swagger UI +api = Api( + app, + version='1.0.0', + title='Memory Module Detection API', + description='AI-powered memory module detection in motherboard images using YOLOv8', + doc='/docs/', # Swagger UI will be available at /docs/ + prefix='/api/v1' +) + +# Create namespaces +ns_health = api.namespace('health', description='System health and status') +ns_detect = api.namespace('detect', description='Memory module detection operations') + +# Initialize detector +MODEL_PATH = 'runs/detect/memory_module_detection/weights/best.pt' +detector = MemoryModuleDetector(MODEL_PATH) + +# Define models for Swagger documentation +detection_model = api.model('Detection', { + 'bbox': fields.List(fields.Float, description='Bounding box coordinates [x1, y1, x2, y2]'), + 'confidence': fields.Float(description='Detection confidence score (0.0-1.0)'), + 'class': fields.Integer(description='Class ID (0 for memory_module)'), + 'class_name': fields.String(description='Class name (memory_module)') +}) + +detection_response = api.model('DetectionResponse', { + 'success': fields.Boolean(description='Whether detection was successful'), + 'detections': fields.List(fields.Nested(detection_model), description='List of detected memory modules'), + 'num_detections': fields.Integer(description='Number of memory modules detected'), + 'annotated_image': fields.String(description='Base64 encoded annotated image'), + 'confidence_threshold': fields.Float(description='Confidence threshold used'), + 'original_filename': fields.String(description='Original filename (for uploads)') +}) + +health_response = api.model('HealthResponse', { + 'status': fields.String(description='System health status'), + 'model_loaded': fields.Boolean(description='Whether the AI model is loaded'), + 'model_path': fields.String(description='Path to the AI model file') +}) + +error_response = api.model('ErrorResponse', { + 'success': fields.Boolean(description='Always false for errors'), + 'error': fields.String(description='Error message') +}) + +# Health endpoint +@ns_health.route('/') +class Health(Resource): + @ns_health.doc('health_check') + @ns_health.marshal_with(health_response) + def get(self): + """Check system health and model status""" + return { + 'status': 'healthy', + 'model_loaded': detector.model is not None, + 'model_path': MODEL_PATH + } + +# File upload parser +upload_parser = reqparse.RequestParser() +upload_parser.add_argument('image', location='files', type=FileStorage, required=True, + help='Motherboard image file (PNG, JPG, JPEG, GIF, BMP)') +upload_parser.add_argument('confidence', type=float, default=0.8, + help='Confidence threshold (0.1-1.0, default: 0.8)') + +@ns_detect.route('/upload') +class DetectUpload(Resource): + @ns_detect.doc('detect_upload') + @ns_detect.expect(upload_parser) + @ns_detect.marshal_with(detection_response, code=200) + @ns_detect.marshal_with(error_response, code=400) + @ns_detect.marshal_with(error_response, code=500) + def post(self): + """Upload and analyze motherboard image for memory modules""" + try: + if detector.model is None: + return {'success': False, 'error': 'Model not loaded'}, 500 + + args = upload_parser.parse_args() + file = args['image'] + confidence = args['confidence'] + + if not file: + return {'success': False, 'error': 'No image file provided'}, 400 + + # Save file temporarily + temp_path = f"temp_{file.filename}" + file.save(temp_path) + + try: + # Run detection + detections, annotated_image = detector.detect(temp_path, conf_threshold=confidence) + + # Convert annotated image to base64 + import io + import base64 + buffer = io.BytesIO() + annotated_image.save(buffer, format='PNG') + annotated_base64 = base64.b64encode(buffer.getvalue()).decode() + + return { + 'success': True, + 'detections': detections, + 'num_detections': len(detections), + 'annotated_image': annotated_base64, + 'confidence_threshold': confidence, + 'original_filename': file.filename + } + + finally: + # Clean up + if os.path.exists(temp_path): + os.remove(temp_path) + + except Exception as e: + return {'success': False, 'error': str(e)}, 500 + +# Hardcoded image parser +hardcoded_parser = reqparse.RequestParser() +hardcoded_parser.add_argument('confidence', type=float, default=0.8, location='args', + help='Confidence threshold (0.1-1.0, default: 0.8)') + +@ns_detect.route('/hardcoded') +class DetectHardcoded(Resource): + @ns_detect.doc('detect_hardcoded') + @ns_detect.expect(hardcoded_parser) + @ns_detect.marshal_with(detection_response, code=200) + @ns_detect.marshal_with(error_response, code=404) + @ns_detect.marshal_with(error_response, code=500) + def get(self): + """Analyze predefined test image for memory modules""" + try: + if detector.model is None: + return {'success': False, 'error': 'Model not loaded'}, 500 + + args = hardcoded_parser.parse_args() + confidence = args['confidence'] + + test_image_path = 'training/memory/out1.png' + if not os.path.exists(test_image_path): + return {'success': False, 'error': f'Test image not found at {test_image_path}'}, 404 + + # Run detection + detections, annotated_image = detector.detect(test_image_path, conf_threshold=confidence) + + # Convert annotated image to base64 + import io + import base64 + buffer = io.BytesIO() + annotated_image.save(buffer, format='PNG') + annotated_base64 = base64.b64encode(buffer.getvalue()).decode() + + return { + 'success': True, + 'detections': detections, + 'num_detections': len(detections), + 'annotated_image': annotated_base64, + 'confidence_threshold': confidence, + 'test_image_path': test_image_path + } + + except Exception as e: + return {'success': False, 'error': str(e)}, 500 + +# Base64 image model +base64_model = api.model('Base64Request', { + 'image': fields.String(required=True, description='Base64 encoded image data'), + 'confidence': fields.Float(default=0.8, description='Confidence threshold (0.1-1.0)') +}) + +@ns_detect.route('/base64') +class DetectBase64(Resource): + @ns_detect.doc('detect_base64') + @ns_detect.expect(base64_model) + @ns_detect.marshal_with(detection_response, code=200) + @ns_detect.marshal_with(error_response, code=400) + @ns_detect.marshal_with(error_response, code=500) + def post(self): + """Analyze base64 encoded image for memory modules""" + try: + if detector.model is None: + return {'success': False, 'error': 'Model not loaded'}, 500 + + data = request.get_json() + if not data or 'image' not in data: + return {'success': False, 'error': 'No base64 image data provided'}, 400 + + confidence = data.get('confidence', 0.8) + + # Decode base64 image + import base64 + import io + from PIL import Image + import numpy as np + + try: + img_data = base64.b64decode(data['image']) + image = Image.open(io.BytesIO(img_data)) + except Exception as e: + return {'success': False, 'error': f'Invalid base64 image data: {str(e)}'}, 400 + + # Run detection + detections, annotated_image = detector.detect_from_array(np.array(image), conf_threshold=confidence) + + # Convert annotated image to base64 + buffer = io.BytesIO() + annotated_image.save(buffer, format='PNG') + annotated_base64 = base64.b64encode(buffer.getvalue()).decode() + + return { + 'success': True, + 'detections': detections, + 'num_detections': len(detections), + 'annotated_image': annotated_base64, + 'confidence_threshold': confidence + } + + except Exception as e: + return {'success': False, 'error': str(e)}, 500 + +if __name__ == '__main__': + print("Starting Memory Module Detection API with Swagger UI...") + print(f"Model path: {MODEL_PATH}") + print(f"Model loaded: {detector.model is not None}") + print("Swagger UI available at: http://localhost:5003/docs/") + + app.run(host='0.0.0.0', port=5003, debug=True) diff --git a/static/script.js b/static/script.js index 1b37ea8..4395bf5 100644 --- a/static/script.js +++ b/static/script.js @@ -23,7 +23,14 @@ function setupEventListeners() { uploadArea.addEventListener('dragover', handleDragOver); uploadArea.addEventListener('dragleave', handleDragLeave); uploadArea.addEventListener('drop', handleDrop); - uploadArea.addEventListener('click', () => document.getElementById('fileInput').click()); + + // Click to upload (only on the upload area, not buttons inside it) + uploadArea.addEventListener('click', function(event) { + // Only trigger file input if clicking on the upload area itself, not buttons + if (event.target === uploadArea || event.target.closest('.upload-content') && !event.target.closest('button')) { + document.getElementById('fileInput').click(); + } + }); } async function checkApiStatus() { @@ -94,17 +101,21 @@ function handleFileSelect(event) { function handleDragOver(event) { event.preventDefault(); + event.stopPropagation(); event.currentTarget.classList.add('dragover'); } function handleDragLeave(event) { + event.preventDefault(); + event.stopPropagation(); event.currentTarget.classList.remove('dragover'); } function handleDrop(event) { event.preventDefault(); + event.stopPropagation(); event.currentTarget.classList.remove('dragover'); - + const files = event.dataTransfer.files; if (files.length > 0) { handleFile(files[0]); @@ -116,23 +127,53 @@ function handleFile(file) { alert('Please select an image file'); return; } - + uploadedFile = file; - - // Show file info + + // Show file info with change file option const uploadArea = document.getElementById('uploadArea'); uploadArea.innerHTML = `

File selected: ${file.name}

Size: ${(file.size / 1024 / 1024).toFixed(2)} MB

+
`; - + // Show controls document.getElementById('uploadControls').style.display = 'block'; } +function resetFileUpload() { + uploadedFile = null; + + // Reset file input + const fileInput = document.getElementById('fileInput'); + fileInput.value = ''; + + // Reset upload area + const uploadArea = document.getElementById('uploadArea'); + uploadArea.innerHTML = ` +
+ +

Drag and drop an image here or click to select

+

Supported formats: PNG, JPG, JPEG, GIF, BMP

+ +
+ `; + + // Hide controls + document.getElementById('uploadControls').style.display = 'none'; + + // Hide results if showing + document.getElementById('resultsSection').style.display = 'none'; +} + async function processUploadedImage() { if (!uploadedFile) { alert('Please select an image first'); @@ -157,6 +198,8 @@ async function processUploadedImage() { if (result.success) { displayResults(result, 'Uploaded Image Detection'); + // Add option to upload another file + addUploadAnotherOption(); } else { alert(`Detection failed: ${result.error}`); } @@ -166,6 +209,18 @@ async function processUploadedImage() { } } +function addUploadAnotherOption() { + const uploadControls = document.getElementById('uploadControls'); + if (!uploadControls.querySelector('.upload-another')) { + const uploadAnotherBtn = document.createElement('button'); + uploadAnotherBtn.className = 'btn btn-secondary upload-another'; + uploadAnotherBtn.style.marginLeft = '10px'; + uploadAnotherBtn.innerHTML = ' Upload Another Image'; + uploadAnotherBtn.onclick = resetFileUpload; + uploadControls.appendChild(uploadAnotherBtn); + } +} + async function testHardcodedImage() { showLoading('Testing hardcoded image...');