File size: 6,877 Bytes
1c1788b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/**
 * 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}`);
    }
  });
});