@blueharford/scrypted-spatial-awareness 0.6.13 → 0.6.15
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 +223 -57
- 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 +142 -38
- package/src/core/tracking-engine.ts +73 -31
- package/src/models/discovery.ts +17 -0
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
DiscoveryStatus,
|
|
24
24
|
DEFAULT_DISCOVERY_STATUS,
|
|
25
25
|
RATE_LIMIT_WARNING_THRESHOLD,
|
|
26
|
+
DistanceEstimate,
|
|
27
|
+
distanceToFeet,
|
|
26
28
|
} from '../models/discovery';
|
|
27
29
|
import {
|
|
28
30
|
CameraTopology,
|
|
@@ -40,60 +42,141 @@ interface ChatCompletionDevice extends ScryptedDevice {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/** Scene analysis prompt for single camera */
|
|
43
|
-
const SCENE_ANALYSIS_PROMPT = `
|
|
45
|
+
const SCENE_ANALYSIS_PROMPT = `You are analyzing a security camera image. Describe EVERYTHING you can see in detail.
|
|
46
|
+
|
|
47
|
+
## INSTRUCTIONS
|
|
48
|
+
Look at this image carefully and identify ALL visible objects, structures, and areas. Be thorough - even small or partially visible items are important for security awareness.
|
|
49
|
+
|
|
50
|
+
## 1. LANDMARKS - List EVERY distinct object or feature you can see:
|
|
51
|
+
|
|
52
|
+
**Structures** (buildings, parts of buildings):
|
|
53
|
+
- Houses, garages, sheds, porches, decks, patios, carports, gazebos
|
|
54
|
+
- Walls, pillars, columns, railings, stairs, steps
|
|
55
|
+
|
|
56
|
+
**Vegetation** (plants, trees, landscaping):
|
|
57
|
+
- Trees (describe type if identifiable: oak, palm, pine, etc.)
|
|
58
|
+
- Bushes, shrubs, hedges
|
|
59
|
+
- Flower beds, gardens, planters, potted plants
|
|
60
|
+
- Grass/lawn areas, mulch beds
|
|
61
|
+
|
|
62
|
+
**Boundaries & Barriers**:
|
|
63
|
+
- Fences (wood, chain-link, aluminum, vinyl, iron, privacy)
|
|
64
|
+
- Walls (brick, stone, concrete, retaining)
|
|
65
|
+
- Gates, gate posts
|
|
66
|
+
- Hedges used as boundaries
|
|
67
|
+
|
|
68
|
+
**Access Points & Pathways**:
|
|
69
|
+
- Doors (front, side, garage, screen)
|
|
70
|
+
- Driveways (concrete, asphalt, gravel, pavers)
|
|
71
|
+
- Walkways, sidewalks, paths, stepping stones
|
|
72
|
+
- Stairs, ramps, porches
|
|
73
|
+
|
|
74
|
+
**Utility & Fixtures**:
|
|
75
|
+
- Mailboxes, package boxes
|
|
76
|
+
- Light fixtures, lamp posts, solar lights
|
|
77
|
+
- A/C units, utility boxes, meters
|
|
78
|
+
- Trash cans, recycling bins
|
|
79
|
+
- Hoses, spigots, sprinklers
|
|
80
|
+
|
|
81
|
+
**Outdoor Items**:
|
|
82
|
+
- Vehicles (cars, trucks, motorcycles, boats, trailers)
|
|
83
|
+
- Furniture (chairs, tables, benches, swings)
|
|
84
|
+
- Grills, fire pits, outdoor kitchens
|
|
85
|
+
- Play equipment, trampolines, pools
|
|
86
|
+
- Decorations, flags, signs
|
|
87
|
+
|
|
88
|
+
**Off-Property Elements** (important for security context):
|
|
89
|
+
- Street, road, sidewalk
|
|
90
|
+
- Neighbor's property/fence/house
|
|
91
|
+
- Public areas visible
|
|
92
|
+
|
|
93
|
+
For EACH landmark, estimate its DISTANCE from the camera:
|
|
94
|
+
- "close" = 0-10 feet (within arm's reach of camera)
|
|
95
|
+
- "near" = 10-30 feet
|
|
96
|
+
- "medium" = 30-60 feet
|
|
97
|
+
- "far" = 60-100 feet
|
|
98
|
+
- "distant" = 100+ feet (edge of property or beyond)
|
|
99
|
+
|
|
100
|
+
## 2. ZONES - Identify distinct AREAS visible:
|
|
101
|
+
- Front yard, backyard, side yard
|
|
102
|
+
- Driveway, parking area
|
|
103
|
+
- Patio, deck, porch
|
|
104
|
+
- Garden area, lawn
|
|
105
|
+
- Street/road
|
|
106
|
+
- Neighbor's yard
|
|
107
|
+
|
|
108
|
+
For each zone, estimate what percentage of the image it covers (0.0 to 1.0).
|
|
109
|
+
|
|
110
|
+
## 3. EDGES - What's at each edge of the frame:
|
|
111
|
+
This helps understand what's just out of view.
|
|
112
|
+
|
|
113
|
+
## 4. CAMERA CONTEXT:
|
|
114
|
+
- Estimated mounting height (ground level, 8ft, 12ft, roofline, etc.)
|
|
115
|
+
- Approximate field of view (narrow, medium, wide)
|
|
116
|
+
- Facing direction if determinable (north, south, street-facing, etc.)
|
|
44
117
|
|
|
45
|
-
|
|
46
|
-
- Structures (house, garage, shed, porch, deck)
|
|
47
|
-
- Features (mailbox, tree, pool, garden, fountain)
|
|
48
|
-
- Access points (door, gate, driveway entrance, walkway)
|
|
49
|
-
- Boundaries (fence, wall, hedge)
|
|
50
|
-
|
|
51
|
-
2. ZONES - Identify area types visible:
|
|
52
|
-
- What type of area is this? (front yard, backyard, driveway, street, patio, walkway)
|
|
53
|
-
- Estimate what percentage of the frame each zone covers (0.0 to 1.0)
|
|
54
|
-
|
|
55
|
-
3. EDGES - What's visible at the frame edges:
|
|
56
|
-
- Top edge: (sky, roof, trees, etc.)
|
|
57
|
-
- Left edge: (fence, neighbor, street, etc.)
|
|
58
|
-
- Right edge: (fence, garage, etc.)
|
|
59
|
-
- Bottom edge: (ground, driveway, grass, etc.)
|
|
60
|
-
|
|
61
|
-
4. ORIENTATION - Estimate camera facing direction based on shadows, sun position, or landmarks
|
|
62
|
-
|
|
63
|
-
Respond with ONLY valid JSON in this exact format:
|
|
118
|
+
Respond with ONLY valid JSON:
|
|
64
119
|
{
|
|
65
120
|
"landmarks": [
|
|
66
|
-
{"name": "
|
|
121
|
+
{"name": "Mailbox", "type": "feature", "distance": "medium", "confidence": 0.95, "description": "Black metal mailbox on wooden post, approximately 40 feet from camera"},
|
|
122
|
+
{"name": "Aluminum Fence", "type": "boundary", "distance": "near", "confidence": 0.9, "description": "Silver aluminum fence running along left side of property, about 15-20 feet away"},
|
|
123
|
+
{"name": "Large Oak Tree", "type": "feature", "distance": "far", "confidence": 0.85, "description": "Mature oak tree near property line, roughly 80 feet from camera"}
|
|
67
124
|
],
|
|
68
125
|
"zones": [
|
|
69
|
-
{"name": "Front Yard", "type": "yard", "coverage": 0.
|
|
126
|
+
{"name": "Front Yard", "type": "yard", "coverage": 0.5, "description": "Grass lawn with some bare patches"},
|
|
127
|
+
{"name": "Driveway", "type": "driveway", "coverage": 0.25, "description": "Concrete driveway leading to garage"}
|
|
70
128
|
],
|
|
71
|
-
"edges": {
|
|
72
|
-
|
|
73
|
-
|
|
129
|
+
"edges": {
|
|
130
|
+
"top": "sky, tree canopy",
|
|
131
|
+
"left": "aluminum fence, neighbor's yard beyond",
|
|
132
|
+
"right": "side of house, garage door",
|
|
133
|
+
"bottom": "concrete walkway, grass edge"
|
|
134
|
+
},
|
|
135
|
+
"cameraContext": {
|
|
136
|
+
"mountHeight": "8 feet",
|
|
137
|
+
"fieldOfView": "wide",
|
|
138
|
+
"facingDirection": "street-facing"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
BE THOROUGH. List every distinct item you can identify. A typical outdoor scene should have 5-15+ landmarks.`;
|
|
74
143
|
|
|
75
144
|
/** Multi-camera correlation prompt */
|
|
76
|
-
const CORRELATION_PROMPT = `I have scene analyses from multiple security cameras at the same property. Help me
|
|
145
|
+
const CORRELATION_PROMPT = `I have detailed scene analyses from multiple security cameras at the same property. Help me understand which landmarks appear in multiple camera views.
|
|
77
146
|
|
|
78
147
|
CAMERA SCENES:
|
|
79
148
|
{scenes}
|
|
80
149
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
150
|
+
## PRIORITY ORDER (most important first):
|
|
151
|
+
|
|
152
|
+
### 1. SHARED LANDMARKS (HIGHEST PRIORITY)
|
|
153
|
+
Identify features that are visible from MULTIPLE cameras. This is crucial for understanding the property layout.
|
|
154
|
+
- Look for the SAME fence, tree, mailbox, driveway, structure, etc. appearing in different camera views
|
|
155
|
+
- Even partial visibility counts (e.g., a tree visible in full from one camera and just the edge from another)
|
|
156
|
+
- Include landmarks that are at the boundary between camera views
|
|
157
|
+
|
|
158
|
+
### 2. PROPERTY LAYOUT
|
|
159
|
+
Based on what each camera sees and their overlapping features, describe:
|
|
160
|
+
- Which areas each camera covers
|
|
161
|
+
- How the cameras relate spatially (e.g., "Camera A looks toward Camera B's direction")
|
|
162
|
+
- Overall property shape and features
|
|
163
|
+
|
|
164
|
+
### 3. CONNECTIONS (Lower Priority)
|
|
165
|
+
Only if clearly determinable, suggest walking paths between camera views.
|
|
85
166
|
|
|
86
167
|
IMPORTANT: For camera references, use the EXACT device ID shown in parentheses (e.g., "device_123"), NOT the camera name.
|
|
87
168
|
|
|
88
169
|
Respond with ONLY valid JSON:
|
|
89
170
|
{
|
|
90
171
|
"sharedLandmarks": [
|
|
91
|
-
{"name": "
|
|
172
|
+
{"name": "Aluminum Fence", "type": "boundary", "seenByCameras": ["device_123", "device_456"], "confidence": 0.85, "description": "Silver aluminum fence visible on right edge of Camera A and left edge of Camera B"},
|
|
173
|
+
{"name": "Large Oak Tree", "type": "feature", "seenByCameras": ["device_123", "device_789"], "confidence": 0.9, "description": "Mature oak tree in front yard, visible from both front and side cameras"},
|
|
174
|
+
{"name": "Concrete Driveway", "type": "access", "seenByCameras": ["device_123", "device_456", "device_789"], "confidence": 0.95, "description": "Driveway visible from multiple angles"}
|
|
92
175
|
],
|
|
93
176
|
"connections": [
|
|
94
|
-
{"from": "device_123", "to": "device_456", "transitSeconds":
|
|
177
|
+
{"from": "device_123", "to": "device_456", "transitSeconds": 8, "via": "along driveway", "confidence": 0.6, "bidirectional": true}
|
|
95
178
|
],
|
|
96
|
-
"layoutDescription": "
|
|
179
|
+
"layoutDescription": "Ranch-style house. Front camera covers front yard and street. Garage camera covers driveway entrance. Side camera covers side yard with aluminum fence separating from neighbor. Backyard camera shows deck and pool area."
|
|
97
180
|
}`;
|
|
98
181
|
|
|
99
182
|
export class TopologyDiscoveryEngine {
|
|
@@ -328,6 +411,7 @@ export class TopologyDiscoveryEngine {
|
|
|
328
411
|
name: l.name || 'Unknown',
|
|
329
412
|
type: this.mapLandmarkType(l.type),
|
|
330
413
|
confidence: typeof l.confidence === 'number' ? l.confidence : 0.7,
|
|
414
|
+
distance: this.mapDistance(l.distance),
|
|
331
415
|
description: l.description || '',
|
|
332
416
|
boundingBox: l.boundingBox,
|
|
333
417
|
}));
|
|
@@ -449,6 +533,17 @@ export class TopologyDiscoveryEngine {
|
|
|
449
533
|
return 'unknown';
|
|
450
534
|
}
|
|
451
535
|
|
|
536
|
+
/** Map LLM distance to our type */
|
|
537
|
+
private mapDistance(distance: string): DistanceEstimate {
|
|
538
|
+
const dist = distance?.toLowerCase();
|
|
539
|
+
if (dist?.includes('close')) return 'close';
|
|
540
|
+
if (dist?.includes('near')) return 'near';
|
|
541
|
+
if (dist?.includes('medium')) return 'medium';
|
|
542
|
+
if (dist?.includes('far') && !dist?.includes('distant')) return 'far';
|
|
543
|
+
if (dist?.includes('distant')) return 'distant';
|
|
544
|
+
return 'medium'; // Default to medium if not specified
|
|
545
|
+
}
|
|
546
|
+
|
|
452
547
|
/** Resolve a camera reference (name or deviceId) to its deviceId */
|
|
453
548
|
private resolveCameraRef(ref: string): string | null {
|
|
454
549
|
if (!this.topology?.cameras || !ref) return null;
|
|
@@ -644,9 +739,14 @@ export class TopologyDiscoveryEngine {
|
|
|
644
739
|
private generateSuggestionsFromAnalysis(analysis: SceneAnalysis): void {
|
|
645
740
|
if (!analysis.isValid) return;
|
|
646
741
|
|
|
742
|
+
this.console.log(`[Discovery] Generating suggestions from ${analysis.landmarks.length} landmarks, ${analysis.zones.length} zones`);
|
|
743
|
+
|
|
647
744
|
// Generate landmark suggestions
|
|
648
745
|
for (const landmark of analysis.landmarks) {
|
|
649
746
|
if (landmark.confidence >= this.config.minLandmarkConfidence) {
|
|
747
|
+
// Calculate distance in feet from distance estimate
|
|
748
|
+
const distanceFeet = landmark.distance ? distanceToFeet(landmark.distance) : 50;
|
|
749
|
+
|
|
650
750
|
const suggestion: DiscoverySuggestion = {
|
|
651
751
|
id: `landmark_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
652
752
|
type: 'landmark',
|
|
@@ -659,27 +759,31 @@ export class TopologyDiscoveryEngine {
|
|
|
659
759
|
type: landmark.type,
|
|
660
760
|
description: landmark.description,
|
|
661
761
|
visibleFromCameras: [analysis.cameraId],
|
|
662
|
-
// Include
|
|
762
|
+
// Include extra metadata for positioning
|
|
663
763
|
boundingBox: landmark.boundingBox,
|
|
664
|
-
|
|
764
|
+
distance: landmark.distance,
|
|
765
|
+
distanceFeet: distanceFeet,
|
|
766
|
+
} as any, // Extra metadata not in base Landmark interface
|
|
665
767
|
};
|
|
666
768
|
this.suggestions.set(suggestion.id, suggestion);
|
|
769
|
+
this.console.log(`[Discovery] Landmark suggestion: ${landmark.name} (${landmark.type}, ${landmark.distance || 'medium'}, ~${distanceFeet}ft)`);
|
|
667
770
|
}
|
|
668
771
|
}
|
|
669
772
|
|
|
670
|
-
// Generate zone suggestions
|
|
773
|
+
// Generate zone suggestions (even for smaller coverage - 10% is enough)
|
|
671
774
|
for (const zone of analysis.zones) {
|
|
672
|
-
if (zone.coverage >= 0.
|
|
775
|
+
if (zone.coverage >= 0.1) {
|
|
673
776
|
const suggestion: DiscoverySuggestion = {
|
|
674
777
|
id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
675
778
|
type: 'zone',
|
|
676
779
|
timestamp: Date.now(),
|
|
677
780
|
sourceCameras: [analysis.cameraId],
|
|
678
|
-
confidence: 0.
|
|
781
|
+
confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
|
|
679
782
|
status: 'pending',
|
|
680
783
|
zone: zone,
|
|
681
784
|
};
|
|
682
785
|
this.suggestions.set(suggestion.id, suggestion);
|
|
786
|
+
this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage)`);
|
|
683
787
|
}
|
|
684
788
|
}
|
|
685
789
|
}
|
|
@@ -141,6 +141,8 @@ export class TrackingEngine {
|
|
|
141
141
|
// ==================== Snapshot Cache ====================
|
|
142
142
|
/** Cached snapshots for tracked objects (for faster notifications) */
|
|
143
143
|
private snapshotCache: Map<GlobalTrackingId, MediaObject> = new Map();
|
|
144
|
+
/** Pending LLM description promises (started when snapshot is captured) */
|
|
145
|
+
private pendingDescriptions: Map<GlobalTrackingId, Promise<SpatialReasoningResult>> = new Map();
|
|
144
146
|
|
|
145
147
|
constructor(
|
|
146
148
|
topology: CameraTopology,
|
|
@@ -555,18 +557,27 @@ export class TrackingEngine {
|
|
|
555
557
|
// Check if we've already alerted for this object
|
|
556
558
|
if (this.isInAlertCooldown(globalId)) return;
|
|
557
559
|
|
|
558
|
-
// Use
|
|
559
|
-
let
|
|
560
|
-
this.
|
|
560
|
+
// Use prefetched LLM result if available (started when snapshot was captured)
|
|
561
|
+
let spatialResult: SpatialReasoningResult;
|
|
562
|
+
const pendingDescription = this.pendingDescriptions.get(globalId);
|
|
561
563
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
564
|
+
if (pendingDescription) {
|
|
565
|
+
this.console.log(`[Entry Alert] Using prefetched LLM result for ${globalId.slice(0, 8)}`);
|
|
566
|
+
try {
|
|
567
|
+
spatialResult = await pendingDescription;
|
|
568
|
+
this.console.log(`[Entry Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
569
|
+
} catch (e) {
|
|
570
|
+
this.console.warn(`[Entry Alert] Prefetch failed, generating fallback: ${e}`);
|
|
571
|
+
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
|
|
572
|
+
}
|
|
573
|
+
this.pendingDescriptions.delete(globalId);
|
|
574
|
+
} else {
|
|
575
|
+
// Fallback: generate description now (slower path)
|
|
576
|
+
this.console.log(`[Entry Alert] No prefetch available, generating now`);
|
|
577
|
+
const mediaObject = this.snapshotCache.get(globalId);
|
|
578
|
+
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId, mediaObject);
|
|
579
|
+
this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
580
|
+
}
|
|
570
581
|
|
|
571
582
|
if (isEntryPoint) {
|
|
572
583
|
// Entry point - generate property entry alert
|
|
@@ -599,8 +610,12 @@ export class TrackingEngine {
|
|
|
599
610
|
}, this.config.loiteringThreshold);
|
|
600
611
|
}
|
|
601
612
|
|
|
602
|
-
/** Capture and cache a snapshot for a tracked object */
|
|
603
|
-
private async captureAndCacheSnapshot(
|
|
613
|
+
/** Capture and cache a snapshot for a tracked object, and start LLM analysis immediately */
|
|
614
|
+
private async captureAndCacheSnapshot(
|
|
615
|
+
globalId: GlobalTrackingId,
|
|
616
|
+
cameraId: string,
|
|
617
|
+
eventType: 'entry' | 'exit' | 'movement' = 'entry'
|
|
618
|
+
): Promise<void> {
|
|
604
619
|
try {
|
|
605
620
|
const camera = systemManager.getDeviceById<Camera>(cameraId);
|
|
606
621
|
if (camera?.interfaces?.includes(ScryptedInterface.Camera)) {
|
|
@@ -608,6 +623,24 @@ export class TrackingEngine {
|
|
|
608
623
|
if (mediaObject) {
|
|
609
624
|
this.snapshotCache.set(globalId, mediaObject);
|
|
610
625
|
this.console.log(`[Snapshot] Cached snapshot for ${globalId.slice(0, 8)} from ${cameraId}`);
|
|
626
|
+
|
|
627
|
+
// Start LLM analysis immediately in parallel (don't await)
|
|
628
|
+
const tracked = this.state.getObject(globalId);
|
|
629
|
+
if (tracked && this.config.useLlmDescriptions) {
|
|
630
|
+
this.console.log(`[LLM Prefetch] Starting ${eventType} analysis for ${globalId.slice(0, 8)}`);
|
|
631
|
+
const descriptionPromise = eventType === 'exit'
|
|
632
|
+
? this.spatialReasoning.generateExitDescription(tracked, cameraId, mediaObject)
|
|
633
|
+
: this.spatialReasoning.generateEntryDescription(tracked, cameraId, mediaObject);
|
|
634
|
+
|
|
635
|
+
this.pendingDescriptions.set(globalId, descriptionPromise);
|
|
636
|
+
|
|
637
|
+
// Log when complete (for debugging)
|
|
638
|
+
descriptionPromise.then(result => {
|
|
639
|
+
this.console.log(`[LLM Prefetch] ${eventType} analysis ready for ${globalId.slice(0, 8)}: "${result.description.substring(0, 40)}..."`);
|
|
640
|
+
}).catch(e => {
|
|
641
|
+
this.console.warn(`[LLM Prefetch] Failed for ${globalId.slice(0, 8)}: ${e}`);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
611
644
|
}
|
|
612
645
|
}
|
|
613
646
|
} catch (e) {
|
|
@@ -663,8 +696,9 @@ export class TrackingEngine {
|
|
|
663
696
|
this.state.markPending(tracked.globalId);
|
|
664
697
|
|
|
665
698
|
// Capture a fresh snapshot now while object is still visible (before they leave)
|
|
699
|
+
// Also starts LLM analysis immediately in parallel
|
|
666
700
|
if (this.config.useLlmDescriptions) {
|
|
667
|
-
this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId).catch(e => {
|
|
701
|
+
this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId, 'exit').catch(e => {
|
|
668
702
|
this.console.warn(`[Exit Snapshot] Failed to update snapshot: ${e}`);
|
|
669
703
|
});
|
|
670
704
|
}
|
|
@@ -675,21 +709,27 @@ export class TrackingEngine {
|
|
|
675
709
|
if (current && current.state === 'pending') {
|
|
676
710
|
this.state.markExited(tracked.globalId, sighting.cameraId, sighting.cameraName);
|
|
677
711
|
|
|
678
|
-
// Use
|
|
679
|
-
let
|
|
680
|
-
this.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
712
|
+
// Use prefetched LLM result if available (started when exit was first detected)
|
|
713
|
+
let spatialResult: SpatialReasoningResult;
|
|
714
|
+
const pendingDescription = this.pendingDescriptions.get(tracked.globalId);
|
|
715
|
+
|
|
716
|
+
if (pendingDescription) {
|
|
717
|
+
this.console.log(`[Exit Alert] Using prefetched LLM result for ${tracked.globalId.slice(0, 8)}`);
|
|
718
|
+
try {
|
|
719
|
+
spatialResult = await pendingDescription;
|
|
720
|
+
this.console.log(`[Exit Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
721
|
+
} catch (e) {
|
|
722
|
+
this.console.warn(`[Exit Alert] Prefetch failed, generating fallback: ${e}`);
|
|
723
|
+
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
|
|
724
|
+
}
|
|
725
|
+
this.pendingDescriptions.delete(tracked.globalId);
|
|
726
|
+
} else {
|
|
727
|
+
// Fallback: generate description now (slower path)
|
|
728
|
+
this.console.log(`[Exit Alert] No prefetch available, generating now`);
|
|
729
|
+
const mediaObject = this.snapshotCache.get(tracked.globalId);
|
|
730
|
+
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId, mediaObject);
|
|
731
|
+
this.console.log(`[Exit Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
732
|
+
}
|
|
693
733
|
|
|
694
734
|
await this.alertManager.checkAndAlert('property_exit', current, {
|
|
695
735
|
cameraId: sighting.cameraId,
|
|
@@ -700,8 +740,9 @@ export class TrackingEngine {
|
|
|
700
740
|
usedLlm: spatialResult.usedLlm,
|
|
701
741
|
});
|
|
702
742
|
|
|
703
|
-
// Clean up cached snapshot after exit alert
|
|
743
|
+
// Clean up cached snapshot and pending descriptions after exit alert
|
|
704
744
|
this.snapshotCache.delete(tracked.globalId);
|
|
745
|
+
this.pendingDescriptions.delete(tracked.globalId);
|
|
705
746
|
}
|
|
706
747
|
this.pendingTimers.delete(tracked.globalId);
|
|
707
748
|
}, this.config.correlationWindow);
|
|
@@ -724,8 +765,9 @@ export class TrackingEngine {
|
|
|
724
765
|
`(not seen for ${Math.round(timeSinceSeen / 1000)}s)`
|
|
725
766
|
);
|
|
726
767
|
|
|
727
|
-
// Clean up cached snapshot
|
|
768
|
+
// Clean up cached snapshot and pending descriptions
|
|
728
769
|
this.snapshotCache.delete(tracked.globalId);
|
|
770
|
+
this.pendingDescriptions.delete(tracked.globalId);
|
|
729
771
|
|
|
730
772
|
this.alertManager.checkAndAlert('lost_tracking', tracked, {
|
|
731
773
|
objectClass: tracked.className,
|
package/src/models/discovery.ts
CHANGED
|
@@ -58,6 +58,21 @@ export interface DiscoveredZone {
|
|
|
58
58
|
boundingBox?: [number, number, number, number];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Distance estimate from camera */
|
|
62
|
+
export type DistanceEstimate = 'close' | 'near' | 'medium' | 'far' | 'distant';
|
|
63
|
+
|
|
64
|
+
/** Convert distance estimate to approximate feet */
|
|
65
|
+
export function distanceToFeet(distance: DistanceEstimate): number {
|
|
66
|
+
switch (distance) {
|
|
67
|
+
case 'close': return 5; // 0-10 feet
|
|
68
|
+
case 'near': return 20; // 10-30 feet
|
|
69
|
+
case 'medium': return 45; // 30-60 feet
|
|
70
|
+
case 'far': return 80; // 60-100 feet
|
|
71
|
+
case 'distant': return 150; // 100+ feet
|
|
72
|
+
default: return 50;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
/** A landmark discovered in a camera view */
|
|
62
77
|
export interface DiscoveredLandmark {
|
|
63
78
|
/** Name of the landmark */
|
|
@@ -66,6 +81,8 @@ export interface DiscoveredLandmark {
|
|
|
66
81
|
type: LandmarkType;
|
|
67
82
|
/** Confidence score from LLM (0-1) */
|
|
68
83
|
confidence: number;
|
|
84
|
+
/** Estimated distance from camera */
|
|
85
|
+
distance?: DistanceEstimate;
|
|
69
86
|
/** Bounding box in normalized coordinates [x, y, width, height] (0-1) */
|
|
70
87
|
boundingBox?: [number, number, number, number];
|
|
71
88
|
/** Description from LLM analysis */
|