@blueharford/scrypted-spatial-awareness 0.6.13 → 0.6.14

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.13",
3
+ "version": "0.6.14",
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",
@@ -141,6 +141,8 @@ export class TrackingEngine {
141
141
  // ==================== Snapshot Cache ====================
142
142
  /** Cached snapshots for tracked objects (for faster notifications) */
143
143
  private snapshotCache: Map<GlobalTrackingId, MediaObject> = new Map();
144
+ /** Pending LLM description promises (started when snapshot is captured) */
145
+ private pendingDescriptions: Map<GlobalTrackingId, Promise<SpatialReasoningResult>> = new Map();
144
146
 
145
147
  constructor(
146
148
  topology: CameraTopology,
@@ -555,18 +557,27 @@ export class TrackingEngine {
555
557
  // Check if we've already alerted for this object
556
558
  if (this.isInAlertCooldown(globalId)) return;
557
559
 
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}`);
560
+ // Use prefetched LLM result if available (started when snapshot was captured)
561
+ let spatialResult: SpatialReasoningResult;
562
+ const pendingDescription = this.pendingDescriptions.get(globalId);
561
563
 
562
- // Generate spatial description (now async with LLM support)
563
- this.console.log(`[Entry Alert] Calling generateEntryDescription with mediaObject=${!!mediaObject}`);
564
- const spatialResult = await this.spatialReasoning.generateEntryDescription(
565
- tracked,
566
- sighting.cameraId,
567
- mediaObject
568
- );
569
- this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
564
+ if (pendingDescription) {
565
+ this.console.log(`[Entry Alert] Using prefetched LLM result for ${globalId.slice(0, 8)}`);
566
+ try {
567
+ spatialResult = await pendingDescription;
568
+ this.console.log(`[Entry Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
569
+ } catch (e) {
570
+ this.console.warn(`[Entry Alert] Prefetch failed, generating fallback: ${e}`);
571
+ spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
572
+ }
573
+ this.pendingDescriptions.delete(globalId);
574
+ } else {
575
+ // Fallback: generate description now (slower path)
576
+ this.console.log(`[Entry Alert] No prefetch available, generating now`);
577
+ const mediaObject = this.snapshotCache.get(globalId);
578
+ spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId, mediaObject);
579
+ this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
580
+ }
570
581
 
571
582
  if (isEntryPoint) {
572
583
  // Entry point - generate property entry alert
@@ -599,8 +610,12 @@ export class TrackingEngine {
599
610
  }, this.config.loiteringThreshold);
600
611
  }
601
612
 
