/** * AI Camera Hub - Lumi | Backend Server * * Serves images from save_images_v3/ and save_images_v4/ folders, * provides API for image listing, annotation CRUD, and auto-save. */ const express = require('express'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); const app = express(); const PORT = process.env.PORT || 3001; // ========== PATHS ========== // Image directories (relative to project root) const PROJECT_ROOT = path.join(__dirname, '..'); const V3_DIR = path.join(PROJECT_ROOT, 'save_images_v3'); const V4_DIR = path.join(PROJECT_ROOT, 'save_images_v4'); const ANNOTATIONS_FILE = path.join(PROJECT_ROOT, 'annotations.json'); // Supported image extensions const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|bmp|webp|tiff|svg)$/i; // ========== MIDDLEWARE ========== app.use(cors()); app.use(express.json({ limit: '10mb' })); // Serve frontend (index.html at project root) app.use(express.static(PROJECT_ROOT)); // Serve image files statically app.use('/images/v3', express.static(V3_DIR)); app.use('/images/v4', express.static(V4_DIR)); // Log requests in development app.use((req, res, next) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${req.method} ${req.url}`); next(); }); // ========== API ROUTES ========== /** * GET /api/images * Returns list of matched images from both v3 and v4 folders. * Images are matched by filename. */ app.get('/api/images', (req, res) => { try { let v3Files = []; let v4Files = []; // Read v3 directory if (fs.existsSync(V3_DIR)) { v3Files = fs.readdirSync(V3_DIR) .filter(f => IMAGE_EXTENSIONS.test(f)); } else { console.warn('⚠️ save_images_v3 directory not found at:', V3_DIR); } // Read v4 directory if (fs.existsSync(V4_DIR)) { v4Files = fs.readdirSync(V4_DIR) .filter(f => IMAGE_EXTENSIONS.test(f)); } else { console.warn('⚠️ save_images_v4 directory not found at:', V4_DIR); } // Match images by filename const v3Set = new Set(v3Files); const v4Set = new Set(v4Files); const allNames = new Set([...v3Files, ...v4Files]); const matched = []; allNames.forEach(name => { matched.push({ name, hasV3: v3Set.has(name), hasV4: v4Set.has(name), }); }); // Sort alphabetically matched.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })); console.log(`✅ Found ${matched.length} images (v3: ${v3Files.length}, v4: ${v4Files.length})`); res.json({ images: matched, total: matched.length }); } catch (err) { console.error('❌ Error listing images:', err); res.status(500).json({ error: err.message }); } }); /** * GET /api/annotations * Returns saved annotations from annotations.json. */ app.get('/api/annotations', (req, res) => { try { if (fs.existsSync(ANNOTATIONS_FILE)) { const data = fs.readFileSync(ANNOTATIONS_FILE, 'utf8'); const parsed = JSON.parse(data); console.log(`📋 Loaded annotations for ${Object.keys(parsed).length} images`); res.json(parsed); } else { console.log('📋 No existing annotations file, returning empty object'); res.json({}); } } catch (err) { console.error('❌ Error reading annotations:', err); res.status(500).json({ error: err.message }); } }); /** * POST /api/annotations * Saves annotations to annotations.json. * Auto-creates backup before overwriting. */ app.post('/api/annotations', (req, res) => { try { const annotations = req.body; // Create backup if file exists if (fs.existsSync(ANNOTATIONS_FILE)) { const backupPath = ANNOTATIONS_FILE.replace('.json', `_backup_${Date.now()}.json`); fs.copyFileSync(ANNOTATIONS_FILE, backupPath); // Keep only last 5 backups const backupDir = path.dirname(ANNOTATIONS_FILE); const backups = fs.readdirSync(backupDir) .filter(f => f.startsWith('annotations_backup_')) .sort() .map(f => path.join(backupDir, f)); while (backups.length > 5) { fs.unlinkSync(backups.shift()); } } // Write new annotations fs.writeFileSync( ANNOTATIONS_FILE, JSON.stringify(annotations, null, 2), 'utf8' ); const count = Object.keys(annotations).length; console.log(`💾 Saved annotations for ${count} images`); res.json({ success: true, count }); } catch (err) { console.error('❌ Error saving annotations:', err); res.status(500).json({ error: err.message }); } }); /** * GET /api/export * Export annotations as downloadable JSON file. */ app.get('/api/export', (req, res) => { try { if (!fs.existsSync(ANNOTATIONS_FILE)) { return res.status(404).json({ error: 'No annotations found' }); } const data = fs.readFileSync(ANNOTATIONS_FILE, 'utf8'); const filename = `annotations_export_${new Date().toISOString().slice(0, 10)}.json`; res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.setHeader('Content-Type', 'application/json'); res.send(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // SPA fallback - serve index.html for any unmatched routes app.get('*', (req, res) => { const indexPath = path.join(PROJECT_ROOT, 'index.html'); if (fs.existsSync(indexPath)) { res.sendFile(indexPath); } else { res.status(404).send('Frontend not found. Place index.html in project root.'); } }); // ========== START SERVER ========== app.listen(PORT, () => { console.log(''); console.log('═══════════════════════════════════════════════════════════'); console.log(' 🎯 AI Camera Hub - Lumi | Error Analysis Dashboard'); console.log('═══════════════════════════════════════════════════════════'); console.log(` Server: http://localhost:${PORT}`); console.log(` API: http://localhost:${PORT}/api/images`); console.log(''); console.log(` v3 path: ${V3_DIR}`); console.log(` v4 path: ${V4_DIR}`); console.log(` Save to: ${ANNOTATIONS_FILE}`); console.log(''); console.log(' Press Ctrl+C to stop'); console.log('═══════════════════════════════════════════════════════════'); console.log(''); // Create image directories if they don't exist [V3_DIR, V4_DIR].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); console.log(`📁 Created directory: ${dir}`); } }); });