@blueharford/scrypted-spatial-awareness 0.6.17 → 0.6.19

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
@@ -36236,7 +36236,10 @@ For EACH landmark, estimate its DISTANCE from the camera:
36236
36236
  - Street/road
36237
36237
  - Neighbor's yard
36238
36238
 
36239
- For each zone, estimate what percentage of the image it covers (0.0 to 1.0).
36239
+ For each zone, provide:
36240
+ - coverage: percentage of the image it covers (0.0 to 1.0)
36241
+ - distance: how far the CENTER of the zone is from camera ("close", "near", "medium", "far", "distant")
36242
+ - boundingBox: [x, y, width, height] in normalized coordinates (0-1) where the zone appears in the image
36240
36243
 
36241
36244
  ## 3. EDGES - What's at each edge of the frame:
36242
36245
  This helps understand what's just out of view.
@@ -36254,8 +36257,9 @@ Respond with ONLY valid JSON:
36254
36257
  {"name": "Large Oak Tree", "type": "feature", "distance": "far", "confidence": 0.85, "description": "Mature oak tree near property line, roughly 80 feet from camera"}
36255
36258
  ],
36256
36259
  "zones": [
36257
- {"name": "Front Yard", "type": "yard", "coverage": 0.5, "description": "Grass lawn with some bare patches"},
36258
- {"name": "Driveway", "type": "driveway", "coverage": 0.25, "description": "Concrete driveway leading to garage"}
36260
+ {"name": "Front Yard", "type": "yard", "coverage": 0.5, "distance": "medium", "boundingBox": [0.2, 0.4, 0.6, 0.4], "description": "Grass lawn with some bare patches"},
36261
+ {"name": "Driveway", "type": "driveway", "coverage": 0.25, "distance": "near", "boundingBox": [0.6, 0.5, 0.3, 0.4], "description": "Concrete driveway leading to garage"},
36262
+ {"name": "Street", "type": "street", "coverage": 0.1, "distance": "distant", "boundingBox": [0.0, 0.1, 1.0, 0.15], "description": "Public road beyond property line"}
36259
36263
  ],
36260
36264
  "edges": {
36261
36265
  "top": "sky, tree canopy",
@@ -36536,6 +36540,7 @@ Use the mount height to help estimate distances - objects at ground level will a
36536
36540
  coverage: typeof z.coverage === 'number' ? z.coverage : 0.5,
36537
36541
  description: z.description || '',
36538
36542
  boundingBox: z.boundingBox,
36543
+ distance: this.mapDistance(z.distance), // Parse distance for zones too
36539
36544
  }));
36540
36545
  }
36541
36546
  if (parsed.edges && typeof parsed.edges === 'object') {
@@ -36657,6 +36662,26 @@ Use the mount height to help estimate distances - objects at ground level will a
36657
36662
  return 'distant';
36658
36663
  return 'medium'; // Default to medium if not specified
36659
36664
  }
36665
+ /** Get default distance in feet based on zone type */
36666
+ getDefaultZoneDistance(zoneType) {
36667
+ switch (zoneType) {
36668
+ case 'patio':
36669
+ case 'walkway':
36670
+ return 10; // Close zones
36671
+ case 'driveway':
36672
+ case 'parking':
36673
+ return 25; // Near zones
36674
+ case 'yard':
36675
+ case 'garden':
36676
+ case 'pool':
36677
+ return 40; // Medium zones
36678
+ case 'street':
36679
+ return 100; // Far zones
36680
+ case 'unknown':
36681
+ default:
36682
+ return 50; // Default to medium distance
36683
+ }
36684
+ }
36660
36685
  /** Try to parse JSON with recovery for truncated responses */
