@blueharford/scrypted-spatial-awareness 0.6.30 → 0.6.32

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.30",
3
+ "version": "0.6.32",
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",
Binary file
@@ -110,6 +110,14 @@ export class AlertManager {
110
110
  ? rule.notifiers
111
111
  : this.getDefaultNotifiers();
112
112
 
113
+ // Debug: log which notifiers we're using
114
+ this.console.log(`[Notification] Rule ${rule.id} has ${rule.notifiers.length} notifiers, using ${notifierIds.length} notifier(s): ${notifierIds.join(', ') || 'NONE'}`);
115
+
116
+ if (notifierIds.length === 0) {
117
+ this.console.warn(`[Notification] No notifiers configured! Configure a notifier in plugin settings.`);
118
+ return;
119
+ }
120
+
113
121
  // Try to get a thumbnail from the camera
114
122
  let mediaObject: MediaObject | undefined;
115
123
  const cameraId = alert.details.toCameraId || alert.details.cameraId;
@@ -212,25 +220,33 @@ export class AlertManager {
212
220
  try {
213
221
  // Try new multiple notifiers setting first
214
222
  const notifiers = this.storage.getItem('defaultNotifiers');
223
+ this.console.log(`[Notifiers] Raw storage value: ${notifiers}`);
224
+
215
225
  if (notifiers) {
216
226
  // Could be JSON array or comma-separated string
217
227
  try {
218
228
  const parsed = JSON.parse(notifiers);
219
229
  if (Array.isArray(parsed)) {
230
+ this.console.log(`[Notifiers] Parsed JSON array: ${parsed.join(', ')}`);
220
231
  return parsed;
221
232
  }
222
233
  } catch {
223
234
  // Not JSON, might be comma-separated or single value
224
235
  if (notifiers.includes(',')) {
225
- return notifiers.split(',').map(s => s.trim()).filter(Boolean);
236
+ const result = notifiers.split(',').map(s => s.trim()).filter(Boolean);
237
+ this.console.log(`[Notifiers] Parsed comma-separated: ${result.join(', ')}`);
238
+ return result;
226
239
  }
240
+ this.console.log(`[Notifiers] Single value: ${notifiers}`);
227
241
  return [notifiers];
228
242
  }
229
243
  }
230
244
  // Fallback to old single notifier setting
231
245
  const defaultNotifier = this.storage.getItem('defaultNotifier');
246
+ this.console.log(`[Notifiers] Fallback single notifier: ${defaultNotifier || 'NONE'}`);
232
247
  return defaultNotifier ? [defaultNotifier] : [];
233
- } catch {
248
+ } catch (e) {
249
+ this.console.error(`[Notifiers] Error reading notifiers:`, e);
234
250
  return [];
235
251
  }
236
252
  }
@@ -466,7 +466,7 @@ export class SpatialReasoningEngine {
466
466
  id: deviceId,
467
467
  name: device.name || deviceId,
468
468
  providerType: providerTypeEnum,
469
- lastUsed: 0,
469
+ lastUsed: Date.now() - 60000, // Initialize to 1 minute ago so all LLMs start equal
470
470
  errorCount: 0,
471
471
  });
472
472
  this.console.log(`[LLM] Using configured LLM: ${device.name}`);
@@ -496,7 +496,7 @@ export class SpatialReasoningEngine {
496
496
  id,
497
497
  name: device.name || id,
498
498
  providerType: providerTypeEnum,
499
- lastUsed: 0,
499
+ lastUsed: Date.now() - 60000, // Initialize to 1 minute ago so all LLMs start equal
500
500
  errorCount: 0,
501
501
  });
502
502
 
@@ -328,15 +328,24 @@ export class TrackingEngine {
328
328
  }
329
329
 
330
330
  /** Check and record LLM call - returns false if rate limited */
