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/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -1793,18 +1793,18 @@ 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
|
|
1796
|
+
// Serve browse event image via getVideoClipThumbnail
|
|
1797
1797
|
if (path === '/api/browse-img') {
|
|
1798
1798
|
const params = new URL(request.url, 'http://localhost').searchParams;
|
|
1799
1799
|
const cameraId = params.get('cameraId')?.replace(/[^a-zA-Z0-9_\-]/g, '');
|
|
1800
|
-
const
|
|
1801
|
-
if (!cameraId || !
|
|
1800
|
+
const thumbnailId = params.get('thumbnailId')?.replace(/[^a-zA-Z0-9_\-:.]/g, '');
|
|
1801
|
+
if (!cameraId || !thumbnailId)
|
|
1802
1802
|
return response.send('Missing params', { code: 400 });
|
|
1803
1803
|
try {
|
|
1804
1804
|
const cam = systemManager.getDeviceById(cameraId);
|
|
1805
|
-
const mo = await cam.
|
|
1805
|
+
const mo = await cam.getVideoClipThumbnail(thumbnailId);
|
|
1806
1806
|
const jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
1807
|
-
return response.send(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'max-age=
|
|
1807
|
+
return response.send(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'max-age=3600' } });
|
|
1808
1808
|
}
|
|
1809
1809
|
catch (e) {
|
|
1810
1810
|
return response.send('Image unavailable', { code: 404 });
|
|
@@ -1859,25 +1859,22 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
|
|
|
1859
1859
|
return response.send('Camera not found', { code: 404 });
|
|
1860
1860
|
const endTime = Date.now();
|
|
1861
1861
|
const startTime = endTime - hours * 3600 * 1000;
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
.filter((e) => e.details?.eventInterface === 'ObjectDetector' && e.data?.detections?.length)
|
|
1862
|
+
const clips = await cam.getVideoClips({ startTime, endTime });
|
|
1863
|
+
const events = (clips || [])
|
|
1864
|
+
.filter((c) => c.detectionClasses?.length && c.thumbnailId)
|
|
1866
1865
|
.slice(0, 100)
|
|
1867
|
-
.map((
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
inputDimensions: e.data?.inputDimensions,
|
|
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,
|
|
1876
1874
|
cameraId,
|
|
1877
1875
|
cameraName: cam.name,
|
|
1878
|
-
}))
|
|
1879
|
-
|
|
1880
|
-
return response.send(JSON.stringify(detectionEvents), { headers: { 'Content-Type': 'application/json' } });
|
|
1876
|
+
}));
|
|
1877
|
+
return response.send(JSON.stringify(events), { headers: { 'Content-Type': 'application/json' } });
|
|
1881
1878
|
}
|
|
1882
1879
|
catch (e) {
|
|
1883
1880
|
return response.send(JSON.stringify({ error: e.message }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
|
|
@@ -1887,25 +1884,30 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
|
|
|
1887
1884
|
if (path === '/api/add-event' && request.body) {
|
|
1888
1885
|
const rawBody = request.body;
|
|
1889
1886
|
const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
|
|
1890
|
-
const { cameraId, cameraName,
|
|
1887
|
+
const { cameraId, cameraName, thumbnailId, timestamp, detectedClass, boundingBox, inputDimensions, label } = body;
|
|
1891
1888
|
if (!label || label === 'discard')
|
|
1892
1889
|
return response.send(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
|
|
1893
|
-
//
|
|
1890
|
+
// Get image via thumbnail
|
|
1894
1891
|
let jpeg;
|
|
1895
1892
|
try {
|
|
1896
1893
|
const cam = systemManager.getDeviceById(cameraId);
|
|
1897
|
-
const mo = await cam.
|
|
1894
|
+
const mo = await cam.getVideoClipThumbnail(thumbnailId);
|
|
1898
1895
|
jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
1899
1896
|
}
|
|
1900
1897
|
catch (e) {
|
|
1901
|
-
this.console.warn(`Could not get
|
|
1898
|
+
this.console.warn(`Could not get thumbnail for browse event:`, e);
|
|
1902
1899
|
}
|
|
1903
1900
|
if (!jpeg)
|
|
1904
1901
|
return response.send(JSON.stringify({ error: 'Could not retrieve image' }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
|
|
1905
1902
|
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
|
|
1906
1904
|
const record = {
|
|
1907
|
-
id, cameraId, cameraName, timestamp,
|
|
1908
|
-
|
|
1905
|
+
id, cameraId, cameraName, timestamp,
|
|
1906
|
+
detectedClass: detectedClass || 'unknown',
|
|
1907
|
+
score: 1,
|
|
1908
|
+
boundingBox: boundingBox || [0, 0, inputDimensions?.[0] || 1920, inputDimensions?.[1] || 1080],
|
|
1909
|
+
inputDimensions: inputDimensions || [1920, 1080],
|
|
1910
|
+
reviewed: true, label,
|
|
1909
1911
|
};
|
|
1910
1912
|
this.captures.set(id, record);
|
|
1911
1913
|
this.saveImage(id, jpeg);
|
|
@@ -2282,6 +2284,22 @@ function openLightbox(r) {
|
|
|
2282
2284
|
document.addEventListener('keydown', lbKeyHandler);
|
|
2283
2285
|
}
|
|
2284
2286
|
|
|
2287
|
+
function openLightboxImg(img, cameraName, timestamp) {
|
|
2288
|
+
const lb = document.getElementById('lightbox');
|
|
2289
|
+
const lbCanvas = document.getElementById('lightbox-canvas');
|
|
2290
|
+
const maxW = window.innerWidth * 0.9;
|
|
2291
|
+
const maxH = window.innerHeight * 0.8;
|
|
2292
|
+
const scale = Math.min(maxW / img.naturalWidth, maxH / img.naturalHeight, 1);
|
|
2293
|
+
lbCanvas.width = Math.round(img.naturalWidth * scale);
|
|
2294
|
+
lbCanvas.height = Math.round(img.naturalHeight * scale);
|
|
2295
|
+
const ctx = lbCanvas.getContext('2d');
|
|
2296
|
+
ctx.drawImage(img, 0, 0, lbCanvas.width, lbCanvas.height);
|
|
2297
|
+
document.getElementById('lightbox-meta').textContent =
|
|
2298
|
+
cameraName + ' · ' + new Date(timestamp).toLocaleString();
|
|
2299
|
+
lb.classList.add('open');
|
|
2300
|
+
document.addEventListener('keydown', lbKeyHandler);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2285
2303
|
function closeLightbox() {
|
|
2286
2304
|
document.getElementById('lightbox').classList.remove('open');
|
|
2287
2305
|
document.removeEventListener('keydown', lbKeyHandler);
|
|
@@ -2467,12 +2485,7 @@ async function loadBrowse() {
|
|
|
2467
2485
|
}
|
|
2468
2486
|
|
|
2469
2487
|
function loadBrowseImage(i, ev) {
|
|
2470
|
-
|
|
2471
|
-
if (!primary?.boundingBox) return;
|
|
2472
|
-
// Request image via the img endpoint using detectionId as the key
|
|
2473
|
-
// We store a browse-prefixed image server-side only after adding — for preview
|
|
2474
|
-
// use a placeholder fetch to trigger server-side caching
|
|
2475
|
-
fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&detectionId=' + ev.detectionId)
|
|
2488
|
+
fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&thumbnailId=' + encodeURIComponent(ev.thumbnailId))
|
|
2476
2489
|
.then(r => r.ok ? r.blob() : null)
|
|
2477
2490
|
.then(blob => {
|
|
2478
2491
|
if (!blob) return;
|
|
@@ -2480,15 +2493,27 @@ function loadBrowseImage(i, ev) {
|
|
|
2480
2493
|
const img = new Image();
|
|
2481
2494
|
img.onload = () => {
|
|
2482
2495
|
imgCache.set('browse-' + i, img);
|
|
2483
|
-
// Draw on full canvas
|
|
2484
2496
|
const fullCanvas = document.getElementById('bcanvas-full-' + i);
|
|
2485
2497
|
const cropCanvas = document.getElementById('bcanvas-crop-' + i);
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2498
|
+
const iw = img.naturalWidth, ih = img.naturalHeight;
|
|
2499
|
+
// No bounding box for clip thumbnails — just draw the full image
|
|
2500
|
+
if (fullCanvas) {
|
|
2501
|
+
const ctx = fullCanvas.getContext('2d');
|
|
2502
|
+
const cw = fullCanvas.width, ch = fullCanvas.height;
|
|
2503
|
+
const scale = Math.min(cw / iw, ch / ih);
|
|
2504
|
+
const dw = iw * scale, dh = ih * scale;
|
|
2505
|
+
ctx.fillStyle = '#111'; ctx.fillRect(0, 0, cw, ch);
|
|
2506
|
+
ctx.drawImage(img, (cw-dw)/2, (ch-dh)/2, dw, dh);
|
|
2507
|
+
// Label classes
|
|
2508
|
+
const labels = (ev.detectionClasses || []).join(', ');
|
|
2509
|
+
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, ch-20, cw, 20);
|
|
2510
|
+
ctx.fillStyle = '#f90'; ctx.font = 'bold 11px sans-serif';
|
|
2511
|
+
ctx.fillText(labels, 4, ch-5);
|
|
2512
|
+
fullCanvas.onclick = () => openLightboxImg(img, ev.cameraName, ev.timestamp);
|
|
2513
|
+
}
|
|
2514
|
+
// Hide crop panel — no bounding box available
|
|
2515
|
+
const cropPanel = document.getElementById('bcanvas-crop-' + i)?.closest('.img-panel') as HTMLElement;
|
|
2516
|
+
if (cropPanel) cropPanel.style.display = 'none';
|
|
2492
2517
|
URL.revokeObjectURL(url);
|
|
2493
2518
|
};
|
|
2494
2519
|
img.src = url;
|
|
@@ -2502,8 +2527,6 @@ async function addBrowseEvent(i, label) {
|
|
|
2502
2527
|
if (el) { el.style.opacity = '0.4'; el.querySelectorAll('button').forEach(b => b.disabled = true); }
|
|
2503
2528
|
|
|
2504
2529
|
if (label !== 'discard') {
|
|
2505
|
-
const primary = (ev.detections || [])[0];
|
|
2506
|
-
if (!primary) return;
|
|
2507
2530
|
try {
|
|
2508
2531
|
const res = await fetch(BASE + '/api/add-event', {
|
|
2509
2532
|
method: 'POST',
|
|
@@ -2511,12 +2534,11 @@ async function addBrowseEvent(i, label) {
|
|
|
2511
2534
|
body: JSON.stringify({
|
|
2512
2535
|
cameraId: ev.cameraId,
|
|
2513
2536
|
cameraName: ev.cameraName,
|
|
2514
|
-
|
|
2537
|
+
thumbnailId: ev.thumbnailId,
|
|
2515
2538
|
timestamp: ev.timestamp,
|
|
2516
|
-
detectedClass:
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
inputDimensions: ev.inputDimensions,
|
|
2539
|
+
detectedClass: (ev.detectionClasses || [])[0] || 'unknown',
|
|
2540
|
+
boundingBox: null,
|
|
2541
|
+
inputDimensions: null,
|
|
2520
2542
|
label,
|
|
2521
2543
|
}),
|
|
2522
2544
|
});
|