@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/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 +66 -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 +47 -28
- package/src/core/tracking-engine.ts +3 -0
- package/src/main.ts +18 -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 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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
241
|
-
|
|
242
|
-
title: '
|
|
243
|
-
type: '
|
|
244
|
-
|
|
245
|
-
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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.
|
|
695
|
+
// ==================== 6. Integrations ====================
|
|
696
|
+
addGroup('Integrations');
|
|
697
|
+
|
|
698
|
+
// ==================== 7. AI & Spatial Reasoning ====================
|
|
701
699
|
addGroup('AI & Spatial Reasoning');
|
|
702
700
|
|
|
703
|
-
// ====================
|
|
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' ||
|