scrypted-detection-trainer 0.1.6 → 0.1.7

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
@@ -1620,7 +1620,14 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1620
1620
  .filter(d => d &&
1621
1621
  (d.type === sdk_1.ScryptedDeviceType.Camera || d.type === sdk_1.ScryptedDeviceType.Doorbell) &&
1622
1622
  d.interfaces?.includes(sdk_1.ScryptedInterface.ObjectDetector));
1623
- const uiUrl = await sdk_1.default.endpointManager.getLocalEndpoint(undefined, { public: true }).catch(() => '/endpoint/scrypted-detection-trainer/public/');
1623
+ let uiUrl;
1624
+ try {
1625
+ const authPath = await sdk_1.default.endpointManager.getAuthenticatedPath();
1626
+ uiUrl = `${authPath}endpoint/scrypted-detection-trainer/public/`;
1627
+ }
1628
+ catch {
1629
+ uiUrl = '/endpoint/scrypted-detection-trainer/public/';
1630
+ }
1624
1631
  const settings = [
1625
1632
  {
1626
1633
  key: 'info',
@@ -1745,6 +1752,11 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1745
1752
  }
1746
1753
  // ── HTTP Handler ──────────────────────────────────────────────────────────
1747
1754
  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
+ }
1748
1760
  const url = new URL(request.url, 'http://localhost');
1749
1761
  const path = url.pathname.replace(request.rootPath, '');
1750
1762
  // Serve image