36661
36686
  parseJsonWithRecovery(jsonStr, context) {
36662
36687
  // First, try direct parse
@@ -36950,6 +36975,9 @@ Use the mount height to help estimate distances - objects at ground level will a
36950
36975
  // Generate zone suggestions (even for smaller coverage - 10% is enough)
36951
36976
  for (const zone of analysis.zones) {
36952
36977
  if (zone.coverage >= 0.1) {
36978
+ // Calculate distance in feet from distance estimate (for zones with distance info)
36979
+ const zoneWithDist = zone;
36980
+ const distanceFeet = zoneWithDist.distance ? (0, discovery_1.distanceToFeet)(zoneWithDist.distance) : this.getDefaultZoneDistance(zone.type);
36953
36981
  const suggestion = {
36954
36982
  id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
36955
36983
  type: 'zone',
@@ -36957,10 +36985,14 @@ Use the mount height to help estimate distances - objects at ground level will a
36957
36985
  sourceCameras: [analysis.cameraId],
36958
36986
  confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
36959
36987
  status: 'pending',
36960
- zone: zone,
36988
+ zone: {
36989
+ ...zone,
36990
+ // Include distance metadata for positioning
36991
+ distanceFeet: distanceFeet,
36992
+ },
36961
36993
  };
36962
36994
  this.suggestions.set(suggestion.id, suggestion);
36963
- this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage)`);
36995
+ this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage, ~${distanceFeet}ft)`);
36964
36996
  }
36965
36997
  }
36966
36998
  }
@@ -40784,61 +40816,38 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40784
40816
  // Convert direction to radians (0 = up/north, 90 = right/east)
40785
40817
  const dirRad = (direction - 90) * Math.PI / 180;
40786
40818
  const halfFov = (fovAngle / 2) * Math.PI / 180;
40787
- // Use landmark TYPE to determine distance - some things are inherently far away
40788
- // regardless of where they appear in the camera frame
40789
- const landmarkType = suggestion.landmark.type;
40790
- const farTypes = ['neighbor', 'boundary', 'street']; // Always place at edge of FOV
40791
- const isFarType = farTypes.includes(landmarkType || '');
40792
- // Use bounding box if available for horizontal positioning
40793
- // boundingBox format: [x, y, width, height] normalized 0-1
40794
- const bbox = suggestion.landmark.boundingBox;
40819
+ // Get floor plan scale (pixels per foot) - default 5
40820
+ const floorPlanScale = topology.floorPlanScale || 5;
40821
+ // Use the ACTUAL distance from LLM analysis (distanceFeet) - this is the key fix!
40822
+ // The LLM estimates distance based on object size, perspective, and camera context
40823
+ const landmarkData = suggestion.landmark;
40824
+ const distanceFeet = landmarkData.distanceFeet || 50; // Default 50ft if not set
40825
+ const distanceInPixels = distanceFeet * floorPlanScale;
40826
+ // Use bounding box for horizontal positioning within the FOV
40827
+ const bbox = landmarkData.boundingBox;
40795
40828
  let angleOffset;
40796
- let distanceMultiplier;
40797
40829
  if (bbox && bbox.length >= 2) {
40798
40830
  // Use bounding box X for horizontal position in FOV
40799
40831
  const bboxCenterX = bbox[0] + (bbox[2] || 0) / 2; // 0 = left edge, 1 = right edge
40800
40832
  // Map X position to angle within FOV
40801
40833
  angleOffset = (bboxCenterX - 0.5) * 2 * halfFov;
40802
- // For distance: use TYPE first, then bbox Y as hint
40803
- if (isFarType) {
40804
- // Neighbors, boundaries, streets are BEYOND the camera's normal range
40805
- // Place at 150-200% of range to indicate they're distant background features
40806
- distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
40807
- this.console.log(`[Discovery] Far-type landmark "${landmarkType}" placed BEYOND FOV (${(distanceMultiplier * 100).toFixed(0)}% of range)`);
40808
- }
40809
- else {
40810
- // For other types, use bbox Y as a hint (but cap minimum distance)
40811
- const bboxCenterY = bbox[1] + (bbox[3] || 0) / 2;
40812
- // Map Y to distance: 0 (top) = far, 1 (bottom) = closer (but not too close)
40813
- distanceMultiplier = Math.max(0.4, 0.9 - (bboxCenterY * 0.5));
40814
- }
40815
- this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal position`);
40834
+ this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal angle offset`);
40816
40835
  }
