HoangThe's picture
Initial DeepSite commit
1c1788b verified
/**
* 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}`);
}
});
});