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/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +65 -54
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +72 -58
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
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
|
|
295
|
-
if (path === '/api/browse-img') {
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
if (!cameraId || !
|
|
294
|
+
// Serve browse event image via getDetectionInput
|
|
295
|
+
if (path === '/api/browse-img' && request.body) {
|
|
296
|
+
const rawBody = request.body as any;
|
|
297
|
+
const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
|
|
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
|
|
302
|
-
const mo = await cam.
|
|
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,24 +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
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
361
|
+
|
|
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
|
+
)
|
|
372
|
+
.slice(0, limit)
|
|
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' } });
|
|
378
390
|
} catch (e: any) {
|
|
379
391
|
return response.send(JSON.stringify({ error: e.message }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
|
|
380
392
|
}
|
|
@@ -384,30 +396,29 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
384
396
|
if (path === '/api/add-event' && request.body) {
|
|
385
397
|
const rawBody = request.body as any;
|
|
386
398
|
const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
|
|
387
|
-
const { cameraId, cameraName,
|
|
399
|
+
const { cameraId, cameraName, detectionId, timestamp, detectedClass, score, boundingBox, inputDimensions, label } = body;
|
|
388
400
|
|
|
389
401
|
if (!label || label === 'discard') return response.send(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
|
|
390
402
|
|
|
391
|
-
// Get image via thumbnail
|
|
392
403
|
let jpeg: Buffer | undefined;
|
|
393
404
|
try {
|
|
394
|
-
const cam = systemManager.getDeviceById(cameraId) as
|
|
395
|
-
const mo = await cam.
|
|
405
|
+
const cam = systemManager.getDeviceById(cameraId) as unknown as ObjectDetector;
|
|
406
|
+
const mo = await cam.getDetectionInput(detectionId);
|
|
396
407
|
jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
397
408
|
} catch (e) {
|
|
398
|
-
this.console.warn(`Could not get
|
|
409
|
+
this.console.warn(`Could not get image for browse event:`, e);
|
|
399
410
|
}
|
|
400
411
|
|
|
401
412
|
if (!jpeg) return response.send(JSON.stringify({ error: 'Could not retrieve image' }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
|
|
402
413
|
|
|
403
414
|
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
|
|
405
415
|
const record: CaptureRecord = {
|
|
406
416
|
id, cameraId, cameraName, timestamp,
|
|
407
417
|
detectedClass: detectedClass || 'unknown',
|
|
408
|
-
score: 1,
|
|
418
|
+
score: score || 1,
|
|
409
419
|
boundingBox: boundingBox || [0, 0, inputDimensions?.[0] || 1920, inputDimensions?.[1] || 1080],
|
|
410
420
|
inputDimensions: inputDimensions || [1920, 1080],
|
|
421
|
+
detectionId,
|
|
411
422
|
reviewed: true, label,
|
|
412
423
|
};
|
|
413
424
|
this.captures.set(id, record);
|
|
@@ -641,6 +652,12 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
641
652
|
<option value="24" selected>Last 24 hours</option>
|
|
642
653
|
<option value="72">Last 3 days</option>
|
|
643
654
|
</select>
|
|
655
|
+
<select id="browse-limit" style="padding:8px 12px;background:#222;border:1px solid #444;color:#fff;border-radius:6px;font-size:13px;">
|
|
656
|
+
<option value="50">50 events</option>
|
|
657
|
+
<option value="100" selected>100 events</option>
|
|
658
|
+
<option value="250">250 events</option>
|
|
659
|
+
<option value="500">500 events</option>
|
|
660
|
+
</select>
|
|
644
661
|
<button class="export-btn" onclick="loadBrowse()" style="padding:8px 16px;">Load Events</button>
|
|
645
662
|
<span id="browse-status" style="font-size:13px;color:#888;"></span>
|
|
646
663
|
</div>
|
|
@@ -860,6 +877,7 @@ let browseEvents = [];
|
|
|
860
877
|
async function loadBrowse() {
|
|
861
878
|
const cameraId = document.getElementById('browse-camera').value;
|
|
862
879
|
const hours = document.getElementById('browse-hours').value;
|
|
880
|
+
const limit = document.getElementById('browse-limit').value;
|
|
863
881
|
const status = document.getElementById('browse-status');
|
|
864
882
|
const list = document.getElementById('browse-list');
|
|
865
883
|
|
|
@@ -870,7 +888,7 @@ async function loadBrowse() {
|
|
|
870
888
|
browseEvents = [];
|
|
871
889
|
|
|
872
890
|
try {
|
|
873
|
-
const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours);
|
|
891
|
+
const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours + '&limit=' + limit);
|
|
874
892
|
const events = await res.json();
|
|
875
893
|
|
|
876
894
|
if (events.error) { status.textContent = 'Error: ' + events.error; return; }
|
|
@@ -930,35 +948,29 @@ async function loadBrowse() {
|
|
|
930
948
|
}
|
|
931
949
|
|
|
932
950
|
function loadBrowseImage(i, ev) {
|
|
933
|
-
fetch(BASE + '/api/browse-img
|
|
951
|
+
fetch(BASE + '/api/browse-img', {
|
|
952
|
+
method: 'POST',
|
|
953
|
+
headers: { 'Content-Type': 'application/json' },
|
|
954
|
+
body: JSON.stringify({ cameraId: ev.cameraId, detectionId: ev.detectionId }),
|
|
955
|
+
})
|
|
934
956
|
.then(r => r.ok ? r.blob() : null)
|
|
935
957
|
.then(blob => {
|
|
936
958
|
if (!blob) return;
|
|
937
959
|
const url = URL.createObjectURL(blob);
|
|
938
960
|
const img = new Image();
|
|
939
961
|
img.onload = () => {
|
|
962
|
+
const primary = (ev.detections || [])[0];
|
|
963
|
+
if (!primary?.boundingBox) return;
|
|
940
964
|
imgCache.set('browse-' + i, img);
|
|
965
|
+
// Use drawDetection by temporarily remapping canvas IDs
|
|
941
966
|
const fullCanvas = document.getElementById('bcanvas-full-' + i);
|
|
942
967
|
const cropCanvas = document.getElementById('bcanvas-crop-' + i);
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
const dw = iw * scale, dh = ih * scale;
|
|
950
|
-
ctx.fillStyle = '#111'; ctx.fillRect(0, 0, cw, ch);
|
|
951
|
-
ctx.drawImage(img, (cw-dw)/2, (ch-dh)/2, dw, dh);
|
|
952
|
-
// Label classes
|
|
953
|
-
const labels = (ev.detectionClasses || []).join(', ');
|
|
954
|
-
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, ch-20, cw, 20);
|
|
955
|
-
ctx.fillStyle = '#f90'; ctx.font = 'bold 11px sans-serif';
|
|
956
|
-
ctx.fillText(labels, 4, ch-5);
|
|
957
|
-
fullCanvas.onclick = () => openLightboxImg(img, ev.cameraName, ev.timestamp);
|
|
958
|
-
}
|
|
959
|
-
// Hide crop panel — no bounding box available
|
|
960
|
-
const cropPanel = document.getElementById('bcanvas-crop-' + i)?.closest('.img-panel');
|
|
961
|
-
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); }
|
|
962
974
|
URL.revokeObjectURL(url);
|
|
963
975
|
};
|
|
964
976
|
img.src = url;
|
|
@@ -973,17 +985,19 @@ async function addBrowseEvent(i, label) {
|
|
|
973
985
|
|
|
974
986
|
if (label !== 'discard') {
|
|
975
987
|
try {
|
|
988
|
+
const primary = (ev.detections || [])[0] || {};
|
|
976
989
|
const res = await fetch(BASE + '/api/add-event', {
|
|
977
990
|
method: 'POST',
|
|
978
991
|
headers: { 'Content-Type': 'application/json' },
|
|
979
992
|
body: JSON.stringify({
|
|
980
993
|
cameraId: ev.cameraId,
|
|
981
994
|
cameraName: ev.cameraName,
|
|
982
|
-
|
|
995
|
+
detectionId: ev.detectionId,
|
|
983
996
|
timestamp: ev.timestamp,
|
|
984
|
-
detectedClass:
|
|
985
|
-
|
|
986
|
-
|
|
997
|
+
detectedClass: primary.className || 'unknown',
|
|
998
|
+
score: primary.score || 1,
|
|
999
|
+
boundingBox: primary.boundingBox || null,
|
|
1000
|
+
inputDimensions: ev.inputDimensions || null,
|
|
987
1001
|
label,
|
|
988
1002
|
}),
|
|
989
1003
|
});
|