@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/dist/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +109 -24
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/topology-discovery.ts +71 -17
- package/src/main.ts +51 -7
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -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": ["
|
|
91
|
+
{"name": "Driveway", "type": "access", "seenByCameras": ["device_123", "device_456"], "confidence": 0.8, "description": "Concrete driveway"}
|
|
90
92
|
],
|
|
91
93
|
"connections": [
|
|
92
|
-
{"from": "
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
1832
|
-
const
|
|
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(
|
|
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
|
|
1904
|
-
|
|
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 =>
|
|
1991
|
-
|
|
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
|