@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/plugin.zip CHANGED
Binary file
@@ -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
- // Generate movement alert for cross-camera transition
35201
- await this.alertManager.checkAndAlert('movement', tracked, {
35202
- fromCameraId: lastSighting.cameraId,
35203
- fromCameraName: lastSighting.cameraName,
35204
- toCameraId: sighting.cameraId,
35205
- toCameraName: sighting.cameraName,
35206
- transitTime: transitDuration,
35207
- objectClass: sighting.detection.className,
35208
- objectLabel: sighting.detection.label,
35209
- detectionId: sighting.detectionId,
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
- if (isEntryPoint) {
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 {