40817
40836
  else {
40818
- // No bounding box - use type and spread pattern
40837
+ // No bounding box - spread horizontally based on existing landmarks
40819
40838
  const cameraDeviceId = camera.deviceId;
40820
40839
  const existingFromCamera = (topology.landmarks || []).filter(l => l.visibleFromCameras?.includes(cameraDeviceId) ||
40821
40840
  l.visibleFromCameras?.includes(camera.name)).length;
40822
40841
  // Spread horizontally across FOV
40823
40842
  angleOffset = (existingFromCamera % 3 - 1) * halfFov * 0.6;
40824
- // Distance based on type
40825
- if (isFarType) {
40826
- // Neighbors, boundaries, streets are BEYOND the camera's normal range
40827
- distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
40828
- this.console.log(`[Discovery] Far-type landmark "${landmarkType}" (no bbox) placed BEYOND FOV`);
40829
- }
40830
- else {
40831
- distanceMultiplier = 0.5 + (existingFromCamera % 2) * 0.3;
40832
- this.console.log(`[Discovery] No bounding box, using fallback spread (existing: ${existingFromCamera})`);
40833
- }
40843
+ this.console.log(`[Discovery] No bounding box, spreading horizontally (existing: ${existingFromCamera})`);
40834
40844
  }
40835
40845
  const finalAngle = dirRad + angleOffset;
40836
- const distance = range * distanceMultiplier;
40837
40846
  position = {
40838
- x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distance,
40839
- y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distance,
40847
+ x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distanceInPixels,
40848
+ y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distanceInPixels,
40840
40849
  };
40841
- this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}" in ${camera.name}'s FOV: dir=${direction}°, angle=${(angleOffset * 180 / Math.PI).toFixed(1)}°, dist=${distance.toFixed(0)}px`);
40850
+ this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}: dir=${direction}°, angle=${(angleOffset * 180 / Math.PI).toFixed(1)}°`);
40842
40851
  }
40843
40852
  else {
40844
40853
  // Position in a grid pattern starting from center
@@ -40891,48 +40900,53 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40891
40900
  let polygon = [];
40892
40901
  const timestamp = Date.now();
40893
40902
  if (camera?.floorPlanPosition) {
40894
- // Get camera's FOV direction and range (cast to any for flexible access)
40903
+ // Get camera's FOV direction (cast to any for flexible access)
40895
40904
  const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 });
40896
40905
  const direction = fov.direction || 0;
40897
- const range = fov.range || 80;
40898
40906
  const fovAngle = fov.angle || 90;
40907
+ // Get floor plan scale (pixels per foot)
40908
+ const floorPlanScale = topology.floorPlanScale || 5;
40899
40909
  // Convert direction to radians (0 = up/north, 90 = right/east)
40900
40910
  const dirRad = (direction - 90) * Math.PI / 180;
40901
40911
  const halfFov = (fovAngle / 2) * Math.PI / 180;
40902
40912
  const camX = camera.floorPlanPosition.x;
40903
40913
  const camY = camera.floorPlanPosition.y;
40904
- // Use bounding box if available to position zone accurately within FOV
40914
+ // Use distanceFeet from the zone metadata for accurate positioning
40915
+ const zoneData = zone;
40916
+ const distanceFeet = zoneData.distanceFeet || 40; // Default 40ft if not set
40917
+ const distanceInPixels = distanceFeet * floorPlanScale;
40918
+ // Zone size based on coverage (larger coverage = wider zone)
40919
+ const zoneWidthFeet = Math.sqrt(zone.coverage) * 30; // e.g., 50% coverage = ~21ft wide
40920
+ const zoneWidthPixels = zoneWidthFeet * floorPlanScale;
40921
+ const zoneDepthPixels = (zone.coverage * 20) * floorPlanScale; // Depth based on coverage
40922
+ // Use bounding box for horizontal positioning if available
40905
40923
  const bbox = zone.boundingBox; // [x, y, width, height] normalized 0-1
40906
- let innerRadius;
40907
- let outerRadius;
40908
40924
  let angleStart;
40909
40925
  let angleEnd;
40926
+ let innerRadius;
40927
+ let outerRadius;
40910
40928
  if (bbox && bbox.length >= 4) {
40911
- // Map bounding box to position within FOV
40929
+ // Map bounding box X to angle within FOV
40912
40930
  const bboxLeft = bbox[0];
40913
40931
  const bboxRight = bbox[0] + bbox[2];
40914
- const bboxTop = bbox[1];
40915
- const bboxBottom = bbox[1] + bbox[3];
40916
- // Map X to angle within FOV (0 = left edge, 1 = right edge)
40917
40932
  angleStart = dirRad + (bboxLeft - 0.5) * 2 * halfFov;
40918
40933
  angleEnd = dirRad + (bboxRight - 0.5) * 2 * halfFov;
40919
- // Map Y to distance (0 = far, 1 = close)
40920
- innerRadius = range * (0.9 - bboxBottom * 0.6);
40921
- outerRadius = range * (0.9 - bboxTop * 0.6);
40922
- // Ensure min size
40923
- if (outerRadius - innerRadius < 20) {
40924
- outerRadius = innerRadius + 20;
40925
- }
40926
- this.console.log(`[Discovery] Zone "${zone.name}" using bbox [${bbox.join(',')}] → angles ${(angleStart * 180 / Math.PI).toFixed(1)}° to ${(angleEnd * 180 / Math.PI).toFixed(1)}°`);
40934
+ // Use distanceFeet for depth, with a spread based on coverage
40935
+ innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
40936
+ outerRadius = distanceInPixels + zoneDepthPixels / 2;
40937
+ this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using bbox [${bbox.join(',')}]`);
40927
40938
  }
