scrypted-detection-trainer 0.1.1 → 0.1.3

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/out/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrypted-detection-trainer",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Collect and label detections to fine-tune the Scrypted NVR object detection model.",
5
5
  "keywords": [
6
6
  "scrypted-plugin"
package/src/main.ts CHANGED
@@ -116,15 +116,16 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
116
116
  {
117
117
  key: 'info',
118
118
  title: 'Detection Trainer',
119
- description: `${this.captures.size} captures stored (${[...this.captures.values()].filter(c => !c.reviewed).length} pending review, ${[...this.captures.values()].filter(c => c.reviewed && c.label !== 'discard').length} labeled). Open the web UI to review and export.`,
119
+ description: `${this.captures.size} captures stored (${[...this.captures.values()].filter(c => !c.reviewed).length} pending review, ${[...this.captures.values()].filter(c => c.reviewed && c.label !== 'discard').length} labeled).`,
120
120
  readonly: true,
121
121
  value: '',
122
122
  },
123
123
  {
124
- key: 'open_ui',
125
- title: 'Open Review UI',
124
+ key: 'ui_link',
125
+ title: 'Review UI',
126
126
  description: 'Open the detection review and labeling interface.',
127
- type: 'button',
127
+ readonly: true,
128
+ value: await sdk.endpointManager.getLocalEndpoint('scrypted-detection-trainer', { public: true }).catch(() => '/endpoint/scrypted-detection-trainer/public/'),
128
129
  },
129
130
  ];
130
131
 
@@ -144,7 +145,7 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
144
145
  }
145
146
 
146
147
  async putSetting(key: string, value: string) {
147
- if (key === 'open_ui') return;
148
+ if (key === 'open_ui' || key === 'ui_link' || key === 'info') return;
148
149
  this.storage.setItem(key, value);
149
150
  if (key.startsWith('rate:')) {
150
151
  // Re-register listeners when rates change
@@ -371,6 +372,7 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
371
372
  <meta charset="UTF-8">
372
373
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
373
374
  <title>Detection Trainer</title>
375
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
374
376
  <style>
375
377
  * { box-sizing: border-box; margin: 0; padding: 0; }
376
378
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f0f; color: #e8e8e8; min-height: 100vh; }
@@ -497,10 +499,14 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
497
499
  <div class="toast" id="toast"></div>
498
500
 
499
501
  <script>
500
- const BASE = location.pathname.replace(/\\/$/, '');
502
+ const BASE = location.pathname.replace(/\/$/, '');
501
503
  let pending = [];
502
504
  let labeledCount = 0;
503
505
 
506
+ function imgError(img) {
507
+ img.parentElement.innerHTML = '<div style="padding:20px;color:#555;font-size:12px;text-align:center">No image</div>';
508
+ }
509
+
504
510
  function showTab(name) {
505
511
  document.querySelectorAll('.tab').forEach((t, i) => {
506
512
  const names = ['review', 'stats', 'export'];
@@ -542,7 +548,7 @@ async function loadPending() {
542
548
  return \`
543
549
  <div class="detection" id="det-\${r.id}">
544
550
  <div class="detection-img">
545
- <img src="\${BASE}/img/\${r.id}" alt="\${r.detectedClass}" loading="lazy" onerror="this.parentElement.innerHTML='<div style=\\"padding:20px;color:#555;font-size:12px;text-align:center\\">Image unavailable</div>'">
551
+ <img src="\${BASE}/img/\${r.id}" alt="\${r.detectedClass}" loading="lazy" onerror="imgError(this)">
546
552
  <div class="detection-class">\${r.detectedClass} \${score}%</div>
547
553
  </div>
548
554
  <div class="detection-info">
@@ -613,7 +619,7 @@ async function exportDataset() {
613
619
  const btn = document.getElementById('export-btn');
614
620
  const status = document.getElementById('export-status');
615
621
  btn.disabled = true;
616
- status.textContent = 'Preparing…';
622
+ status.textContent = 'Fetching data…';
617
623
 
618
624
  try {
619
625
  const res = await fetch(BASE + '/api/export');
@@ -621,13 +627,22 @@ async function exportDataset() {
621
627
  const data = await res.json();
622
628
  if (data.error) { status.textContent = data.error; btn.disabled = false; return; }
623
629
 
624
- // Build a zip-like structure using a self-extracting HTML page
625
- // Actually just download as a JSON bundle that train.py can consume
626
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
630
+ status.textContent = 'Building zip…';
631
+
632
+ const zip = new JSZip();
633
+ for (const f of data.files) {
634
+ if (f.encoding === 'base64') {
635
+ zip.file(f.filename, f.content, { base64: true });
636
+ } else {
637
+ zip.file(f.filename, f.content);
638
+ }
639
+ }
640
+
641
+ const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' });
627
642
  const url = URL.createObjectURL(blob);
628
643
  const a = document.createElement('a');
629
644
  a.href = url;
630
- a.download = 'scrypted_dataset_' + new Date().toISOString().slice(0,10) + '.json';
645
+ a.download = 'scrypted_dataset_' + new Date().toISOString().slice(0,10) + '.zip';
631
646
  a.click();
632
647
  URL.revokeObjectURL(url);
633
648
  status.textContent = \`Downloaded \${data.count} samples.\`;