@blueharford/scrypted-spatial-awareness 0.6.3 → 0.6.4

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": "@blueharford/scrypted-spatial-awareness",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Cross-camera object tracking for Scrypted NVR with spatial awareness",
5
5
  "author": "Joshua Seidel <blueharford>",
6
6
  "license": "Apache-2.0",
@@ -83,13 +83,15 @@ Identify:
83
83
  2. Camera connections - How someone could move between camera views and estimated walking time
84
84
  3. Overall layout - Describe the property layout based on what you see
85
85
 
86
+ IMPORTANT: For camera references, use the EXACT device ID shown in parentheses (e.g., "device_123"), NOT the camera name.
87
+
86
88
  Respond with ONLY valid JSON:
87
89
  {
88
90
  "sharedLandmarks": [
89
- {"name": "Driveway", "type": "access", "seenByCameras": ["camera1", "camera2"], "confidence": 0.8, "description": "Concrete driveway"}
91
+ {"name": "Driveway", "type": "access", "seenByCameras": ["device_123", "device_456"], "confidence": 0.8, "description": "Concrete driveway"}
90
92
  ],
91
93
  "connections": [
92
- {"from": "camera1", "to": "camera2", "transitSeconds": 10, "via": "driveway", "confidence": 0.7, "bidirectional": true}
94
+ {"from": "device_123", "to": "device_456", "transitSeconds": 10, "via": "driveway", "confidence": 0.7, "bidirectional": true}
93
95
  ],
94
96
  "layoutDescription": "Single-story house with front yard facing street, driveway on the left side, backyard accessible through side gate"
95
97
  }`;
@@ -447,6 +449,40 @@ export class TopologyDiscoveryEngine {
447
449
  return 'unknown';
448
450
  }
449
451
 
452
+ /** Resolve a camera reference (name or deviceId) to its deviceId */
453
+ private resolveCameraRef(ref: string): string | null {
454
+ if (!this.topology?.cameras || !ref) return null;
455
+
456
+ // Try exact deviceId match first
457
+ const byId = this.topology.cameras.find(c => c.deviceId === ref);
458
+ if (byId) return byId.deviceId;
459
+
460
+ // Try exact name match
461
+ const byName = this.topology.cameras.find(c => c.name === ref);
462
+ if (byName) return byName.deviceId;
463
+
464
+ // Try case-insensitive name match
465
+ const refLower = ref.toLowerCase();
466
+ const byNameLower = this.topology.cameras.find(c => c.name.toLowerCase() === refLower);
467
+ if (byNameLower) return byNameLower.deviceId;
468
+
469
+ // Try partial name match (LLM might truncate or abbreviate)
470
+ const byPartial = this.topology.cameras.find(c =>
471
+ c.name.toLowerCase().includes(refLower) || refLower.includes(c.name.toLowerCase())
472
+ );
473
+ if (byPartial) return byPartial.deviceId;
474
+
475
+ this.console.warn(`[Discovery] Could not resolve camera reference: "${ref}"`);
476
+ return null;
477
+ }
478
+
479
+ /** Normalize camera references in an array to deviceIds */
480
+ private normalizeCameraRefs(refs: string[]): string[] {
481
+ return refs
482
+ .map(ref => this.resolveCameraRef(ref))
483
+ .filter((id): id is string => id !== null);
484
+ }
485
+
450
486
  /** Analyze all cameras and correlate findings */
451
487
  async runFullDiscovery(): Promise<TopologyCorrelation | null> {
452
488
  if (!this.topology?.cameras?.length) {
@@ -552,24 +588,42 @@ export class TopologyDiscoveryEngine {
552
588
  };
553
589
 
554
590
  if (Array.isArray(parsed.sharedLandmarks)) {
555
- correlation.sharedLandmarks = parsed.sharedLandmarks.map((l: any) => ({
556
- name: l.name || 'Unknown',
557
- type: this.mapLandmarkType(l.type),
558
- seenByCameras: Array.isArray(l.seenByCameras) ? l.seenByCameras : [],
559
- confidence: typeof l.confidence === 'number' ? l.confidence : 0.7,
560
- description: l.description,
561
- }));
591
+ correlation.sharedLandmarks = parsed.sharedLandmarks.map((l: any) => {
592
+ // Normalize camera references to deviceIds
593
+ const rawRefs = Array.isArray(l.seenByCameras) ? l.seenByCameras : [];
594
+ const normalizedRefs = this.normalizeCameraRefs(rawRefs);
595
+ if (rawRefs.length > 0 && normalizedRefs.length === 0) {
596
+ this.console.warn(`[Discovery] Landmark "${l.name}" has no resolvable camera refs: ${JSON.stringify(rawRefs)}`);
597
+ }
598
+ return {
599
+ name: l.name || 'Unknown',
600
+ type: this.mapLandmarkType(l.type),
601
+ seenByCameras: normalizedRefs,
602
+ confidence: typeof l.confidence === 'number' ? l.confidence : 0.7,
603
+ description: l.description,
604
+ };
605
+ });
562
606
  }
563
607
 
564
608
  if (Array.isArray(parsed.connections)) {
565
- correlation.suggestedConnections = parsed.connections.map((c: any) => ({
566
- fromCameraId: c.from || c.fromCameraId || '',
567
- toCameraId: c.to || c.toCameraId || '',
568
- transitSeconds: typeof c.transitSeconds === 'number' ? c.transitSeconds : 15,
569
- via: c.via || '',
570
- confidence: typeof c.confidence === 'number' ? c.confidence : 0.6,
571
- bidirectional: c.bidirectional !== false,
572
- }));
609
+ correlation.suggestedConnections = parsed.connections.map((c: any) => {
610
+ // Normalize camera references to deviceIds
611
+ const fromRef = c.from || c.fromCameraId || '';
612
+ const toRef = c.to || c.toCameraId || '';
613
+ const fromId = this.resolveCameraRef(fromRef);
614
+ const toId = this.resolveCameraRef(toRef);
615
+ if (!fromId || !toId) {
616
+ this.console.warn(`[Discovery] Connection has unresolvable camera refs: from="${fromRef}" to="${toRef}"`);
617
+ }
618
+ return {
619
+ fromCameraId: fromId || fromRef,
620
+ toCameraId: toId || toRef,
621
+ transitSeconds: typeof c.transitSeconds === 'number' ? c.transitSeconds : 15,
622
+ via: c.via || '',
623
+ confidence: typeof c.confidence === 'number' ? c.confidence : 0.6,
624
+ bidirectional: c.bidirectional !== false,
625
+ };
626
+ });
573
627
  }
574
628
 
575
629
  this.console.log(`[Discovery] Correlation found ${correlation.sharedLandmarks.length} shared landmarks, ${correlation.suggestedConnections.length} connections`);
package/src/main.ts CHANGED
@@ -1829,21 +1829,29 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1829
1829
  if (!position || (position.x === 0 && position.y === 0)) {
1830
1830
  // Debug logging
1831
1831
  this.console.log(`[Discovery] Processing landmark "${suggestion.landmark.name}"`);
1832
+ this.console.log(`[Discovery] sourceCameras: ${JSON.stringify(suggestion.sourceCameras)}`);
1832
1833
  this.console.log(`[Discovery] visibleFromCameras: ${JSON.stringify(suggestion.landmark.visibleFromCameras)}`);
1833
1834
  this.console.log(`[Discovery] Available cameras: ${topology.cameras.map(c => `${c.name}(${c.deviceId})`).join(', ')}`);
1834
1835
 
1835
- // Find a camera that can see this landmark - use flexible matching (deviceId, name, or case-insensitive)
1836
+ // Find a camera that can see this landmark
1837
+ // PREFER sourceCameras (set from analysis.cameraId) over visibleFromCameras (from LLM parsing)
1838
+ const sourceCameraRef = suggestion.sourceCameras?.[0];
1836
1839
  const visibleCameraRef = suggestion.landmark.visibleFromCameras?.[0];
1837
- const camera = visibleCameraRef ? topology.cameras.find(c =>
1838
- c.deviceId === visibleCameraRef ||
1839
- c.name === visibleCameraRef ||
1840
- c.name.toLowerCase() === visibleCameraRef.toLowerCase()
1840
+ const cameraRef = sourceCameraRef || visibleCameraRef;
1841
+
1842
+ // Use flexible matching (deviceId, name, or case-insensitive)
1843
+ const camera = cameraRef ? topology.cameras.find(c =>
1844
+ c.deviceId === cameraRef ||
1845
+ c.name === cameraRef ||
1846
+ c.name.toLowerCase() === cameraRef.toLowerCase()
1841
1847
  ) : null;
1842
1848
 
1843
1849
  if (camera) {
1844
- this.console.log(`[Discovery] Matched camera: ${camera.name}, position: ${JSON.stringify(camera.floorPlanPosition)}, fov: ${JSON.stringify(camera.fov)}`);
1850
+ this.console.log(`[Discovery] Matched camera: ${camera.name} (${camera.deviceId})`);
1851
+ this.console.log(`[Discovery] Camera position: ${JSON.stringify(camera.floorPlanPosition)}`);
1852
+ this.console.log(`[Discovery] Camera FOV: ${JSON.stringify(camera.fov)}`);
1845
1853
  } else {
1846
- this.console.warn(`[Discovery] No camera matched for "${visibleCameraRef}"`);
1854
+ this.console.warn(`[Discovery] No camera matched for ref="${cameraRef}" (source="${sourceCameraRef}", visible="${visibleCameraRef}")`);
1847
1855
  }
1848
1856
 
1849
1857
  if (camera?.floorPlanPosition) {
@@ -1917,10 +1925,22 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1917
1925
 
1918
1926
  // Find cameras that see this zone
1919
1927
  const sourceCameras = suggestion.sourceCameras || [];
1920
- const camera = sourceCameras[0]
1921
- ? topology.cameras.find(c => c.deviceId === sourceCameras[0] || c.name === sourceCameras[0])
1928
+ const cameraRef = sourceCameras[0];
1929
+ const camera = cameraRef
1930
+ ? topology.cameras.find(c =>
1931
+ c.deviceId === cameraRef ||
1932
+ c.name === cameraRef ||
1933
+ c.name.toLowerCase() === cameraRef.toLowerCase()
1934
+ )
1922
1935
  : null;
1923
1936
 
1937
+ this.console.log(`[Discovery] Processing zone "${zone.name}" from camera ref="${cameraRef}"`);
1938
+ if (camera) {
1939
+ this.console.log(`[Discovery] Matched camera: ${camera.name} (${camera.deviceId})`);
1940
+ } else if (cameraRef) {
1941
+ this.console.warn(`[Discovery] No camera matched for zone source ref="${cameraRef}"`);
1942
+ }
1943
+
1924
1944
  // Create zone polygon WITHIN the camera's field of view
1925
1945
  let polygon: { x: number; y: number }[] = [];
1926
1946
  const timestamp = Date.now();
@@ -2004,8 +2024,15 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
2004
2024
  const globalZoneType = this.mapZoneTypeToGlobalType(zone.type);
2005
2025
  const cameraZones: CameraZoneMapping[] = sourceCameras
2006
2026
  .map(camRef => {
2007
- const cam = topology.cameras.find(c => c.deviceId === camRef || c.name === camRef);
2008
- if (!cam) return null;
2027
+ const cam = topology.cameras.find(c =>
2028
+ c.deviceId === camRef ||
2029
+ c.name === camRef ||
2030
+ c.name.toLowerCase() === camRef.toLowerCase()
2031
+ );
2032
+ if (!cam) {
2033
+ this.console.warn(`[Discovery] GlobalZone: No camera matched for ref="${camRef}"`);
2034
+ return null;
2035
+ }
2009
2036
  return {
2010
2037
  cameraId: cam.deviceId,
2011
2038
  zone: [[0, 0], [100, 0], [100, 100], [0, 100]] as ClipPath, // Full frame default