scrypted-detection-trainer 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.zip CHANGED
Binary file
@@ -1793,18 +1793,18 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1793
1793
  async onRequest(request, response) {
1794
1794
  const url = new URL(request.url, 'http://localhost');
1795
1795
  const path = url.pathname.replace(request.rootPath, '');
1796
- // Serve browse event image (fetch on demand from camera)
1796
+ // Serve browse event image via getVideoClipThumbnail
1797
1797
  if (path === '/api/browse-img') {
1798
1798
  const params = new URL(request.url, 'http://localhost').searchParams;
1799
1799
  const cameraId = params.get('cameraId')?.replace(/[^a-zA-Z0-9_\-]/g, '');
1800
- const detectionId = params.get('detectionId')?.replace(/[^a-zA-Z0-9_\-]/g, '');
1801
- if (!cameraId || !detectionId)
1800
+ const thumbnailId = params.get('thumbnailId')?.replace(/[^a-zA-Z0-9_\-:.]/g, '');
1801
+ if (!cameraId || !thumbnailId)
1802
1802
  return response.send('Missing params', { code: 400 });
1803
1803
  try {
1804
1804
  const cam = systemManager.getDeviceById(cameraId);
1805
- const mo = await cam.getDetectionInput(detectionId);
1805
+ const mo = await cam.getVideoClipThumbnail(thumbnailId);
1806
1806
  const jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
1807
- return response.send(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'max-age=60' } });
1807
+ return response.send(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'max-age=3600' } });
1808
1808
  }
1809
1809
  catch (e) {
1810
1810
  return response.send('Image unavailable', { code: 404 });
@@ -1859,25 +1859,22 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1859
1859
  return response.send('Camera not found', { code: 404 });
1860
1860
  const endTime = Date.now();
1861
1861
  const startTime = endTime - hours * 3600 * 1000;
1862
- const events = await cam.getRecordedEvents({ startTime, endTime });
1863
- // Filter to ObjectDetector events only and limit to 100
1864
- const detectionEvents = (events || [])
1865
- .filter((e) => e.details?.eventInterface === 'ObjectDetector' && e.data?.detections?.length)
1862
+ const clips = await cam.getVideoClips({ startTime, endTime });
1863
+ const events = (clips || [])
1864
+ .filter((c) => c.detectionClasses?.length && c.thumbnailId)
1866
1865
  .slice(0, 100)
1867
- .map((e) => ({
1868
- detectionId: e.data?.detectionId,
1869
- timestamp: e.details?.eventTime || e.data?.timestamp,
1870
- detections: (e.data?.detections || []).map((d) => ({
1871
- className: d.className,
1872
- score: d.score,
1873
- boundingBox: d.boundingBox,
1874
- })),
1875
- inputDimensions: e.data?.inputDimensions,
1866
+ .map((c) => ({
1867
+ clipId: c.id,
1868
+ thumbnailId: c.thumbnailId,
1869
+ timestamp: c.startTime,
1870
+ detectionClasses: c.detectionClasses || [],
1871
+ // bounding box not available at clip level — use full frame
1872
+ boundingBox: null,
1873
+ inputDimensions: null,
1876
1874
  cameraId,
1877
1875
  cameraName: cam.name,
1878
- }))
1879
- .filter((e) => e.detectionId && e.inputDimensions);
1880
- return response.send(JSON.stringify(detectionEvents), { headers: { 'Content-Type': 'application/json' } });
1876
+ }));
1877
+ return response.send(JSON.stringify(events), { headers: { 'Content-Type': 'application/json' } });
1881
1878
  }
