@blueharford/scrypted-spatial-awareness 0.1.14 → 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 +140 -16
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/alerts/alert-manager.ts +18 -0
- package/src/core/tracking-engine.ts +122 -13
- package/src/main.ts +34 -3
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -34525,6 +34525,25 @@ class AlertManager {
|
|
|
34525
34525
|
*/
|
|
34526
34526
|
getDefaultNotifiers() {
|
|
34527
34527
|
try {
|
|
34528
|
+
// Try new multiple notifiers setting first
|
|
34529
|
+
const notifiers = this.storage.getItem('defaultNotifiers');
|
|
34530
|
+
if (notifiers) {
|
|
34531
|
+
// Could be JSON array or comma-separated string
|
|
34532
|
+
try {
|
|
34533
|
+
const parsed = JSON.parse(notifiers);
|
|
34534
|
+
if (Array.isArray(parsed)) {
|
|
34535
|
+
return parsed;
|
|
34536
|
+
}
|
|
34537
|
+
}
|
|
34538
|
+
catch {
|
|
34539
|
+
// Not JSON, might be comma-separated or single value
|
|
34540
|
+
if (notifiers.includes(',')) {
|
|
34541
|
+
return notifiers.split(',').map(s => s.trim()).filter(Boolean);
|
|
34542
|
+
}
|
|
34543
|
+
return [notifiers];
|
|
34544
|
+
}
|
|
34545
|
+
}
|
|
34546
|
+
// Fallback to old single notifier setting
|
|
34528
34547
|
const defaultNotifier = this.storage.getItem('defaultNotifier');
|
|
34529
34548
|
return defaultNotifier ? [defaultNotifier] : [];
|
|
34530
34549
|
}
|
|
@@ -35051,6 +35070,10 @@ class TrackingEngine {
|
|
|
35051
35070
|
listeners = new Map();
|
|
35052
35071
|
pendingTimers = new Map();
|
|
35053
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;
|
|
35054
35077
|
constructor(topology, state, alertManager, config, console) {
|
|
35055
35078
|
this.topology = topology;
|
|
35056
35079
|
this.state = state;
|
|
@@ -35153,6 +35176,68 @@ class TrackingEngine {
|
|
|
35153
35176
|
await this.processSighting(sighting, camera.isEntryPoint, camera.isExitPoint);
|
|
35154
35177
|
}
|
|
35155
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
|
+
}
|
|
35156
35241
|
/** Process a single sighting */
|
|
35157
35242
|
async processSighting(sighting, isEntryPoint, isExitPoint) {
|
|
35158
35243
|
// Try to correlate with existing tracked objects
|
|
@@ -35178,17 +35263,23 @@ class TrackingEngine {
|
|
|
35178
35263
|
this.console.log(`Object ${tracked.globalId.slice(0, 8)} transited: ` +
|
|
35179
35264
|
`${lastSighting.cameraName} → ${sighting.cameraName} ` +
|
|
35180
35265
|
`(confidence: ${(correlation.confidence * 100).toFixed(0)}%)`);
|
|
35181
|
-
//
|
|
35182
|
-
|
|
35183
|
-
|
|
35184
|
-
|
|
35185
|
-
|
|
35186
|
-
|
|
35187
|
-
|
|
35188
|
-
|
|
35189
|
-
|
|
35190
|
-
|
|
35191
|
-
|
|
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
|
+
}
|
|
35192
35283
|
}
|
|
35193
35284
|
// Add sighting to tracked object
|
|
35194
35285
|
this.state.addSighting(tracked.globalId, sighting);
|
|
@@ -35212,14 +35303,17 @@ class TrackingEngine {
|
|
|
35212
35303
|
this.console.log(`New ${sighting.detection.className} detected on ${sighting.cameraName} ` +
|
|
35213
35304
|
`(ID: ${globalId.slice(0, 8)})`);
|
|
35214
35305
|
// Generate entry alert if this is an entry point
|
|
35215
|
-
|
|
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);
|
|
35216
35309
|
await this.alertManager.checkAndAlert('property_entry', tracked, {
|
|
35217
35310
|
cameraId: sighting.cameraId,
|
|
35218
35311
|
cameraName: sighting.cameraName,
|
|
35219
35312
|
objectClass: sighting.detection.className,
|
|
35220
|
-
objectLabel: sighting.detection.label,
|
|
35313
|
+
objectLabel: llmDescription || sighting.detection.label,
|
|
35221
35314
|
detectionId: sighting.detectionId,
|
|
35222
35315
|
});
|
|
35316
|
+
this.recordAlertTime(globalId);
|
|
35223
35317
|
}
|
|
35224
35318
|
}
|
|
35225
35319
|
}
|
|
@@ -36072,6 +36166,28 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
36072
36166
|
description: 'Use visual embeddings for object correlation (requires compatible detectors)',
|
|
36073
36167
|
group: 'Tracking',
|
|
36074
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
|
+
},
|
|
36075
36191
|
// MQTT Settings
|
|
36076
36192
|
enableMqtt: {
|
|
36077
36193
|
title: 'Enable MQTT',
|
|
@@ -36108,10 +36224,12 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
36108
36224
|
defaultValue: true,
|
|
36109
36225
|
group: 'Alerts',
|
|
36110
36226
|
},
|
|
36111
|
-
|
|
36112
|
-
title: '
|
|
36227
|
+
defaultNotifiers: {
|
|
36228
|
+
title: 'Notifiers',
|
|
36113
36229
|
type: 'device',
|
|
36230
|
+
multiple: true,
|
|
36114
36231
|
deviceFilter: `interfaces.includes('${sdk_1.ScryptedInterface.Notifier}')`,
|
|
36232
|
+
description: 'Select one or more notifiers to receive alerts',
|
|
36115
36233
|
group: 'Alerts',
|
|
36116
36234
|
},
|
|
36117
36235
|
// Tracked Cameras
|
|
@@ -36212,6 +36330,9 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
36212
36330
|
correlationThreshold: this.storageSettings.values.correlationThreshold || 0.6,
|
|
36213
36331
|
lostTimeout: (this.storageSettings.values.lostTimeout || 300) * 1000,
|
|
36214
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,
|
|
36215
36336
|
};
|
|
36216
36337
|
this.trackingEngine = new tracking_engine_1.TrackingEngine(topology, this.trackingState, this.alertManager, config, this.console);
|
|
36217
36338
|
await this.trackingEngine.startTracking();
|
|
@@ -36450,7 +36571,10 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
36450
36571
|
key === 'correlationWindow' ||
|
|
36451
36572
|
key === 'correlationThreshold' ||
|
|
36452
36573
|
key === 'lostTimeout' ||
|
|
36453
|
-
key === 'useVisualMatching'
|
|
36574
|
+
key === 'useVisualMatching' ||
|
|
36575
|
+
key === 'loiteringThreshold' ||
|
|
36576
|
+
key === 'objectAlertCooldown' ||
|
|
36577
|
+
key === 'useLlmDescriptions') {
|
|
36454
36578
|
const topologyJson = this.storage.getItem('topology');
|
|
36455
36579
|
if (topologyJson) {
|
|
36456
36580
|
try {
|