scrypted-detection-trainer 0.1.13 → 0.1.15

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
1797
- if (path === '/api/browse-img') {
1798
- const params = new URL(request.url, 'http://localhost').searchParams;
1799
- const cameraId = params.get('cameraId')?.replace(/[^a-zA-Z0-9_\-]/g, '');
1800
- const thumbnailId = params.get('thumbnailId')?.replace(/[^a-zA-Z0-9_\-:.]/g, '');
1801
- if (!cameraId || !thumbnailId)
1796
+ // Serve browse event image via getDetectionInput
1797
+ if (path === '/api/browse-img' && request.body) {
1798
+ const rawBody = request.body;
1799
+ const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
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,22 +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 events = (clips || [])
1864
- .filter((c) => c.detectionClasses?.length && c.thumbnailId)
1865
- .slice(0, 100)
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,
1874
- cameraId,
1875
- cameraName: cam.name,
1876
- }));
1877
- return response.send(JSON.stringify(events), { headers: { 'Content-Type': 'application/json' } });
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)
1870
+ .slice(0, limit)
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' } });
1878
1887
  }
1879
1888
  catch (e) {
1880
1889
  return response.send(JSON.stringify({ error: e.message }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
@@ -1884,29 +1893,28 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1884
1893
  if (path === '/api/add-event' && request.body) {
1885
1894
  const rawBody = request.body;
1886
1895
  const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
1887
- const { cameraId, cameraName, thumbnailId, timestamp, detectedClass, boundingBox, inputDimensions, label } = body;
1896
+ const { cameraId, cameraName, detectionId, timestamp, detectedClass, score, boundingBox, inputDimensions, label } = body;
1888
1897
  if (!label || label === 'discard')
1889
1898
  return response.send(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
1890
- // Get image via thumbnail
1891
1899
  let jpeg;
1892
1900
  try {
1893
1901
  const cam = systemManager.getDeviceById(cameraId);
1894
- const mo = await cam.getVideoClipThumbnail(thumbnailId);
1902
+ const mo = await cam.getDetectionInput(detectionId);
1895
1903
  jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
1896
1904
  }
1897
1905
  catch (e) {
1898
- this.console.warn(`Could not get thumbnail for browse event:`, e);
1906
+ this.console.warn(`Could not get image for browse event:`, e);
1899
1907
  }
1900
1908
  if (!jpeg)
1901
1909
  return response.send(JSON.stringify({ error: 'Could not retrieve image' }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
1902
1910
  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
1904
1911
  const record = {
1905
1912
  id, cameraId, cameraName, timestamp,
1906
1913
  detectedClass: detectedClass || 'unknown',
1907
- score: 1,
1914
+ score: score || 1,
1908
1915
  boundingBox: boundingBox || [0, 0, inputDimensions?.[0] || 1920, inputDimensions?.[1] || 1080],
1909
1916
  inputDimensions: inputDimensions || [1920, 1080],
1917
+ detectionId,
1910
1918
  reviewed: true, label,
1911
1919
  };
1912
1920
  this.captures.set(id, record);
@@ -2127,6 +2135,12 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
2127
2135
  <option value="24" selected>Last 24 hours</option>
2128
2136
  <option value="72">Last 3 days</option>
2129
2137
  </select>
2138
+ <select id="browse-limit" style="padding:8px 12px;background:#222;border:1px solid #444;color:#fff;border-radius:6px;font-size:13px;">
2139
+ <option value="50">50 events</option>
2140
+ <option value="100" selected>100 events</option>
2141
+ <option value="250">250 events</option>
2142
+ <option value="500">500 events</option>
2143
+ </select>
2130
2144
  <button class="export-btn" onclick="loadBrowse()" style="padding:8px 16px;">Load Events</button>
2131
2145
  <span id="browse-status" style="font-size:13px;color:#888;"></span>
2132
2146
  </div>
@@ -2346,6 +2360,7 @@ let browseEvents = [];
2346
2360
  async function loadBrowse() {
2347
2361
  const cameraId = document.getElementById('browse-camera').value;
2348
2362
  const hours = document.getElementById('browse-hours').value;
2363
+ const limit = document.getElementById('browse-limit').value;
2349
2364
  const status = document.getElementById('browse-status');
2350
2365
  const list = document.getElementById('browse-list');
2351
2366
 
@@ -2356,7 +2371,7 @@ async function loadBrowse() {
2356
2371
  browseEvents = [];
2357
2372
 
2358
2373
  try {
2359
- const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours);
2374
+ const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours + '&limit=' + limit);
2360
2375
  const events = await res.json();
2361
2376
 
2362
2377
  if (events.error) { status.textContent = 'Error: ' + events.error; return; }
@@ -2416,35 +2431,29 @@ async function loadBrowse() {
2416
2431
  }
2417
2432
 
2418
2433
  function loadBrowseImage(i, ev) {
2419
- fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&thumbnailId=' + encodeURIComponent(ev.thumbnailId))
2434
+ fetch(BASE + '/api/browse-img', {
2435
+ method: 'POST',
2436
+ headers: { 'Content-Type': 'application/json' },
2437
+ body: JSON.stringify({ cameraId: ev.cameraId, detectionId: ev.detectionId }),
2438
+ })
2420
2439
  .then(r => r.ok ? r.blob() : null)
2421
2440
  .then(blob => {
2422
2441
  if (!blob) return;
2423
2442
  const url = URL.createObjectURL(blob);
2424
2443
  const img = new Image();
2425
2444
  img.onload = () => {
2445
+ const primary = (ev.detections || [])[0];
2446
+ if (!primary?.boundingBox) return;
2426
2447
  imgCache.set('browse-' + i, img);
2448
+ // Use drawDetection by temporarily remapping canvas IDs
2427
2449
  const fullCanvas = document.getElementById('bcanvas-full-' + i);
2428
2450
  const cropCanvas = document.getElementById('bcanvas-crop-' + i);
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');
2447
- 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); }
2448
2457
  URL.revokeObjectURL(url);
2449
2458
  };
2450
2459
  img.src = url;
@@ -2459,17 +2468,19 @@ async function addBrowseEvent(i, label) {
2459
2468
 
2460
2469
  if (label !== 'discard') {
2461
2470
  try {
2471
+ const primary = (ev.detections || [])[0] || {};
2462
2472
  const res = await fetch(BASE + '/api/add-event', {
2463
2473
  method: 'POST',
2464
2474
  headers: { 'Content-Type': 'application/json' },
2465
2475
  body: JSON.stringify({
2466
2476
  cameraId: ev.cameraId,
2467
2477
  cameraName: ev.cameraName,
2468
- thumbnailId: ev.thumbnailId,
2478
+ detectionId: ev.detectionId,
2469
2479
  timestamp: ev.timestamp,
2470
- detectedClass: (ev.detectionClasses || [])[0] || 'unknown',
2471
- boundingBox: null,
2472
- inputDimensions: null,
2480
+ detectedClass: primary.className || 'unknown',
2481
+ score: primary.score || 1,
2482
+ boundingBox: primary.boundingBox || null,
2483
+ inputDimensions: ev.inputDimensions || null,
2473
2484
  label,
2474
2485
  }),
2475
2486
  });