@blueharford/scrypted-spatial-awareness 0.6.11 → 0.6.13

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/out/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueharford/scrypted-spatial-awareness",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "description": "Cross-camera object tracking for Scrypted NVR with spatial awareness",
5
5
  "author": "Joshua Seidel <blueharford>",
6
6
  "license": "Apache-2.0",
@@ -70,10 +70,11 @@ export class AlertManager {
70
70
  }
71
71
 
72
72
  // Create alert
73
+ // Note: details.objectLabel may contain LLM-generated description - preserve it if provided
73
74
  const fullDetails: AlertDetails = {
74
75
  ...details,
75
76
  objectClass: tracked.className,
76
- objectLabel: tracked.label,
77
+ objectLabel: details.objectLabel || tracked.label,
77
78
  };
78
79
 
79
80
  const alert = createAlert(
@@ -138,6 +138,10 @@ export class TrackingEngine {
138
138
  /** Callback for training status updates */
139
139
  private onTrainingStatusUpdate?: (status: TrainingStatusUpdate) => void;
140
140
 
141
+ // ==================== Snapshot Cache ====================
142
+ /** Cached snapshots for tracked objects (for faster notifications) */
143
+ private snapshotCache: Map<GlobalTrackingId, MediaObject> = new Map();
144
+
141
145
  constructor(
142
146
  topology: CameraTopology,
143
147
  state: TrackingState,
@@ -437,6 +441,13 @@ export class TrackingEngine {
437
441
  if (lastSighting && lastSighting.cameraId !== sighting.cameraId) {
438
442
  const transitDuration = sighting.timestamp - lastSighting.timestamp;
439
443
 
444
+ // Update cached snapshot from new camera (object is now visible here)
445
+ if (this.config.useLlmDescriptions) {
446
+ this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId).catch(e => {
447
+ this.console.warn(`[Transition Snapshot] Failed to update snapshot: ${e}`);
448
+ });
449
+ }
450
+
440
451
  // Add journey segment
441
452
  this.state.addJourney(tracked.globalId, {
442
453
  fromCameraId: lastSighting.cameraId,
@@ -528,6 +539,14 @@ export class TrackingEngine {
528
539
  sighting: ObjectSighting,
529
540
  isEntryPoint: boolean
530
541
  ): void {
542
+ // Capture snapshot IMMEDIATELY when object is first detected (don't wait for loitering threshold)
543
+ // This ensures we have a good image while the person/object is still in frame
544
+ if (this.config.useLlmDescriptions) {
545
+ this.captureAndCacheSnapshot(globalId, sighting.cameraId).catch(e => {
546
+ this.console.warn(`[Snapshot] Failed to cache initial snapshot: ${e}`);
547
+ });
548
+ }
549
+
531
550
  // Check after loitering threshold if object is still being tracked
532
551
  setTimeout(async () => {
533
552
  const tracked = this.state.getObject(globalId);
@@ -536,21 +555,9 @@ export class TrackingEngine {
536
555
  // Check if we've already alerted for this object
537
556
  if (this.isInAlertCooldown(globalId)) return;
538
557
 
539
- // Get snapshot for LLM description (if LLM is enabled)
540
- let mediaObject: MediaObject | undefined;
541
- this.console.log(`[Entry Alert] useLlmDescriptions=${this.config.useLlmDescriptions}`);
542
- if (this.config.useLlmDescriptions) {
543
- try {
544
- const camera = systemManager.getDeviceById<Camera>(sighting.cameraId);
545
- this.console.log(`[Entry Alert] Camera ${sighting.cameraId} has Camera interface: ${camera?.interfaces?.includes(ScryptedInterface.Camera)}`);
546
- if (camera?.interfaces?.includes(ScryptedInterface.Camera)) {
547
- mediaObject = await camera.takePicture();
548
- this.console.log(`[Entry Alert] Got snapshot: ${!!mediaObject}`);
549
- }
550
- } catch (e) {
551
- this.console.warn('[Entry Alert] Failed to get snapshot:', e);
552
- }
553
- }
558
+ // Use cached snapshot (captured when object was first detected)
559
+ let mediaObject = this.snapshotCache.get(globalId);
560
+ this.console.log(`[Entry Alert] Using cached snapshot: ${!!mediaObject}`);
554
561
 
555
562
  // Generate spatial description (now async with LLM support)
556
563
  this.console.log(`[Entry Alert] Calling generateEntryDescription with mediaObject=${!!mediaObject}`);
@@ -592,6 +599,22 @@ export class TrackingEngine {
592
599
  }, this.config.loiteringThreshold);
593
600
  }
594
601
 
602
+ /** Capture and cache a snapshot for a tracked object */
603
+ private async captureAndCacheSnapshot(globalId: GlobalTrackingId, cameraId: string): Promise<void> {
604
+ try {
605
+ const camera = systemManager.getDeviceById<Camera>(cameraId);
606
+ if (camera?.interfaces?.includes(ScryptedInterface.Camera)) {
607
+ const mediaObject = await camera.takePicture();
608
+ if (mediaObject) {
609
+ this.snapshotCache.set(globalId, mediaObject);
610
+ this.console.log(`[Snapshot] Cached snapshot for ${globalId.slice(0, 8)} from ${cameraId}`);
611
+ }
612
+ }
613
+ } catch (e) {
614
+ this.console.warn(`[Snapshot] Failed to capture snapshot: ${e}`);
615
+ }
616
+ }
617
+
595
618
  /** Attempt to correlate a sighting with existing tracked objects */
596
619
  private async correlateDetection(
597
620
  sighting: ObjectSighting
@@ -639,27 +662,22 @@ export class TrackingEngine {
639
662
  // Mark as pending and set timer
640
663
  this.state.markPending(tracked.globalId);
641
664
 
665
+ // Capture a fresh snapshot now while object is still visible (before they leave)
666
+ if (this.config.useLlmDescriptions) {
667
+ this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId).catch(e => {
668
+ this.console.warn(`[Exit Snapshot] Failed to update snapshot: ${e}`);
669
+ });
670
+ }
671
+
642
672
  // Wait for correlation window before marking as exited
643
673
  const timer = setTimeout(async () => {
644
674
  const current = this.state.getObject(tracked.globalId);
645
675
  if (current && current.state === 'pending') {
646
676
  this.state.markExited(tracked.globalId, sighting.cameraId, sighting.cameraName);
647
677
 
648
- // Get snapshot for LLM description (if LLM is enabled)
649
- let mediaObject: MediaObject | undefined;
650
- this.console.log(`[Exit Alert] useLlmDescriptions=${this.config.useLlmDescriptions}`);
651
- if (this.config.useLlmDescriptions) {
652
- try {
653
- const camera = systemManager.getDeviceById<Camera>(sighting.cameraId);
654
- this.console.log(`[Exit Alert] Camera ${sighting.cameraId} has Camera interface: ${camera?.interfaces?.includes(ScryptedInterface.Camera)}`);
655
- if (camera?.interfaces?.includes(ScryptedInterface.Camera)) {
656
- mediaObject = await camera.takePicture();
657
- this.console.log(`[Exit Alert] Got snapshot: ${!!mediaObject}`);
658
- }
659
- } catch (e) {
660
- this.console.warn('[Exit Alert] Failed to get snapshot:', e);
661
- }
662
- }
678
+ // Use cached snapshot (captured when exit was first detected, while object was still visible)
679
+ let mediaObject = this.snapshotCache.get(tracked.globalId);
680
+ this.console.log(`[Exit Alert] Using cached snapshot: ${!!mediaObject}`);
663
681
 
664
682
  // Generate rich exit description using topology context (now async with LLM support)
665
683
  this.console.log(`[Exit Alert] Calling generateExitDescription with mediaObject=${!!mediaObject}`);
@@ -681,6 +699,9 @@ export class TrackingEngine {
681
699
  involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
682
700
  usedLlm: spatialResult.usedLlm,
683
701
  });
702
+
703
+ // Clean up cached snapshot after exit alert
704
+ this.snapshotCache.delete(tracked.globalId);
684
705
  }
685
706
  this.pendingTimers.delete(tracked.globalId);
686
707
  }, this.config.correlationWindow);
@@ -703,6 +724,9 @@ export class TrackingEngine {
703
724
  `(not seen for ${Math.round(timeSinceSeen / 1000)}s)`
704
725
  );
705
726
 
727
+ // Clean up cached snapshot
728
+ this.snapshotCache.delete(tracked.globalId);
729
+
706
730
  this.alertManager.checkAndAlert('lost_tracking', tracked, {
707
731
  objectClass: tracked.className,
708
732
  objectLabel: tracked.label,