1882
1879
  catch (e) {
1883
1880
  return response.send(JSON.stringify({ error: e.message }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
@@ -1887,25 +1884,30 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1887
1884
  if (path === '/api/add-event' && request.body) {
1888
1885
  const rawBody = request.body;
1889
1886
  const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
1890
- const { cameraId, cameraName, detectionId, timestamp, detectedClass, score, boundingBox, inputDimensions, label } = body;
1887
+ const { cameraId, cameraName, thumbnailId, timestamp, detectedClass, boundingBox, inputDimensions, label } = body;
1891
1888
  if (!label || label === 'discard')
1892
1889
  return response.send(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
1893
- // Try to get the image
1890
+ // Get image via thumbnail
1894
1891
  let jpeg;
1895
1892
  try {
1896
1893
  const cam = systemManager.getDeviceById(cameraId);
1897
- const mo = await cam.getDetectionInput(detectionId);
1894
+ const mo = await cam.getVideoClipThumbnail(thumbnailId);
1898
1895
  jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
1899
1896
  }
1900
1897
  catch (e) {
1901
- this.console.warn(`Could not get image for browse event ${detectionId}:`, e);
1898
+ this.console.warn(`Could not get thumbnail for browse event:`, e);
1902
1899
  }
1903
1900
  if (!jpeg)
1904
1901
  return response.send(JSON.stringify({ error: 'Could not retrieve image' }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
1905
1902
  const id = `browse-${timestamp}-${Math.random().toString(36).slice(2, 6)}`;
1903
+ // For clips we don't have per-detection bounding boxes — store full frame dimensions
1906
1904
  const record = {
1907
- id, cameraId, cameraName, timestamp, detectedClass, score,
1908
- boundingBox, inputDimensions, detectionId, reviewed: true, label,
1905
+ id, cameraId, cameraName, timestamp,
1906
+ detectedClass: detectedClass || 'unknown',
1907
+ score: 1,
1908
+ boundingBox: boundingBox || [0, 0, inputDimensions?.[0] || 1920, inputDimensions?.[1] || 1080],
1909
+ inputDimensions: inputDimensions || [1920, 1080],
1910
+ reviewed: true, label,
1909
1911
  };
1910
1912
  this.captures.set(id, record);
1911
1913
  this.saveImage(id, jpeg);
@@ -2282,6 +2284,22 @@ function openLightbox(r) {
2282
2284
  document.addEventListener('keydown', lbKeyHandler);
2283
2285
  }
2284
2286
 
2287
+ function openLightboxImg(img, cameraName, timestamp) {
2288
+ const lb = document.getElementById('lightbox');
2289
+ const lbCanvas = document.getElementById('lightbox-canvas');
2290
+ const maxW = window.innerWidth * 0.9;
2291
+ const maxH = window.innerHeight * 0.8;
2292
+ const scale = Math.min(maxW / img.naturalWidth, maxH / img.naturalHeight, 1);
2293
+ lbCanvas.width = Math.round(img.naturalWidth * scale);
2294
+ lbCanvas.height = Math.round(img.naturalHeight * scale);
2295
+ const ctx = lbCanvas.getContext('2d');
2296
+ ctx.drawImage(img, 0, 0, lbCanvas.width, lbCanvas.height);
2297
+ document.getElementById('lightbox-meta').textContent =
2298
+ cameraName + ' · ' + new Date(timestamp).toLocaleString();
2299
+ lb.classList.add('open');
2300
+ document.addEventListener('keydown', lbKeyHandler);
2301
+ }
2302
+
2285
2303
  function closeLightbox() {
2286
2304
  document.getElementById('lightbox').classList.remove('open');
2287
2305
  document.removeEventListener('keydown', lbKeyHandler);
@@ -2322,75 +2340,6 @@ async function initBrowse() {
2322
2340
  }
2323
2341
  }
2324
2342
 
2325
- async function loadBrowse() {
2326
- const cameraId = document.getElementById('browse-camera').value;
2327
- const hours = document.getElementById('browse-hours').value;
2328
- const status = document.getElementById('browse-status');
2329
- const list = document.getElementById('browse-list');
2330
-
2331
- if (!cameraId) { status.textContent = 'Select a camera first'; return; }
2332
-
2333
- status.textContent = 'Loading…';
2334
- list.innerHTML = '';
2335
-
2336
- try {
2337
- const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours);
2338
- const events = await res.json();
2339
-
2340
- if (events.error) { status.textContent = 'Error: ' + events.error; return; }
2341
- if (!events.length) { status.textContent = 'No detection events found.'; list.innerHTML = '<div class="empty"><div class="icon">🔍</div><div>No ObjectDetector events in this time range.</div></div>'; return; }
2342
-
2343
- status.textContent = events.length + ' events found';
2344
-
2345
- list.innerHTML = events.map((ev, i) => {
2346
- const date = new Date(ev.timestamp).toLocaleString();
2347
- const dets = ev.detections || [];
2348
- const primary = dets[0] || {};
2349
- const score = Math.round((primary.score || 0) * 100);
2350
- const allClasses = dets.map(d => d.className + ' ' + Math.round((d.score||0)*100) + '%').join(', ');
2351
- return \`
2352
- <div class="detection" id="bev-\${i}" style="opacity:1;transition:opacity .3s">
2353
- <div class="detection-imgs">
2354
- <div class="img-panel">
2355
- <div class="img-label">Full frame</div>
2356
- <canvas id="bcanvas-\${i}" class="det-canvas" width="240" height="160"></canvas>
2357
- </div>
2358
- <div class="img-panel" id="bcrop-panel-\${i}">
2359
- <div class="img-label">Crop</div>
2360
- <canvas id="bcanvas-crop-\${i}" class="det-canvas" width="160" height="160"></canvas>
2361
- </div>
2362
- </div>
2363
- <div class="detection-info">
2364
- <div class="detection-meta">
2365
- <div><strong>\${ev.cameraName}</strong></div>
2366
- <div>\${date}</div>
2367
- <div class="det-class-badge">\${allClasses}</div>
2368
- </div>
2369
- <div style="font-size:12px;color:#888;">Add to dataset as:</div>
2370
- <div class="label-buttons" id="blabels-\${i}">
2371
- <button class="label-btn person" onclick="addEvent(\${i})('person')">👤 Person</button>
2372
- <button class="label-btn animal" onclick="addEvent(\${i})('animal')">🐾 Animal</button>
2373
- <button class="label-btn face" onclick="addEvent(\${i})('face')">😀 Face</button>
2374
- <button class="label-btn vehicle" onclick="addEvent(\${i})('vehicle')">🚗 Vehicle</button>
2375
- <button class="label-btn" onclick="addEvent(\${i})('plate')">🔢 Plate</button>
2376
- <button class="label-btn" onclick="addEvent(\${i})('package')">📦 Package</button>
2377
- <button class="label-btn discard" onclick="addEvent(\${i})('discard')">🗑 Skip</button>
2378
- </div>
2379
- </div>
2380
- </div>\`;
2381
- }).join('');
2382
-
2383
- // Load images for each event
2384
- for (let i = 0; i < events.length; i++) {
2385
- const ev = events[i];
2386
- loadBrowseImage(i, ev);
2387
- }
2388
-
2389
- } catch(e) {
2390
- status.textContent = 'Error: ' + e.message;
2391
- }
2392
- }
2393
-
2394
2343
  // Store browse events for addEvent closure
2395
2344
  let browseEvents = [];
2396
2345
 
@@ -2467,12 +2416,7 @@ async function loadBrowse() {
2467
2416
  }
2468
2417
 
2469
2418
  function loadBrowseImage(i, ev) {
2470
- const primary = (ev.detections || [])[0];
2471
- if (!primary?.boundingBox) return;
2472
- // Request image via the img endpoint using detectionId as the key
2473
- // We store a browse-prefixed image server-side only after adding — for preview
2474
- // use a placeholder fetch to trigger server-side caching
2475
- fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&detectionId=' + ev.detectionId)
2419
+ fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&thumbnailId=' + encodeURIComponent(ev.thumbnailId))
2476
2420
  .then(r => r.ok ? r.blob() : null)
2477
2421
  .then(blob => {
2478
2422
  if (!blob) return;
@@ -2480,15 +2424,27 @@ function loadBrowseImage(i, ev) {
2480
2424
  const img = new Image();
2481
2425
  img.onload = () => {
2482
2426
  imgCache.set('browse-' + i, img);
2483
- // Draw on full canvas
2484
2427
  const fullCanvas = document.getElementById('bcanvas-full-' + i);
2485
2428
  const cropCanvas = document.getElementById('bcanvas-crop-' + i);
2486
- if (fullCanvas) fullCanvas.id = 'canvas-full-browse' + i;
2487
- if (cropCanvas) cropCanvas.id = 'canvas-crop-browse' + i;
2488
- const fakeR = { ...ev, id: 'browse' + i, boundingBox: primary.boundingBox, detectedClass: primary.className, score: primary.score };
2489
- drawDetection(img, fakeR);
2490
- if (fullCanvas) { fullCanvas.id = 'bcanvas-full-' + i; fullCanvas.onclick = () => openLightbox(fakeR); }
2491
- if (cropCanvas) { cropCanvas.id = 'bcanvas-crop-' + i; cropCanvas.onclick = () => openLightbox(fakeR); }
2429
+ const iw = img.naturalWidth, ih = img.naturalHeight;
2430
+ // No bounding box for clip thumbnails — just draw the full image
2431
+ if (fullCanvas) {
2432
+ const ctx = fullCanvas.getContext('2d');
2433
+ const cw = fullCanvas.width, ch = fullCanvas.height;
2434
+ const scale = Math.min(cw / iw, ch / ih);
2435
+ const dw = iw * scale, dh = ih * scale;
2436
+ ctx.fillStyle = '#111'; ctx.fillRect(0, 0, cw, ch);
2437
+ ctx.drawImage(img, (cw-dw)/2, (ch-dh)/2, dw, dh);
2438
+ // Label classes
2439
+ const labels = (ev.detectionClasses || []).join(', ');
2440
+ ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, ch-20, cw, 20);
2441
+ ctx.fillStyle = '#f90'; ctx.font = 'bold 11px sans-serif';
2442
+ ctx.fillText(labels, 4, ch-5);
2443
+ fullCanvas.onclick = () => openLightboxImg(img, ev.cameraName, ev.timestamp);
2444
+ }
2445
+ // Hide crop panel — no bounding box available
2446
+ const cropPanel = document.getElementById('bcanvas-crop-' + i)?.closest('.img-panel') as HTMLElement;
2447
+ if (cropPanel) cropPanel.style.display = 'none';
2492
2448
  URL.revokeObjectURL(url);
2493
2449
  };
2494
2450
  img.src = url;
@@ -2502,8 +2458,6 @@ async function addBrowseEvent(i, label) {
2502
2458
  if (el) { el.style.opacity = '0.4'; el.querySelectorAll('button').forEach(b => b.disabled = true); }
2503
2459
 
2504
2460
  if (label !== 'discard') {
2505
- const primary = (ev.detections || [])[0];
2506
- if (!primary) return;
2507
2461
  try {
2508
2462
  const res = await fetch(BASE + '/api/add-event', {
2509
2463
  method: 'POST',
@@ -2511,12 +2465,11 @@ async function addBrowseEvent(i, label) {
2511
2465
  body: JSON.stringify({
2512
2466
  cameraId: ev.cameraId,
2513
2467
  cameraName: ev.cameraName,
2514
- detectionId: ev.detectionId,
2468
+ thumbnailId: ev.thumbnailId,
2515
2469
  timestamp: ev.timestamp,
2516
- detectedClass: primary.className,
2517
- score: primary.score,
2518
- boundingBox: primary.boundingBox,
2519
- inputDimensions: ev.inputDimensions,
2470
+ detectedClass: (ev.detectionClasses || [])[0] || 'unknown',
2471
+ boundingBox: null,
2472
+ inputDimensions: null,
2520
2473
  label,
2521
2474
  }),
2522
2475
  });