VISHAL18for4 commited on
Commit
bfde5c8
·
verified ·
1 Parent(s): f8c9b84

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +132 -317
server.js CHANGED
@@ -1,368 +1,183 @@
1
- // ============================================================
2
- // 1. DOM References (unchanged)
3
- // ============================================================
4
- const video = document.getElementById('webcam');
5
- const canvas = document.getElementById('drawCanvas');
6
- const ctx = canvas.getContext('2d');
7
- const statusDiv = document.getElementById('status');
8
- const gestureIndicator = document.getElementById('gestureIndicator');
9
- const fpsSpan = document.getElementById('fpsCounter');
10
- const savedSpan = document.getElementById('savedCounter');
11
 
12
- const brushColorInput = document.getElementById('brushColor');
13
- const sizeDisplay = document.getElementById('sizeDisplay');
14
- const eraserBtn = document.getElementById('eraserToggle');
15
- const clearBtn = document.getElementById('clearCanvas');
16
- const snapshotBtn = document.getElementById('snapshotBtn');
17
- const toggleCamBtn = document.getElementById('toggleCamera');
18
- const sizeDownBtn = document.getElementById('brushSizeDown');
19
- const sizeUpBtn = document.getElementById('brushSizeUp');
20
 
21
  // ============================================================
22
- // 2. State Variables
23
  // ============================================================
24
- let drawing = false;
25
- let lastX = null;
26
- let lastY = null;
27
- let currentColor = '#FF4500';
28
- let brushSize = 5;
29
- let eraserMode = false;
30
- let cameraVisible = true;
31
- let savedCount = 0;
32
- let frameCount = 0;
33
- let lastFpsUpdate = performance.now();
34
- let handLandmarker = null;
35
- let cameraStream = null;
36
- let isModelReady = false;
37
- let canvasWidth = 0;
38
- let canvasHeight = 0;
39
- let modelLoadAttempts = 0;
40
- const MAX_MODEL_ATTEMPTS = 3;
41
 
42
- // ============================================================
43
- // 3. Canvas Resize (unchanged)
44
- // ============================================================
45
- function resizeCanvas() {
46
- const rect = canvas.parentElement.getBoundingClientRect();
47
- canvas.width = rect.width;
48
- canvas.height = rect.height;
49
- canvasWidth = canvas.width;
50
- canvasHeight = canvas.height;
51
- }
52
- window.addEventListener('resize', resizeCanvas);
53
 
54
  // ============================================================
55
- // 4. MediaPipe Hand Landmarker with Fallbacks
56
  // ============================================================
57
- async function initHandLandmarker() {
58
- try {
59
- statusDiv.textContent = '⏳ Loading hand model (attempt ' + (modelLoadAttempts + 1) + ')...';
60
-
61
- // Use the FilesetResolver to load wasm
62
- const vision = await FilesetResolver.forVisionTasks(
63
- 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.8/wasm'
64
- );
65
-
66
- // Model sources - try in order
67
- const modelSources = [
68
- // Local file (you placed in public/)
69
- '/hand_landmarker.task',
70
- // Google CDN (official)
71
- 'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task',
72
- // Hugging Face mirror (community hosted)
73
- 'https://huggingface.co/spaces/GoogleMediaPipe/hand_landmarker/resolve/main/hand_landmarker.task'
74
- ];
75
-
76
- let lastError = null;
77
- for (let i = 0; i < modelSources.length; i++) {
78
- try {
79
- statusDiv.textContent = `⏳ Loading model from source ${i+1}/${modelSources.length}...`;
80
- console.log(`[MediaPipe] Trying source ${i+1}: ${modelSources[i]}`);
81
-
82
- handLandmarker = await HandLandmarker.createFromOptions(vision, {
83
- baseOptions: {
84
- modelAssetPath: modelSources[i],
85
- delegate: 'GPU'
86
- },
87
- runningMode: 'VIDEO',
88
- numHands: 1
89
- });
90
-
91
- // If we get here, model loaded successfully
92
- isModelReady = true;
93
- statusDiv.textContent = '✅ Model ready · Move index finger up to draw';
94
- console.log('[MediaPipe] HandLandmarker initialized successfully from source', i+1);
95
- return; // Exit function on success
96
- } catch (err) {
97
- lastError = err;
98
- console.warn(`[MediaPipe] Source ${i+1} failed:`, err.message);
99
- // Continue to next source
100
- }
101
- }
102
-
103
- // If all sources fail, try CPU delegate as last resort
104
- try {
105
- statusDiv.textContent = '⏳ Trying CPU fallback...';
106
- handLandmarker = await HandLandmarker.createFromOptions(vision, {
107
- baseOptions: {
108
- modelAssetPath: modelSources[0], // try local file again
109
- delegate: 'CPU'
110
- },
111
- runningMode: 'VIDEO',
112
- numHands: 1
113
- });
114
- isModelReady = true;
115
- statusDiv.textContent = '✅ Model ready (CPU mode) · Move index finger up to draw';
116
- console.log('[MediaPipe] Model loaded with CPU delegate');
117
- return;
118
- } catch (cpuErr) {
119
- console.error('[MediaPipe] CPU fallback also failed:', cpuErr);
120
- }
121
-
122
- // If we reach here, all attempts failed
123
- throw lastError || new Error('All model sources failed');
124
 
125
- } catch (err) {
126
- console.error('[MediaPipe] Fatal initialization error:', err);
127
- statusDiv.textContent = '❌ Model load failed. Click here to retry.';
128
- // Make status clickable to retry
129
- statusDiv.style.cursor = 'pointer';
130
- statusDiv.onclick = () => {
131
- modelLoadAttempts++;
132
- if (modelLoadAttempts < MAX_MODEL_ATTEMPTS) {
133
- initHandLandmarker();
134
- } else {
135
- statusDiv.textContent = '❌ Max retries reached. Please refresh the page.';
136
- statusDiv.onclick = null;
137
- }
138
- };
139
- }
140
  }
