@blueharford/scrypted-spatial-awareness 0.1.15 → 0.1.16
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 +117 -14
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/tracking-engine.ts +122 -13
- package/src/main.ts +30 -1
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -35070,6 +35070,10 @@ class TrackingEngine {
|
|
|
35070
35070
|
listeners = new Map();
|
|
35071
35071
|
pendingTimers = new Map();
|
|
35072
35072
|
lostCheckInterval = null;
|
|
35073
|
+
/** Track last alert time per object to enforce cooldown */
|
|
35074
|
+
objectLastAlertTime = new Map();
|
|
35075
|
+
/** Cache for LLM device reference */
|
|
35076
|
+
llmDevice = null;
|
|
35073
35077
|
constructor(topology, state, alertManager, config, console) {
|
|
35074
35078
|
this.topology = topology;
|
|
35075
35079
|
this.state = state;
|
|
@@ -35172,6 +35176,68 @@ class TrackingEngine {
|
|
|
35172
35176
|
await this.processSighting(sighting, camera.isEntryPoint, camera.isExitPoint);
|
|
35173
35177
|
}
|
|
35174
35178
|
}
|
|
35179
|
+
/** Check if object passes loitering threshold */
|
|
35180
|
+
passesLoiteringThreshold(tracked) {
|
|
35181
|
+
const visibleDuration = tracked.lastSeen - tracked.firstSeen;
|
|
35182
|
+
return visibleDuration >= this.config.loiteringThreshold;
|
|
35183
|
+
}
|
|
35184
|
+
/** Check if object is in alert cooldown */
|
|
35185
|
+
isInAlertCooldown(globalId) {
|
|
35186
|
+
const lastAlertTime = this.objectLastAlertTime.get(globalId);
|
|
35187
|
+
if (!lastAlertTime)
|
|
35188
|
+
return false;
|
|
35189
|
+
return (Date.now() - lastAlertTime) < this.config.objectAlertCooldown;
|
|
35190
|
+
}
|
|
35191
|
+
/** Record that we alerted for this object */
|
|
35192
|
+
recordAlertTime(globalId) {
|
|
35193
|
+
this.objectLastAlertTime.set(globalId, Date.now());
|
|
35194
|
+
}
|
|
35195
|
+
/** Try to get LLM-enhanced description for movement */
|
|
35196
|
+
async getLlmDescription(tracked, fromCamera, toCamera, cameraId) {
|
|
35197
|
+
if (!this.config.useLlmDescriptions)
|
|
35198
|
+
return null;
|
|
35199
|
+
try {
|
|
35200
|
+
// Find LLM plugin device if not cached
|
|
35201
|
+
if (!this.llmDevice) {
|
|
35202
|
+
for (const id of Object.keys(systemManager.getSystemState())) {
|
|
35203
|
+
const device = systemManager.getDeviceById(id);
|
|
35204
|
+
if (device?.interfaces?.includes(sdk_1.ScryptedInterface.ObjectDetection) &&
|
|
35205
|
+
device.name?.toLowerCase().includes('llm')) {
|
|
35206
|
+
this.llmDevice = device;
|
|
35207
|
+
this.console.log(`Found LLM device: ${device.name}`);
|
|
35208
|
+
break;
|
|
35209
|
+
}
|
|
35210
|
+
}
|
|
35211
|
+
}
|
|
35212
|
+
if (!this.llmDevice)
|
|
35213
|
+
return null;
|
|
35214
|
+
// Get snapshot from camera for LLM analysis
|
|
35215
|
+
const camera = systemManager.getDeviceById(cameraId);
|
|
35216
|
+
if (!camera?.interfaces?.includes(sdk_1.ScryptedInterface.Camera))
|
|
35217
|
+
return null;
|
|
35218
|
+
const picture = await camera.takePicture();
|
|
35219
|
+
if (!picture)
|
|
35220
|
+
return null;
|
|
35221
|
+
// Ask LLM to describe the movement
|
|
35222
|
+
const prompt = `Describe this ${tracked.className} in one short sentence. ` +
|
|
35223
|
+
`They are moving from the ${fromCamera} area towards the ${toCamera}. ` +
|
|
35224
|
+
`Include details like: gender (man/woman), clothing color, vehicle color/type if applicable. ` +
|
|
35225
|
+
`Example: "Man in blue jacket walking from garage towards front door" or ` +
|
|
35226
|
+
`"Black SUV driving from driveway towards street"`;
|
|
35227
|
+
const result = await this.llmDevice.detectObjects(picture, {
|
|
35228
|
+
settings: { prompt }
|
|
35229
|
+
});
|
|
35230
|
+
// Extract description from LLM response
|
|
35231
|
+
if (result.detections?.[0]?.label) {
|
|
35232
|
+
return result.detections[0].label;
|
|
35233
|
+
}
|
|
35234
|
+
return null;
|
|
35235
|
+
}
|
|
35236
|
+
catch (e) {
|
|
35237
|
+
this.console.warn('LLM description failed:', e);
|
|
35238
|
+
return null;
|
|
35239
|
+
}
|
|
35240
|
+
}
|
|
35175
35241
|
/** Process a single sighting */
|
|
35176
35242
|
async processSighting(sighting, isEntryPoint, isExitPoint) {
|
|
35177
35243
|
// Try to correlate with existing tracked objects
|
|
@@ -35197,17 +35263,23 @@ class TrackingEngine {
|
|
|
35197
35263
|
this.console.log(`Object ${tracked.globalId.slice(0, 8)} transited: ` +
|
|
35198
35264
|
`${lastSighting.cameraName} → ${sighting.cameraName} ` +
|
|
35199
35265
|
`(confidence: ${(correlation.confidence * 100).toFixed(0)}%)`);
|
|
35200
|
-
//
|
|
35201
|
-
|
|
35202
|
-
|
|
35203
|
-
|
|
35204
|
-
|
|
35205
|
-
|
|
35206
|
-
|
|
35207
|
-
|
|
35208
|
-
|
|
35209
|
-
|
|
35210
|
-
|
|
35266
|
+
// Check loitering threshold and per-object cooldown before alerting
|
|
35267
|
+
if (this.passesLoiteringThreshold(tracked) && !this.isInAlertCooldown(tracked.globalId)) {
|
|
35268
|
+
// Try to get LLM-enhanced description
|
|
35269
|
+
const llmDescription = await this.getLlmDescription(tracked, lastSighting.cameraName, sighting.cameraName, sighting.cameraId);
|
|
35270
|
+
// Generate movement alert for cross-camera transition
|
|
35271
|
+
await this.alertManager.checkAndAlert('movement', tracked, {
|
|
35272
|
+
fromCameraId: lastSighting.cameraId,
|
|
35273
|
+
fromCameraName: lastSighting.cameraName,
|
|
35274
|
+
toCameraId: sighting.cameraId,
|
|
35275
|
+
toCameraName: sighting.cameraName,
|
|
35276
|
+
transitTime: transitDuration,
|
|
35277
|
+
objectClass: sighting.detection.className,
|
|
35278
|
+
objectLabel: llmDescription || sighting.detection.label,
|
|
35279
|
+
detectionId: sighting.detectionId,
|
|
35280
|
+
});
|
|
35281
|
+
this.recordAlertTime(tracked.globalId);
|
|
35282
|
+
}
|
|
35211
35283
|
}
|
|
35212
35284
|
// Add sighting to tracked object
|
|
35213
35285
|
this.state.addSighting(tracked.globalId, sighting);
|
|
@@ -35231,14 +35303,17 @@ class TrackingEngine {
|
|
|
35231
35303
|
this.console.log(`New ${sighting.detection.className} detected on ${sighting.cameraName} ` +
|
|
35232
35304
|
`(ID: ${globalId.slice(0, 8)})`);
|
|
35233
35305
|
// Generate entry alert if this is an entry point
|
|
35234
|
-
|
|
35306
|
+
// Entry alerts also respect loitering threshold and cooldown
|
|
35307
|
+
if (isEntryPoint && this.passesLoiteringThreshold(tracked) && !this.isInAlertCooldown(globalId)) {
|
|
35308
|
+
const llmDescription = await this.getLlmDescription(tracked, 'outside', sighting.cameraName, sighting.cameraId);
|
|
35235
35309
|
await this.alertManager.checkAndAlert('property_entry', tracked, {
|
|
35236
35310
|
cameraId: sighting.cameraId,
|
|
35237
35311
|
cameraName: sighting.cameraName,
|
|
35238
35312
|
objectClass: sighting.detection.className,
|
|
35239
|
-
objectLabel: sighting.detection.label,
|
|
35313
|
+
objectLabel: llmDescription || sighting.detection.label,
|
|
35240
35314
|
detectionId: sighting.detectionId,
|
|
35241
35315
|
});
|
|
35316
|
+
this.recordAlertTime(globalId);
|
|
35242
35317
|
}
|
|
35243
35318
|
}
|
|
35244
35319
|
}
|
|
@@ -36091,6 +36166,28 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
36091
36166
|
description: 'Use visual embeddings for object correlation (requires compatible detectors)',
|
|
36092
36167
|
group: 'Tracking',
|
|
36093
36168
|
},
|
|
36169
|
+
loiteringThreshold: {
|
|
36170
|
+
title: 'Loitering Threshold (seconds)',
|
|
36171
|
+
type: 'number',
|
|
36172
|
+
defaultValue: 3,
|
|
36173
|
+
description: 'Object must be visible for this duration before triggering movement alerts',
|
|
36174
|
+
group: 'Tracking',
|
|
36175
|
+
},
|
|
36176
|
+
objectAlertCooldown: {
|
|
36177
|
+
title: 'Per-Object Alert Cooldown (seconds)',
|
|
36178
|
+
type: 'number',
|
|
36179
|
+
defaultValue: 30,
|
|
36180
|
+
description: 'Minimum time between alerts for the same tracked object',
|
|
36181
|
+
group: 'Tracking',
|
|
36182
|
+
},
|
|
36183
|
+
// LLM Integration
|
|
36184
|
+
useLlmDescriptions: {
|
|
36185
|
+
title: 'Use LLM for Rich Descriptions',
|
|
36186
|
+
type: 'boolean',
|
|
36187
|
+
defaultValue: true,
|
|
36188
|
+
description: 'Use LLM plugin (if installed) to generate descriptive alerts like "Man walking from garage towards front door"',
|
|
36189
|
+
group: 'Tracking',
|
|
36190
|
+
},
|
|
36094
36191
|
// MQTT Settings
|
|
36095
36192
|
enableMqtt: {
|
|
36096
36193
|
title: 'Enable MQTT',
|
|
@@ -36233,6 +36330,9 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
36233
36330
|
correlationThreshold: this.storageSettings.values.correlationThreshold || 0.6,
|
|
36234
36331
|
lostTimeout: (this.storageSettings.values.lostTimeout || 300) * 1000,
|
|
36235
36332
|
useVisualMatching: this.storageSettings.values.useVisualMatching ?? true,
|
|
36333
|
+
loiteringThreshold: (this.storageSettings.values.loiteringThreshold || 3) * 1000,
|
|
36334
|
+
objectAlertCooldown: (this.storageSettings.values.objectAlertCooldown || 30) * 1000,
|
|
36335
|
+
useLlmDescriptions: this.storageSettings.values.useLlmDescriptions ?? true,
|
|
36236
36336
|
};
|
|
36237
36337
|
this.trackingEngine = new tracking_engine_1.TrackingEngine(topology, this.trackingState, this.alertManager, config, this.console);
|
|
36238
36338
|
await this.trackingEngine.startTracking();
|
|
@@ -36471,7 +36571,10 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
36471
36571
|
key === 'correlationWindow' ||
|
|
36472
36572
|
key === 'correlationThreshold' ||
|
|
36473
36573
|
key === 'lostTimeout' ||
|
|
36474
|
-
key === 'useVisualMatching'
|
|
36574
|
+
key === 'useVisualMatching' ||
|
|
36575
|
+
key === 'loiteringThreshold' ||
|
|
36576
|
+
key === 'objectAlertCooldown' ||
|
|
36577
|
+
key === 'useLlmDescriptions') {
|
|
36475
36578
|
const topologyJson = this.storage.getItem('topology');
|
|
36476
36579
|
if (topologyJson) {
|
|
36477
36580
|
try {
|