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/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +69 -47
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +71 -49
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -291,17 +291,17 @@ 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
|
|
294
|
+
// Serve browse event image via getVideoClipThumbnail
|
|
295
295
|
if (path === '/api/browse-img') {
|
|
296
296
|
const params = new URL(request.url, 'http://localhost').searchParams;
|
|
297
297
|
const cameraId = params.get('cameraId')?.replace(/[^a-zA-Z0-9_\-]/g, '');
|
|
298
|
-
const
|
|
299
|
-
if (!cameraId || !
|
|
298
|
+
const thumbnailId = params.get('thumbnailId')?.replace(/[^a-zA-Z0-9_\-:.]/g, '');
|
|
299
|
+
if (!cameraId || !thumbnailId) return response.send('Missing params', { code: 400 });
|
|
300
300
|
try {
|
|
301
|
-
const cam = systemManager.getDeviceById(cameraId) as
|
|
302
|
-
const mo = await cam.
|
|
301
|
+
const cam = systemManager.getDeviceById(cameraId) as any;
|
|
302
|
+
const mo = await cam.getVideoClipThumbnail(thumbnailId);
|
|
303
303
|
const jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
304
|
-
return response.send(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'max-age=
|
|
304
|
+
return response.send(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'max-age=3600' } });
|
|
305
305
|
} catch (e) {
|
|
306
306
|
return response.send('Image unavailable', { code: 404 });
|
|
307
307
|
}
|
|
@@ -357,27 +357,24 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
357
357
|
|
|
358
358
|
const endTime = Date.now();
|
|
359
359
|
const startTime = endTime - hours * 3600 * 1000;
|
|
360
|
-
const
|
|
360
|
+
const clips = await cam.getVideoClips({ startTime, endTime });
|
|
361
361
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
.filter((e: any) => e.details?.eventInterface === 'ObjectDetector' && e.data?.detections?.length)
|
|
362
|
+
const events = (clips || [])
|
|
363
|
+
.filter((c: any) => c.detectionClasses?.length && c.thumbnailId)
|
|
365
364
|
.slice(0, 100)
|
|
366
|
-
.map((
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
inputDimensions: e.data?.inputDimensions,
|
|
365
|
+
.map((c: any) => ({
|
|
366
|
+
clipId: c.id,
|
|
367
|
+
thumbnailId: c.thumbnailId,
|
|
368
|
+
timestamp: c.startTime,
|
|
369
|
+
detectionClasses: c.detectionClasses || [],
|
|
370
|
+
// bounding box not available at clip level — use full frame
|
|
371
|
+
boundingBox: null,
|
|
372
|
+
inputDimensions: null,
|
|
375
373
|
cameraId,
|
|
376
374
|
cameraName: cam.name,
|
|
377
|
-
}))
|
|
378
|
-
.filter((e: any) => e.detectionId && e.inputDimensions);
|
|
375
|
+
}));
|
|
379
376
|
|
|
380
|
-
return response.send(JSON.stringify(
|
|
377
|
+
return response.send(JSON.stringify(events), { headers: { 'Content-Type': 'application/json' } });
|
|
381
378
|
} catch (e: any) {
|
|
382
379
|
return response.send(JSON.stringify({ error: e.message }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
|
|
383
380
|
}
|
|
@@ -387,26 +384,31 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
387
384
|
if (path === '/api/add-event' && request.body) {
|
|
388
385
|
const rawBody = request.body as any;
|
|
389
386
|
const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
|
|
390
|
-
const { cameraId, cameraName,
|
|
387
|
+
const { cameraId, cameraName, thumbnailId, timestamp, detectedClass, boundingBox, inputDimensions, label } = body;
|
|
391
388
|
|
|
392
389
|
if (!label || label === 'discard') return response.send(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
|
|
393
390
|
|
|
394
|
-
//
|
|
391
|
+
// Get image via thumbnail
|
|
395
392
|
let jpeg: Buffer | undefined;
|
|
396
393
|
try {
|
|
397
|
-
const cam = systemManager.getDeviceById(cameraId) as
|
|
398
|
-
const mo = await cam.
|
|
394
|
+
const cam = systemManager.getDeviceById(cameraId) as any;
|
|
395
|
+
const mo = await cam.getVideoClipThumbnail(thumbnailId);
|
|
399
396
|
jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
400
397
|
} catch (e) {
|
|
401
|
-
this.console.warn(`Could not get
|
|
398
|
+
this.console.warn(`Could not get thumbnail for browse event:`, e);
|
|
402
399
|
}
|
|
403
400
|
|
|
404
401
|
if (!jpeg) return response.send(JSON.stringify({ error: 'Could not retrieve image' }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
|
|
405
402
|
|
|
406
403
|
const id = `browse-${timestamp}-${Math.random().toString(36).slice(2, 6)}`;
|
|
404
|
+
// For clips we don't have per-detection bounding boxes — store full frame dimensions
|
|
407
405
|
const record: CaptureRecord = {
|
|
408
|
-
id, cameraId, cameraName, timestamp,
|
|
409
|
-
|
|
406
|
+
id, cameraId, cameraName, timestamp,
|
|
407
|
+
detectedClass: detectedClass || 'unknown',
|
|
408
|
+
score: 1,
|
|
409
|
+
boundingBox: boundingBox || [0, 0, inputDimensions?.[0] || 1920, inputDimensions?.[1] || 1080],
|
|
410
|
+
inputDimensions: inputDimensions || [1920, 1080],
|
|
411
|
+
reviewed: true, label,
|
|
410
412
|
};
|
|
411
413
|
this.captures.set(id, record);
|
|
412
414
|
this.saveImage(id, jpeg);
|
|
@@ -796,6 +798,22 @@ function openLightbox(r) {
|
|
|
796
798
|
document.addEventListener('keydown', lbKeyHandler);
|
|
797
799
|
}
|
|
798
800
|
|
|
801
|
+
function openLightboxImg(img, cameraName, timestamp) {
|
|
802
|
+
const lb = document.getElementById('lightbox');
|
|
803
|
+
const lbCanvas = document.getElementById('lightbox-canvas');
|
|
804
|
+
const maxW = window.innerWidth * 0.9;
|
|
805
|
+
const maxH = window.innerHeight * 0.8;
|
|
806
|
+
const scale = Math.min(maxW / img.naturalWidth, maxH / img.naturalHeight, 1);
|
|
807
|
+
lbCanvas.width = Math.round(img.naturalWidth * scale);
|
|
808
|
+
lbCanvas.height = Math.round(img.naturalHeight * scale);
|
|
809
|
+
const ctx = lbCanvas.getContext('2d');
|
|
810
|
+
ctx.drawImage(img, 0, 0, lbCanvas.width, lbCanvas.height);
|
|
811
|
+
document.getElementById('lightbox-meta').textContent =
|
|
812
|
+
cameraName + ' · ' + new Date(timestamp).toLocaleString();
|
|
813
|
+
lb.classList.add('open');
|
|
814
|
+
document.addEventListener('keydown', lbKeyHandler);
|
|
815
|
+
}
|
|
816
|
+
|
|
799
817
|
function closeLightbox() {
|
|
800
818
|
document.getElementById('lightbox').classList.remove('open');
|
|
801
819
|
document.removeEventListener('keydown', lbKeyHandler);
|
|
@@ -981,12 +999,7 @@ async function loadBrowse() {
|
|
|
981
999
|
}
|
|
982
1000
|
|
|
983
1001
|
function loadBrowseImage(i, ev) {
|
|
984
|
-
|
|
985
|
-
if (!primary?.boundingBox) return;
|
|
986
|
-
// Request image via the img endpoint using detectionId as the key
|
|
987
|
-
// We store a browse-prefixed image server-side only after adding — for preview
|
|
988
|
-
// use a placeholder fetch to trigger server-side caching
|
|
989
|
-
fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&detectionId=' + ev.detectionId)
|
|
1002
|
+
fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&thumbnailId=' + encodeURIComponent(ev.thumbnailId))
|
|
990
1003
|
.then(r => r.ok ? r.blob() : null)
|
|
991
1004
|
.then(blob => {
|
|
992
1005
|
if (!blob) return;
|
|
@@ -994,15 +1007,27 @@ function loadBrowseImage(i, ev) {
|
|
|
994
1007
|
const img = new Image();
|
|
995
1008
|
img.onload = () => {
|
|
996
1009
|
imgCache.set('browse-' + i, img);
|
|
997
|
-
// Draw on full canvas
|
|
998
1010
|
const fullCanvas = document.getElementById('bcanvas-full-' + i);
|
|
999
1011
|
const cropCanvas = document.getElementById('bcanvas-crop-' + i);
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1012
|
+
const iw = img.naturalWidth, ih = img.naturalHeight;
|
|
1013
|
+
// No bounding box for clip thumbnails — just draw the full image
|
|
1014
|
+
if (fullCanvas) {
|
|
1015
|
+
const ctx = fullCanvas.getContext('2d');
|
|
1016
|
+
const cw = fullCanvas.width, ch = fullCanvas.height;
|
|
1017
|
+
const scale = Math.min(cw / iw, ch / ih);
|
|
1018
|
+
const dw = iw * scale, dh = ih * scale;
|
|
1019
|
+
ctx.fillStyle = '#111'; ctx.fillRect(0, 0, cw, ch);
|
|
1020
|
+
ctx.drawImage(img, (cw-dw)/2, (ch-dh)/2, dw, dh);
|
|
1021
|
+
// Label classes
|
|
1022
|
+
const labels = (ev.detectionClasses || []).join(', ');
|
|
1023
|
+
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, ch-20, cw, 20);
|
|
1024
|
+
ctx.fillStyle = '#f90'; ctx.font = 'bold 11px sans-serif';
|
|
1025
|
+
ctx.fillText(labels, 4, ch-5);
|
|
1026
|
+
fullCanvas.onclick = () => openLightboxImg(img, ev.cameraName, ev.timestamp);
|
|
1027
|
+
}
|
|
1028
|
+
// Hide crop panel — no bounding box available
|
|
1029
|
+
const cropPanel = document.getElementById('bcanvas-crop-' + i)?.closest('.img-panel') as HTMLElement;
|
|
1030
|
+
if (cropPanel) cropPanel.style.display = 'none';
|
|
1006
1031
|
URL.revokeObjectURL(url);
|
|
1007
1032
|
};
|
|
1008
1033
|
img.src = url;
|
|
@@ -1016,8 +1041,6 @@ async function addBrowseEvent(i, label) {
|
|
|
1016
1041
|
if (el) { el.style.opacity = '0.4'; el.querySelectorAll('button').forEach(b => b.disabled = true); }
|
|
1017
1042
|
|
|
1018
1043
|
if (label !== 'discard') {
|
|
1019
|
-
const primary = (ev.detections || [])[0];
|
|
1020
|
-
if (!primary) return;
|
|
1021
1044
|
try {
|
|
1022
1045
|
const res = await fetch(BASE + '/api/add-event', {
|
|
1023
1046
|
method: 'POST',
|
|
@@ -1025,12 +1048,11 @@ async function addBrowseEvent(i, label) {
|
|
|
1025
1048
|
body: JSON.stringify({
|
|
1026
1049
|
cameraId: ev.cameraId,
|
|
1027
1050
|
cameraName: ev.cameraName,
|
|
1028
|
-
|
|
1051
|
+
thumbnailId: ev.thumbnailId,
|
|
1029
1052
|
timestamp: ev.timestamp,
|
|
1030
|
-
detectedClass:
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
inputDimensions: ev.inputDimensions,
|
|
1053
|
+
detectedClass: (ev.detectionClasses || [])[0] || 'unknown',
|
|
1054
|
+
boundingBox: null,
|
|
1055
|
+
inputDimensions: null,
|
|
1034
1056
|
label,
|
|
1035
1057
|
}),
|
|
1036
1058
|
});
|