141
 
142
  // ============================================================
143
- // 5. Camera Setup (unchanged)
144
  // ============================================================
145
- async function startCamera() {
146
- try {
147
- cameraStream = await navigator.mediaDevices.getUserMedia({
148
- video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } }
149
- });
150
- video.srcObject = cameraStream;
151
- await video.play();
152
- statusDiv.textContent = '📷 Camera ready · Loading hand model...';
153
- initHandLandmarker();
154
- requestAnimationFrame(drawLoop);
155
- } catch (err) {
156
- console.error('[Camera] Error:', err);
157
- statusDiv.textContent = '❌ Camera access denied. Please allow permissions.';
158
- alert('Camera access is required. Please allow and refresh.');
159
- }
160
- }
161
 
162
  // ============================================================
163
- // 6. Core Drawing Loop (unchanged except for drawing logic)
164
  // ============================================================
165
- function drawLoop(timestamp) {
166
- frameCount++;
167
- if (timestamp - lastFpsUpdate >= 1000) {
168
- fpsSpan.textContent = `${frameCount} FPS`;
169
- frameCount = 0;
170
- lastFpsUpdate = timestamp;
171
- }
172
-
173
- if (video.readyState >= 2) {
174
- ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
175
- } else {
176
- ctx.fillStyle = '#1a1a2e';
177
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
178
  }
179
-
180
- if (isModelReady && handLandmarker && video.readyState >= 2) {
 
 
181
  try {
182
- const results = handLandmarker.detectForVideo(video, performance.now());
183
-
184
- if (results.landmarks && results.landmarks.length > 0) {
185
- const lm = results.landmarks[0];
186
- const tip = lm[8];
187
- const mcp = lm[5];
188
-
189
- const isIndexUp = (tip.y < mcp.y - 0.02);
190
-
191
- if (isIndexUp) {
192
- gestureIndicator.className = 'gesture-on';
193
- gestureIndicator.textContent = '✋';
194
- } else {
195
- gestureIndicator.className = 'gesture-off';
196
- gestureIndicator.textContent = '✊';
197
- }
198
-
199
- const x = tip.x * canvasWidth;
200
- const y = tip.y * canvasHeight;
201
-
202
- if (isIndexUp) {
203
- if (!drawing) {
204
- drawing = true;
205
- lastX = x;
206
- lastY = y;
207
- } else {
208
- if (lastX !== null && lastY !== null) {
209
- ctx.beginPath();
210
- ctx.moveTo(lastX, lastY);
211
- ctx.lineTo(x, y);
212
- ctx.strokeStyle = eraserMode ? '#1a1a2e' : currentColor;
213
- ctx.lineWidth = eraserMode ? brushSize * 2 : brushSize;
214
- ctx.lineCap = 'round';
215
- ctx.lineJoin = 'round';
216
- ctx.stroke();
217
- }
218
- lastX = x;
219
- lastY = y;
220
- }
221
- } else {
222
- drawing = false;
223
- lastX = null;
224
- lastY = null;
225
- }
226
-
227
- // Draw cursor
228
- ctx.beginPath();
229
- ctx.arc(x, y, 4, 0, 2 * Math.PI);
230
- ctx.fillStyle = eraserMode ? '#ff6666' : currentColor;
231
- ctx.fill();
232
- ctx.strokeStyle = '#fff';
233
- ctx.lineWidth = 1;
234
- ctx.stroke();
235
-
236
- } else {
237
- gestureIndicator.className = 'gesture-off';
238
- gestureIndicator.textContent = '🚫';
239
- drawing = false;
240
- lastX = null;
241
- lastY = null;
242
- }
243
  } catch (err) {
244
- // silent
 
245
  }
 
 
 
246
  }
247
-
248
- requestAnimationFrame(drawLoop);
249
- }
250
 
