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/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +174 -10
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +174 -10
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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-
|
|
1879
|
-
.
|
|
1880
|
-
.
|
|
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-
|
|
2036
|
-
<
|
|
2037
|
-
|
|
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
|
|
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
|
-
|
|
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>';
|