602
- /** Capture and cache a snapshot for a tracked object */
603
- private async captureAndCacheSnapshot(globalId: GlobalTrackingId, cameraId: string): Promise<void> {
613
+ /** Capture and cache a snapshot for a tracked object, and start LLM analysis immediately */
614
+ private async captureAndCacheSnapshot(
615
+ globalId: GlobalTrackingId,
616
+ cameraId: string,
617
+ eventType: 'entry' | 'exit' | 'movement' = 'entry'
618
+ ): Promise<void> {
604
619
  try {
605
620
  const camera = systemManager.getDeviceById<Camera>(cameraId);
606
621
  if (camera?.interfaces?.includes(ScryptedInterface.Camera)) {
@@ -608,6 +623,24 @@ export class TrackingEngine {
608
623
  if (mediaObject) {
609
624
  this.snapshotCache.set(globalId, mediaObject);
610
625
  this.console.log(`[Snapshot] Cached snapshot for ${globalId.slice(0, 8)} from ${cameraId}`);
626
+
627
+ // Start LLM analysis immediately in parallel (don't await)
628
+ const tracked = this.state.getObject(globalId);
629
+ if (tracked && this.config.useLlmDescriptions) {
630
+ this.console.log(`[LLM Prefetch] Starting ${eventType} analysis for ${globalId.slice(0, 8)}`);
631
+ const descriptionPromise = eventType === 'exit'
632
+ ? this.spatialReasoning.generateExitDescription(tracked, cameraId, mediaObject)
633
+ : this.spatialReasoning.generateEntryDescription(tracked, cameraId, mediaObject);
634
+
635
+ this.pendingDescriptions.set(globalId, descriptionPromise);
636
+
637
+ // Log when complete (for debugging)
638
+ descriptionPromise.then(result => {
639
+ this.console.log(`[LLM Prefetch] ${eventType} analysis ready for ${globalId.slice(0, 8)}: "${result.description.substring(0, 40)}..."`);
640
+ }).catch(e => {
641
+ this.console.warn(`[LLM Prefetch] Failed for ${globalId.slice(0, 8)}: ${e}`);
642
+ });
643
+ }
611
644
  }
612
645
  }
613
646
  } catch (e) {
@@ -663,8 +696,9 @@ export class TrackingEngine {
663
696
  this.state.markPending(tracked.globalId);
664
697
 
665
698
  // Capture a fresh snapshot now while object is still visible (before they leave)
699
+ // Also starts LLM analysis immediately in parallel
666
700
  if (this.config.useLlmDescriptions) {
667
- this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId).catch(e => {
701
+ this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId, 'exit').catch(e => {
668
702
  this.console.warn(`[Exit Snapshot] Failed to update snapshot: ${e}`);
669
703
  });
670
704
  }
@@ -675,21 +709,27 @@ export class TrackingEngine {
675
709
  if (current && current.state === 'pending') {
676
710
  this.state.markExited(tracked.globalId, sighting.cameraId, sighting.cameraName);
677
711
 
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}`);
681
-
682
- // Generate rich exit description using topology context (now async with LLM support)
683
- this.console.log(`[Exit Alert] Calling generateExitDescription with mediaObject=${!!mediaObject}`);
684
- const spatialResult = await this.spatialReasoning.generateExitDescription(
685
- current,
686
- sighting.cameraId,
687
- mediaObject
688
- );
689
-
690
- this.console.log(
691
- `[Exit Alert] Object ${tracked.globalId.slice(0, 8)} exited: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`
692
- );
712
+ // Use prefetched LLM result if available (started when exit was first detected)
713
+ let spatialResult: SpatialReasoningResult;
714
+ const pendingDescription = this.pendingDescriptions.get(tracked.globalId);
715
+
716
+ if (pendingDescription) {
717
+ this.console.log(`[Exit Alert] Using prefetched LLM result for ${tracked.globalId.slice(0, 8)}`);
718
+ try {
719
+ spatialResult = await pendingDescription;
720
+ this.console.log(`[Exit Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
721
+ } catch (e) {
722
+ this.console.warn(`[Exit Alert] Prefetch failed, generating fallback: ${e}`);
723
+ spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
724
+ }
725
+ this.pendingDescriptions.delete(tracked.globalId);
726
+ } else {
727
+ // Fallback: generate description now (slower path)
728
+ this.console.log(`[Exit Alert] No prefetch available, generating now`);
729
+ const mediaObject = this.snapshotCache.get(tracked.globalId);
730
+ spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId, mediaObject);
731
+ this.console.log(`[Exit Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
732
+ }
693
733
 
694
734
  await this.alertManager.checkAndAlert('property_exit', current, {
695
735
  cameraId: sighting.cameraId,
@@ -700,8 +740,9 @@ export class TrackingEngine {
700
740
  usedLlm: spatialResult.usedLlm,
701
741
  });
702
742
 
703
- // Clean up cached snapshot after exit alert
743
+ // Clean up cached snapshot and pending descriptions after exit alert
704
744
  this.snapshotCache.delete(tracked.globalId);
745
+ this.pendingDescriptions.delete(tracked.globalId);
705
746
  }
706
747
  this.pendingTimers.delete(tracked.globalId);
707
748
  }, this.config.correlationWindow);
@@ -724,8 +765,9 @@ export class TrackingEngine {
724
765
  `(not seen for ${Math.round(timeSinceSeen / 1000)}s)`
725
766
  );
726
767
 
727
- // Clean up cached snapshot
768
+ // Clean up cached snapshot and pending descriptions
728
769
  this.snapshotCache.delete(tracked.globalId);
770
+ this.pendingDescriptions.delete(tracked.globalId);
729
771
 
730
772
  this.alertManager.checkAndAlert('lost_tracking', tracked, {
731
773
  objectClass: tracked.className,