Spaces:
Running
Running
| /** | |
| * 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}`); | |
| } | |
| }); | |
| }); |