scrypted-detection-trainer 0.1.14 → 0.1.16

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,16 +1793,16 @@ 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 via getVideoClipThumbnail
1796
+ // Serve browse event image via getDetectionInput
1797
1797
  if (path === '/api/browse-img' && request.body) {
1798
1798
  const rawBody = request.body;
1799
1799
  const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
1800
- const { cameraId, thumbnailId } = body;
1801
- if (!cameraId || thumbnailId === undefined)
1800
+ const { cameraId, detectionId } = body;
1801
+ if (!cameraId || !detectionId)
1802
1802
  return response.send('Missing params', { code: 400 });
1803
1803
  try {
1804
1804
  const cam = systemManager.getDeviceById(cameraId);
1805
- const mo = await cam.getVideoClipThumbnail(thumbnailId);
1805
+ const mo = await cam.getDetectionInput(detectionId);
1806
1806
  const jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
1807
1807
  return response.send(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'max-age=3600' } });
1808
1808
  }
@@ -1851,6 +1851,7 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1851
1851
  const params = new URL(request.url, 'http://localhost').searchParams;
1852
1852
  const cameraId = params.get('cameraId');
1853
1853
  const hours = parseInt(params.get('hours') || '24');
1854
+ const limit = parseInt(params.get('limit') || '100');
1854
1855
  if (!cameraId)
1855
1856
  return response.send('Missing cameraId', { code: 400 });
1856
1857
  try {
@@ -1859,23 +1860,30 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1859
1860
  return response.send('Camera not found', { code: 404 });
1860
1861
  const endTime = Date.now();
1861
1862
  const startTime = endTime - hours * 3600 * 1000;
1862
- const clips = await cam.getVideoClips({ startTime, endTime });
1863
- const limit = parseInt(params.get('limit') || '100');
1864
- const events = (clips || [])
1865
- .filter((c) => c.detectionClasses?.length && c.thumbnailId)
1863
+ // Use getRecordedEvents to get ObjectDetector events with bounding boxes
1864
+ const recorded = await cam.getRecordedEvents({ startTime, endTime });
1865
+ const detectionEvents = (recorded || [])
1866
+ .filter((e) => e.details?.eventInterface === 'ObjectDetector' &&
1867
+ e.data?.detections?.length &&
1868
+ e.data?.detectionId &&
1869
+ e.data?.inputDimensions)
1866
1870
  .slice(0, limit)
1867
- .map((c) => ({
1868
- clipId: c.id,
1869
- thumbnailId: c.thumbnailId,
1870
- timestamp: c.startTime,
1871
- detectionClasses: c.detectionClasses || [],
1872
- // bounding box not available at clip level — use full frame
1873
- boundingBox: null,
1874
- inputDimensions: null,
1875
- cameraId,
1876
- cameraName: cam.name,
1877
- }));
1878
- return response.send(JSON.stringify(events), { headers: { 'Content-Type': 'application/json' } });
1871
+ .map((e) => {
1872
+ const ts = e.details?.eventTime || e.data?.timestamp;
1873
+ return {
1874
+ detectionId: e.data.detectionId,
1875
+ timestamp: ts,
1876
+ detections: (e.data.detections || []).map((d) => ({
1877
+ className: d.className,
1878
+ score: d.score,
1879
+ boundingBox: d.boundingBox,
1880
+ })),
1881
+ inputDimensions: e.data.inputDimensions,
1882
+ cameraId,
1883
+ cameraName: cam.name,
1884
+ };
1885
+ });
1886
+ return response.send(JSON.stringify(detectionEvents), { headers: { 'Content-Type': 'application/json' } });
1879
1887
  }
