@blueharford/scrypted-spatial-awareness 0.6.23 → 0.6.25
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 +86 -54
- 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 +17 -1
- package/src/core/tracking-engine.ts +59 -46
- package/src/main.ts +2 -2
- package/src/models/alert.ts +5 -5
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -155,6 +155,7 @@ export class AlertManager {
|
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
157
|
* Get notification title based on alert type
|
|
158
|
+
* For movement alerts with LLM descriptions, use the smart description as title
|
|
158
159
|
*/
|
|
159
160
|
private getNotificationTitle(alert: Alert): string {
|
|
160
161
|
const prefix = alert.severity === 'critical' ? '🚨 ' :
|
|
@@ -167,11 +168,26 @@ export class AlertManager {
|
|
|
167
168
|
|
|
168
169
|
switch (alert.type) {
|
|
169
170
|
case 'property_entry':
|
|
171
|
+
// Legacy - use simple title
|
|
170
172
|
return `${prefix}${objectType} Arrived`;
|
|
171
173
|
case 'property_exit':
|
|
174
|
+
// Legacy - use simple title
|
|
172
175
|
return `${prefix}${objectType} Left`;
|
|
173
176
|
case 'movement':
|
|
174
|
-
//
|
|
177
|
+
// For smart activity alerts, use the LLM description as title if available
|
|
178
|
+
// This gives us rich context like "Person walking toward front door"
|
|
179
|
+
if (alert.details.objectLabel && alert.details.usedLlm) {
|
|
180
|
+
// Truncate to reasonable title length (first sentence or 60 chars)
|
|
181
|
+
let smartTitle = alert.details.objectLabel;
|
|
182
|
+
const firstPeriod = smartTitle.indexOf('.');
|
|
183
|
+
if (firstPeriod > 0 && firstPeriod < 60) {
|
|
184
|
+
smartTitle = smartTitle.substring(0, firstPeriod);
|
|
185
|
+
} else if (smartTitle.length > 60) {
|
|
186
|
+
smartTitle = smartTitle.substring(0, 57) + '...';
|
|
187
|
+
}
|
|
188
|
+
return `${prefix}${smartTitle}`;
|
|
189
|
+
}
|
|
190
|
+
// Fallback: include destination in title
|
|
175
191
|
const dest = alert.details.toCameraName || 'area';
|
|
176
192
|
return `${prefix}${objectType} → ${dest}`;
|
|
177
193
|
case 'unusual_path':
|
|
@@ -324,6 +324,16 @@ export class TrackingEngine {
|
|
|
324
324
|
this.lastLlmCallTime = Date.now();
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
/** Check and record LLM call - returns false if rate limited */
|
|
328
|
+
private tryLlmCall(): boolean {
|
|
329
|
+
if (!this.isLlmCallAllowed()) {
|
|
330
|
+
this.console.log('[LLM] Rate limited, skipping LLM call');
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
this.recordLlmCall();
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
327
337
|
/** Get spatial reasoning result for movement (uses RAG + LLM) with debouncing and fallback */
|
|
328
338
|
private async getSpatialDescription(
|
|
329
339
|
tracked: TrackedObject,
|
|
@@ -337,8 +347,8 @@ export class TrackingEngine {
|
|
|
337
347
|
|
|
338
348
|
try {
|
|
339
349
|
// Check rate limiting - if not allowed, return null to use basic description
|
|
340
|
-
if (!this.
|
|
341
|
-
this.console.log('LLM rate-limited, using basic notification');
|
|
350
|
+
if (!this.tryLlmCall()) {
|
|
351
|
+
this.console.log('[Movement] LLM rate-limited, using basic notification');
|
|
342
352
|
return null;
|
|
343
353
|
}
|
|
344
354
|
|
|
@@ -351,9 +361,6 @@ export class TrackingEngine {
|
|
|
351
361
|
}
|
|
352
362
|
}
|
|
353
363
|
|
|
354
|
-
// Record that we're making an LLM call
|
|
355
|
-
this.recordLlmCall();
|
|
356
|
-
|
|
357
364
|
// Use spatial reasoning engine for rich context-aware description
|
|
358
365
|
// Apply timeout if fallback is enabled
|
|
359
366
|
let result: SpatialReasoningResult;
|
|
@@ -567,44 +574,38 @@ export class TrackingEngine {
|
|
|
567
574
|
spatialResult = await pendingDescription;
|
|
568
575
|
this.console.log(`[Entry Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
569
576
|
} catch (e) {
|
|
570
|
-
this.console.warn(`[Entry Alert] Prefetch failed,
|
|
577
|
+
this.console.warn(`[Entry Alert] Prefetch failed, using basic description: ${e}`);
|
|
578
|
+
// Don't make another LLM call - use basic description (no mediaObject = no LLM)
|
|
571
579
|
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
|
|
572
580
|
}
|
|
573
581
|
this.pendingDescriptions.delete(globalId);
|
|
574
582
|
} else {
|
|
575
|
-
//
|
|
576
|
-
this.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
583
|
+
// No prefetch available - only call LLM if rate limit allows
|
|
584
|
+
if (this.tryLlmCall()) {
|
|
585
|
+
this.console.log(`[Entry Alert] No prefetch, generating with LLM`);
|
|
586
|
+
const mediaObject = this.snapshotCache.get(globalId);
|
|
587
|
+
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId, mediaObject);
|
|
588
|
+
this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
589
|
+
} else {
|
|
590
|
+
// Rate limited - use basic description (no LLM)
|
|
591
|
+
this.console.log(`[Entry Alert] Rate limited, using basic description`);
|
|
592
|
+
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
|
|
593
|
+
}
|
|
580
594
|
}
|
|
581
595
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
// This notifies about any activity around the property using topology context
|
|
596
|
-
await this.alertManager.checkAndAlert('movement', tracked, {
|
|
597
|
-
cameraId: sighting.cameraId,
|
|
598
|
-
cameraName: sighting.cameraName,
|
|
599
|
-
toCameraId: sighting.cameraId,
|
|
600
|
-
toCameraName: sighting.cameraName,
|
|
601
|
-
objectClass: sighting.detection.className,
|
|
602
|
-
objectLabel: spatialResult.description, // Use spatial reasoning description (topology-based)
|
|
603
|
-
detectionId: sighting.detectionId,
|
|
604
|
-
involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
|
|
605
|
-
usedLlm: spatialResult.usedLlm,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
596
|
+
// Always use movement alert type for smart notifications with LLM descriptions
|
|
597
|
+
// The property_entry/property_exit types are legacy and disabled by default
|
|
598
|
+
await this.alertManager.checkAndAlert('movement', tracked, {
|
|
599
|
+
cameraId: sighting.cameraId,
|
|
600
|
+
cameraName: sighting.cameraName,
|
|
601
|
+
toCameraId: sighting.cameraId,
|
|
602
|
+
toCameraName: sighting.cameraName,
|
|
603
|
+
objectClass: sighting.detection.className,
|
|
604
|
+
objectLabel: spatialResult.description, // Smart LLM-generated description
|
|
605
|
+
detectionId: sighting.detectionId,
|
|
606
|
+
involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
|
|
607
|
+
usedLlm: spatialResult.usedLlm,
|
|
608
|
+
});
|
|
608
609
|
|
|
609
610
|
this.recordAlertTime(globalId);
|
|
610
611
|
}, this.config.loiteringThreshold);
|
|
@@ -624,9 +625,9 @@ export class TrackingEngine {
|
|
|
624
625
|
this.snapshotCache.set(globalId, mediaObject);
|
|
625
626
|
this.console.log(`[Snapshot] Cached snapshot for ${globalId.slice(0, 8)} from ${cameraId}`);
|
|
626
627
|
|
|
627
|
-
// Start LLM analysis immediately in parallel (don't await)
|
|
628
|
+
// Start LLM analysis immediately in parallel (don't await) - but respect rate limits
|
|
628
629
|
const tracked = this.state.getObject(globalId);
|
|
629
|
-
if (tracked && this.config.useLlmDescriptions) {
|
|
630
|
+
if (tracked && this.config.useLlmDescriptions && this.tryLlmCall()) {
|
|
630
631
|
this.console.log(`[LLM Prefetch] Starting ${eventType} analysis for ${globalId.slice(0, 8)}`);
|
|
631
632
|
const descriptionPromise = eventType === 'exit'
|
|
632
633
|
? this.spatialReasoning.generateExitDescription(tracked, cameraId, mediaObject)
|
|
@@ -640,6 +641,8 @@ export class TrackingEngine {
|
|
|
640
641
|
}).catch(e => {
|
|
641
642
|
this.console.warn(`[LLM Prefetch] Failed for ${globalId.slice(0, 8)}: ${e}`);
|
|
642
643
|
});
|
|
644
|
+
} else if (tracked && this.config.useLlmDescriptions) {
|
|
645
|
+
this.console.log(`[LLM Prefetch] Skipped for ${globalId.slice(0, 8)} - rate limited`);
|
|
643
646
|
}
|
|
644
647
|
}
|
|
645
648
|
}
|
|
@@ -719,21 +722,31 @@ export class TrackingEngine {
|
|
|
719
722
|
spatialResult = await pendingDescription;
|
|
720
723
|
this.console.log(`[Exit Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
721
724
|
} catch (e) {
|
|
722
|
-
this.console.warn(`[Exit Alert] Prefetch failed,
|
|
725
|
+
this.console.warn(`[Exit Alert] Prefetch failed, using basic description: ${e}`);
|
|
726
|
+
// Don't make another LLM call - use basic description
|
|
723
727
|
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
|
|
724
728
|
}
|
|
725
729
|
this.pendingDescriptions.delete(tracked.globalId);
|
|
726
730
|
} else {
|
|
727
|
-
//
|
|
728
|
-
this.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
731
|
+
// No prefetch available - only call LLM if rate limit allows
|
|
732
|
+
if (this.tryLlmCall()) {
|
|
733
|
+
this.console.log(`[Exit Alert] No prefetch, generating with LLM`);
|
|
734
|
+
const mediaObject = this.snapshotCache.get(tracked.globalId);
|
|
735
|
+
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId, mediaObject);
|
|
736
|
+
this.console.log(`[Exit Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
737
|
+
} else {
|
|
738
|
+
// Rate limited - use basic description (no LLM)
|
|
739
|
+
this.console.log(`[Exit Alert] Rate limited, using basic description`);
|
|
740
|
+
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
|
|
741
|
+
}
|
|
732
742
|
}
|
|
733
743
|
|
|
734
|
-
|
|
744
|
+
// Use movement alert for exit too - smart notifications with LLM descriptions
|
|
745
|
+
await this.alertManager.checkAndAlert('movement', current, {
|
|
735
746
|
cameraId: sighting.cameraId,
|
|
736
747
|
cameraName: sighting.cameraName,
|
|
748
|
+
toCameraId: sighting.cameraId,
|
|
749
|
+
toCameraName: sighting.cameraName,
|
|
737
750
|
objectClass: current.className,
|
|
738
751
|
objectLabel: spatialResult.description,
|
|
739
752
|
involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
|
package/src/main.ts
CHANGED
|
@@ -130,8 +130,8 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
130
130
|
llmDebounceInterval: {
|
|
131
131
|
title: 'LLM Rate Limit (seconds)',
|
|
132
132
|
type: 'number',
|
|
133
|
-
defaultValue:
|
|
134
|
-
description: 'Minimum time between LLM calls to prevent API
|
|
133
|
+
defaultValue: 30,
|
|
134
|
+
description: 'Minimum time between LLM calls to prevent API rate limiting. Increase if you get rate limit errors. (0 = no limit)',
|
|
135
135
|
group: 'AI & Spatial Reasoning',
|
|
136
136
|
},
|
|
137
137
|
llmFallbackEnabled: {
|
package/src/models/alert.ts
CHANGED
|
@@ -128,8 +128,8 @@ export function createDefaultRules(): AlertRule[] {
|
|
|
128
128
|
return [
|
|
129
129
|
{
|
|
130
130
|
id: 'property-entry',
|
|
131
|
-
name: 'Property Entry',
|
|
132
|
-
enabled:
|
|
131
|
+
name: 'Property Entry (Legacy)',
|
|
132
|
+
enabled: false, // Disabled - use movement alerts with LLM descriptions instead
|
|
133
133
|
type: 'property_entry',
|
|
134
134
|
conditions: [],
|
|
135
135
|
severity: 'info',
|
|
@@ -138,8 +138,8 @@ export function createDefaultRules(): AlertRule[] {
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
id: 'property-exit',
|
|
141
|
-
name: 'Property Exit',
|
|
142
|
-
enabled:
|
|
141
|
+
name: 'Property Exit (Legacy)',
|
|
142
|
+
enabled: false, // Disabled - use movement alerts with LLM descriptions instead
|
|
143
143
|
type: 'property_exit',
|
|
144
144
|
conditions: [],
|
|
145
145
|
severity: 'info',
|
|
@@ -148,7 +148,7 @@ export function createDefaultRules(): AlertRule[] {
|
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
id: 'movement',
|
|
151
|
-
name: '
|
|
151
|
+
name: 'Smart Activity Alerts',
|
|
152
152
|
enabled: true,
|
|
153
153
|
type: 'movement',
|
|
154
154
|
conditions: [],
|