40928
40939
  else {
40929
- // Fallback: wedge-shaped zone offset by existing count
40940
+ // Fallback: wedge-shaped zone covering portion of FOV
40930
40941
  const existingFromCamera = (topology.drawnZones || []).filter((z) => z.linkedCameras?.includes(sourceCameras[0])).length;
40931
- innerRadius = range * 0.3 + existingFromCamera * 20;
40932
- outerRadius = range * 0.8 + existingFromCamera * 20;
40933
- angleStart = dirRad - halfFov * 0.7;
40934
- angleEnd = dirRad + halfFov * 0.7;
40935
- this.console.log(`[Discovery] Zone "${zone.name}" using fallback spread (existing: ${existingFromCamera})`);
40942
+ // Spread horizontally based on existing zones
40943
+ const offset = (existingFromCamera % 3 - 1) * halfFov * 0.5;
40944
+ angleStart = dirRad + offset - halfFov * 0.3;
40945
+ angleEnd = dirRad + offset + halfFov * 0.3;
40946
+ // Use distanceFeet for depth
40947
+ innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
40948
+ outerRadius = distanceInPixels + zoneDepthPixels / 2;
40949
+ this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using fallback spread`);
40936
40950
  }
40937
40951
  // Create arc polygon
40938
40952
  const steps = 8;
@@ -40952,7 +40966,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40952
40966
  y: camY + Math.sin(angle) * outerRadius,
40953
40967
  });
40954
40968
  }
40955
- this.console.log(`[Discovery] Creating zone "${zone.name}" in ${camera.name}'s FOV: dir=${direction}°`);
40969
+ this.console.log(`[Discovery] Creating zone "${zone.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}`);
40956
40970
  }
