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/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -1622,8 +1622,7 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
|
|
|
1622
1622
|
d.interfaces?.includes(sdk_1.ScryptedInterface.ObjectDetector));
|
|
1623
1623
|
let uiUrl;
|
|
1624
1624
|
try {
|
|
1625
|
-
|
|
1626
|
-
uiUrl = `${authPath}endpoint/scrypted-detection-trainer/public/`;
|
|
1625
|
+
uiUrl = await sdk_1.default.endpointManager.getLocalEndpoint(undefined, { public: true });
|
|
1627
1626
|
}
|
|
1628
1627
|
catch {
|
|
1629
1628
|
uiUrl = '/endpoint/scrypted-detection-trainer/public/';
|
|
@@ -1752,11 +1751,6 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
|
|
|
1752
1751
|
}
|
|
1753
1752
|
// ── HTTP Handler ──────────────────────────────────────────────────────────
|
|
1754
1753
|
async onRequest(request, response) {
|
|
1755
|
-
// Require authentication — reject unauthenticated requests
|
|
1756
|
-
if (!request.username) {
|
|
1757
|
-
response.send('Unauthorized', { code: 401, headers: { 'WWW-Authenticate': 'Basic realm="Detection Trainer"' } });
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
1754
|
const url = new URL(request.url, 'http://localhost');
|
|
1761
1755
|
const path = url.pathname.replace(request.rootPath, '');
|
|
1762
1756
|
// Serve image
|
|
@@ -1793,6 +1787,16 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
|
|
|
1793
1787
|
.slice(0, 50);
|
|
1794
1788
|
return response.send(JSON.stringify(pending), { headers: { 'Content-Type': 'application/json' } });
|
|
1795
1789
|
}
|
|
1790
|
+
// API: get labeled captures
|
|
1791
|
+
if (path === '/api/labeled') {
|
|
1792
|
+
const page = parseInt(new URL(request.url, 'http://localhost').searchParams.get('page') || '0');
|
|
1793
|
+
const pageSize = 50;
|
|
1794
|
+
const labeled = [...this.captures.values()]
|
|
1795
|
+
.filter(c => c.reviewed)
|
|
1796
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
1797
|
+
const slice = labeled.slice(page * pageSize, (page + 1) * pageSize);
|
|
1798
|
+
return response.send(JSON.stringify({ items: slice, total: labeled.length, page, pageSize }), { headers: { 'Content-Type': 'application/json' } });
|
|
1799
|
+
}
|
|
1796
1800
|
// API: stats
|
|
1797
1801
|
if (path === '/api/stats') {
|
|
1798
1802
|
const all = [...this.captures.values()];
|
|
@@ -1964,6 +1968,7 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
|
|
|
1964
1968
|
<div class="card">
|
|
1965
1969
|
<div class="tab-bar">
|
|
1966
1970
|
<div class="tab active" onclick="showTab('review')">Review</div>
|
|
1971
|
+
<div class="tab" onclick="showTab('labeled')">Labeled</div>
|
|
1967
1972
|
<div class="tab" onclick="showTab('stats')">Stats</div>
|
|
1968
1973
|
<div class="tab" onclick="showTab('export')">Export Dataset</div>
|
|
1969
1974
|
</div>
|
|
@@ -1973,6 +1978,11 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
|
|
|
1973
1978
|
<div id="detections-list"></div>
|
|
1974
1979
|
</div>
|
|
1975
1980
|
|
|
1981
|
+
<!-- Labeled tab -->
|
|
1982
|
+
<div class="tab-panel" id="tab-labeled">
|
|
1983
|
+
<div id="labeled-list"></div>
|
|
1984
|
+
</div>
|
|
1985
|
+
|
|
1976
1986
|
<!-- Stats tab -->
|
|
1977
1987
|
<div class="tab-panel" id="tab-stats">
|
|
1978
1988
|
<div class="tab-content">
|
|
@@ -2130,14 +2140,134 @@ function lbKeyHandler(e) {
|
|
|
2130
2140
|
}
|
|
2131
2141
|
|
|
2132
2142
|
function showTab(name) {
|
|
2143
|
+
const names = ['review', 'labeled', 'stats', 'export'];
|
|
2133
2144
|
document.querySelectorAll('.tab').forEach((t, i) => {
|
|
2134
|
-
const names = ['review', 'stats', 'export'];
|
|
2135
2145
|
t.classList.toggle('active', names[i] === name);
|
|
2136
2146
|
});
|
|
2137
2147
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
2138
2148
|
document.getElementById('tab-' + name).classList.add('active');
|
|
2139
2149
|
if (name === 'stats') loadStats();
|
|
2140
2150
|
if (name === 'export') loadExportInfo();
|
|
2151
|
+
if (name === 'labeled') loadLabeled(0);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
const LABEL_COLORS = { person:'#4d9', animal:'#d85', face:'#6be', vehicle:'#99d', plate:'#cc9', package:'#fc9', discard:'#a44' };
|
|
2155
|
+
|
|
2156
|
+
async function loadLabeled(page) {
|
|
2157
|
+
const list = document.getElementById('labeled-list');
|
|
2158
|
+
list.innerHTML = '<div class="empty"><div style="color:#888">Loading…</div></div>';
|
|
2159
|
+
try {
|
|
2160
|
+
const res = await fetch(BASE + '/api/labeled?page=' + page);
|
|
2161
|
+
const data = await res.json();
|
|
2162
|
+
const { items, total, pageSize } = data;
|
|
2163
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
2164
|
+
|
|
2165
|
+
if (!items.length) {
|
|
2166
|
+
list.innerHTML = '<div class="empty"><div class="icon">🏷️</div><div>No labeled detections yet.</div></div>';
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const pagerHtml = totalPages > 1 ? \`
|
|
2171
|
+
<div style="display:flex;align-items:center;gap:12px;padding:14px 16px;border-top:1px solid #222;font-size:13px;color:#888;">
|
|
2172
|
+
\${page > 0 ? \`<button class="label-btn" onclick="loadLabeled(\${page-1})">← Prev</button>\` : ''}
|
|
2173
|
+
<span>Page \${page+1} of \${totalPages} · \${total} total</span>
|
|
2174
|
+
\${page < totalPages-1 ? \`<button class="label-btn" onclick="loadLabeled(\${page+1})">Next →</button>\` : ''}
|
|
2175
|
+
</div>\` : '';
|
|
2176
|
+
|
|
2177
|
+
list.innerHTML = items.map(r => {
|
|
2178
|
+
const date = new Date(r.timestamp).toLocaleString();
|
|
2179
|
+
const score = Math.round(r.score * 100);
|
|
2180
|
+
const labelColor = LABEL_COLORS[r.label] || '#aaa';
|
|
2181
|
+
return \`
|
|
2182
|
+
<div class="detection" id="ldet-\${r.id}">
|
|
2183
|
+
<div class="detection-imgs">
|
|
2184
|
+
<div class="img-panel">
|
|
2185
|
+
<div class="img-label">Full frame</div>
|
|
2186
|
+
<canvas id="lcanvas-full-\${r.id}" class="det-canvas" width="240" height="160"></canvas>
|
|
2187
|
+
</div>
|
|
2188
|
+
<div class="img-panel">
|
|
2189
|
+
<div class="img-label">Crop</div>
|
|
2190
|
+
<canvas id="lcanvas-crop-\${r.id}" class="det-canvas" width="160" height="160"></canvas>
|
|
2191
|
+
</div>
|
|
2192
|
+
</div>
|
|
2193
|
+
<div class="detection-info">
|
|
2194
|
+
<div class="detection-meta">
|
|
2195
|
+
<div><strong>\${r.cameraName}</strong></div>
|
|
2196
|
+
<div>\${date}</div>
|
|
2197
|
+
<div class="det-class-badge">\${r.detectedClass} \${score}%</div>
|
|
2198
|
+
<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>
|
|
2199
|
+
</div>
|
|
2200
|
+
<div style="font-size:12px;color:#888;">Change label:</div>
|
|
2201
|
+
<div class="label-buttons">
|
|
2202
|
+
<button class="label-btn person" onclick="relabel('\${r.id}', 'person')">👤 Person</button>
|
|
2203
|
+
<button class="label-btn animal" onclick="relabel('\${r.id}', 'animal')">🐾 Animal</button>
|
|
2204
|
+
<button class="label-btn face" onclick="relabel('\${r.id}', 'face')">😀 Face</button>
|
|
2205
|
+
<button class="label-btn vehicle" onclick="relabel('\${r.id}', 'vehicle')">🚗 Vehicle</button>
|
|
2206
|
+
<button class="label-btn" onclick="relabel('\${r.id}', 'plate')">🔢 Plate</button>
|
|
2207
|
+
<button class="label-btn" onclick="relabel('\${r.id}', 'package')">📦 Package</button>
|
|
2208
|
+
<button class="label-btn discard" onclick="relabel('\${r.id}', 'discard')">🗑 Discard</button>
|
|
2209
|
+
</div>
|
|
2210
|
+
</div>
|
|
2211
|
+
</div>\`;
|
|
2212
|
+
}).join('') + pagerHtml;
|
|
2213
|
+
|
|
2214
|
+
// Draw bounding boxes
|
|
2215
|
+
for (const r of items) {
|
|
2216
|
+
const img = new Image();
|
|
2217
|
+
img.onload = () => {
|
|
2218
|
+
imgCache.set(r.id, img);
|
|
2219
|
+
// Reuse drawDetection with the labeled canvases
|
|
2220
|
+
const origFull = document.getElementById('canvas-full-' + r.id);
|
|
2221
|
+
const origCrop = document.getElementById('canvas-crop-' + r.id);
|
|
2222
|
+
// Temporarily point to labeled canvases
|
|
2223
|
+
const fakeFull = document.getElementById('lcanvas-full-' + r.id);
|
|
2224
|
+
const fakeCrop = document.getElementById('lcanvas-crop-' + r.id);
|
|
2225
|
+
if (fakeFull) fakeFull.id = 'canvas-full-' + r.id;
|
|
2226
|
+
if (fakeCrop) fakeCrop.id = 'canvas-crop-' + r.id;
|
|
2227
|
+
drawDetection(img, r);
|
|
2228
|
+
if (fakeFull) fakeFull.id = 'lcanvas-full-' + r.id;
|
|
2229
|
+
if (fakeCrop) fakeCrop.id = 'lcanvas-crop-' + r.id;
|
|
2230
|
+
if (fakeFull) fakeFull.onclick = () => openLightbox(r);
|
|
2231
|
+
if (fakeCrop) fakeCrop.onclick = () => openLightbox(r);
|
|
2232
|
+
};
|
|
2233
|
+
img.src = BASE + '/img/' + r.id;
|
|
2234
|
+
}
|
|
2235
|
+
} catch(e) {
|
|
2236
|
+
list.innerHTML = '<div class="empty"><div style="color:#a44">Error: ' + e.message + '</div></div>';
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
async function relabel(id, labelVal) {
|
|
2241
|
+
await fetch(BASE + '/api/label', {
|
|
2242
|
+
method: 'POST',
|
|
2243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2244
|
+
body: JSON.stringify({ id, label: labelVal }),
|
|
2245
|
+
});
|
|
2246
|
+
const el = document.getElementById('ldet-' + id);
|
|
2247
|
+
if (labelVal === 'discard') {
|
|
2248
|
+
if (el) el.remove();
|
|
2249
|
+
toast('Discarded', '#633');
|
|
2250
|
+
} else {
|
|
2251
|
+
// Update the label badge in place
|
|
2252
|
+
const badge = el && el.querySelector('[style*="✓"]');
|
|
2253
|
+
const labelColor = LABEL_COLORS[labelVal] || '#aaa';
|
|
2254
|
+
if (el) {
|
|
2255
|
+
const badges = el.querySelectorAll('.detection-meta > div');
|
|
2256
|
+
badges.forEach(b => { if (b.textContent.startsWith('✓')) b.remove(); });
|
|
2257
|
+
const meta = el.querySelector('.detection-meta');
|
|
2258
|
+
const newBadge = document.createElement('div');
|
|
2259
|
+
newBadge.style.cssText = \`display:inline-block;background:#1a1a1a;border:1px solid \${labelColor};color:\${labelColor};font-size:11px;padding:2px 8px;border-radius:4px;\`;
|
|
2260
|
+
newBadge.textContent = '✓ ' + labelVal;
|
|
2261
|
+
meta.appendChild(newBadge);
|
|
2262
|
+
}
|
|
2263
|
+
toast('Re-labeled: ' + labelVal, '#1a6');
|
|
2264
|
+
}
|
|
2265
|
+
// Refresh stats
|
|
2266
|
+
const statsRes = await fetch(BASE + '/api/stats');
|
|
2267
|
+
const stats = await statsRes.json();
|
|
2268
|
+
document.getElementById('stat-pending').textContent = stats.pending;
|
|
2269
|
+
document.getElementById('stat-labeled').textContent = stats.labeled;
|
|
2270
|
+
document.getElementById('stat-total').textContent = stats.total;
|
|
2141
2271
|
}
|
|
2142
2272
|
|
|
2143
2273
|
function toast(msg, color='#1a3') {
|