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/out/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrypted-detection-trainer",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Collect and label detections to fine-tune the Scrypted NVR object detection model.",
5
5
  "keywords": [
6
6
  "scrypted-plugin"
package/src/main.ts CHANGED
@@ -291,15 +291,15 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
291
291
  const url = new URL(request.url, 'http://localhost');
292
292
  const path = url.pathname.replace(request.rootPath, '');
293
293
 
294
- // Serve browse event image via getVideoClipThumbnail
294
+ // Serve browse event image via getDetectionInput
295
295
  if (path === '/api/browse-img' && request.body) {
296
296
  const rawBody = request.body as any;
297
297
  const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
298
- const { cameraId, thumbnailId } = body;
299
- if (!cameraId || thumbnailId === undefined) return response.send('Missing params', { code: 400 });
298
+ const { cameraId, detectionId } = body;
299
+ if (!cameraId || !detectionId) return response.send('Missing params', { code: 400 });
300
300
  try {
301
- const cam = systemManager.getDeviceById(cameraId) as any;
302
- const mo = await cam.getVideoClipThumbnail(thumbnailId);
301
+ const cam = systemManager.getDeviceById(cameraId) as unknown as ObjectDetector;
302
+ const mo = await cam.getDetectionInput(detectionId);
303
303
  const jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
304
304
  return response.send(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'max-age=3600' } });
305
305
  } catch (e) {
@@ -349,6 +349,7 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
349
349
  const params = new URL(request.url, 'http://localhost').searchParams;
350
350
  const cameraId = params.get('cameraId');
351
351
  const hours = parseInt(params.get('hours') || '24');
352
+ const limit = parseInt(params.get('limit') || '100');
352
353
  if (!cameraId) return response.send('Missing cameraId', { code: 400 });
353
354
 
354
355
  try {
@@ -357,25 +358,35 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
357
358
 
358
359
  const endTime = Date.now();
359
360
  const startTime = endTime - hours * 3600 * 1000;
360
- const clips = await cam.getVideoClips({ startTime, endTime });
361
361
 
362
- const limit = parseInt(params.get('limit') || '100');
363
- const events = (clips || [])
364
- .filter((c: any) => c.detectionClasses?.length && c.thumbnailId)
362
+ // Use getRecordedEvents to get ObjectDetector events with bounding boxes
363
+ const recorded = await cam.getRecordedEvents({ startTime, endTime });
364
+
365
+ const detectionEvents = (recorded || [])
366
+ .filter((e: any) =>
367
+ e.details?.eventInterface === 'ObjectDetector' &&
368
+ e.data?.detections?.length &&
369
+ e.data?.detectionId &&
370
+ e.data?.inputDimensions
371
+ )
365
372
  .slice(0, limit)
366
- .map((c: any) => ({
367
- clipId: c.id,
368
- thumbnailId: c.thumbnailId,
369
- timestamp: c.startTime,
370
- detectionClasses: c.detectionClasses || [],
371
- // bounding box not available at clip level — use full frame
372
- boundingBox: null,
373
- inputDimensions: null,
374
- cameraId,
375
- cameraName: cam.name,
376
- }));
377
-
378
- return response.send(JSON.stringify(events), { headers: { 'Content-Type': 'application/json' } });
373
+ .map((e: any) => {
374
+ const ts = e.details?.eventTime || e.data?.timestamp;
375
+ return {
376
+ detectionId: e.data.detectionId,
377
+ timestamp: ts,
378
+ detections: (e.data.detections || []).map((d: any) => ({
379
+ className: d.className,
380
+ score: d.score,
381
+ boundingBox: d.boundingBox,
382
+ })),
383
+ inputDimensions: e.data.inputDimensions,
384
+ cameraId,
385
+ cameraName: cam.name,
386
+ };
387
+ });
388
+
389
+ return response.send(JSON.stringify(detectionEvents), { headers: { 'Content-Type': 'application/json' } });
379
390
  } catch (e: any) {
380
391
  return response.send(JSON.stringify({ error: e.message }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
381
392
  }
@@ -385,30 +396,29 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
385
396
  if (path === '/api/add-event' && request.body) {
386
397
  const rawBody = request.body as any;
387
398
  const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
388
- const { cameraId, cameraName, thumbnailId, timestamp, detectedClass, boundingBox, inputDimensions, label } = body;
399
+ const { cameraId, cameraName, detectionId, timestamp, detectedClass, score, boundingBox, inputDimensions, label } = body;
389
400
 
390
401
  if (!label || label === 'discard') return response.send(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
391
402
 
392
- // Get image via thumbnail
393
403
  let jpeg: Buffer | undefined;
394
404
  try {
395
- const cam = systemManager.getDeviceById(cameraId) as any;
396
- const mo = await cam.getVideoClipThumbnail(thumbnailId);
405
+ const cam = systemManager.getDeviceById(cameraId) as unknown as ObjectDetector;
406
+ const mo = await cam.getDetectionInput(detectionId);
397
407
  jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
398
408
  } catch (e) {
399
- this.console.warn(`Could not get thumbnail for browse event:`, e);
409
+ this.console.warn(`Could not get image for browse event:`, e);
400
410
  }
401
411
 
402
412
  if (!jpeg) return response.send(JSON.stringify({ error: 'Could not retrieve image' }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
403
413
 
404
414
  const id = `browse-${timestamp}-${Math.random().toString(36).slice(2, 6)}`;
405
- // For clips we don't have per-detection bounding boxes — store full frame dimensions
406
415
  const record: CaptureRecord = {
407
416
  id, cameraId, cameraName, timestamp,
408
417
  detectedClass: detectedClass || 'unknown',
409
- score: 1,
418
+ score: score || 1,
410
419
  boundingBox: boundingBox || [0, 0, inputDimensions?.[0] || 1920, inputDimensions?.[1] || 1080],
411
420
  inputDimensions: inputDimensions || [1920, 1080],
421
+ detectionId,
412
422
  reviewed: true, label,
413
423
  };
414
424
  this.captures.set(id, record);
@@ -941,7 +951,7 @@ function loadBrowseImage(i, ev) {
941
951
  fetch(BASE + '/api/browse-img', {
942
952
  method: 'POST',
943
953
  headers: { 'Content-Type': 'application/json' },
944
- body: JSON.stringify({ cameraId: ev.cameraId, thumbnailId: ev.thumbnailId }),
954
+ body: JSON.stringify({ cameraId: ev.cameraId, detectionId: ev.detectionId }),
945
955
  })
946
956
  .then(r => r.ok ? r.blob() : null)
947
957
  .then(blob => {
@@ -949,28 +959,18 @@ function loadBrowseImage(i, ev) {
949
959
  const url = URL.createObjectURL(blob);
950
960
  const img = new Image();
951
961
  img.onload = () => {
962
+ const primary = (ev.detections || [])[0];
963
+ if (!primary?.boundingBox) return;
952
964
  imgCache.set('browse-' + i, img);
965
+ // Use drawDetection by temporarily remapping canvas IDs
953
966
  const fullCanvas = document.getElementById('bcanvas-full-' + i);
954
967
  const cropCanvas = document.getElementById('bcanvas-crop-' + i);
955
- const iw = img.naturalWidth, ih = img.naturalHeight;
956
- // No bounding box for clip thumbnails — just draw the full image
957
- if (fullCanvas) {
958
- const ctx = fullCanvas.getContext('2d');
959
- const cw = fullCanvas.width, ch = fullCanvas.height;
960
- const scale = Math.min(cw / iw, ch / ih);
961
- const dw = iw * scale, dh = ih * scale;
962
- ctx.fillStyle = '#111'; ctx.fillRect(0, 0, cw, ch);
963
- ctx.drawImage(img, (cw-dw)/2, (ch-dh)/2, dw, dh);
964
- // Label classes
965
- const labels = (ev.detectionClasses || []).join(', ');
966
- ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, ch-20, cw, 20);
967
- ctx.fillStyle = '#f90'; ctx.font = 'bold 11px sans-serif';
968
- ctx.fillText(labels, 4, ch-5);
969
- fullCanvas.onclick = () => openLightboxImg(img, ev.cameraName, ev.timestamp);
970
- }
971
- // Hide crop panel — no bounding box available
972
- const cropPanel = document.getElementById('bcanvas-crop-' + i)?.closest('.img-panel');
973
- if (cropPanel) cropPanel.style.display = 'none';
968
+ if (fullCanvas) fullCanvas.id = 'canvas-full-' + i;
969
+ if (cropCanvas) cropCanvas.id = 'canvas-crop-' + i;
970
+ const fakeR = { id: String(i), boundingBox: primary.boundingBox, detectedClass: primary.className, score: primary.score, inputDimensions: ev.inputDimensions };
971
+ drawDetection(img, fakeR);
972
+ if (fullCanvas) { fullCanvas.id = 'bcanvas-full-' + i; fullCanvas.onclick = () => openLightbox(fakeR); }
973
+ if (cropCanvas) { cropCanvas.id = 'bcanvas-crop-' + i; cropCanvas.onclick = () => openLightbox(fakeR); }
974
974
  URL.revokeObjectURL(url);
975
975
  };
976
976
  img.src = url;
@@ -985,17 +985,19 @@ async function addBrowseEvent(i, label) {
985
985
 
986
986
  if (label !== 'discard') {
987
987
  try {
988
+ const primary = (ev.detections || [])[0] || {};
988
989
  const res = await fetch(BASE + '/api/add-event', {
989
990
  method: 'POST',
990
991
  headers: { 'Content-Type': 'application/json' },
991
992
  body: JSON.stringify({
992
993
  cameraId: ev.cameraId,
993
994
  cameraName: ev.cameraName,
994
- thumbnailId: ev.thumbnailId,
995
+ detectionId: ev.detectionId,
995
996
  timestamp: ev.timestamp,
996
- detectedClass: (ev.detectionClasses || [])[0] || 'unknown',
997
- boundingBox: null,
998
- inputDimensions: null,
997
+ detectedClass: primary.className || 'unknown',
998
+ score: primary.score || 1,
999
+ boundingBox: primary.boundingBox || null,
1000
+ inputDimensions: ev.inputDimensions || null,
999
1001
  label,
1000
1002
  }),
1001
1003
  });