@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/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.23",
3
+ "version": "0.6.25",
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",
@@ -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
- // Include destination in title
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.isLlmCallAllowed()) {
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, generating fallback: ${e}`);
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
- // 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}`);
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
- if (isEntryPoint) {
583
- // Entry point - generate property entry alert
584
- await this.alertManager.checkAndAlert('property_entry', tracked, {
585
- cameraId: sighting.cameraId,
586
- cameraName: sighting.cameraName,
587
- objectClass: sighting.detection.className,
588
- objectLabel: spatialResult.description,
589
- detectionId: sighting.detectionId,
590
- involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
591
- usedLlm: spatialResult.usedLlm,
592
- });
593
- } else {
594
- // Non-entry point - still alert about activity using movement alert type
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, generating fallback: ${e}`);
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
- // 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}`);
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
- await this.alertManager.checkAndAlert('property_exit', current, {
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: 10,
134
- description: 'Minimum time between LLM calls to prevent API overload (0 = no limit)',
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: {
@@ -128,8 +128,8 @@ export function createDefaultRules(): AlertRule[] {
128
128
  return [
129
129
  {
130
130
  id: 'property-entry',
131
- name: 'Property Entry',
132
- enabled: true,
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: true,
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: 'Movement Between Cameras',
151
+ name: 'Smart Activity Alerts',
152
152
  enabled: true,
153
153
  type: 'movement',
154
154
  conditions: [],