40957
40971
  else {
40958
40972
  // Fallback: rectangular zone at default location
@@ -42554,6 +42568,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
42554
42568
  const response = await fetch('../api/topology');
42555
42569
  if (response.ok) {
42556
42570
  topology = await response.json();
42571
+ console.log('[Load] Loaded topology with', (topology.drawnZones || []).length, 'zones');
42557
42572
  if (!topology.drawings) topology.drawings = [];
42558
42573
  // Load floor plan scale if saved
42559
42574
  if (topology.floorPlanScale) {
@@ -43071,14 +43086,24 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43071
43086
  async function saveTopology() {
43072
43087
  try {
43073
43088
  setStatus('Saving...', 'warning');
43089
+ console.log('[Save] Saving topology with', (topology.drawnZones || []).length, 'zones');
43074
43090
  const response = await fetch('../api/topology', {
43075
43091
  method: 'PUT',
43076
43092
  headers: { 'Content-Type': 'application/json' },
43077
43093
  body: JSON.stringify(topology)
43078
43094
  });
43079
- if (response.ok) { setStatus('Saved successfully', 'success'); }
43080
- else { setStatus('Failed to save', 'error'); }
43081
- } catch (e) { console.error('Failed to save topology:', e); setStatus('Failed to save', 'error'); }
43095
+ if (response.ok) {
43096
+ console.log('[Save] Save successful');
43097
+ setStatus('Saved successfully', 'success');
43098
+ } else {
43099
+ const errorText = await response.text();
43100
+ console.error('[Save] Save failed:', response.status, errorText);
43101
+ setStatus('Failed to save: ' + errorText, 'error');
43102
+ }
43103
+ } catch (e) {
43104
+ console.error('Failed to save topology:', e);
43105
+ setStatus('Failed to save', 'error');
43106
+ }
43082
43107
  }
43083
43108
 
43084
43109
  function resizeCanvas() {
@@ -43878,9 +43903,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43878
43903
  polygon: currentZonePoints.map(pt => ({ x: pt.x, y: pt.y })),
43879
43904
  };
43880
43905
 
43906
+ console.log('[Zone] Creating zone:', zone.name, 'with', zone.polygon.length, 'points');
43907
+
43881
43908
  if (!topology.drawnZones) topology.drawnZones = [];
43882
43909
  topology.drawnZones.push(zone);
43883
43910
 
43911
+ console.log('[Zone] Total zones now:', topology.drawnZones.length);
43912
+
43884
43913
  // Reset state
43885
43914
  zoneDrawingMode = false;
43886
43915
  currentZonePoints = [];
@@ -43889,7 +43918,19 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43889
43918
  setTool('select');
43890
43919
  updateUI();
43891
43920
  render();
43892
- setStatus('Zone "' + zone.name + '" created with ' + zone.polygon.length + ' points', 'success');
43921
+
43922
+ console.log('[Zone] After updateUI/render, zones in topology:', (topology.drawnZones || []).length);
43923
+
43924
+ setStatus('Zone "' + zone.name + '" created - saving...', 'success');
43925
+
43926
+ // Auto-save after creating a zone
43927
+ saveTopology().then(() => {
43928
+ console.log('[Zone] Save completed, zones in topology:', (topology.drawnZones || []).length);
43929
+ setStatus('Zone "' + zone.name + '" saved', 'success');
43930
+ }).catch(err => {
43931
+ console.error('Failed to auto-save zone:', err);
43932
+ setStatus('Zone created but auto-save failed - click Save', 'warning');
43933
+ });
43893
43934
  }
43894
43935
 
43895
43936
  function selectZone(id) {
@@ -43914,9 +43955,9 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43914
43955
  '<button class="btn btn-primary" onclick="deleteZone(\\'' + id + '\\')" style="background: #dc2626; width: 100%;">Delete Zone</button>';
43915
43956
  }
43916
43957
 
43917
- function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); } }
43918
- function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); } }
43919
- function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; } }
43958
+ function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); saveTopology(); } }
43959
+ function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); saveTopology(); } }
43960
+ function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; saveTopology(); } }
43920
43961
  function deleteZone(id) {
43921
43962
  if (!confirm('Delete this zone?')) return;
43922
43963
  topology.drawnZones = (topology.drawnZones || []).filter(z => z.id !== id);
@@ -43924,7 +43965,8 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43924
43965
  document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
43925
43966
  updateUI();
43926
43967
  render();
43927
- setStatus('Zone deleted', 'success');
43968
+ setStatus('Zone deleted - saving...', 'success');
43969
+ saveTopology().then(() => setStatus('Zone deleted', 'success'));
43928
43970
  }
43929
43971
 
43930
43972
  function useBlankCanvas() {