scrypted-detection-trainer 0.1.10 → 0.1.12
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 -116
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +71 -118
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);
|
|
@@ -2322,75 +2340,6 @@ async function initBrowse() {
|
|
|
2322
2340
|
}
|
|
2323
2341
|
}
|
|
2324
2342
|
|
|
2325
|
-
async function loadBrowse() {
|
|
2326
|
-
const cameraId = document.getElementById('browse-camera').value;
|
|
2327
|
-
const hours = document.getElementById('browse-hours').value;
|
|
2328
|
-
const status = document.getElementById('browse-status');
|
|
2329
|
-
const list = document.getElementById('browse-list');
|
|
2330
|
-
|
|
2331
|
-
if (!cameraId) { status.textContent = 'Select a camera first'; return; }
|
|
2332
|
-
|
|
2333
|
-
status.textContent = 'Loading…';
|
|
2334
|
-
list.innerHTML = '';
|
|
2335
|
-
|
|
2336
|
-
try {
|
|
2337
|
-
const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours);
|
|
2338
|
-
const events = await res.json();
|
|
2339
|
-
|
|
2340
|
-
if (events.error) { status.textContent = 'Error: ' + events.error; return; }
|
|
2341
|
-
if (!events.length) { status.textContent = 'No detection events found.'; list.innerHTML = '<div class="empty"><div class="icon">🔍</div><div>No ObjectDetector events in this time range.</div></div>'; return; }
|
|
2342
|
-
|
|
2343
|
-
status.textContent = events.length + ' events found';
|
|
2344
|
-
|
|
2345
|
-
list.innerHTML = events.map((ev, i) => {
|
|
2346
|
-
const date = new Date(ev.timestamp).toLocaleString();
|
|
2347
|
-
const dets = ev.detections || [];
|
|
2348
|
-
const primary = dets[0] || {};
|
|
2349
|
-
const score = Math.round((primary.score || 0) * 100);
|
|
2350
|
-
const allClasses = dets.map(d => d.className + ' ' + Math.round((d.score||0)*100) + '%').join(', ');
|
|
2351
|
-
return \`
|
|
2352
|
-
<div class="detection" id="bev-\${i}" style="opacity:1;transition:opacity .3s">
|
|
2353
|
-
<div class="detection-imgs">
|
|
2354
|
-
<div class="img-panel">
|
|
2355
|
-
<div class="img-label">Full frame</div>
|
|
2356
|
-
<canvas id="bcanvas-\${i}" class="det-canvas" width="240" height="160"></canvas>
|
|
2357
|
-
</div>
|
|
2358
|
-
<div class="img-panel" id="bcrop-panel-\${i}">
|
|
2359
|
-
<div class="img-label">Crop</div>
|
|
2360
|
-
<canvas id="bcanvas-crop-\${i}" class="det-canvas" width="160" height="160"></canvas>
|
|
2361
|
-
</div>
|
|
2362
|
-
</div>
|
|
2363
|
-
<div class="detection-info">
|
|
2364
|
-
<div class="detection-meta">
|
|
2365
|
-
<div><strong>\${ev.cameraName}</strong></div>
|
|
2366
|
-
<div>\${date}</div>
|
|
2367
|
-
<div class="det-class-badge">\${allClasses}</div>
|
|
2368
|
-
</div>
|
|
2369
|
-
<div style="font-size:12px;color:#888;">Add to dataset as:</div>
|
|
2370
|
-
<div class="label-buttons" id="blabels-\${i}">
|
|
2371
|
-
<button class="label-btn person" onclick="addEvent(\${i})('person')">👤 Person</button>
|
|
2372
|
-
<button class="label-btn animal" onclick="addEvent(\${i})('animal')">🐾 Animal</button>
|
|
2373
|
-
<button class="label-btn face" onclick="addEvent(\${i})('face')">😀 Face</button>
|
|
2374
|
-
<button class="label-btn vehicle" onclick="addEvent(\${i})('vehicle')">🚗 Vehicle</button>
|
|
2375
|
-
<button class="label-btn" onclick="addEvent(\${i})('plate')">🔢 Plate</button>
|
|
2376
|
-
<button class="label-btn" onclick="addEvent(\${i})('package')">📦 Package</button>
|
|
2377
|
-
<button class="label-btn discard" onclick="addEvent(\${i})('discard')">🗑 Skip</button>
|
|
2378
|
-
</div>
|
|
2379
|
-
</div>
|
|
2380
|
-
</div>\`;
|
|
2381
|
-
}).join('');
|
|
2382
|
-
|
|
2383
|
-
// Load images for each event
|
|
2384
|
-
for (let i = 0; i < events.length; i++) {
|
|
2385
|
-
const ev = events[i];
|
|
2386
|
-
loadBrowseImage(i, ev);
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
} catch(e) {
|
|
2390
|
-
status.textContent = 'Error: ' + e.message;
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
2343
|
// Store browse events for addEvent closure
|
|
2395
2344
|
let browseEvents = [];
|
|
2396
2345
|
|
|
@@ -2467,12 +2416,7 @@ async function loadBrowse() {
|
|
|
2467
2416
|
}
|
|
2468
2417
|
|
|
2469
2418
|
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)
|
|
2419
|
+
fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&thumbnailId=' + encodeURIComponent(ev.thumbnailId))
|
|
2476
2420
|
.then(r => r.ok ? r.blob() : null)
|
|
2477
2421
|
.then(blob => {
|
|
2478
2422
|
if (!blob) return;
|
|
@@ -2480,15 +2424,27 @@ function loadBrowseImage(i, ev) {
|
|
|
2480
2424
|
const img = new Image();
|
|
2481
2425
|
img.onload = () => {
|
|
2482
2426
|
imgCache.set('browse-' + i, img);
|
|
2483
|
-
// Draw on full canvas
|
|
2484
2427
|
const fullCanvas = document.getElementById('bcanvas-full-' + i);
|
|
2485
2428
|
const cropCanvas = document.getElementById('bcanvas-crop-' + i);
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
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') as HTMLElement;
|
|
2447
|
+
if (cropPanel) cropPanel.style.display = 'none';
|
|
2492
2448
|
URL.revokeObjectURL(url);
|
|
2493
2449
|
};
|
|
2494
2450
|
img.src = url;
|
|
@@ -2502,8 +2458,6 @@ async function addBrowseEvent(i, label) {
|
|
|
2502
2458
|
if (el) { el.style.opacity = '0.4'; el.querySelectorAll('button').forEach(b => b.disabled = true); }
|
|
2503
2459
|
|
|
2504
2460
|
if (label !== 'discard') {
|
|
2505
|
-
const primary = (ev.detections || [])[0];
|
|
2506
|
-
if (!primary) return;
|
|
2507
2461
|
try {
|
|
2508
2462
|
const res = await fetch(BASE + '/api/add-event', {
|
|
2509
2463
|
method: 'POST',
|
|
@@ -2511,12 +2465,11 @@ async function addBrowseEvent(i, label) {
|
|
|
2511
2465
|
body: JSON.stringify({
|
|
2512
2466
|
cameraId: ev.cameraId,
|
|
2513
2467
|
cameraName: ev.cameraName,
|
|
2514
|
-
|
|
2468
|
+
thumbnailId: ev.thumbnailId,
|
|
2515
2469
|
timestamp: ev.timestamp,
|
|
2516
|
-
detectedClass:
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
inputDimensions: ev.inputDimensions,
|
|
2470
|
+
detectedClass: (ev.detectionClasses || [])[0] || 'unknown',
|
|
2471
|
+
boundingBox: null,
|
|
2472
|
+
inputDimensions: null,
|
|
2520
2473
|
label,
|
|
2521
2474
|
}),
|
|
2522
2475
|
});
|