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/plugin.zip CHANGED
Binary file
@@ -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
- const authPath = await sdk_1.default.endpointManager.getAuthenticatedPath();
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') {