@@ -1873,11 +1885,13 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1873
1885
  .badge.green { background: #0d2d0d; color: #4c4; }
1874
1886
 
1875
1887
  /* Detection card */
1876
- .detection { display: grid; grid-template-columns: 200px 1fr; gap: 0; border-bottom: 1px solid #222; }
1888
+ .detection { display: grid; grid-template-columns: 420px 1fr; gap: 0; border-bottom: 1px solid #222; }
1877
1889
  .detection:last-child { border-bottom: none; }
1878
- .detection-img { position: relative; background: #111; display: flex; align-items: center; justify-content: center; min-height: 150px; }
1879
- .detection-img img { width: 100%; height: 150px; object-fit: cover; display: block; }
1880
- .detection-class { position: absolute; top: 6px; left: 6px; background: rgba(0,0,0,0.7); color: #fff; font-size: 11px; padding: 2px 6px; border-radius: 4px; }
1890
+ .detection-imgs { display: flex; gap: 8px; padding: 10px; background: #111; align-items: center; }
1891
+ .img-panel { display: flex; flex-direction: column; align-items: center; gap: 4px; }
1892
+ .img-label { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: .5px; }
1893
+ .det-canvas { border-radius: 6px; display: block; }
1894
+ .det-class-badge { display: inline-block; background: #3d2a00; color: #f90; font-size: 11px; padding: 2px 8px; border-radius: 4px; }
1881
1895
  .detection-info { padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
1882
1896
  .detection-meta { font-size: 12px; color: #888; display: flex; flex-wrap: wrap; gap: 10px; }
1883
1897
  .detection-meta strong { color: #ccc; }
@@ -1925,6 +1939,15 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1925
1939
  .breakdown-item { background: #222; border-radius: 8px; padding: 12px; }
1926
1940
  .breakdown-item .label { font-size: 12px; color: #888; margin-bottom: 4px; }
1927
1941
  .breakdown-item .value { font-size: 20px; font-weight: 600; color: #fff; }
1942
+
1943
+ /* Lightbox */
1944
+ .lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.92); z-index: 1000; align-items: center; justify-content: center; flex-direction: column; gap: 12px; }
1945
+ .lightbox.open { display: flex; }
1946
+ .lightbox canvas { max-width: 92vw; max-height: 82vh; border-radius: 8px; cursor: zoom-out; }
1947
+ .lightbox-meta { color: #aaa; font-size: 13px; text-align: center; }
1948
+ .lightbox-close { position: absolute; top: 16px; right: 20px; font-size: 28px; color: #888; cursor: pointer; line-height: 1; }
1949
+ .lightbox-close:hover { color: #fff; }
1950
+ .det-canvas { cursor: zoom-in; }
1928
1951
  </style>
1929
1952
  </head>
1930
1953
  <body>
@@ -1980,6 +2003,12 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1980
2003
  </div>
1981
2004
  </div>
1982
2005
 
2006
+ <div class="lightbox" id="lightbox" onclick="closeLightbox()">
2007
+ <div class="lightbox-close" onclick="closeLightbox()">✕</div>
2008
+ <canvas id="lightbox-canvas"></canvas>
2009
+ <div class="lightbox-meta" id="lightbox-meta"></div>
2010
+ </div>
2011
+
1983
2012
  <div class="toast" id="toast"></div>
1984
2013
 
1985
2014
  <script>
@@ -1991,6 +2020,115 @@ function imgError(img) {
1991
2020
  img.parentElement.innerHTML = '<div style="padding:20px;color:#555;font-size:12px;text-align:center">No image</div>';
1992
2021
  }
1993
2022
 
2023
+ function drawDetection(img, r) {
2024
+ const [bx, by, bw, bh] = r.boundingBox;
2025
+ const [iw, ih] = r.inputDimensions;
2026
+
2027
+ // --- Full frame canvas with box overlay ---
2028
+ const fullCanvas = document.getElementById('canvas-full-' + r.id);
2029
+ if (fullCanvas) {
2030
+ const cw = fullCanvas.width, ch = fullCanvas.height;
2031
+ const scale = Math.min(cw / iw, ch / ih);
2032
+ const dw = iw * scale, dh = ih * scale;
2033
+ const ox = (cw - dw) / 2, oy = (ch - dh) / 2;
2034
+ const ctx = fullCanvas.getContext('2d');
2035
+ ctx.fillStyle = '#111';
2036
+ ctx.fillRect(0, 0, cw, ch);
2037
+ ctx.drawImage(img, ox, oy, dw, dh);
2038
+ // Draw bounding box
2039
+ const rx = ox + bx * scale, ry = oy + by * scale;
2040
+ const rw = bw * scale, rh = bh * scale;
2041
+ ctx.strokeStyle = '#f90';
2042
+ ctx.lineWidth = 2;
2043
+ ctx.strokeRect(rx, ry, rw, rh);
2044
+ // Label badge
2045
+ ctx.fillStyle = 'rgba(255,153,0,0.85)';
2046
+ ctx.fillRect(rx, ry - 18, rw, 18);
2047
+ ctx.fillStyle = '#000';
2048
+ ctx.font = 'bold 11px sans-serif';
2049
+ ctx.fillText(r.detectedClass + ' ' + Math.round(r.score * 100) + '%', rx + 3, ry - 4);
2050
+ }
2051
+
2052
+ // --- Crop canvas ---
2053
+ const cropCanvas = document.getElementById('canvas-crop-' + r.id);
2054
+ if (cropCanvas) {
2055
+ const cc = cropCanvas.width, ch2 = cropCanvas.height;
2056
+ const ctx2 = cropCanvas.getContext('2d');
2057
+ ctx2.fillStyle = '#111';
2058
+ ctx2.fillRect(0, 0, cc, ch2);
2059
+ // Add padding around the crop
2060
+ const pad = Math.min(bw, bh) * 0.15;
2061
+ const sx = Math.max(0, bx - pad), sy = Math.max(0, by - pad);
2062
+ const sw = Math.min(iw - sx, bw + pad * 2), sh = Math.min(ih - sy, bh + pad * 2);
2063
+ const scale2 = Math.min(cc / sw, ch2 / sh);
2064
+ const dw2 = sw * scale2, dh2 = sh * scale2;
2065
+ const ox2 = (cc - dw2) / 2, oy2 = (ch2 - dh2) / 2;
2066
+ ctx2.drawImage(img, sx, sy, sw, sh, ox2, oy2, dw2, dh2);
2067
+ // Thin box outline on crop
2068
+ ctx2.strokeStyle = '#f90';
2069
+ ctx2.lineWidth = 1.5;
2070
+ const rx2 = ox2 + pad * scale2, ry2 = oy2 + pad * scale2;
2071
+ ctx2.strokeRect(rx2, ry2, bw * scale2, bh * scale2);
2072
+ }
2073
+ }
2074
+
2075
+ // Cache loaded images so lightbox reuses them
2076
+ const imgCache = new Map();
2077
+
2078
+ function openLightbox(r) {
2079
+ const img = imgCache.get(r.id);
2080
+ if (!img) return;
2081
+
2082
+ const lb = document.getElementById('lightbox');
2083
+ const lbCanvas = document.getElementById('lightbox-canvas');
2084
+
2085
+ // Size canvas to image, capped at viewport
2086
+ const maxW = window.innerWidth * 0.9;
2087
+ const maxH = window.innerHeight * 0.8;
2088
+ const [iw, ih] = r.inputDimensions;
2089
+ const scale = Math.min(maxW / iw, maxH / ih, 1);
2090
+ lbCanvas.width = Math.round(iw * scale);
2091
+ lbCanvas.height = Math.round(ih * scale);
2092
+
2093
+ const ctx = lbCanvas.getContext('2d');
2094
+ ctx.drawImage(img, 0, 0, lbCanvas.width, lbCanvas.height);
2095
+
2096
+ // Draw all bounding boxes for this detection (primary + others in same event if available)
2097
+ const boxes = r.allDetections || [{ boundingBox: r.boundingBox, detectedClass: r.detectedClass, score: r.score }];
2098
+ const colors = ['#f90', '#4af', '#f44', '#4f4', '#f4f', '#ff4'];
2099
+ boxes.forEach((d, i) => {
2100
+ const [bx, by, bw, bh] = d.boundingBox;
2101
+ const color = colors[i % colors.length];
2102
+ const rx = bx * scale, ry = by * scale, rw = bw * scale, rh = bh * scale;
2103
+ ctx.strokeStyle = color;
2104
+ ctx.lineWidth = 2;
2105
+ ctx.strokeRect(rx, ry, rw, rh);
2106
+ const label = d.detectedClass + ' ' + Math.round((d.score || 0) * 100) + '%';
2107
+ const textW = ctx.measureText(label).width + 8;
2108
+ ctx.fillStyle = color;
2109
+ ctx.globalAlpha = 0.85;
2110
+ ctx.fillRect(rx, Math.max(0, ry - 20), textW, 20);
2111
+ ctx.globalAlpha = 1;
2112
+ ctx.fillStyle = '#000';
2113
+ ctx.font = 'bold 12px sans-serif';
2114
+ ctx.fillText(label, rx + 4, Math.max(14, ry - 4));
2115
+ });
2116
+
2117
+ document.getElementById('lightbox-meta').textContent =
2118
+ r.cameraName + ' · ' + new Date(r.timestamp).toLocaleString() + ' · ' + iw + '×' + ih;
2119
+ lb.classList.add('open');
2120
+ document.addEventListener('keydown', lbKeyHandler);
2121
+ }
2122
+
2123
+ function closeLightbox() {
2124
+ document.getElementById('lightbox').classList.remove('open');
2125
+ document.removeEventListener('keydown', lbKeyHandler);
2126
+ }
2127
+
2128
+ function lbKeyHandler(e) {
2129
+ if (e.key === 'Escape') closeLightbox();
2130
+ }
2131
+
1994
2132
  function showTab(name) {
1995
2133
  document.querySelectorAll('.tab').forEach((t, i) => {
1996
2134
  const names = ['review', 'stats', 'export'];
@@ -2030,17 +2168,25 @@ async function loadPending() {
2030
2168
  list.innerHTML = pending.map(r => {
2031
2169
  const date = new Date(r.timestamp).toLocaleString();
2032
2170
  const score = Math.round(r.score * 100);
2171
+ const bb = r.boundingBox;
2172
+ const dim = r.inputDimensions;
2033
2173
  return \`
2034
2174
  <div class="detection" id="det-\${r.id}">
2035
- <div class="detection-img">
2036
- <img src="\${BASE}/img/\${r.id}" alt="\${r.detectedClass}" loading="lazy" onerror="imgError(this)">
2037
- <div class="detection-class">\${r.detectedClass} \${score}%</div>
2175
+ <div class="detection-imgs">
2176
+ <div class="img-panel">
2177
+ <div class="img-label">Full frame</div>
2178
+ <canvas id="canvas-full-\${r.id}" class="det-canvas" width="240" height="160"></canvas>
2179
+ </div>
2180
+ <div class="img-panel">
2181
+ <div class="img-label">Crop</div>
2182
+ <canvas id="canvas-crop-\${r.id}" class="det-canvas" width="160" height="160"></canvas>
2183
+ </div>
2038
2184
  </div>
2039
2185
  <div class="detection-info">
2040
2186
  <div class="detection-meta">
2041
2187
  <div><strong>\${r.cameraName}</strong></div>
2042
2188
  <div>\${date}</div>
2043
- <div>Box: \${r.boundingBox.map(v => Math.round(v)).join(', ')}</div>
2189
+ <div class="det-class-badge">\${r.detectedClass} \${score}%</div>
2044
2190
  </div>
2045
2191
  <div style="font-size:12px;color:#888;">What is this actually?</div>
2046
2192
  <div class="label-buttons">
@@ -2055,7 +2201,25 @@ async function loadPending() {
2055
2201
  </div>
2056
2202
  </div>\`;
2057
2203
  }).join('');
2058
- } catch(e) {
2204
+
2205
+ // Draw bounding boxes and crops onto canvases after DOM is ready
2206
+ for (const r of pending) {
2207
+ const img = new Image();
2208
+ img.onload = () => {
2209
+ imgCache.set(r.id, img);
2210
+ drawDetection(img, r);
2211
+ // Wire up click to open lightbox
2212
+ const fullCanvas = document.getElementById('canvas-full-' + r.id);
2213
+ const cropCanvas = document.getElementById('canvas-crop-' + r.id);
2214
+ if (fullCanvas) fullCanvas.onclick = () => openLightbox(r);
2215
+ if (cropCanvas) cropCanvas.onclick = () => openLightbox(r);
2216
+ };
2217
+ img.onerror = () => {
2218
+ const c = document.getElementById('canvas-full-' + r.id);
2219
+ if (c) c.parentElement.innerHTML = '<div style="padding:10px;color:#555;font-size:11px">No image</div>';
2220
+ };
2221
+ img.src = BASE + '/img/' + r.id;
2222
+ } } catch(e) {
2059
2223
  console.error('loadPending error', e);
2060
2224
  const list = document.getElementById('detections-list');
2061
2225
  if (list) list.innerHTML = '<div class="empty"><div style="color:#a44">Error loading captures: ' + e.message + '</div></div>';