1880
1888
  catch (e) {
1881
1889
  return response.send(JSON.stringify({ error: e.message }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
@@ -1885,29 +1893,28 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1885
1893
  if (path === '/api/add-event' && request.body) {
1886
1894
  const rawBody = request.body;
1887
1895
  const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
1888
- const { cameraId, cameraName, thumbnailId, timestamp, detectedClass, boundingBox, inputDimensions, label } = body;
1896
+ const { cameraId, cameraName, detectionId, timestamp, detectedClass, score, boundingBox, inputDimensions, label } = body;
1889
1897
  if (!label || label === 'discard')
1890
1898
  return response.send(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
1891
- // Get image via thumbnail
1892
1899
  let jpeg;
1893
1900
  try {
1894
1901
  const cam = systemManager.getDeviceById(cameraId);
1895
- const mo = await cam.getVideoClipThumbnail(thumbnailId);
1902
+ const mo = await cam.getDetectionInput(detectionId);
1896
1903
  jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
1897
1904
  }
1898
1905
  catch (e) {
1899
- this.console.warn(`Could not get thumbnail for browse event:`, e);
1906
+ this.console.warn(`Could not get image for browse event:`, e);
1900
1907
  }
1901
1908
  if (!jpeg)
1902
1909
  return response.send(JSON.stringify({ error: 'Could not retrieve image' }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
1903
1910
  const id = `browse-${timestamp}-${Math.random().toString(36).slice(2, 6)}`;
1904
- // For clips we don't have per-detection bounding boxes — store full frame dimensions
1905
1911
  const record = {
1906
1912
  id, cameraId, cameraName, timestamp,
1907
1913
  detectedClass: detectedClass || 'unknown',
1908
- score: 1,
1914
+ score: score || 1,
1909
1915
  boundingBox: boundingBox || [0, 0, inputDimensions?.[0] || 1920, inputDimensions?.[1] || 1080],
1910
1916
  inputDimensions: inputDimensions || [1920, 1080],
1917
+ detectionId,
1911
1918
  reviewed: true, label,
1912
1919
  };
1913
1920
  this.captures.set(id, record);
@@ -2427,7 +2434,7 @@ function loadBrowseImage(i, ev) {
2427
2434
  fetch(BASE + '/api/browse-img', {
2428
2435
  method: 'POST',
2429
2436
  headers: { 'Content-Type': 'application/json' },
2430
- body: JSON.stringify({ cameraId: ev.cameraId, thumbnailId: ev.thumbnailId }),
2437
+ body: JSON.stringify({ cameraId: ev.cameraId, detectionId: ev.detectionId }),
2431
2438
  })
2432
2439
  .then(r => r.ok ? r.blob() : null)
2433
2440
  .then(blob => {
@@ -2435,28 +2442,18 @@ function loadBrowseImage(i, ev) {
2435
2442
  const url = URL.createObjectURL(blob);
2436
2443
  const img = new Image();
2437
2444
  img.onload = () => {
2445
+ const primary = (ev.detections || [])[0];
2446
+ if (!primary?.boundingBox) return;
2438
2447
  imgCache.set('browse-' + i, img);
2448
+ // Use drawDetection by temporarily remapping canvas IDs
2439
2449
  const fullCanvas = document.getElementById('bcanvas-full-' + i);
2440
2450
  const cropCanvas = document.getElementById('bcanvas-crop-' + i);
2441
- const iw = img.naturalWidth, ih = img.naturalHeight;
2442
- // No bounding box for clip thumbnails — just draw the full image
2443
- if (fullCanvas) {
2444
- const ctx = fullCanvas.getContext('2d');
2445
- const cw = fullCanvas.width, ch = fullCanvas.height;
2446
- const scale = Math.min(cw / iw, ch / ih);
2447
- const dw = iw * scale, dh = ih * scale;
2448
- ctx.fillStyle = '#111'; ctx.fillRect(0, 0, cw, ch);
2449
- ctx.drawImage(img, (cw-dw)/2, (ch-dh)/2, dw, dh);
2450
- // Label classes
2451
- const labels = (ev.detectionClasses || []).join(', ');
2452
- ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, ch-20, cw, 20);
2453
- ctx.fillStyle = '#f90'; ctx.font = 'bold 11px sans-serif';
2454
- ctx.fillText(labels, 4, ch-5);
2455
- fullCanvas.onclick = () => openLightboxImg(img, ev.cameraName, ev.timestamp);
2456
- }
2457
- // Hide crop panel — no bounding box available
2458
- const cropPanel = document.getElementById('bcanvas-crop-' + i)?.closest('.img-panel');
2459
- if (cropPanel) cropPanel.style.display = 'none';
2451
+ if (fullCanvas) fullCanvas.id = 'canvas-full-' + i;
2452
+ if (cropCanvas) cropCanvas.id = 'canvas-crop-' + i;
2453
+ const fakeR = { id: String(i), boundingBox: primary.boundingBox, detectedClass: primary.className, score: primary.score, inputDimensions: ev.inputDimensions };
2454
+ drawDetection(img, fakeR);
2455
+ if (fullCanvas) { fullCanvas.id = 'bcanvas-full-' + i; fullCanvas.onclick = () => openLightbox(fakeR); }
2456
+ if (cropCanvas) { cropCanvas.id = 'bcanvas-crop-' + i; cropCanvas.onclick = () => openLightbox(fakeR); }
2460
2457
  URL.revokeObjectURL(url);
2461
2458
  };
2462
2459
  img.src = url;
@@ -2471,17 +2468,19 @@ async function addBrowseEvent(i, label) {
2471
2468
 
2472
2469
  if (label !== 'discard') {
2473
2470
  try {
2471
+ const primary = (ev.detections || [])[0] || {};
2474
2472
  const res = await fetch(BASE + '/api/add-event', {
2475
2473
  method: 'POST',
2476
2474
  headers: { 'Content-Type': 'application/json' },
2477
2475
  body: JSON.stringify({
2478
2476
  cameraId: ev.cameraId,
2479
2477
  cameraName: ev.cameraName,
2480
- thumbnailId: ev.thumbnailId,
2478
+ detectionId: ev.detectionId,
2481
2479
  timestamp: ev.timestamp,
2482
- detectedClass: (ev.detectionClasses || [])[0] || 'unknown',
2483
- boundingBox: null,
2484
- inputDimensions: null,
2480
+ detectedClass: primary.className || 'unknown',
2481
+ score: primary.score || 1,
2482
+ boundingBox: primary.boundingBox || null,
2483
+ inputDimensions: ev.inputDimensions || null,
2485
2484
  label,
2486
2485
  }),
2487
2486
  });