@blueharford/scrypted-spatial-awareness 0.4.6 → 0.4.8-beta.1

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.
@@ -516,31 +516,61 @@ export class TrackingEngine {
516
516
  `(ID: ${globalId.slice(0, 8)})`
517
517
  );
518
518
 
519
- // Generate entry alert if this is an entry point
520
- // Entry alerts also respect loitering threshold and cooldown
521
- if (isEntryPoint && this.passesLoiteringThreshold(tracked) && !this.isInAlertCooldown(globalId)) {
522
- // Get spatial reasoning for entry event
523
- const spatialResult = await this.getSpatialDescription(
524
- tracked,
525
- 'outside', // Virtual "outside" location for entry
526
- sighting.cameraId,
527
- 0,
528
- sighting.cameraId
529
- );
519
+ // Schedule loitering check - alert after object passes loitering threshold
520
+ // This ensures we don't miss alerts for brief appearances while still filtering noise
521
+ this.scheduleLoiteringAlert(globalId, sighting, isEntryPoint);
522
+ }
523
+ }
524
+
525
+ /** Schedule an alert after loitering threshold passes */
526
+ private scheduleLoiteringAlert(
527
+ globalId: GlobalTrackingId,
528
+ sighting: ObjectSighting,
529
+ isEntryPoint: boolean
530
+ ): void {
531
+ // Check after loitering threshold if object is still being tracked
532
+ setTimeout(async () => {
533
+ const tracked = this.state.getObject(globalId);
534
+ if (!tracked || tracked.state !== 'active') return;
535
+
536
+ // Check if we've already alerted for this object
537
+ if (this.isInAlertCooldown(globalId)) return;
538
+
539
+ // Generate spatial description
540
+ const spatialResult = this.spatialReasoning.generateEntryDescription(
541
+ tracked,
542
+ sighting.cameraId
543
+ );
530
544
 
545
+ if (isEntryPoint) {
546
+ // Entry point - generate property entry alert
531
547
  await this.alertManager.checkAndAlert('property_entry', tracked, {
532
548
  cameraId: sighting.cameraId,
533
549
  cameraName: sighting.cameraName,
534
550
  objectClass: sighting.detection.className,
535
- objectLabel: spatialResult?.description || sighting.detection.label,
551
+ objectLabel: spatialResult.description,
536
552
  detectionId: sighting.detectionId,
537
- involvedLandmarks: spatialResult?.involvedLandmarks?.map(l => l.name),
538
- usedLlm: spatialResult?.usedLlm,
553
+ involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
554
+ usedLlm: spatialResult.usedLlm,
555
+ });
556
+ } else {
557
+ // Non-entry point - still alert about activity using movement alert type
558
+ // This notifies about any activity around the property using topology context
559
+ await this.alertManager.checkAndAlert('movement', tracked, {
560
+ cameraId: sighting.cameraId,
561
+ cameraName: sighting.cameraName,
562
+ toCameraId: sighting.cameraId,
563
+ toCameraName: sighting.cameraName,
564
+ objectClass: sighting.detection.className,
565
+ objectLabel: spatialResult.description, // Use spatial reasoning description (topology-based)
566
+ detectionId: sighting.detectionId,
567
+ involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
568
+ usedLlm: spatialResult.usedLlm,
539
569
  });
540
-
541
- this.recordAlertTime(globalId);
542
570
  }
543
- }
571
+
572
+ this.recordAlertTime(globalId);
573
+ }, this.config.loiteringThreshold);
544
574
  }
545
575
 
546
576
  /** Attempt to correlate a sighting with existing tracked objects */