251
  // ============================================================
252
- // 7. Snapshot Capture (unchanged)
253
  // ============================================================
254
- async function captureSnapshot() {
255
  try {
256
- const tempCanvas = document.createElement('canvas');
257
- tempCanvas.width = canvasWidth;
258
- tempCanvas.height = canvasHeight;
259
- const tempCtx = tempCanvas.getContext('2d');
 
260
 
261
- if (video.readyState >= 2) {
262
- tempCtx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
 
263
  }
264
- tempCtx.drawImage(canvas, 0, 0);
265
 
266
- const dataURL = tempCanvas.toDataURL('image/png');
267
- const base64 = dataURL.split(',')[1];
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
- const response = await fetch('/api/snapshot', {
270
- method: 'POST',
271
- headers: { 'Content-Type': 'application/json' },
272
- body: JSON.stringify({ image: base64 })
273
- });
274
 
275
- const result = await response.json();
276
- if (result.success) {
277
- savedCount++;
278
- savedSpan.textContent = `Saved: ${savedCount}`;
279
- statusDiv.textContent = `✅ Snapshot saved: ${result.filename}`;
280
- canvas.style.transition = 'opacity 0.1s';
281
- canvas.style.opacity = '0.7';
282
- setTimeout(() => { canvas.style.opacity = '1'; }, 150);
283
  } else {
284
- statusDiv.textContent = ` Save failed: ${result.error}`;
 
285
  }
 
 
 
 
 
 
 
286
  } catch (err) {
287
- console.error('[Snapshot] Error:', err);
288
- statusDiv.textContent = ' Network error while saving snapshot.';
289
  }
290
- }
291
 
292
  // ============================================================
293
- // 8. Admin Panel Trigger (Triple-tap '3' key)
294
  // ============================================================
295
- let keyPressCount = 0;
296
- let keyPressTimer = null;
297
-
298
- document.addEventListener('keydown', (e) => {
299
- // Admin trigger: press '3' three times within 2 seconds
300
- if (e.key === '3') {
301
- keyPressCount++;
302
- clearTimeout(keyPressTimer);
303
- keyPressTimer = setTimeout(() => { keyPressCount = 0; }, 2000);
304
 
305
- if (keyPressCount >= 3) {
306
- keyPressCount = 0;
307
- // Open admin.html in new tab
308
- window.open('/admin.html', '_blank');
309
- statusDiv.textContent = '🔐 Admin panel opened in new tab';
310
- }
311
  }
312
-
313
- // Existing shortcuts
314
- if (e.key === 's' || e.key === 'S') captureSnapshot();
315
- if (e.key === 'e' || e.key === 'E') eraserBtn.click();
316
- if (e.key === 'c' || e.key === 'C') clearBtn.click();
317
  });
318
 
319
  // ============================================================
320
- // 9. UI Event Bindings (unchanged)
321
  // ============================================================
