scrypted-detection-trainer 0.1.7 → 0.1.8
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 +138 -8
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +139 -9
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -114,8 +114,7 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
114
114
|
|
|
115
115
|
let uiUrl: string;
|
|
116
116
|
try {
|
|
117
|
-
|
|
118
|
-
uiUrl = `${authPath}endpoint/scrypted-detection-trainer/public/`;
|
|
117
|
+
uiUrl = await sdk.endpointManager.getLocalEndpoint(undefined, { public: true });
|
|
119
118
|
} catch {
|
|
120
119
|
uiUrl = '/endpoint/scrypted-detection-trainer/public/';
|
|
121
120
|
}
|
|
@@ -260,12 +259,6 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
260
259
|
// ── HTTP Handler ──────────────────────────────────────────────────────────
|
|
261
260
|
|
|
262
261
|
async onRequest(request: HttpRequest, response: HttpResponse) {
|
|
263
|
-
// Require authentication — reject unauthenticated requests
|
|
264
|
-
if (!request.username) {
|
|
265
|
-
response.send('Unauthorized', { code: 401, headers: { 'WWW-Authenticate': 'Basic realm="Detection Trainer"' } });
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
262
|
const url = new URL(request.url, 'http://localhost');
|
|
270
263
|
const path = url.pathname.replace(request.rootPath, '');
|
|
271
264
|
|
|
@@ -303,6 +296,17 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
303
296
|
return response.send(JSON.stringify(pending), { headers: { 'Content-Type': 'application/json' } });
|
|
304
297
|
}
|
|
305
298
|
|
|
299
|
+
// API: get labeled captures
|
|
300
|
+
if (path === '/api/labeled') {
|
|
301
|
+
const page = parseInt(new URL(request.url, 'http://localhost').searchParams.get('page') || '0');
|
|
302
|
+
const pageSize = 50;
|
|
303
|
+
const labeled = [...this.captures.values()]
|
|
304
|
+
.filter(c => c.reviewed)
|
|
305
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
306
|
+
const slice = labeled.slice(page * pageSize, (page + 1) * pageSize);
|
|
307
|
+
return response.send(JSON.stringify({ items: slice, total: labeled.length, page, pageSize }), { headers: { 'Content-Type': 'application/json' } });
|
|
308
|
+
}
|
|
309
|
+
|
|
306
310
|
// API: stats
|
|
307
311
|
if (path === '/api/stats') {
|
|
308
312
|
const all = [...this.captures.values()];
|
|
@@ -483,6 +487,7 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
483
487
|
<div class="card">
|
|
484
488
|
<div class="tab-bar">
|
|
485
489
|
<div class="tab active" onclick="showTab('review')">Review</div>
|
|
490
|
+
<div class="tab" onclick="showTab('labeled')">Labeled</div>
|
|
486
491
|
<div class="tab" onclick="showTab('stats')">Stats</div>
|
|
487
492
|
<div class="tab" onclick="showTab('export')">Export Dataset</div>
|
|
488
493
|
</div>
|
|
@@ -492,6 +497,11 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
492
497
|
<div id="detections-list"></div>
|
|
493
498
|
</div>
|
|
494
499
|
|
|
500
|
+
<!-- Labeled tab -->
|
|
501
|
+
<div class="tab-panel" id="tab-labeled">
|
|
502
|
+
<div id="labeled-list"></div>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
495
505
|
<!-- Stats tab -->
|
|
496
506
|
<div class="tab-panel" id="tab-stats">
|
|
497
507
|
<div class="tab-content">
|
|
@@ -649,14 +659,134 @@ function lbKeyHandler(e) {
|
|
|
649
659
|
}
|
|
650
660
|
|
|
651
661
|
function showTab(name) {
|
|
662
|
+
const names = ['review', 'labeled', 'stats', 'export'];
|
|
652
663
|
document.querySelectorAll('.tab').forEach((t, i) => {
|
|
653
|
-
const names = ['review', 'stats', 'export'];
|
|
654
664
|
t.classList.toggle('active', names[i] === name);
|
|
655
665
|
});
|
|
656
666
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
657
667
|
document.getElementById('tab-' + name).classList.add('active');
|
|
658
668
|
if (name === 'stats') loadStats();
|
|
659
669
|
if (name === 'export') loadExportInfo();
|
|
670
|
+
if (name === 'labeled') loadLabeled(0);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const LABEL_COLORS = { person:'#4d9', animal:'#d85', face:'#6be', vehicle:'#99d', plate:'#cc9', package:'#fc9', discard:'#a44' };
|
|
674
|
+
|
|
675
|
+
async function loadLabeled(page) {
|
|
676
|
+
const list = document.getElementById('labeled-list');
|
|
677
|
+
list.innerHTML = '<div class="empty"><div style="color:#888">Loading…</div></div>';
|
|
678
|
+
try {
|
|
679
|
+
const res = await fetch(BASE + '/api/labeled?page=' + page);
|
|
680
|
+
const data = await res.json();
|
|
681
|
+
const { items, total, pageSize } = data;
|
|
682
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
683
|
+
|
|
684
|
+
if (!items.length) {
|
|
685
|
+
list.innerHTML = '<div class="empty"><div class="icon">🏷️</div><div>No labeled detections yet.</div></div>';
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const pagerHtml = totalPages > 1 ? \`
|
|
690
|
+
<div style="display:flex;align-items:center;gap:12px;padding:14px 16px;border-top:1px solid #222;font-size:13px;color:#888;">
|
|
691
|
+
\${page > 0 ? \`<button class="label-btn" onclick="loadLabeled(\${page-1})">← Prev</button>\` : ''}
|
|
692
|
+
<span>Page \${page+1} of \${totalPages} · \${total} total</span>
|
|
693
|
+
\${page < totalPages-1 ? \`<button class="label-btn" onclick="loadLabeled(\${page+1})">Next →</button>\` : ''}
|
|
694
|
+
</div>\` : '';
|
|
695
|
+
|
|
696
|
+
list.innerHTML = items.map(r => {
|
|
697
|
+
const date = new Date(r.timestamp).toLocaleString();
|
|
698
|
+
const score = Math.round(r.score * 100);
|
|
699
|
+
const labelColor = LABEL_COLORS[r.label] || '#aaa';
|
|
700
|
+
return \`
|
|
701
|
+
<div class="detection" id="ldet-\${r.id}">
|
|
702
|
+
<div class="detection-imgs">
|
|
703
|
+
<div class="img-panel">
|
|
704
|
+
<div class="img-label">Full frame</div>
|
|
705
|
+
<canvas id="lcanvas-full-\${r.id}" class="det-canvas" width="240" height="160"></canvas>
|
|
706
|
+
</div>
|
|
707
|
+
<div class="img-panel">
|
|
708
|
+
<div class="img-label">Crop</div>
|
|
709
|
+
<canvas id="lcanvas-crop-\${r.id}" class="det-canvas" width="160" height="160"></canvas>
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
<div class="detection-info">
|
|
713
|
+
<div class="detection-meta">
|
|
714
|
+
<div><strong>\${r.cameraName}</strong></div>
|
|
715
|
+
<div>\${date}</div>
|
|
716
|
+
<div class="det-class-badge">\${r.detectedClass} \${score}%</div>
|
|
717
|
+
<div style="display:inline-block;background:#1a1a1a;border:1px solid \${labelColor};color:\${labelColor};font-size:11px;padding:2px 8px;border-radius:4px;">✓ \${r.label}</div>
|
|
718
|
+
</div>
|
|
719
|
+
<div style="font-size:12px;color:#888;">Change label:</div>
|
|
720
|
+
<div class="label-buttons">
|
|
721
|
+
<button class="label-btn person" onclick="relabel('\${r.id}', 'person')">👤 Person</button>
|
|
722
|
+
<button class="label-btn animal" onclick="relabel('\${r.id}', 'animal')">🐾 Animal</button>
|
|
723
|
+
<button class="label-btn face" onclick="relabel('\${r.id}', 'face')">😀 Face</button>
|
|
724
|
+
<button class="label-btn vehicle" onclick="relabel('\${r.id}', 'vehicle')">🚗 Vehicle</button>
|
|
725
|
+
<button class="label-btn" onclick="relabel('\${r.id}', 'plate')">🔢 Plate</button>
|
|
726
|
+
<button class="label-btn" onclick="relabel('\${r.id}', 'package')">📦 Package</button>
|
|
727
|
+
<button class="label-btn discard" onclick="relabel('\${r.id}', 'discard')">🗑 Discard</button>
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
</div>\`;
|
|
731
|
+
}).join('') + pagerHtml;
|
|
732
|
+
|
|
733
|
+
// Draw bounding boxes
|
|
734
|
+
for (const r of items) {
|
|
735
|
+
const img = new Image();
|
|
736
|
+
img.onload = () => {
|
|
737
|
+
imgCache.set(r.id, img);
|
|
738
|
+
// Reuse drawDetection with the labeled canvases
|
|
739
|
+
const origFull = document.getElementById('canvas-full-' + r.id);
|
|
740
|
+
const origCrop = document.getElementById('canvas-crop-' + r.id);
|
|
741
|
+
// Temporarily point to labeled canvases
|
|
742
|
+
const fakeFull = document.getElementById('lcanvas-full-' + r.id);
|
|
743
|
+
const fakeCrop = document.getElementById('lcanvas-crop-' + r.id);
|
|
744
|
+
if (fakeFull) fakeFull.id = 'canvas-full-' + r.id;
|
|
745
|
+
if (fakeCrop) fakeCrop.id = 'canvas-crop-' + r.id;
|
|
746
|
+
drawDetection(img, r);
|
|
747
|
+
if (fakeFull) fakeFull.id = 'lcanvas-full-' + r.id;
|
|
748
|
+
if (fakeCrop) fakeCrop.id = 'lcanvas-crop-' + r.id;
|
|
749
|
+
if (fakeFull) fakeFull.onclick = () => openLightbox(r);
|
|
750
|
+
if (fakeCrop) fakeCrop.onclick = () => openLightbox(r);
|
|
751
|
+
};
|
|
752
|
+
img.src = BASE + '/img/' + r.id;
|
|
753
|
+
}
|
|
754
|
+
} catch(e) {
|
|
755
|
+
list.innerHTML = '<div class="empty"><div style="color:#a44">Error: ' + e.message + '</div></div>';
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function relabel(id, labelVal) {
|
|
760
|
+
await fetch(BASE + '/api/label', {
|
|
761
|
+
method: 'POST',
|
|
762
|
+
headers: { 'Content-Type': 'application/json' },
|
|
763
|
+
body: JSON.stringify({ id, label: labelVal }),
|
|
764
|
+
});
|
|
765
|
+
const el = document.getElementById('ldet-' + id);
|
|
766
|
+
if (labelVal === 'discard') {
|
|
767
|
+
if (el) el.remove();
|
|
768
|
+
toast('Discarded', '#633');
|
|
769
|
+
} else {
|
|
770
|
+
// Update the label badge in place
|
|
771
|
+
const badge = el && el.querySelector('[style*="✓"]');
|
|
772
|
+
const labelColor = LABEL_COLORS[labelVal] || '#aaa';
|
|
773
|
+
if (el) {
|
|
774
|
+
const badges = el.querySelectorAll('.detection-meta > div');
|
|
775
|
+
badges.forEach(b => { if (b.textContent.startsWith('✓')) b.remove(); });
|
|
776
|
+
const meta = el.querySelector('.detection-meta');
|
|
777
|
+
const newBadge = document.createElement('div');
|
|
778
|
+
newBadge.style.cssText = \`display:inline-block;background:#1a1a1a;border:1px solid \${labelColor};color:\${labelColor};font-size:11px;padding:2px 8px;border-radius:4px;\`;
|
|
779
|
+
newBadge.textContent = '✓ ' + labelVal;
|
|
780
|
+
meta.appendChild(newBadge);
|
|
781
|
+
}
|
|
782
|
+
toast('Re-labeled: ' + labelVal, '#1a6');
|
|
783
|
+
}
|
|
784
|
+
// Refresh stats
|
|
785
|
+
const statsRes = await fetch(BASE + '/api/stats');
|
|
786
|
+
const stats = await statsRes.json();
|
|
787
|
+
document.getElementById('stat-pending').textContent = stats.pending;
|
|
788
|
+
document.getElementById('stat-labeled').textContent = stats.labeled;
|
|
789
|
+
document.getElementById('stat-total').textContent = stats.total;
|
|
660
790
|
}
|
|
661
791
|
|
|
662
792
|
function toast(msg, color='#1a3') {
|