@blueharford/scrypted-spatial-awareness 0.6.26 → 0.6.28
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 +102 -119
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/spatial-reasoning.ts +55 -28
- package/src/core/tracking-engine.ts +3 -0
- package/src/main.ts +49 -96
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -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 IDs to use (if not set, auto-discovers all) */
|
|
37
|
+
llmDeviceIds?: string[];
|
|
36
38
|
/** Enable landmark learning/suggestions */
|
|
37
39
|
enableLandmarkLearning: boolean;
|
|
38
40
|
/** Minimum confidence for landmark suggestions */
|
|
@@ -447,43 +449,47 @@ export class SpatialReasoningEngine {
|
|
|
447
449
|
}> = [];
|
|
448
450
|
private llmIndex: number = 0;
|
|
449
451
|
|
|
450
|
-
/** Find
|
|
452
|
+
/** Find LLM devices - uses configured devices 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 specific LLM devices are configured, use only those
|
|
459
|
+
if (this.config.llmDeviceIds && this.config.llmDeviceIds.length > 0) {
|
|
460
|
+
for (const deviceId of this.config.llmDeviceIds) {
|
|
461
|
+
const device = systemManager.getDeviceById(deviceId);
|
|
462
|
+
if (device?.interfaces?.includes('ChatCompletion')) {
|
|
463
|
+
const providerTypeEnum = this.detectProviderType(device);
|
|
464
|
+
this.llmDevices.push({
|
|
465
|
+
device: device as unknown as ChatCompletionDevice,
|
|
466
|
+
id: deviceId,
|
|
467
|
+
name: device.name || deviceId,
|
|
468
|
+
providerType: providerTypeEnum,
|
|
469
|
+
lastUsed: 0,
|
|
470
|
+
errorCount: 0,
|
|
471
|
+
});
|
|
472
|
+
this.console.log(`[LLM] Using configured LLM: ${device.name}`);
|
|
473
|
+
} else {
|
|
474
|
+
this.console.warn(`[LLM] Configured device ${deviceId} not found or doesn't support ChatCompletion`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (this.llmDevices.length > 0) {
|
|
479
|
+
this.console.log(`[LLM] Using ${this.llmDevices.length} configured LLM provider(s)`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
// Fall through to auto-discovery if none of the configured devices worked
|
|
483
|
+
this.console.warn('[LLM] No configured devices available, falling back to auto-discovery');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Auto-discover all LLM devices for load balancing
|
|
456
487
|
for (const id of Object.keys(systemManager.getSystemState())) {
|
|
457
488
|
const device = systemManager.getDeviceById(id);
|
|
458
489
|
if (!device) continue;
|
|
459
490
|
|
|
460
491
|
if (device.interfaces?.includes('ChatCompletion')) {
|
|
461
|
-
const
|
|
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
|
-
}
|
|
492
|
+
const providerTypeEnum = this.detectProviderType(device);
|
|
487
493
|
|
|
488
494
|
this.llmDevices.push({
|
|
489
495
|
device: device as unknown as ChatCompletionDevice,
|
|
@@ -494,7 +500,7 @@ export class SpatialReasoningEngine {
|
|
|
494
500
|
errorCount: 0,
|
|
495
501
|
});
|
|
496
502
|
|
|
497
|
-
this.console.log(`[LLM]
|
|
503
|
+
this.console.log(`[LLM] Auto-discovered: ${device.name}`);
|
|
498
504
|
}
|
|
499
505
|
}
|
|
500
506
|
|
|
@@ -508,6 +514,27 @@ export class SpatialReasoningEngine {
|
|
|
508
514
|
}
|
|
509
515
|
}
|
|
510
516
|
|
|
517
|
+
/** Detect the provider type from device name */
|
|
518
|
+
private detectProviderType(device: ScryptedDevice): LlmProvider {
|
|
519
|
+
const deviceName = device.name?.toLowerCase() || '';
|
|
520
|
+
const pluginId = (device as any).pluginId?.toLowerCase() || '';
|
|
521
|
+
|
|
522
|
+
if (deviceName.includes('openai') || deviceName.includes('gpt')) {
|
|
523
|
+
return 'openai';
|
|
524
|
+
} else if (deviceName.includes('anthropic') || deviceName.includes('claude')) {
|
|
525
|
+
return 'anthropic';
|
|
526
|
+
} else if (deviceName.includes('ollama')) {
|
|
527
|
+
return 'openai'; // Ollama uses OpenAI-compatible format
|
|
528
|
+
} else if (deviceName.includes('gemini') || deviceName.includes('google')) {
|
|
529
|
+
return 'openai'; // Google uses OpenAI-compatible format
|
|
530
|
+
} else if (deviceName.includes('llama')) {
|
|
531
|
+
return 'openai'; // llama.cpp uses OpenAI-compatible format
|
|
532
|
+
} else if (pluginId.includes('@scrypted/llm') || pluginId.includes('llm')) {
|
|
533
|
+
return 'unknown';
|
|
534
|
+
}
|
|
535
|
+
return 'unknown';
|
|
536
|
+
}
|
|
537
|
+
|
|
511
538
|
/** Get the next available LLM using round-robin with least-recently-used preference */
|
|
512
539
|
private async findLlmDevice(): Promise<ChatCompletionDevice | null> {
|
|
513
540
|
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 IDs to use (if not set, auto-discovers all for load balancing) */
|
|
67
|
+
llmDeviceIds?: 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
|
+
llmDeviceIds: config.llmDeviceIds,
|
|
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,22 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
237
237
|
group: 'MQTT Integration',
|
|
238
238
|
},
|
|
239
239
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
title: '
|
|
243
|
-
type: '
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
// Integrations
|
|
241
|
+
llmDevices: {
|
|
242
|
+
title: 'LLM Providers',
|
|
243
|
+
type: 'device',
|
|
244
|
+
multiple: true,
|
|
245
|
+
deviceFilter: `interfaces.includes('ChatCompletion')`,
|
|
246
|
+
description: 'Select one or more LLM providers for smart descriptions. Multiple providers will be load-balanced.',
|
|
247
|
+
group: 'Integrations',
|
|
246
248
|
},
|
|
247
249
|
defaultNotifiers: {
|
|
248
|
-
title: '
|
|
250
|
+
title: 'Notification Service',
|
|
249
251
|
type: 'device',
|
|
250
252
|
multiple: true,
|
|
251
253
|
deviceFilter: `interfaces.includes('${ScryptedInterface.Notifier}')`,
|
|
252
|
-
description: 'Select one or more notifiers to receive alerts',
|
|
253
|
-
group: '
|
|
254
|
+
description: 'Select one or more notifiers to receive alerts (e.g., Pushover, Home Assistant)',
|
|
255
|
+
group: 'Integrations',
|
|
254
256
|
},
|
|
255
257
|
|
|
256
258
|
// Tracked Cameras
|
|
@@ -262,13 +264,6 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
262
264
|
group: 'Cameras',
|
|
263
265
|
description: 'Select cameras with object detection to track',
|
|
264
266
|
},
|
|
265
|
-
|
|
266
|
-
// Alert Rules (stored as JSON)
|
|
267
|
-
alertRules: {
|
|
268
|
-
title: 'Alert Rules',
|
|
269
|
-
type: 'string',
|
|
270
|
-
hide: true,
|
|
271
|
-
},
|
|
272
267
|
});
|
|
273
268
|
|
|
274
269
|
constructor(nativeId?: ScryptedNativeId) {
|
|
@@ -368,7 +363,8 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
368
363
|
loiteringThreshold: (this.storageSettings.values.loiteringThreshold as number || 3) * 1000,
|
|
369
364
|
objectAlertCooldown: (this.storageSettings.values.objectAlertCooldown as number || 30) * 1000,
|
|
370
365
|
useLlmDescriptions: this.storageSettings.values.useLlmDescriptions as boolean ?? true,
|
|
371
|
-
|
|
366
|
+
llmDeviceIds: this.parseLlmDevices(),
|
|
367
|
+
llmDebounceInterval: (this.storageSettings.values.llmDebounceInterval as number || 30) * 1000,
|
|
372
368
|
llmFallbackEnabled: this.storageSettings.values.llmFallbackEnabled as boolean ?? true,
|
|
373
369
|
llmFallbackTimeout: (this.storageSettings.values.llmFallbackTimeout as number || 3) * 1000,
|
|
374
370
|
enableTransitTimeLearning: this.storageSettings.values.enableTransitTimeLearning as boolean ?? true,
|
|
@@ -422,6 +418,36 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
422
418
|
}
|
|
423
419
|
}
|
|
424
420
|
|
|
421
|
+
/** Parse LLM devices from settings - handles both array and single value formats */
|
|
422
|
+
private parseLlmDevices(): string[] | undefined {
|
|
423
|
+
const value = this.storageSettings.values.llmDevices;
|
|
424
|
+
if (!value) return undefined;
|
|
425
|
+
|
|
426
|
+
// Handle array format
|
|
427
|
+
if (Array.isArray(value)) {
|
|
428
|
+
const filtered = value.filter(Boolean);
|
|
429
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Handle JSON string format
|
|
433
|
+
if (typeof value === 'string') {
|
|
434
|
+
try {
|
|
435
|
+
const parsed = JSON.parse(value);
|
|
436
|
+
if (Array.isArray(parsed)) {
|
|
437
|
+
const filtered = parsed.filter(Boolean);
|
|
438
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
439
|
+
}
|
|
440
|
+
// Single device ID string
|
|
441
|
+
return value ? [value] : undefined;
|
|
442
|
+
} catch {
|
|
443
|
+
// Not JSON, treat as single device ID
|
|
444
|
+
return value ? [value] : undefined;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
425
451
|
// ==================== DeviceProvider Implementation ====================
|
|
426
452
|
|
|
427
453
|
async getDevice(nativeId: string): Promise<any> {
|
|
@@ -697,95 +723,21 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
697
723
|
// ==================== 5. Tracking ====================
|
|
698
724
|
addGroup('Tracking');
|
|
699
725
|
|
|
700
|
-
// ==================== 6.
|
|
726
|
+
// ==================== 6. Integrations ====================
|
|
727
|
+
addGroup('Integrations');
|
|
728
|
+
|
|
729
|
+
// ==================== 7. AI & Spatial Reasoning ====================
|
|
701
730
|
addGroup('AI & Spatial Reasoning');
|
|
702
731
|
|
|
703
|
-
// ====================
|
|
732
|
+
// ==================== 8. Auto-Topology Discovery ====================
|
|
704
733
|
addGroup('Auto-Topology Discovery');
|
|
705
734
|
|
|
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
735
|
// ==================== 9. MQTT Integration ====================
|
|
721
736
|
addGroup('MQTT Integration');
|
|
722
737
|
|
|
723
738
|
return settings;
|
|
724
739
|
}
|
|
725
740
|
|
|
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
741
|
async putSetting(key: string, value: SettingValue): Promise<void> {
|
|
790
742
|
await this.storageSettings.putSetting(key, value);
|
|
791
743
|
|
|
@@ -802,6 +754,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
802
754
|
key === 'llmDebounceInterval' ||
|
|
803
755
|
key === 'llmFallbackEnabled' ||
|
|
804
756
|
key === 'llmFallbackTimeout' ||
|
|
757
|
+
key === 'llmDevices' ||
|
|
805
758
|
key === 'enableTransitTimeLearning' ||
|
|
806
759
|
key === 'enableConnectionSuggestions' ||
|
|
807
760
|
key === 'enableLandmarkLearning' ||
|