322
- brushColorInput.addEventListener('input', (e) => {
323
- currentColor = e.target.value;
324
- eraserMode = false;
325
- eraserBtn.classList.remove('active');
326
- });
327
-
328
- sizeDownBtn.addEventListener('click', () => {
329
- brushSize = Math.max(1, brushSize - 1);
330
- sizeDisplay.textContent = brushSize;
331
- });
332
- sizeUpBtn.addEventListener('click', () => {
333
- brushSize = Math.min(50, brushSize + 1);
334
- sizeDisplay.textContent = brushSize;
335
- });
336
-
337
- eraserBtn.addEventListener('click', () => {
338
- eraserMode = !eraserMode;
339
- eraserBtn.classList.toggle('active');
340
- statusDiv.textContent = eraserMode ? '🧹 Eraser mode ON' : '🎨 Drawing mode';
341
- });
342
-
343
- clearBtn.addEventListener('click', () => {
344
- if (confirm('Clear all drawings?')) {
345
- drawing = false;
346
- lastX = null;
347
- lastY = null;
348
- statusDiv.textContent = '🗑️ Canvas cleared';
349
- if (video.readyState >= 2) {
350
- ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
351
  }
 
 
352
  }
353
  });
354
 
355
- snapshotBtn.addEventListener('click', captureSnapshot);
356
-
357
- toggleCamBtn.addEventListener('click', () => {
358
- cameraVisible = !cameraVisible;
359
- video.style.display = cameraVisible ? 'block' : 'none';
360
- toggleCamBtn.classList.toggle('active');
361
  });
362
 
363
  // ============================================================
364
- // 10. Initialization
365
  // ============================================================
366
- resizeCanvas();
367
- startCamera();
368
- console.log('[Gesture Draw] App initialized.');
 
 
 
1
+ const express = require('express');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const cors = require('cors');
 
 
 
 
 
 
5
 
6
+ const app = express();
7
+ const PORT = process.env.PORT || 7860;
 
 
 
 
 
 
8
 
9
  // ============================================================
10
+ // 1. Middleware
11
  // ============================================================
