scrypted-detection-trainer 0.1.2 → 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/dist/plugin.zip CHANGED
Binary file
@@ -1624,15 +1624,16 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1624
1624
  {
1625
1625
  key: 'info',
1626
1626
  title: 'Detection Trainer',
1627
- 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.`,
1627
+ 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).`,
1628
1628
  readonly: true,
1629
1629
  value: '',
1630
1630
  },
1631
1631
  {
1632
- key: 'open_ui',
1633
- title: 'Open Review UI',
1632
+ key: 'ui_link',
1633
+ title: 'Review UI',
1634
1634
  description: 'Open the detection review and labeling interface.',
1635
- type: 'button',
1635
+ readonly: true,
1636
+ value: await sdk_1.default.endpointManager.getLocalEndpoint('scrypted-detection-trainer', { public: true }).catch(() => '/endpoint/scrypted-detection-trainer/public/'),
1636
1637
  },
1637
1638
  ];
1638
1639
  for (const cam of cameras) {
@@ -1649,7 +1650,7 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1649
1650
  return settings;
1650
1651
  }
1651
1652
  async putSetting(key, value) {
1652
- if (key === 'open_ui')
1653
+ if (key === 'open_ui' || key === 'ui_link' || key === 'info')
1653
1654
  return;
1654
1655
  this.storage.setItem(key, value);
1655
1656
  if (key.startsWith('rate:')) {
@@ -1853,6 +1854,7 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1853
1854
  <meta charset="UTF-8">
1854
1855
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1855
1856
  <title>Detection Trainer</title>
1857
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
1856
1858
  <style>
1857
1859
  * { box-sizing: border-box; margin: 0; padding: 0; }
1858
1860
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f0f; color: #e8e8e8; min-height: 100vh; }
@@ -1979,10 +1981,14 @@ class DetectionTrainer extends sdk_1.ScryptedDeviceBase {
1979
1981
  <div class="toast" id="toast"></div>
1980
1982
 
1981
1983
  <script>
1982
- const BASE = location.pathname.replace(/\\/$/, '');
1984
+ const BASE = location.pathname.replace(/\/$/, '');
1983
1985
  let pending = [];
1984
1986
  let labeledCount = 0;
1985
1987
 
1988
+ function imgError(img) {
1989
+ img.parentElement.innerHTML = '<div style="padding:20px;color:#555;font-size:12px;text-align:center">No image</div>';
1990
+ }
1991
+
1986
1992
  function showTab(name) {
1987
1993
  document.querySelectorAll('.tab').forEach((t, i) => {
1988
1994
  const names = ['review', 'stats', 'export'];
@@ -2024,7 +2030,7 @@ async function loadPending() {
2024
2030
  return \`
2025
2031
  <div class="detection" id="det-\${r.id}">
2026
2032
  <div class="detection-img">
2027
- <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>'">
2033
+ <img src="\${BASE}/img/\${r.id}" alt="\${r.detectedClass}" loading="lazy" onerror="imgError(this)">
2028
2034
  <div class="detection-class">\${r.detectedClass} \${score}%</div>
2029
2035
  </div>
2030
2036
  <div class="detection-info">
@@ -2095,7 +2101,7 @@ async function exportDataset() {
2095
2101
  const btn = document.getElementById('export-btn');
2096
2102
  const status = document.getElementById('export-status');
2097
2103
  btn.disabled = true;
2098
- status.textContent = 'Preparing…';
2104
+ status.textContent = 'Fetching data…';
2099
2105
 
2100
2106
  try {
2101
2107
  const res = await fetch(BASE + '/api/export');
@@ -2103,13 +2109,22 @@ async function exportDataset() {
2103
2109
  const data = await res.json();
2104
2110
  if (data.error) { status.textContent = data.error; btn.disabled = false; return; }
2105
2111
 
2106
- // Build a zip-like structure using a self-extracting HTML page
2107
- // Actually just download as a JSON bundle that train.py can consume
2108
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
2112
+ status.textContent = 'Building zip…';
2113
+
2114
+ const zip = new JSZip();
2115
+ for (const f of data.files) {
2116
+ if (f.encoding === 'base64') {
2117
+ zip.file(f.filename, f.content, { base64: true });
2118
+ } else {
2119
+ zip.file(f.filename, f.content);
2120
+ }
2121
+ }
2122
+
2123
+ const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' });
2109
2124
  const url = URL.createObjectURL(blob);
2110
2125
  const a = document.createElement('a');
2111
2126
  a.href = url;
2112
- a.download = 'scrypted_dataset_' + new Date().toISOString().slice(0,10) + '.json';
2127
+ a.download = 'scrypted_dataset_' + new Date().toISOString().slice(0,10) + '.zip';
2113
2128
  a.click();
2114
2129
  URL.revokeObjectURL(url);
2115
2130
  status.textContent = \`Downloaded \${data.count} samples.\`;