@blueharford/scrypted-spatial-awareness 0.6.2 → 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.2",
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
@@ -1827,9 +1827,32 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1827
1827
  // Calculate position for the landmark WITHIN the camera's field of view
1828
1828
  let position = suggestion.landmark.position;
1829
1829
  if (!position || (position.x === 0 && position.y === 0)) {
1830
+ // Debug logging
1831
+ this.console.log(`[Discovery] Processing landmark "${suggestion.landmark.name}"`);
1832
+ this.console.log(`[Discovery] sourceCameras: ${JSON.stringify(suggestion.sourceCameras)}`);
1833
+ this.console.log(`[Discovery] visibleFromCameras: ${JSON.stringify(suggestion.landmark.visibleFromCameras)}`);
1834
+ this.console.log(`[Discovery] Available cameras: ${topology.cameras.map(c => `${c.name}(${c.deviceId})`).join(', ')}`);
1835
+
1830
1836
  // Find a camera that can see this landmark
1831
- const visibleCameraId = suggestion.landmark.visibleFromCameras?.[0];
1832
- const camera = visibleCameraId ? topology.cameras.find(c => c.deviceId === visibleCameraId) : null;
1837
+ // PREFER sourceCameras (set from analysis.cameraId) over visibleFromCameras (from LLM parsing)
1838
+ const sourceCameraRef = suggestion.sourceCameras?.[0];
1839
+ const visibleCameraRef = suggestion.landmark.visibleFromCameras?.[0];
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()
1847
+ ) : null;
1848
+
1849
+ if (camera) {
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)}`);
1853
+ } else {
1854
+ this.console.warn(`[Discovery] No camera matched for ref="${cameraRef}" (source="${sourceCameraRef}", visible="${visibleCameraRef}")`);
1855
+ }
1833
1856
 
1834
1857
  if (camera?.floorPlanPosition) {
1835
1858
  // Get camera's FOV direction and range (cast to any for flexible access)
@@ -1839,8 +1862,10 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1839
1862
  const fovAngle = fov.angle || 90;
1840
1863
 
1841
1864
  // Count existing landmarks from this camera to spread them out
1865
+ const cameraDeviceId = camera.deviceId;
1842
1866
  const existingFromCamera = (topology.landmarks || []).filter(l =>
1843
- l.visibleFromCameras?.includes(visibleCameraId)
1867
+ l.visibleFromCameras?.includes(cameraDeviceId) ||
1868
+ l.visibleFromCameras?.includes(camera.name)
1844
1869
  ).length;
1845
1870
 
1846
1871
  // Calculate position in front of camera within its FOV
@@ -1900,10 +1925,22 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1900
1925
 
1901
1926
  // Find cameras that see this zone
1902
1927
  const sourceCameras = suggestion.sourceCameras || [];
1903
- const camera = sourceCameras[0]
1904
- ? 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
+ )
1905
1935
  : null;
1906
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
+
1907
1944
  // Create zone polygon WITHIN the camera's field of view
1908
1945
  let polygon: { x: number; y: number }[] = [];
1909
1946
  const timestamp = Date.now();
@@ -1987,8 +2024,15 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1987
2024
  const globalZoneType = this.mapZoneTypeToGlobalType(zone.type);
1988
2025
  const cameraZones: CameraZoneMapping[] = sourceCameras
1989
2026
  .map(camRef => {
1990
- const cam = topology.cameras.find(c => c.deviceId === camRef || c.name === camRef);
1991
- 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
+ }
1992
2036
  return {
1993
2037
  cameraId: cam.deviceId,
1994
2038
  zone: [[0, 0], [100, 0], [100, 100], [0, 100]] as ClipPath, // Full frame default