12
+ app.use(cors());
13
+ app.use(express.json({ limit: '10mb' }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ // Force correct MIME type for MediaPipe .task files
16
+ app.use((req, res, next) => {
17
+ if (req.url.endsWith('.task')) {
18
+ res.setHeader('Content-Type', 'application/octet-stream');
19
+ res.setHeader('Content-Disposition', 'attachment; filename="hand_landmarker.task"');
20
+ }
21
+ next();
22
+ });
 
 
 
23
 
24
  // ============================================================
25
+ // 2. Snapshots Directory Setup
26
  // ============================================================
27
+ const SNAPSHOT_DIR = path.join(__dirname, 'snapshots');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ if (!fs.existsSync(SNAPSHOT_DIR)) {
30
+ fs.mkdirSync(SNAPSHOT_DIR, { recursive: true });
31
+ console.log(`[SERVER] Created snapshots directory at ${SNAPSHOT_DIR}`);
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
 
34
  // ============================================================
35
+ // 3. Serve Static Frontend Files
36
  // ============================================================
37
+ app.use(express.static(path.join(__dirname, 'public')));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  // ============================================================
40
+ // 4. Snapshot Binary Endpoint (bypasses static route restrictions)
41
  // ============================================================
42
+ app.get('/snapshots/:filename', (req, res) => {
43
+ const filename = req.params.filename;
44
+
45
+ // Security: prevent directory traversal
46
+ if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
47
+ console.warn(`[SERVER] Blocked malicious filename: ${filename}`);
48
+ return res.status(400).send('Invalid filename');
 
 
 
 
 
 
49
  }
50
+
51
+ const filePath = path.join(SNAPSHOT_DIR, filename);
52
+
53
+ if (fs.existsSync(filePath)) {
54
  try {
55
+ const data = fs.readFileSync(filePath);
56
+ res.setHeader('Content-Type', 'image/png');
57
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
58
+ res.send(data);
59
+ console.log(`[SERVER] Served snapshot: ${filename} (${data.length} bytes)`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  } catch (err) {
61
+ console.error(`[SERVER] Error reading snapshot ${filename}:`, err);
62
+ res.status(500).send('Error reading file');
63
  }
64
+ } else {
65
+ console.log(`[SERVER] Snapshot not found: ${filename}`);
66
+ res.status(404).send('Snapshot not found');
67
  }
68
+ });
 
 
69
 
70
  // ============================================================
71
+ // 5. POST /api/snapshot – Save New Snapshot
72
  // ============================================================
73
+ app.post('/api/snapshot', (req, res) => {
74
  try {
75
+ const { image } = req.body;
76
+
77
+ if (!image) {
78
+ return res.status(400).json({ success: false, error: 'No image data provided' });
79
+ }
80
 
81
+ // Validate base64 format
82
+ if (typeof image !== 'string' || image.length < 100) {
83
+ return res.status(400).json({ success: false, error: 'Invalid image data format' });
84
  }
 
85
 
86
+ const buffer = Buffer.from(image, 'base64');
87
+
88
+ // Generate timestamp-based filename (YYYY-MM-DD_HH-MM-SS_SSS.png)
89
+ const now = new Date();
90
+ const dateStr =
91
+ now.getFullYear() + '-' +
92
+ String(now.getMonth() + 1).padStart(2, '0') + '-' +
93
+ String(now.getDate()).padStart(2, '0') + '_' +
94
+ String(now.getHours()).padStart(2, '0') + '-' +
95
+ String(now.getMinutes()).padStart(2, '0') + '-' +
96
+ String(now.getSeconds()).padStart(2, '0') + '_' +
97
+ String(now.getMilliseconds()).padStart(3, '0');
98
+
99
+ const filename = `snapshot_${dateStr}.png`;
100
+ const filePath = path.join(SNAPSHOT_DIR, filename);
101
 
102
+ // Write file synchronously
103
+ fs.writeFileSync(filePath, buffer);
104
+ console.log(`[SERVER] Snapshot saved: ${filename} (${buffer.length} bytes) at ${filePath}`);
 
 
105
 
106
+ // Verify write succeeded
107
+ if (fs.existsSync(filePath)) {
108
+ const stats = fs.statSync(filePath);
109
+ console.log(`[SERVER] Verification: file exists, size ${stats.size} bytes`);
 
 
 
 
110
  } else {
111
+ console.error(`[SERVER] Verification FAILED: file not found after write`);
112
+ return res.status(500).json({ success: false, error: 'Write verification failed' });
113
  }
114
+
115
+ res.json({
116
+ success: true,
117
+ path: `/snapshots/${filename}`,
118
+ filename: filename
119
+ });
120
+
121
  } catch (err) {
122
+ console.error('[SERVER] Save error:', err);
123
+ res.status(500).json({ success: false, error: 'Internal server error: ' + err.message });
124
  }
125
+ });
126
 
127
  // ============================================================
128
+ // 6. GET /api/snapshots/list List All Snapshots (for Admin)
129
  // ============================================================
130
+ app.get('/api/snapshots/list', (req, res) => {
131
+ try {
132
+ const files = fs.readdirSync(SNAPSHOT_DIR)
133
+ .filter(f => f.endsWith('.png'))
134
+ .sort();
 
 
 
 
135
 
136
+ console.log(`[SERVER] Listed ${files.length} snapshots`);
137
+ res.json({ success: true, files });
138
+ } catch (err) {
139
+ console.error('[SERVER] List error:', err);
140
+ res.status(500).json({ success: false, error: err.message });
 
141
  }
 
 
 
 
 
142
  });
143
 
144
  // ============================================================
145
+ // 7. DELETE /api/snapshots/:filename Delete a Snapshot (optional)
146
  // ============================================================
147
+ app.delete('/api/snapshots/:filename', (req, res) => {
148
+ const filename = req.params.filename;
149
+
150
+ if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
151
+ return res.status(400).json({ success: false, error: 'Invalid filename' });
152
+ }
153
+
154
+ const filePath = path.join(SNAPSHOT_DIR, filename);
155
+
156
+ if (fs.existsSync(filePath)) {
157
+ try {
158
+ fs.unlinkSync(filePath);
159
+ console.log(`[SERVER] Deleted snapshot: ${filename}`);
160
+ res.json({ success: true, message: 'Deleted' });
161
+ } catch (err) {
162
+ res.status(500).json({ success: false, error: err.message });
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  }
164
+ } else {
165
+ res.status(404).json({ success: false, error: 'File not found' });
166
  }
167
  });
168
 
169
+ // ============================================================
170
+ // 8. Health Check
171
+ // ============================================================
172
+ app.get('/health', (req, res) => {
173
+ res.status(200).send('OK');
 
174
  });
175
 
176
  // ============================================================
177
+ // 9. Start Server
178
  // ============================================================
179
+ app.listen(PORT, () => {
180
+ console.log(`[SERVER] Running on port ${PORT}`);
181
+ console.log(`[SERVER] Snapshots directory: ${SNAPSHOT_DIR}`);
182
+ console.log(`[SERVER] Serving static files from: ${path.join(__dirname, 'public')}`);
183
+ });