@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.
- package/README.md +141 -355
- 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 +554 -137
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/alerts/alert-manager.ts +16 -9
- package/src/core/object-correlator.ts +41 -7
- package/src/core/spatial-reasoning.ts +315 -44
- package/src/core/tracking-engine.ts +57 -19
- package/src/main.ts +3 -3
- package/src/models/alert.ts +41 -14
|
@@ -516,31 +516,61 @@ export class TrackingEngine {
|
|
|
516
516
|
`(ID: ${globalId.slice(0, 8)})`
|
|
517
517
|
);
|
|
518
518
|
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
|
551
|
+
objectLabel: spatialResult.description,
|
|
536
552
|
detectionId: sighting.detectionId,
|
|
537
|
-
involvedLandmarks: spatialResult
|
|
538
|
-
usedLlm: spatialResult
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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,
|
package/src/models/alert.ts
CHANGED
|
@@ -214,26 +214,53 @@ export function generateAlertMessage(
|
|
|
214
214
|
|
|
215
215
|
switch (type) {
|
|
216
216
|
case 'property_entry':
|
|
217
|
-
|
|
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
|
-
|
|
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
|
|
222
|
-
if (details.objectLabel && details.
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
261
|
+
return `${objectDesc} detected near ${details.involvedLandmarks[0]}`;
|
|
235
262
|
}
|
|
236
|
-
return `${
|
|
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':
|