331
- private tryLlmCall(): boolean {
331
+ private tryLlmCall(silent: boolean = false): boolean {
332
332
  if (!this.isLlmCallAllowed()) {
333
- this.console.log('[LLM] Rate limited, skipping LLM call');
333
+ // Only log once per rate limit window, not every call
334
+ if (!silent && !this.rateLimitLogged) {
335
+ const remaining = Math.ceil((this.config.llmDebounceInterval || 30000) - (Date.now() - this.lastLlmCallTime)) / 1000;
336
+ this.console.log(`[LLM] Rate limited, ${remaining.toFixed(0)}s until next call allowed`);
337
+ this.rateLimitLogged = true;
338
+ }
334
339
  return false;
335
340
  }
341
+ this.rateLimitLogged = false;
336
342
  this.recordLlmCall();
337
343
  return true;
338
344
  }
339
345
 
346
+ /** Track if we've already logged rate limit message */
347
+ private rateLimitLogged: boolean = false;
348
+
340
349
  /** Get spatial reasoning result for movement (uses RAG + LLM) with debouncing and fallback */
341
350
  private async getSpatialDescription(
342
351
  tracked: TrackedObject,
@@ -620,32 +629,34 @@ export class TrackingEngine {
620
629
  cameraId: string,
621
630
  eventType: 'entry' | 'exit' | 'movement' = 'entry'
622
631
  ): Promise<void> {
632
+ // Skip if we already have a recent snapshot for this object (within 5 seconds)
633
+ const existingSnapshot = this.snapshotCache.get(globalId);
634
+ if (existingSnapshot && eventType !== 'exit') {
635
+ // For entry/movement, we can reuse existing snapshot
636
+ // For exit, we want a fresh snapshot while they're still visible
637
+ return;
638
+ }
639
+
623
640
  try {
624
641
  const camera = systemManager.getDeviceById<Camera>(cameraId);
625
642
  if (camera?.interfaces?.includes(ScryptedInterface.Camera)) {
626
643
  const mediaObject = await camera.takePicture();
627
644
  if (mediaObject) {
628
645
  this.snapshotCache.set(globalId, mediaObject);
629
- this.console.log(`[Snapshot] Cached snapshot for ${globalId.slice(0, 8)} from ${cameraId}`);
630
646
 
631
647
  // Start LLM analysis immediately in parallel (don't await) - but respect rate limits
632
648
  const tracked = this.state.getObject(globalId);
633
649
  if (tracked && this.config.useLlmDescriptions && this.tryLlmCall()) {
634
- this.console.log(`[LLM Prefetch] Starting ${eventType} analysis for ${globalId.slice(0, 8)}`);
635
650
  const descriptionPromise = eventType === 'exit'
636
651
  ? this.spatialReasoning.generateExitDescription(tracked, cameraId, mediaObject)
637
652
  : this.spatialReasoning.generateEntryDescription(tracked, cameraId, mediaObject);
638
653
 
639
654
  this.pendingDescriptions.set(globalId, descriptionPromise);
640
655
 
641
- // Log when complete (for debugging)
642
- descriptionPromise.then(result => {
643
- this.console.log(`[LLM Prefetch] ${eventType} analysis ready for ${globalId.slice(0, 8)}: "${result.description.substring(0, 40)}..."`);
644
- }).catch(e => {
656
+ // Log when complete (but don't spam logs)
657
+ descriptionPromise.catch(e => {
645
658
  this.console.warn(`[LLM Prefetch] Failed for ${globalId.slice(0, 8)}: ${e}`);
646
659
  });
647
- } else if (tracked && this.config.useLlmDescriptions) {
648
- this.console.log(`[LLM Prefetch] Skipped for ${globalId.slice(0, 8)} - rate limited`);
649
660
  }
650
661
  }
651
662
  }
package/src/main.ts CHANGED
@@ -130,7 +130,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
130
130
  llmDebounceInterval: {
131
131
  title: 'LLM Rate Limit (seconds)',
132
132
  type: 'number',
133
- defaultValue: 30,
133
+ defaultValue: 5,
134
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
  },