@blueharford/scrypted-spatial-awareness 0.6.26 → 0.6.27

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.26",
3
+ "version": "0.6.27",
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",
@@ -33,6 +33,8 @@ const { systemManager, mediaManager } = sdk;
33
33
  export interface SpatialReasoningConfig {
34
34
  /** Enable LLM-based descriptions */
35
35
  enableLlm: boolean;
36
+ /** Specific LLM device ID to use (if not set, auto-discovers) */
37
+ llmDeviceId?: string;
36
38
  /** Enable landmark learning/suggestions */
37
39
  enableLandmarkLearning: boolean;
38
40
  /** Minimum confidence for landmark suggestions */
@@ -447,43 +449,39 @@ export class SpatialReasoningEngine {
447
449
  }> = [];
448
450
  private llmIndex: number = 0;
449
451
 
450
- /** Find ALL LLM devices for load balancing */
452
+ /** Find LLM devices - uses configured device or auto-discovers all for load balancing */
451
453
  private async findAllLlmDevices(): Promise<void> {
452
454
  if (this.llmSearched) return;
453
455
  this.llmSearched = true;
454
456
 
455
457
  try {
458
+ // If a specific LLM device is configured, use only that one
459
+ if (this.config.llmDeviceId) {
460
+ const device = systemManager.getDeviceById(this.config.llmDeviceId);
461
+ if (device?.interfaces?.includes('ChatCompletion')) {
462
+ const providerTypeEnum = this.detectProviderType(device);
463
+ this.llmDevices.push({
464
+ device: device as unknown as ChatCompletionDevice,
465
+ id: this.config.llmDeviceId,
466
+ name: device.name || this.config.llmDeviceId,
467
+ providerType: providerTypeEnum,
468
+ lastUsed: 0,
469
+ errorCount: 0,
470
+ });
471
+ this.console.log(`[LLM] Using configured LLM: ${device.name}`);
472
+ return;
473
+ } else {
474
+ this.console.warn(`[LLM] Configured device ${this.config.llmDeviceId} not found or doesn't support ChatCompletion`);
475
+ }
476
+ }
477
+
478
+ // Auto-discover all LLM devices for load balancing
456
479
  for (const id of Object.keys(systemManager.getSystemState())) {
457
480
  const device = systemManager.getDeviceById(id);
458
481
  if (!device) continue;
459
482
 
460
483
  if (device.interfaces?.includes('ChatCompletion')) {
461
- const deviceName = device.name?.toLowerCase() || '';
462
- const pluginId = (device as any).pluginId?.toLowerCase() || '';
463
-
464
- // Identify the provider type for image format selection
465
- let providerType = 'Unknown';
466
- let providerTypeEnum: LlmProvider = 'unknown';
467
-
468
- if (deviceName.includes('openai') || deviceName.includes('gpt')) {
469
- providerType = 'OpenAI';
470
- providerTypeEnum = 'openai';
471
- } else if (deviceName.includes('anthropic') || deviceName.includes('claude')) {
472
- providerType = 'Anthropic';
473
- providerTypeEnum = 'anthropic';
474
- } else if (deviceName.includes('ollama')) {
475
- providerType = 'Ollama';
476
- providerTypeEnum = 'openai';
477
- } else if (deviceName.includes('gemini') || deviceName.includes('google')) {
478
- providerType = 'Google';
479
- providerTypeEnum = 'openai';
480
- } else if (deviceName.includes('llama')) {
481
- providerType = 'llama.cpp';
482
- providerTypeEnum = 'openai';
483
- } else if (pluginId.includes('@scrypted/llm') || pluginId.includes('llm')) {
484
- providerType = 'Scrypted LLM';
485
- providerTypeEnum = 'unknown';
486
- }
484
+ const providerTypeEnum = this.detectProviderType(device);
487
485
 
488
486
  this.llmDevices.push({
489
487
  device: device as unknown as ChatCompletionDevice,
@@ -494,7 +492,7 @@ export class SpatialReasoningEngine {
494
492
  errorCount: 0,
495
493
  });
496
494
 
497
- this.console.log(`[LLM] Found ${providerType}: ${device.name}`);
495
+ this.console.log(`[LLM] Found: ${device.name}`);
498
496
  }
499
497
  }
500
498
 
@@ -508,6 +506,27 @@ export class SpatialReasoningEngine {
508
506
  }
509
507
  }
510
508
 
509
+ /** Detect the provider type from device name */
510
+ private detectProviderType(device: ScryptedDevice): LlmProvider {
511
+ const deviceName = device.name?.toLowerCase() || '';
512
+ const pluginId = (device as any).pluginId?.toLowerCase() || '';
513
+
514
+ if (deviceName.includes('openai') || deviceName.includes('gpt')) {
515
+ return 'openai';
516
+ } else if (deviceName.includes('anthropic') || deviceName.includes('claude')) {
517
+ return 'anthropic';
518
+ } else if (deviceName.includes('ollama')) {
519
+ return 'openai'; // Ollama uses OpenAI-compatible format
520
+ } else if (deviceName.includes('gemini') || deviceName.includes('google')) {
521
+ return 'openai'; // Google uses OpenAI-compatible format
522
+ } else if (deviceName.includes('llama')) {
523
+ return 'openai'; // llama.cpp uses OpenAI-compatible format
524
+ } else if (pluginId.includes('@scrypted/llm') || pluginId.includes('llm')) {
525
+ return 'unknown';
526
+ }
527
+ return 'unknown';
528
+ }
529
+
511
530
  /** Get the next available LLM using round-robin with least-recently-used preference */
512
531
  private async findLlmDevice(): Promise<ChatCompletionDevice | null> {
513
532
  await this.findAllLlmDevices();
@@ -63,6 +63,8 @@ export interface TrackingEngineConfig {
63
63
  objectAlertCooldown: number;
64
64
  /** Use LLM for enhanced descriptions */
65
65
  useLlmDescriptions: boolean;
66
+ /** Specific LLM device ID to use (if not set, auto-discovers all for load balancing) */
67
+ llmDeviceId?: string;
66
68
  /** LLM rate limit interval (ms) - minimum time between LLM calls */
67
69
  llmDebounceInterval?: number;
68
70
  /** Whether to fall back to basic notifications when LLM is unavailable or slow */
@@ -161,6 +163,7 @@ export class TrackingEngine {
161
163
  // Initialize spatial reasoning engine
162
164
  const spatialConfig: SpatialReasoningConfig = {
163
165
  enableLlm: config.useLlmDescriptions,
166
+ llmDeviceId: config.llmDeviceId,
164
167
  enableLandmarkLearning: config.enableLandmarkLearning ?? true,
165
168
  landmarkConfidenceThreshold: config.landmarkConfidenceThreshold ?? 0.7,
166
169
  contextCacheTtl: 60000, // 1 minute cache
package/src/main.ts CHANGED
@@ -237,20 +237,21 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
237
237
  group: 'MQTT Integration',
238
238
  },
239
239
 
240
- // Alert Settings
241
- enableAlerts: {
242
- title: 'Enable Alerts',
243
- type: 'boolean',
244
- defaultValue: true,
245
- group: 'Alerts',
240
+ // Integrations
241
+ llmDevice: {
242
+ title: 'LLM Provider',
243
+ type: 'device',
244
+ deviceFilter: `interfaces.includes('ChatCompletion')`,
245
+ description: 'Select the LLM plugin to use for smart descriptions (e.g., OpenAI, Anthropic, Ollama)',
246
+ group: 'Integrations',
246
247
  },
247
248
  defaultNotifiers: {
248
- title: 'Notifiers',
249
+ title: 'Notification Service',
249
250
  type: 'device',
250
251
  multiple: true,
251
252
  deviceFilter: `interfaces.includes('${ScryptedInterface.Notifier}')`,
252
- description: 'Select one or more notifiers to receive alerts',
253
- group: 'Alerts',
253
+ description: 'Select one or more notifiers to receive alerts (e.g., Pushover, Home Assistant)',
254
+ group: 'Integrations',
254
255
  },
255
256
 
256
257
  // Tracked Cameras
@@ -262,13 +263,6 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
262
263
  group: 'Cameras',
263
264
  description: 'Select cameras with object detection to track',
264
265
  },
265
-
266
- // Alert Rules (stored as JSON)
267
- alertRules: {
268
- title: 'Alert Rules',
269
- type: 'string',
270
- hide: true,
271
- },
272
266
  });
273
267
 
274
268
  constructor(nativeId?: ScryptedNativeId) {
@@ -368,7 +362,8 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
368
362
  loiteringThreshold: (this.storageSettings.values.loiteringThreshold as number || 3) * 1000,
369
363
  objectAlertCooldown: (this.storageSettings.values.objectAlertCooldown as number || 30) * 1000,
370
364
  useLlmDescriptions: this.storageSettings.values.useLlmDescriptions as boolean ?? true,
371
- llmDebounceInterval: (this.storageSettings.values.llmDebounceInterval as number || 10) * 1000,
365
+ llmDeviceId: this.storageSettings.values.llmDevice as string || undefined,
366
+ llmDebounceInterval: (this.storageSettings.values.llmDebounceInterval as number || 30) * 1000,
372
367
  llmFallbackEnabled: this.storageSettings.values.llmFallbackEnabled as boolean ?? true,
373
368
  llmFallbackTimeout: (this.storageSettings.values.llmFallbackTimeout as number || 3) * 1000,
374
369
  enableTransitTimeLearning: this.storageSettings.values.enableTransitTimeLearning as boolean ?? true,
@@ -697,95 +692,21 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
697
692
  // ==================== 5. Tracking ====================
698
693
  addGroup('Tracking');
699
694
 
700
- // ==================== 6. AI & Spatial Reasoning ====================
695
+ // ==================== 6. Integrations ====================
696
+ addGroup('Integrations');
697
+
698
+ // ==================== 7. AI & Spatial Reasoning ====================
701
699
  addGroup('AI & Spatial Reasoning');
702
700
 
703
- // ==================== 7. Auto-Topology Discovery ====================
701
+ // ==================== 8. Auto-Topology Discovery ====================
704
702
  addGroup('Auto-Topology Discovery');
705
703
 
706
- // ==================== 8. Alerts ====================
707
- addGroup('Alerts');
708
-
709
- // Add alert rules configuration UI
710
- const alertRules = this.alertManager.getRules();
711
- const rulesHtml = this.generateAlertRulesHtml(alertRules);
712
- settings.push({
713
- key: 'alertRulesEditor',
714
- title: 'Alert Rules',
715
- type: 'html' as any,
716
- value: rulesHtml,
717
- group: 'Alerts',
718
- });
719
-
720
704
  // ==================== 9. MQTT Integration ====================
721
705
  addGroup('MQTT Integration');
722
706
 
723
707
  return settings;
724
708
  }
725
709
 
726
- private generateAlertRulesHtml(rules: any[]): string {
727
- const ruleRows = rules.map(rule => `
728
- <tr data-rule-id="${rule.id}">
729
- <td style="padding:8px;border-bottom:1px solid #333;">
730
- <input type="checkbox" ${rule.enabled ? 'checked' : ''}
731
- onchange="(function(el){var rules=JSON.parse(localStorage.getItem('sa-temp-rules')||'[]');var r=rules.find(x=>x.id==='${rule.id}');if(r)r.enabled=el.checked;localStorage.setItem('sa-temp-rules',JSON.stringify(rules));})(this)" />
732
- </td>
733
- <td style="padding:8px;border-bottom:1px solid #333;color:#fff;">${rule.name}</td>
734
- <td style="padding:8px;border-bottom:1px solid #333;color:#888;">${rule.type}</td>
735
- <td style="padding:8px;border-bottom:1px solid #333;">
736
- <span style="padding:2px 8px;border-radius:4px;font-size:12px;background:${
737
- rule.severity === 'critical' ? '#e94560' :
738
- rule.severity === 'warning' ? '#f39c12' : '#3498db'
739
- };color:white;">${rule.severity}</span>
740
- </td>
741
- <td style="padding:8px;border-bottom:1px solid #333;color:#888;">${Math.round(rule.cooldown / 1000)}s</td>
742
- </tr>
743
- `).join('');
744
-
745
- const initCode = `localStorage.setItem('sa-temp-rules',JSON.stringify(${JSON.stringify(rules)}))`;
746
- const saveCode = `(function(){var rules=JSON.parse(localStorage.getItem('sa-temp-rules')||'[]');fetch('/endpoint/@blueharford/scrypted-spatial-awareness/api/alert-rules',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(rules)}).then(r=>r.json()).then(d=>{if(d.success)alert('Alert rules saved!');else alert('Error: '+d.error);}).catch(e=>alert('Error: '+e));})()`;
747
-
748
- return `
749
- <style>
750
- .sa-rules-table { width:100%; border-collapse:collapse; margin-top:10px; }
751
- .sa-rules-table th { text-align:left; padding:10px 8px; border-bottom:2px solid #e94560; color:#e94560; font-size:13px; }
752
- .sa-save-rules-btn {
753
- background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
754
- color: white;
755
- border: none;
756
- padding: 10px 20px;
757
- border-radius: 6px;
758
- font-size: 14px;
759
- font-weight: 600;
760
- cursor: pointer;
761
- margin-top: 15px;
762
- }
763
- .sa-save-rules-btn:hover { opacity: 0.9; }
764
- .sa-rules-container { background:#16213e; border-radius:8px; padding:15px; }
765
- .sa-rules-desc { color:#888; font-size:13px; margin-bottom:10px; }
766
- </style>
767
- <div class="sa-rules-container">
768
- <p class="sa-rules-desc">Enable or disable alert types. Movement alerts notify you when someone moves between cameras.</p>
769
- <table class="sa-rules-table">
770
- <thead>
771
- <tr>
772
- <th style="width:40px;">On</th>
773
- <th>Alert Type</th>
774
- <th>Event</th>
775
- <th>Severity</th>
776
- <th>Cooldown</th>
777
- </tr>
778
- </thead>
779
- <tbody>
780
- ${ruleRows}
781
- </tbody>
782
- </table>
783
- <button class="sa-save-rules-btn" onclick="${saveCode}">Save Alert Rules</button>
784
- <script>(function(){${initCode}})();</script>
785
- </div>
786
- `;
787
- }
788
-
789
710
  async putSetting(key: string, value: SettingValue): Promise<void> {
790
711
  await this.storageSettings.putSetting(key, value);
791
712
 
@@ -802,6 +723,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
802
723
  key === 'llmDebounceInterval' ||
803
724
  key === 'llmFallbackEnabled' ||
804
725
  key === 'llmFallbackTimeout' ||
726
+ key === 'llmDevice' ||
805
727
  key === 'enableTransitTimeLearning' ||
806
728
  key === 'enableConnectionSuggestions' ||
807
729
  key === 'enableLandmarkLearning' ||