@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/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -36173,59 +36173,140 @@ const topology_1 = __webpack_require__(/*! ../models/topology */ "./src/models/t
|
|
|
36173
36173
|
const spatial_reasoning_1 = __webpack_require__(/*! ./spatial-reasoning */ "./src/core/spatial-reasoning.ts");
|
|
36174
36174
|
const { systemManager } = sdk_1.default;
|
|
36175
36175
|
/** Scene analysis prompt for single camera */
|
|
36176
|
-
const SCENE_ANALYSIS_PROMPT = `
|
|
36176
|
+
const SCENE_ANALYSIS_PROMPT = `You are analyzing a security camera image. Describe EVERYTHING you can see in detail.
|
|
36177
|
+
|
|
36178
|
+
## INSTRUCTIONS
|
|
36179
|
+
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.
|
|
36180
|
+
|
|
36181
|
+
## 1. LANDMARKS - List EVERY distinct object or feature you can see:
|
|
36182
|
+
|
|
36183
|
+
**Structures** (buildings, parts of buildings):
|
|
36184
|
+
- Houses, garages, sheds, porches, decks, patios, carports, gazebos
|
|
36185
|
+
- Walls, pillars, columns, railings, stairs, steps
|
|
36186
|
+
|
|
36187
|
+
**Vegetation** (plants, trees, landscaping):
|
|
36188
|
+
- Trees (describe type if identifiable: oak, palm, pine, etc.)
|
|
36189
|
+
- Bushes, shrubs, hedges
|
|
36190
|
+
- Flower beds, gardens, planters, potted plants
|
|
36191
|
+
- Grass/lawn areas, mulch beds
|
|
36192
|
+
|
|
36193
|
+
**Boundaries & Barriers**:
|
|
36194
|
+
- Fences (wood, chain-link, aluminum, vinyl, iron, privacy)
|
|
36195
|
+
- Walls (brick, stone, concrete, retaining)
|
|
36196
|
+
- Gates, gate posts
|
|
36197
|
+
- Hedges used as boundaries
|
|
36198
|
+
|
|
36199
|
+
**Access Points & Pathways**:
|
|
36200
|
+
- Doors (front, side, garage, screen)
|
|
36201
|
+
- Driveways (concrete, asphalt, gravel, pavers)
|
|
36202
|
+
- Walkways, sidewalks, paths, stepping stones
|
|
36203
|
+
- Stairs, ramps, porches
|
|
36204
|
+
|
|
36205
|
+
**Utility & Fixtures**:
|
|
36206
|
+
- Mailboxes, package boxes
|
|
36207
|
+
- Light fixtures, lamp posts, solar lights
|
|
36208
|
+
- A/C units, utility boxes, meters
|
|
36209
|
+
- Trash cans, recycling bins
|
|
36210
|
+
- Hoses, spigots, sprinklers
|
|
36211
|
+
|
|
36212
|
+
**Outdoor Items**:
|
|
36213
|
+
- Vehicles (cars, trucks, motorcycles, boats, trailers)
|
|
36214
|
+
- Furniture (chairs, tables, benches, swings)
|
|
36215
|
+
- Grills, fire pits, outdoor kitchens
|
|
36216
|
+
- Play equipment, trampolines, pools
|
|
36217
|
+
- Decorations, flags, signs
|
|
36218
|
+
|
|
36219
|
+
**Off-Property Elements** (important for security context):
|
|
36220
|
+
- Street, road, sidewalk
|
|
36221
|
+
- Neighbor's property/fence/house
|
|
36222
|
+
- Public areas visible
|
|
36223
|
+
|
|
36224
|
+
For EACH landmark, estimate its DISTANCE from the camera:
|
|
36225
|
+
- "close" = 0-10 feet (within arm's reach of camera)
|
|
36226
|
+
- "near" = 10-30 feet
|
|
36227
|
+
- "medium" = 30-60 feet
|
|
36228
|
+
- "far" = 60-100 feet
|
|
36229
|
+
- "distant" = 100+ feet (edge of property or beyond)
|
|
36230
|
+
|
|
36231
|
+
## 2. ZONES - Identify distinct AREAS visible:
|
|
36232
|
+
- Front yard, backyard, side yard
|
|
36233
|
+
- Driveway, parking area
|
|
36234
|
+
- Patio, deck, porch
|
|
36235
|
+
- Garden area, lawn
|
|
36236
|
+
- Street/road
|
|
36237
|
+
- Neighbor's yard
|
|
36238
|
+
|
|
36239
|
+
For each zone, estimate what percentage of the image it covers (0.0 to 1.0).
|
|
36240
|
+
|
|
36241
|
+
## 3. EDGES - What's at each edge of the frame:
|
|
36242
|
+
This helps understand what's just out of view.
|
|
36243
|
+
|
|
36244
|
+
## 4. CAMERA CONTEXT:
|
|
36245
|
+
- Estimated mounting height (ground level, 8ft, 12ft, roofline, etc.)
|
|
36246
|
+
- Approximate field of view (narrow, medium, wide)
|
|
36247
|
+
- Facing direction if determinable (north, south, street-facing, etc.)
|
|
36177
36248
|
|
|
36178
|
-
|
|
36179
|
-
- Structures (house, garage, shed, porch, deck)
|
|
36180
|
-
- Features (mailbox, tree, pool, garden, fountain)
|
|
36181
|
-
- Access points (door, gate, driveway entrance, walkway)
|
|
36182
|
-
- Boundaries (fence, wall, hedge)
|
|
36183
|
-
|
|
36184
|
-
2. ZONES - Identify area types visible:
|
|
36185
|
-
- What type of area is this? (front yard, backyard, driveway, street, patio, walkway)
|
|
36186
|
-
- Estimate what percentage of the frame each zone covers (0.0 to 1.0)
|
|
36187
|
-
|
|
36188
|
-
3. EDGES - What's visible at the frame edges:
|
|
36189
|
-
- Top edge: (sky, roof, trees, etc.)
|
|
36190
|
-
- Left edge: (fence, neighbor, street, etc.)
|
|
36191
|
-
- Right edge: (fence, garage, etc.)
|
|
36192
|
-
- Bottom edge: (ground, driveway, grass, etc.)
|
|
36193
|
-
|
|
36194
|
-
4. ORIENTATION - Estimate camera facing direction based on shadows, sun position, or landmarks
|
|
36195
|
-
|
|
36196
|
-
Respond with ONLY valid JSON in this exact format:
|
|
36249
|
+
Respond with ONLY valid JSON:
|
|
36197
36250
|
{
|
|
36198
36251
|
"landmarks": [
|
|
36199
|
-
{"name": "
|
|
36252
|
+
{"name": "Mailbox", "type": "feature", "distance": "medium", "confidence": 0.95, "description": "Black metal mailbox on wooden post, approximately 40 feet from camera"},
|
|
36253
|
+
{"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"},
|
|
36254
|
+
{"name": "Large Oak Tree", "type": "feature", "distance": "far", "confidence": 0.85, "description": "Mature oak tree near property line, roughly 80 feet from camera"}
|
|
36200
36255
|
],
|
|
36201
36256
|
"zones": [
|
|
36202
|
-
{"name": "Front Yard", "type": "yard", "coverage": 0.
|
|
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"}
|
|
36203
36259
|
],
|
|
36204
|
-
"edges": {
|
|
36205
|
-
|
|
36206
|
-
|
|
36260
|
+
"edges": {
|
|
36261
|
+
"top": "sky, tree canopy",
|
|
36262
|
+
"left": "aluminum fence, neighbor's yard beyond",
|
|
36263
|
+
"right": "side of house, garage door",
|
|
36264
|
+
"bottom": "concrete walkway, grass edge"
|
|
36265
|
+
},
|
|
36266
|
+
"cameraContext": {
|
|
36267
|
+
"mountHeight": "8 feet",
|
|
36268
|
+
"fieldOfView": "wide",
|
|
36269
|
+
"facingDirection": "street-facing"
|
|
36270
|
+
}
|
|
36271
|
+
}
|
|
36272
|
+
|
|
36273
|
+
BE THOROUGH. List every distinct item you can identify. A typical outdoor scene should have 5-15+ landmarks.`;
|
|
36207
36274
|
/** Multi-camera correlation prompt */
|
|
36208
|
-
const CORRELATION_PROMPT = `I have scene analyses from multiple security cameras at the same property. Help me
|
|
36275
|
+
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.
|
|
36209
36276
|
|
|
36210
36277
|
CAMERA SCENES:
|
|
36211
36278
|
{scenes}
|
|
36212
36279
|
|
|
36213
|
-
|
|
36214
|
-
|
|
36215
|
-
|
|
36216
|
-
|
|
36280
|
+
## PRIORITY ORDER (most important first):
|
|
36281
|
+
|
|
36282
|
+
### 1. SHARED LANDMARKS (HIGHEST PRIORITY)
|
|
36283
|
+
Identify features that are visible from MULTIPLE cameras. This is crucial for understanding the property layout.
|
|
36284
|
+
- Look for the SAME fence, tree, mailbox, driveway, structure, etc. appearing in different camera views
|
|
36285
|
+
- Even partial visibility counts (e.g., a tree visible in full from one camera and just the edge from another)
|
|
36286
|
+
- Include landmarks that are at the boundary between camera views
|
|
36287
|
+
|
|
36288
|
+
### 2. PROPERTY LAYOUT
|
|
36289
|
+
Based on what each camera sees and their overlapping features, describe:
|
|
36290
|
+
- Which areas each camera covers
|
|
36291
|
+
- How the cameras relate spatially (e.g., "Camera A looks toward Camera B's direction")
|
|
36292
|
+
- Overall property shape and features
|
|
36293
|
+
|
|
36294
|
+
### 3. CONNECTIONS (Lower Priority)
|
|
36295
|
+
Only if clearly determinable, suggest walking paths between camera views.
|
|
36217
36296
|
|
|
36218
36297
|
IMPORTANT: For camera references, use the EXACT device ID shown in parentheses (e.g., "device_123"), NOT the camera name.
|
|
36219
36298
|
|
|
36220
36299
|
Respond with ONLY valid JSON:
|
|
36221
36300
|
{
|
|
36222
36301
|
"sharedLandmarks": [
|
|
36223
|
-
{"name": "
|
|
36302
|
+
{"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"},
|
|
36303
|
+
{"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"},
|
|
36304
|
+
{"name": "Concrete Driveway", "type": "access", "seenByCameras": ["device_123", "device_456", "device_789"], "confidence": 0.95, "description": "Driveway visible from multiple angles"}
|
|
36224
36305
|
],
|
|
36225
36306
|
"connections": [
|
|
36226
|
-
{"from": "device_123", "to": "device_456", "transitSeconds":
|
|
36307
|
+
{"from": "device_123", "to": "device_456", "transitSeconds": 8, "via": "along driveway", "confidence": 0.6, "bidirectional": true}
|
|
36227
36308
|
],
|
|
36228
|
-
"layoutDescription": "
|
|
36309
|
+
"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."
|
|
36229
36310
|
}`;
|
|
36230
36311
|
class TopologyDiscoveryEngine {
|
|
36231
36312
|
config;
|
|
@@ -36429,6 +36510,7 @@ class TopologyDiscoveryEngine {
|
|
|
36429
36510
|
name: l.name || 'Unknown',
|
|
36430
36511
|
type: this.mapLandmarkType(l.type),
|
|
36431
36512
|
confidence: typeof l.confidence === 'number' ? l.confidence : 0.7,
|
|
36513
|
+
distance: this.mapDistance(l.distance),
|
|
36432
36514
|
description: l.description || '',
|
|
36433
36515
|
boundingBox: l.boundingBox,
|
|
36434
36516
|
}));
|
|
@@ -36546,6 +36628,21 @@ class TopologyDiscoveryEngine {
|
|
|
36546
36628
|
return 'west';
|
|
36547
36629
|
return 'unknown';
|
|
36548
36630
|
}
|
|
36631
|
+
/** Map LLM distance to our type */
|
|
36632
|
+
mapDistance(distance) {
|
|
36633
|
+
const dist = distance?.toLowerCase();
|
|
36634
|
+
if (dist?.includes('close'))
|
|
36635
|
+
return 'close';
|
|
36636
|
+
if (dist?.includes('near'))
|
|
36637
|
+
return 'near';
|
|
36638
|
+
if (dist?.includes('medium'))
|
|
36639
|
+
return 'medium';
|
|
36640
|
+
if (dist?.includes('far') && !dist?.includes('distant'))
|
|
36641
|
+
return 'far';
|
|
36642
|
+
if (dist?.includes('distant'))
|
|
36643
|
+
return 'distant';
|
|
36644
|
+
return 'medium'; // Default to medium if not specified
|
|
36645
|
+
}
|
|
36549
36646
|
/** Resolve a camera reference (name or deviceId) to its deviceId */
|
|
36550
36647
|
resolveCameraRef(ref) {
|
|
36551
36648
|
if (!this.topology?.cameras || !ref)
|
|
@@ -36721,9 +36818,12 @@ class TopologyDiscoveryEngine {
|
|
|
36721
36818
|
generateSuggestionsFromAnalysis(analysis) {
|
|
36722
36819
|
if (!analysis.isValid)
|
|
36723
36820
|
return;
|
|
36821
|
+
this.console.log(`[Discovery] Generating suggestions from ${analysis.landmarks.length} landmarks, ${analysis.zones.length} zones`);
|
|
36724
36822
|
// Generate landmark suggestions
|
|
36725
36823
|
for (const landmark of analysis.landmarks) {
|
|
36726
36824
|
if (landmark.confidence >= this.config.minLandmarkConfidence) {
|
|
36825
|
+
// Calculate distance in feet from distance estimate
|
|
36826
|
+
const distanceFeet = landmark.distance ? (0, discovery_1.distanceToFeet)(landmark.distance) : 50;
|
|
36727
36827
|
const suggestion = {
|
|
36728
36828
|
id: `landmark_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
36729
36829
|
type: 'landmark',
|
|
@@ -36736,26 +36836,30 @@ class TopologyDiscoveryEngine {
|
|
|
36736
36836
|
type: landmark.type,
|
|
36737
36837
|
description: landmark.description,
|
|
36738
36838
|
visibleFromCameras: [analysis.cameraId],
|
|
36739
|
-
// Include
|
|
36839
|
+
// Include extra metadata for positioning
|
|
36740
36840
|
boundingBox: landmark.boundingBox,
|
|
36741
|
-
|
|
36841
|
+
distance: landmark.distance,
|
|
36842
|
+
distanceFeet: distanceFeet,
|
|
36843
|
+
}, // Extra metadata not in base Landmark interface
|
|
36742
36844
|
};
|
|
36743
36845
|
this.suggestions.set(suggestion.id, suggestion);
|
|
36846
|
+
this.console.log(`[Discovery] Landmark suggestion: ${landmark.name} (${landmark.type}, ${landmark.distance || 'medium'}, ~${distanceFeet}ft)`);
|
|
36744
36847
|
}
|
|
36745
36848
|
}
|
|
36746
|
-
// Generate zone suggestions
|
|
36849
|
+
// Generate zone suggestions (even for smaller coverage - 10% is enough)
|
|
36747
36850
|
for (const zone of analysis.zones) {
|
|
36748
|
-
if (zone.coverage >= 0.
|
|
36851
|
+
if (zone.coverage >= 0.1) {
|
|
36749
36852
|
const suggestion = {
|
|
36750
36853
|
id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
36751
36854
|
type: 'zone',
|
|
36752
36855
|
timestamp: Date.now(),
|
|
36753
36856
|
sourceCameras: [analysis.cameraId],
|
|
36754
|
-
confidence: 0.
|
|
36857
|
+
confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
|
|
36755
36858
|
status: 'pending',
|
|
36756
36859
|
zone: zone,
|
|
36757
36860
|
};
|
|
36758
36861
|
this.suggestions.set(suggestion.id, suggestion);
|
|
36862
|
+
this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage)`);
|
|
36759
36863
|
}
|
|
36760
36864
|
}
|
|
36761
36865
|
}
|
|
@@ -36946,6 +37050,8 @@ class TrackingEngine {
|
|
|
36946
37050
|
// ==================== Snapshot Cache ====================
|
|
36947
37051
|
/** Cached snapshots for tracked objects (for faster notifications) */
|
|
36948
37052
|
snapshotCache = new Map();
|
|
37053
|
+
/** Pending LLM description promises (started when snapshot is captured) */
|
|
37054
|
+
pendingDescriptions = new Map();
|
|
36949
37055
|
constructor(topology, state, alertManager, config, console) {
|
|
36950
37056
|
this.topology = topology;
|
|
36951
37057
|
this.state = state;
|
|
@@ -37256,13 +37362,28 @@ class TrackingEngine {
|
|
|
37256
37362
|
// Check if we've already alerted for this object
|
|
37257
37363
|
if (this.isInAlertCooldown(globalId))
|
|
37258
37364
|
return;
|
|
37259
|
-
// Use
|
|
37260
|
-
let
|
|
37261
|
-
this.
|
|
37262
|
-
|
|
37263
|
-
|
|
37264
|
-
|
|
37265
|
-
|
|
37365
|
+
// Use prefetched LLM result if available (started when snapshot was captured)
|
|
37366
|
+
let spatialResult;
|
|
37367
|
+
const pendingDescription = this.pendingDescriptions.get(globalId);
|
|
37368
|
+
if (pendingDescription) {
|
|
37369
|
+
this.console.log(`[Entry Alert] Using prefetched LLM result for ${globalId.slice(0, 8)}`);
|
|
37370
|
+
try {
|
|
37371
|
+
spatialResult = await pendingDescription;
|
|
37372
|
+
this.console.log(`[Entry Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
37373
|
+
}
|
|
37374
|
+
catch (e) {
|
|
37375
|
+
this.console.warn(`[Entry Alert] Prefetch failed, generating fallback: ${e}`);
|
|
37376
|
+
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
|
|
37377
|
+
}
|
|
37378
|
+
this.pendingDescriptions.delete(globalId);
|
|
37379
|
+
}
|
|
37380
|
+
else {
|
|
37381
|
+
// Fallback: generate description now (slower path)
|
|
37382
|
+
this.console.log(`[Entry Alert] No prefetch available, generating now`);
|
|
37383
|
+
const mediaObject = this.snapshotCache.get(globalId);
|
|
37384
|
+
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId, mediaObject);
|
|
37385
|
+
this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
37386
|
+
}
|
|
37266
37387
|
if (isEntryPoint) {
|
|
37267
37388
|
// Entry point - generate property entry alert
|
|
37268
37389
|
await this.alertManager.checkAndAlert('property_entry', tracked, {
|
|
@@ -37293,8 +37414,8 @@ class TrackingEngine {
|
|
|
37293
37414
|
this.recordAlertTime(globalId);
|
|
37294
37415
|
}, this.config.loiteringThreshold);
|
|
37295
37416
|
}
|
|
37296
|
-
/** Capture and cache a snapshot for a tracked object */
|
|
37297
|
-
async captureAndCacheSnapshot(globalId, cameraId) {
|
|
37417
|
+
/** Capture and cache a snapshot for a tracked object, and start LLM analysis immediately */
|
|
37418
|
+
async captureAndCacheSnapshot(globalId, cameraId, eventType = 'entry') {
|
|
37298
37419
|
try {
|
|
37299
37420
|
const camera = systemManager.getDeviceById(cameraId);
|
|
37300
37421
|
if (camera?.interfaces?.includes(sdk_1.ScryptedInterface.Camera)) {
|
|
@@ -37302,6 +37423,21 @@ class TrackingEngine {
|
|
|
37302
37423
|
if (mediaObject) {
|
|
37303
37424
|
this.snapshotCache.set(globalId, mediaObject);
|
|
37304
37425
|
this.console.log(`[Snapshot] Cached snapshot for ${globalId.slice(0, 8)} from ${cameraId}`);
|
|
37426
|
+
// Start LLM analysis immediately in parallel (don't await)
|
|
37427
|
+
const tracked = this.state.getObject(globalId);
|
|
37428
|
+
if (tracked && this.config.useLlmDescriptions) {
|
|
37429
|
+
this.console.log(`[LLM Prefetch] Starting ${eventType} analysis for ${globalId.slice(0, 8)}`);
|
|
37430
|
+
const descriptionPromise = eventType === 'exit'
|
|
37431
|
+
? this.spatialReasoning.generateExitDescription(tracked, cameraId, mediaObject)
|
|
37432
|
+
: this.spatialReasoning.generateEntryDescription(tracked, cameraId, mediaObject);
|
|
37433
|
+
this.pendingDescriptions.set(globalId, descriptionPromise);
|
|
37434
|
+
// Log when complete (for debugging)
|
|
37435
|
+
descriptionPromise.then(result => {
|
|
37436
|
+
this.console.log(`[LLM Prefetch] ${eventType} analysis ready for ${globalId.slice(0, 8)}: "${result.description.substring(0, 40)}..."`);
|
|
37437
|
+
}).catch(e => {
|
|
37438
|
+
this.console.warn(`[LLM Prefetch] Failed for ${globalId.slice(0, 8)}: ${e}`);
|
|
37439
|
+
});
|
|
37440
|
+
}
|
|
37305
37441
|
}
|
|
37306
37442
|
}
|
|
37307
37443
|
}
|
|
@@ -37349,8 +37485,9 @@ class TrackingEngine {
|
|
|
37349
37485
|
// Mark as pending and set timer
|
|
37350
37486
|
this.state.markPending(tracked.globalId);
|
|
37351
37487
|
// Capture a fresh snapshot now while object is still visible (before they leave)
|
|
37488
|
+
// Also starts LLM analysis immediately in parallel
|
|
37352
37489
|
if (this.config.useLlmDescriptions) {
|
|
37353
|
-
this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId).catch(e => {
|
|
37490
|
+
this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId, 'exit').catch(e => {
|
|
37354
37491
|
this.console.warn(`[Exit Snapshot] Failed to update snapshot: ${e}`);
|
|
37355
37492
|
});
|
|
37356
37493
|
}
|
|
@@ -37359,13 +37496,28 @@ class TrackingEngine {
|
|
|
37359
37496
|
const current = this.state.getObject(tracked.globalId);
|
|
37360
37497
|
if (current && current.state === 'pending') {
|
|
37361
37498
|
this.state.markExited(tracked.globalId, sighting.cameraId, sighting.cameraName);
|
|
37362
|
-
// Use
|
|
37363
|
-
let
|
|
37364
|
-
this.
|
|
37365
|
-
|
|
37366
|
-
|
|
37367
|
-
|
|
37368
|
-
|
|
37499
|
+
// Use prefetched LLM result if available (started when exit was first detected)
|
|
37500
|
+
let spatialResult;
|
|
37501
|
+
const pendingDescription = this.pendingDescriptions.get(tracked.globalId);
|
|
37502
|
+
if (pendingDescription) {
|
|
37503
|
+
this.console.log(`[Exit Alert] Using prefetched LLM result for ${tracked.globalId.slice(0, 8)}`);
|
|
37504
|
+
try {
|
|
37505
|
+
spatialResult = await pendingDescription;
|
|
37506
|
+
this.console.log(`[Exit Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
37507
|
+
}
|
|
37508
|
+
catch (e) {
|
|
37509
|
+
this.console.warn(`[Exit Alert] Prefetch failed, generating fallback: ${e}`);
|
|
37510
|
+
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
|
|
37511
|
+
}
|
|
37512
|
+
this.pendingDescriptions.delete(tracked.globalId);
|
|
37513
|
+
}
|
|
37514
|
+
else {
|
|
37515
|
+
// Fallback: generate description now (slower path)
|
|
37516
|
+
this.console.log(`[Exit Alert] No prefetch available, generating now`);
|
|
37517
|
+
const mediaObject = this.snapshotCache.get(tracked.globalId);
|
|
37518
|
+
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId, mediaObject);
|
|
37519
|
+
this.console.log(`[Exit Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
37520
|
+
}
|
|
37369
37521
|
await this.alertManager.checkAndAlert('property_exit', current, {
|
|
37370
37522
|
cameraId: sighting.cameraId,
|
|
37371
37523
|
cameraName: sighting.cameraName,
|
|
@@ -37374,8 +37526,9 @@ class TrackingEngine {
|
|
|
37374
37526
|
involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
|
|
37375
37527
|
usedLlm: spatialResult.usedLlm,
|
|
37376
37528
|
});
|
|
37377
|
-
// Clean up cached snapshot after exit alert
|
|
37529
|
+
// Clean up cached snapshot and pending descriptions after exit alert
|
|
37378
37530
|
this.snapshotCache.delete(tracked.globalId);
|
|
37531
|
+
this.pendingDescriptions.delete(tracked.globalId);
|
|
37379
37532
|
}
|
|
37380
37533
|
this.pendingTimers.delete(tracked.globalId);
|
|
37381
37534
|
}, this.config.correlationWindow);
|
|
@@ -37391,8 +37544,9 @@ class TrackingEngine {
|
|
|
37391
37544
|
this.state.markLost(tracked.globalId);
|
|
37392
37545
|
this.console.log(`Object ${tracked.globalId.slice(0, 8)} marked as lost ` +
|
|
37393
37546
|
`(not seen for ${Math.round(timeSinceSeen / 1000)}s)`);
|
|
37394
|
-
// Clean up cached snapshot
|
|
37547
|
+
// Clean up cached snapshot and pending descriptions
|
|
37395
37548
|
this.snapshotCache.delete(tracked.globalId);
|
|
37549
|
+
this.pendingDescriptions.delete(tracked.globalId);
|
|
37396
37550
|
this.alertManager.checkAndAlert('lost_tracking', tracked, {
|
|
37397
37551
|
objectClass: tracked.className,
|
|
37398
37552
|
objectLabel: tracked.label,
|
|
@@ -41107,6 +41261,7 @@ function createAlert(type, trackedObjectId, details, severity = 'info', ruleId)
|
|
|
41107
41261
|
*/
|
|
41108
41262
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
41109
41263
|
exports.DEFAULT_DISCOVERY_STATUS = exports.RATE_LIMIT_WARNING_THRESHOLD = exports.DEFAULT_DISCOVERY_CONFIG = void 0;
|
|
41264
|
+
exports.distanceToFeet = distanceToFeet;
|
|
41110
41265
|
/** Default discovery configuration */
|
|
41111
41266
|
exports.DEFAULT_DISCOVERY_CONFIG = {
|
|
41112
41267
|
discoveryIntervalHours: 0, // Disabled by default
|
|
@@ -41116,6 +41271,17 @@ exports.DEFAULT_DISCOVERY_CONFIG = {
|
|
|
41116
41271
|
};
|
|
41117
41272
|
/** Rate limit warning thresholds (in hours) */
|
|
41118
41273
|
exports.RATE_LIMIT_WARNING_THRESHOLD = 1; // Warn if interval is less than 1 hour
|
|
41274
|
+
/** Convert distance estimate to approximate feet */
|
|
41275
|
+
function distanceToFeet(distance) {
|
|
41276
|
+
switch (distance) {
|
|
41277
|
+
case 'close': return 5; // 0-10 feet
|
|
41278
|
+
case 'near': return 20; // 10-30 feet
|
|
41279
|
+
case 'medium': return 45; // 30-60 feet
|
|
41280
|
+
case 'far': return 80; // 60-100 feet
|
|
41281
|
+
case 'distant': return 150; // 100+ feet
|
|
41282
|
+
default: return 50;
|
|
41283
|
+
}
|
|
41284
|
+
}
|
|
41119
41285
|
/** Default discovery status */
|
|
41120
41286
|
exports.DEFAULT_DISCOVERY_STATUS = {
|
|
41121
41287
|
isRunning: false,
|