scrypted-detection-trainer 0.1.10 → 0.1.11

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);
@@ -2467,12 +2485,7 @@ async function loadBrowse() {
2467
2485
  }
2468
2486
 
2469
2487
  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)
2488
+ fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&thumbnailId=' + encodeURIComponent(ev.thumbnailId))
2476
2489
  .then(r => r.ok ? r.blob() : null)
2477
2490
  .then(blob => {
2478
2491
  if (!blob) return;
@@ -2480,15 +2493,27 @@ function loadBrowseImage(i, ev) {
2480
2493
  const img = new Image();
2481
2494
  img.onload = () => {
2482
2495
  imgCache.set('browse-' + i, img);
2483
- // Draw on full canvas
2484
2496
  const fullCanvas = document.getElementById('bcanvas-full-' + i);
2485
2497
  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); }
2498
+ const iw = img.naturalWidth, ih = img.naturalHeight;
2499
+ // No bounding box for clip thumbnails — just draw the full image
2500
+ if (fullCanvas) {
2501
+ const ctx = fullCanvas.getContext('2d');
2502
+ const cw = fullCanvas.width, ch = fullCanvas.height;
2503
+ const scale = Math.min(cw / iw, ch / ih);
2504
+ const dw = iw * scale, dh = ih * scale;
2505
+ ctx.fillStyle = '#111'; ctx.fillRect(0, 0, cw, ch);
2506
+ ctx.drawImage(img, (cw-dw)/2, (ch-dh)/2, dw, dh);
2507
+ // Label classes
2508
+ const labels = (ev.detectionClasses || []).join(', ');
2509
+ ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, ch-20, cw, 20);
2510
+ ctx.fillStyle = '#f90'; ctx.font = 'bold 11px sans-serif';
2511
+ ctx.fillText(labels, 4, ch-5);
2512
+ fullCanvas.onclick = () => openLightboxImg(img, ev.cameraName, ev.timestamp);
2513
+ }
2514
+ // Hide crop panel — no bounding box available
2515
+ const cropPanel = document.getElementById('bcanvas-crop-' + i)?.closest('.img-panel') as HTMLElement;
2516
+ if (cropPanel) cropPanel.style.display = 'none';
2492
2517
  URL.revokeObjectURL(url);
2493
2518
  };
2494
2519
  img.src = url;
@@ -2502,8 +2527,6 @@ async function addBrowseEvent(i, label) {
2502
2527
  if (el) { el.style.opacity = '0.4'; el.querySelectorAll('button').forEach(b => b.disabled = true); }
2503
2528
 
2504
2529
  if (label !== 'discard') {
2505
- const primary = (ev.detections || [])[0];
2506
- if (!primary) return;
2507
2530
  try {
2508
2531
  const res = await fetch(BASE + '/api/add-event', {
2509
2532
  method: 'POST',
@@ -2511,12 +2534,11 @@ async function addBrowseEvent(i, label) {
2511
2534
  body: JSON.stringify({
2512
2535
  cameraId: ev.cameraId,
2513
2536
  cameraName: ev.cameraName,
2514
- detectionId: ev.detectionId,
2537
+ thumbnailId: ev.thumbnailId,
2515
2538
  timestamp: ev.timestamp,
2516
- detectedClass: primary.className,
2517
- score: primary.score,
2518
- boundingBox: primary.boundingBox,
2519
- inputDimensions: ev.inputDimensions,
2539
+ detectedClass: (ev.detectionClasses || [])[0] || 'unknown',
2540
+ boundingBox: null,
2541
+ inputDimensions: null,
2520
2542
  label,
2521
2543
  }),
2522
2544
  });