@@ -596,15 +626,23 @@ export class TrackingEngine {
596
626
  if (current && current.state === 'pending') {
597
627
  this.state.markExited(tracked.globalId, sighting.cameraId, sighting.cameraName);
598
628
 
629
+ // Generate rich exit description using topology context
630
+ const spatialResult = this.spatialReasoning.generateExitDescription(
631
+ current,
632
+ sighting.cameraId
633
+ );
634
+
599
635
  this.console.log(
600
- `Object ${tracked.globalId.slice(0, 8)} exited via ${sighting.cameraName}`
636
+ `Object ${tracked.globalId.slice(0, 8)} exited: ${spatialResult.description}`
601
637
  );
602
638
 
603
639
  await this.alertManager.checkAndAlert('property_exit', current, {
604
640
  cameraId: sighting.cameraId,
605
641
  cameraName: sighting.cameraName,
606
642
  objectClass: current.className,
607
- objectLabel: current.label,
643
+ objectLabel: spatialResult.description,
644
+ involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
645
+ usedLlm: spatialResult.usedLlm,
608
646
  });
609
647
  }
610
648
  this.pendingTimers.delete(tracked.globalId);
package/src/main.ts CHANGED
@@ -78,8 +78,8 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
78
78
  correlationThreshold: {
79
79
  title: 'Correlation Confidence Threshold',
80
80
  type: 'number',
81
- defaultValue: 0.6,
82
- description: 'Minimum confidence (0-1) for automatic object correlation',
81
+ defaultValue: 0.35,
82
+ description: 'Minimum confidence (0-1) for automatic object correlation. Lower values allow more matches.',
83
83
  group: 'Tracking',
84
84
  },
85
85
  lostTimeout: {
@@ -324,7 +324,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
324
324
 
325
325
  const config: TrackingEngineConfig = {
326
326
  correlationWindow: (this.storageSettings.values.correlationWindow as number || 30) * 1000,
327
- correlationThreshold: this.storageSettings.values.correlationThreshold as number || 0.6,
327
+ correlationThreshold: this.storageSettings.values.correlationThreshold as number || 0.35,
328
328
  lostTimeout: (this.storageSettings.values.lostTimeout as number || 300) * 1000,
329
329
  useVisualMatching: this.storageSettings.values.useVisualMatching as boolean ?? true,
330
330
  loiteringThreshold: (this.storageSettings.values.loiteringThreshold as number || 3) * 1000,
@@ -214,26 +214,53 @@ export function generateAlertMessage(
214
214
 
215
215
  switch (type) {
216
216
  case 'property_entry':
217
- return `${objectDesc} entered property via ${details.cameraName || 'unknown camera'}`;
217
+ // Use the rich description from spatial reasoning if available
218
+ if (details.objectLabel && details.objectLabel !== details.objectClass) {
219
+ return details.objectLabel;
220
+ }
221
+ // Fallback to basic description
222
+ if (details.involvedLandmarks && details.involvedLandmarks.length > 0) {
223
+ return `${objectDesc} entered property near ${details.involvedLandmarks[0]}`;
224
+ }
225
+ return `${objectDesc} entered property at ${details.cameraName || 'entrance'}`;
218
226
  case 'property_exit':
219
- return `${objectDesc} exited property via ${details.cameraName || 'unknown camera'}`;
227
+ // Use the rich description from spatial reasoning if available
228
+ if (details.objectLabel && details.objectLabel !== details.objectClass) {
229
+ return details.objectLabel;
230
+ }
231
+ // Fallback to basic description
232
+ if (details.involvedLandmarks && details.involvedLandmarks.length > 0) {
233
+ return `${objectDesc} left property via ${details.involvedLandmarks[0]}`;
234
+ }
235
+ return `${objectDesc} left property`;
220
236
  case 'movement':
221
- // If we have a rich description from LLM/RAG, use it
222
- if (details.objectLabel && details.usedLlm) {
237
+ // If objectLabel contains a full description, use it directly
238
+ if (details.objectLabel && details.objectLabel !== details.objectClass) {
239
+ // Check if this is a cross-camera movement or initial detection
240
+ if (details.fromCameraId && details.fromCameraId !== details.toCameraId && details.transitTime) {
241
+ const transitSecs = Math.round(details.transitTime / 1000);
242
+ const transitStr = transitSecs > 0 ? ` (${transitSecs}s)` : '';
243
+ const pathContext = details.pathDescription ? ` via ${details.pathDescription}` : '';
244
+ return `${details.objectLabel}${pathContext}${transitStr}`;
245
+ }
246
+ // Initial detection - use the label directly
247
+ return details.objectLabel;
248
+ }
249
+ // Cross-camera movement with basic info
250
+ if (details.fromCameraId && details.fromCameraId !== details.toCameraId) {
223
251
  const transitSecs = details.transitTime ? Math.round(details.transitTime / 1000) : 0;
224
- const transitStr = transitSecs > 0 ? ` (${transitSecs}s)` : '';
225
- // Include path/landmark context if available
226
- const pathContext = details.pathDescription ? ` via ${details.pathDescription}` : '';
227
- return `${details.objectLabel}${pathContext}${transitStr}`;
252
+ const transitStr = transitSecs > 0 ? ` (${transitSecs}s transit)` : '';
253
+ let movementDesc = `${objectDesc} moving from ${details.fromCameraName || 'unknown'} towards ${details.toCameraName || 'unknown'}`;
254
+ if (details.involvedLandmarks && details.involvedLandmarks.length > 0) {
255
+ movementDesc += ` near ${details.involvedLandmarks.join(', ')}`;
256
+ }
257
+ return `${movementDesc}${transitStr}`;
228
258
  }
229
- // Fallback to basic message with landmark info
230
- const transitSecs = details.transitTime ? Math.round(details.transitTime / 1000) : 0;
231
- const transitStr = transitSecs > 0 ? ` (${transitSecs}s transit)` : '';
232
- let movementDesc = `${objectDesc} moving from ${details.fromCameraName || 'unknown'} towards ${details.toCameraName || 'unknown'}`;
259
+ // Initial detection without full label
233
260
  if (details.involvedLandmarks && details.involvedLandmarks.length > 0) {
234
- movementDesc += ` near ${details.involvedLandmarks.join(', ')}`;
261
+ return `${objectDesc} detected near ${details.involvedLandmarks[0]}`;
235
262
  }
236
- return `${movementDesc}${transitStr}`;
263
+ return `${objectDesc} detected at ${details.cameraName || 'camera'}`;
237
264
  case 'unusual_path':
238
265
  return `${objectDesc} took unusual path: ${details.actualPath || 'unknown'}`;
239
266
  case 'dwell_time':