@blueharford/scrypted-spatial-awareness 0.6.12 → 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/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 +96 -30
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/tracking-engine.ts +109 -43
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -138,6 +138,12 @@ 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
|
+
/** Pending LLM description promises (started when snapshot is captured) */
|
|
145
|
+
private pendingDescriptions: Map<GlobalTrackingId, Promise<SpatialReasoningResult>> = new Map();
|
|
146
|
+
|
|
141
147
|
constructor(
|
|
142
148
|
topology: CameraTopology,
|
|
143
149
|
state: TrackingState,
|
|
@@ -437,6 +443,13 @@ export class TrackingEngine {
|
|
|
437
443
|
if (lastSighting && lastSighting.cameraId !== sighting.cameraId) {
|
|
438
444
|
const transitDuration = sighting.timestamp - lastSighting.timestamp;
|
|
439
445
|
|
|
446
|
+
// Update cached snapshot from new camera (object is now visible here)
|
|
447
|
+
if (this.config.useLlmDescriptions) {
|
|
448
|
+
this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId).catch(e => {
|
|
449
|
+
this.console.warn(`[Transition Snapshot] Failed to update snapshot: ${e}`);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
440
453
|
// Add journey segment
|
|
441
454
|
this.state.addJourney(tracked.globalId, {
|
|
442
455
|
fromCameraId: lastSighting.cameraId,
|
|
@@ -528,6 +541,14 @@ export class TrackingEngine {
|
|
|
528
541
|
sighting: ObjectSighting,
|
|
529
542
|
isEntryPoint: boolean
|
|
530
543
|
): void {
|
|
544
|
+
// Capture snapshot IMMEDIATELY when object is first detected (don't wait for loitering threshold)
|
|
545
|
+
// This ensures we have a good image while the person/object is still in frame
|
|
546
|
+
if (this.config.useLlmDescriptions) {
|
|
547
|
+
this.captureAndCacheSnapshot(globalId, sighting.cameraId).catch(e => {
|
|
548
|
+
this.console.warn(`[Snapshot] Failed to cache initial snapshot: ${e}`);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
531
552
|
// Check after loitering threshold if object is still being tracked
|
|
532
553
|
setTimeout(async () => {
|
|
533
554
|
const tracked = this.state.getObject(globalId);
|
|
@@ -536,31 +557,28 @@ export class TrackingEngine {
|
|
|
536
557
|
// Check if we've already alerted for this object
|
|
537
558
|
if (this.isInAlertCooldown(globalId)) return;
|
|
538
559
|
|
|
539
|
-
//
|
|
540
|
-
let
|
|
541
|
-
|
|
542
|
-
|
|
560
|
+
// Use prefetched LLM result if available (started when snapshot was captured)
|
|
561
|
+
let spatialResult: SpatialReasoningResult;
|
|
562
|
+
const pendingDescription = this.pendingDescriptions.get(globalId);
|
|
563
|
+
|
|
564
|
+
if (pendingDescription) {
|
|
565
|
+
this.console.log(`[Entry Alert] Using prefetched LLM result for ${globalId.slice(0, 8)}`);
|
|
543
566
|
try {
|
|
544
|
-
|
|
545
|
-
this.console.log(`[Entry Alert]
|
|
546
|
-
if (camera?.interfaces?.includes(ScryptedInterface.Camera)) {
|
|
547
|
-
mediaObject = await camera.takePicture();
|
|
548
|
-
this.console.log(`[Entry Alert] Got snapshot: ${!!mediaObject}`);
|
|
549
|
-
}
|
|
567
|
+
spatialResult = await pendingDescription;
|
|
568
|
+
this.console.log(`[Entry Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
550
569
|
} catch (e) {
|
|
551
|
-
this.console.warn(
|
|
570
|
+
this.console.warn(`[Entry Alert] Prefetch failed, generating fallback: ${e}`);
|
|
571
|
+
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
|
|
552
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}`);
|
|
553
580
|
}
|
|
554
581
|
|
|
555
|
-
// Generate spatial description (now async with LLM support)
|
|
556
|
-
this.console.log(`[Entry Alert] Calling generateEntryDescription with mediaObject=${!!mediaObject}`);
|
|
557
|
-
const spatialResult = await this.spatialReasoning.generateEntryDescription(
|
|
558
|
-
tracked,
|
|
559
|
-
sighting.cameraId,
|
|
560
|
-
mediaObject
|
|
561
|
-
);
|
|
562
|
-
this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
563
|
-
|
|
564
582
|
if (isEntryPoint) {
|
|
565
583
|
// Entry point - generate property entry alert
|
|
566
584
|
await this.alertManager.checkAndAlert('property_entry', tracked, {
|
|
@@ -592,6 +610,44 @@ export class TrackingEngine {
|
|
|
592
610
|
}, this.config.loiteringThreshold);
|
|
593
611
|
}
|
|
594
612
|
|
|
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> {
|
|
619
|
+
try {
|
|
620
|
+
const camera = systemManager.getDeviceById<Camera>(cameraId);
|
|
621
|
+
if (camera?.interfaces?.includes(ScryptedInterface.Camera)) {
|
|
622
|
+
const mediaObject = await camera.takePicture();
|
|
623
|
+
if (mediaObject) {
|
|
624
|
+
this.snapshotCache.set(globalId, mediaObject);
|
|
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
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
} catch (e) {
|
|
647
|
+
this.console.warn(`[Snapshot] Failed to capture snapshot: ${e}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
595
651
|
/** Attempt to correlate a sighting with existing tracked objects */
|
|
596
652
|
private async correlateDetection(
|
|
597
653
|
sighting: ObjectSighting
|
|
@@ -639,40 +695,42 @@ export class TrackingEngine {
|
|
|
639
695
|
// Mark as pending and set timer
|
|
640
696
|
this.state.markPending(tracked.globalId);
|
|
641
697
|
|
|
698
|
+
// Capture a fresh snapshot now while object is still visible (before they leave)
|
|
699
|
+
// Also starts LLM analysis immediately in parallel
|
|
700
|
+
if (this.config.useLlmDescriptions) {
|
|
701
|
+
this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId, 'exit').catch(e => {
|
|
702
|
+
this.console.warn(`[Exit Snapshot] Failed to update snapshot: ${e}`);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
642
706
|
// Wait for correlation window before marking as exited
|
|
643
707
|
const timer = setTimeout(async () => {
|
|
644
708
|
const current = this.state.getObject(tracked.globalId);
|
|
645
709
|
if (current && current.state === 'pending') {
|
|
646
710
|
this.state.markExited(tracked.globalId, sighting.cameraId, sighting.cameraName);
|
|
647
711
|
|
|
648
|
-
//
|
|
649
|
-
let
|
|
650
|
-
this.
|
|
651
|
-
|
|
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)}`);
|
|
652
718
|
try {
|
|
653
|
-
|
|
654
|
-
this.console.log(`[Exit Alert]
|
|
655
|
-
if (camera?.interfaces?.includes(ScryptedInterface.Camera)) {
|
|
656
|
-
mediaObject = await camera.takePicture();
|
|
657
|
-
this.console.log(`[Exit Alert] Got snapshot: ${!!mediaObject}`);
|
|
658
|
-
}
|
|
719
|
+
spatialResult = await pendingDescription;
|
|
720
|
+
this.console.log(`[Exit Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
659
721
|
} catch (e) {
|
|
660
|
-
this.console.warn(
|
|
722
|
+
this.console.warn(`[Exit Alert] Prefetch failed, generating fallback: ${e}`);
|
|
723
|
+
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
|
|
661
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}`);
|
|
662
732
|
}
|
|
663
733
|
|
|
664
|
-
// Generate rich exit description using topology context (now async with LLM support)
|
|
665
|
-
this.console.log(`[Exit Alert] Calling generateExitDescription with mediaObject=${!!mediaObject}`);
|
|
666
|
-
const spatialResult = await this.spatialReasoning.generateExitDescription(
|
|
667
|
-
current,
|
|
668
|
-
sighting.cameraId,
|
|
669
|
-
mediaObject
|
|
670
|
-
);
|
|
671
|
-
|
|
672
|
-
this.console.log(
|
|
673
|
-
`[Exit Alert] Object ${tracked.globalId.slice(0, 8)} exited: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`
|
|
674
|
-
);
|
|
675
|
-
|
|
676
734
|
await this.alertManager.checkAndAlert('property_exit', current, {
|
|
677
735
|
cameraId: sighting.cameraId,
|
|
678
736
|
cameraName: sighting.cameraName,
|
|
@@ -681,6 +739,10 @@ export class TrackingEngine {
|
|
|
681
739
|
involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
|
|
682
740
|
usedLlm: spatialResult.usedLlm,
|
|
683
741
|
});
|
|
742
|
+
|
|
743
|
+
// Clean up cached snapshot and pending descriptions after exit alert
|
|
744
|
+
this.snapshotCache.delete(tracked.globalId);
|
|
745
|
+
this.pendingDescriptions.delete(tracked.globalId);
|
|
684
746
|
}
|
|
685
747
|
this.pendingTimers.delete(tracked.globalId);
|
|
686
748
|
}, this.config.correlationWindow);
|
|
@@ -703,6 +765,10 @@ export class TrackingEngine {
|
|
|
703
765
|
`(not seen for ${Math.round(timeSinceSeen / 1000)}s)`
|
|
704
766
|
);
|
|
705
767
|
|
|
768
|
+
// Clean up cached snapshot and pending descriptions
|
|
769
|
+
this.snapshotCache.delete(tracked.globalId);
|
|
770
|
+
this.pendingDescriptions.delete(tracked.globalId);
|
|
771
|
+
|
|
706
772
|
this.alertManager.checkAndAlert('lost_tracking', tracked, {
|
|
707
773
|
objectClass: tracked.className,
|
|
708
774
|
objectLabel: tracked.label,
|