@blueharford/scrypted-spatial-awareness 0.6.24 → 0.6.26
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 +203 -65
- 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 +108 -26
- package/src/core/topology-discovery.ts +84 -19
- package/src/core/tracking-engine.ts +42 -19
- package/src/main.ts +2 -2
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -35407,25 +35407,23 @@ class SpatialReasoningEngine {
|
|
|
35407
35407
|
llmSearched = false;
|
|
35408
35408
|
llmProvider = null;
|
|
35409
35409
|
llmProviderType = 'unknown';
|
|
35410
|
-
|
|
35411
|
-
|
|
35412
|
-
|
|
35413
|
-
|
|
35410
|
+
// Load balancing for multiple LLMs
|
|
35411
|
+
llmDevices = [];
|
|
35412
|
+
llmIndex = 0;
|
|
35413
|
+
/** Find ALL LLM devices for load balancing */
|
|
35414
|
+
async findAllLlmDevices() {
|
|
35414
35415
|
if (this.llmSearched)
|
|
35415
|
-
return
|
|
35416
|
+
return;
|
|
35416
35417
|
this.llmSearched = true;
|
|
35417
35418
|
try {
|
|
35418
|
-
// Look for devices with ChatCompletion interface (the correct interface for @scrypted/llm)
|
|
35419
35419
|
for (const id of Object.keys(systemManager.getSystemState())) {
|
|
35420
35420
|
const device = systemManager.getDeviceById(id);
|
|
35421
35421
|
if (!device)
|
|
35422
35422
|
continue;
|
|
35423
|
-
// Check if this device has ChatCompletion interface
|
|
35424
|
-
// The @scrypted/llm plugin exposes ChatCompletion, not ObjectDetection
|
|
35425
35423
|
if (device.interfaces?.includes('ChatCompletion')) {
|
|
35426
35424
|
const deviceName = device.name?.toLowerCase() || '';
|
|
35427
35425
|
const pluginId = device.pluginId?.toLowerCase() || '';
|
|
35428
|
-
// Identify the provider type for
|
|
35426
|
+
// Identify the provider type for image format selection
|
|
35429
35427
|
let providerType = 'Unknown';
|
|
35430
35428
|
let providerTypeEnum = 'unknown';
|
|
35431
35429
|
if (deviceName.includes('openai') || deviceName.includes('gpt')) {
|
|
@@ -35438,38 +35436,96 @@ class SpatialReasoningEngine {
|
|
|
35438
35436
|
}
|
|
35439
35437
|
else if (deviceName.includes('ollama')) {
|
|
35440
35438
|
providerType = 'Ollama';
|
|
35441
|
-
providerTypeEnum = 'openai';
|
|
35439
|
+
providerTypeEnum = 'openai';
|
|
35442
35440
|
}
|
|
35443
35441
|
else if (deviceName.includes('gemini') || deviceName.includes('google')) {
|
|
35444
35442
|
providerType = 'Google';
|
|
35445
|
-
providerTypeEnum = 'openai';
|
|
35443
|
+
providerTypeEnum = 'openai';
|
|
35446
35444
|
}
|
|
35447
35445
|
else if (deviceName.includes('llama')) {
|
|
35448
35446
|
providerType = 'llama.cpp';
|
|
35449
|
-
providerTypeEnum = 'openai';
|
|
35447
|
+
providerTypeEnum = 'openai';
|
|
35450
35448
|
}
|
|
35451
35449
|
else if (pluginId.includes('@scrypted/llm') || pluginId.includes('llm')) {
|
|
35452
35450
|
providerType = 'Scrypted LLM';
|
|
35453
35451
|
providerTypeEnum = 'unknown';
|
|
35454
35452
|
}
|
|
35455
|
-
this.
|
|
35456
|
-
|
|
35457
|
-
|
|
35458
|
-
|
|
35459
|
-
|
|
35460
|
-
|
|
35461
|
-
|
|
35462
|
-
|
|
35453
|
+
this.llmDevices.push({
|
|
35454
|
+
device: device,
|
|
35455
|
+
id,
|
|
35456
|
+
name: device.name || id,
|
|
35457
|
+
providerType: providerTypeEnum,
|
|
35458
|
+
lastUsed: 0,
|
|
35459
|
+
errorCount: 0,
|
|
35460
|
+
});
|
|
35461
|
+
this.console.log(`[LLM] Found ${providerType}: ${device.name}`);
|
|
35463
35462
|
}
|
|
35464
35463
|
}
|
|
35465
|
-
|
|
35466
|
-
|
|
35467
|
-
|
|
35464
|
+
if (this.llmDevices.length === 0) {
|
|
35465
|
+
this.console.warn('[LLM] No ChatCompletion devices found. Install @scrypted/llm for enhanced descriptions.');
|
|
35466
|
+
}
|
|
35467
|
+
else {
|
|
35468
|
+
this.console.log(`[LLM] Load balancing across ${this.llmDevices.length} LLM device(s)`);
|
|
35469
|
+
}
|
|
35468
35470
|
}
|
|
35469
35471
|
catch (e) {
|
|
35470
|
-
this.console.error('[LLM] Error searching for LLM
|
|
35472
|
+
this.console.error('[LLM] Error searching for LLM devices:', e);
|
|
35473
|
+
}
|
|
35474
|
+
}
|
|
35475
|
+
/** Get the next available LLM using round-robin with least-recently-used preference */
|
|
35476
|
+
async findLlmDevice() {
|
|
35477
|
+
await this.findAllLlmDevices();
|
|
35478
|
+
if (this.llmDevices.length === 0)
|
|
35479
|
+
return null;
|
|
35480
|
+
// If only one LLM, just use it
|
|
35481
|
+
if (this.llmDevices.length === 1) {
|
|
35482
|
+
const llm = this.llmDevices[0];
|
|
35483
|
+
this.llmDevice = llm.device;
|
|
35484
|
+
this.llmProvider = llm.name;
|
|
35485
|
+
this.llmProviderType = llm.providerType;
|
|
35486
|
+
return llm.device;
|
|
35487
|
+
}
|
|
35488
|
+
// Find the LLM with the oldest lastUsed time (least recently used)
|
|
35489
|
+
// Also prefer LLMs with fewer errors
|
|
35490
|
+
let bestIndex = 0;
|
|
35491
|
+
let bestScore = Infinity;
|
|
35492
|
+
for (let i = 0; i < this.llmDevices.length; i++) {
|
|
35493
|
+
const llm = this.llmDevices[i];
|
|
35494
|
+
// Score = lastUsed time + (errorCount * 60 seconds penalty)
|
|
35495
|
+
const score = llm.lastUsed + (llm.errorCount * 60000);
|
|
35496
|
+
if (score < bestScore) {
|
|
35497
|
+
bestScore = score;
|
|
35498
|
+
bestIndex = i;
|
|
35499
|
+
}
|
|
35500
|
+
}
|
|
35501
|
+
const selected = this.llmDevices[bestIndex];
|
|
35502
|
+
this.llmDevice = selected.device;
|
|
35503
|
+
this.llmProvider = selected.name;
|
|
35504
|
+
this.llmProviderType = selected.providerType;
|
|
35505
|
+
this.console.log(`[LLM] Selected: ${selected.name} (last used ${Math.round((Date.now() - selected.lastUsed) / 1000)}s ago, errors: ${selected.errorCount})`);
|
|
35506
|
+
return selected.device;
|
|
35507
|
+
}
|
|
35508
|
+
/** Mark an LLM as used (for load balancing) */
|
|
35509
|
+
markLlmUsed(device) {
|
|
35510
|
+
const llm = this.llmDevices.find(l => l.device === device);
|
|
35511
|
+
if (llm) {
|
|
35512
|
+
llm.lastUsed = Date.now();
|
|
35513
|
+
}
|
|
35514
|
+
}
|
|
35515
|
+
/** Mark an LLM as having an error (for load balancing - will be deprioritized) */
|
|
35516
|
+
markLlmError(device) {
|
|
35517
|
+
const llm = this.llmDevices.find(l => l.device === device);
|
|
35518
|
+
if (llm) {
|
|
35519
|
+
llm.errorCount++;
|
|
35520
|
+
this.console.log(`[LLM] ${llm.name} error count: ${llm.errorCount}`);
|
|
35521
|
+
}
|
|
35522
|
+
}
|
|
35523
|
+
/** Reset error count for an LLM after successful call */
|
|
35524
|
+
markLlmSuccess(device) {
|
|
35525
|
+
const llm = this.llmDevices.find(l => l.device === device);
|
|
35526
|
+
if (llm && llm.errorCount > 0) {
|
|
35527
|
+
llm.errorCount = Math.max(0, llm.errorCount - 1); // Gradually reduce error count
|
|
35471
35528
|
}
|
|
35472
|
-
return null;
|
|
35473
35529
|
}
|
|
35474
35530
|
/** Get the current LLM provider name */
|
|
35475
35531
|
getLlmProvider() {
|
|
@@ -35804,6 +35860,8 @@ class SpatialReasoningEngine {
|
|
|
35804
35860
|
// Fallback to text-only if image conversion failed
|
|
35805
35861
|
messageContent = prompt;
|
|
35806
35862
|
}
|
|
35863
|
+
// Mark LLM as used for load balancing
|
|
35864
|
+
this.markLlmUsed(llm);
|
|
35807
35865
|
// Call LLM using ChatCompletion interface
|
|
35808
35866
|
const result = await llm.getChatCompletion({
|
|
35809
35867
|
messages: [
|
|
@@ -35818,12 +35876,14 @@ class SpatialReasoningEngine {
|
|
|
35818
35876
|
// Extract description from ChatCompletion result
|
|
35819
35877
|
const content = result?.choices?.[0]?.message?.content;
|
|
35820
35878
|
if (content && typeof content === 'string') {
|
|
35879
|
+
this.markLlmSuccess(llm);
|
|
35821
35880
|
return content.trim();
|
|
35822
35881
|
}
|
|
35823
35882
|
return null;
|
|
35824
35883
|
}
|
|
35825
35884
|
catch (e) {
|
|
35826
35885
|
this.console.warn('LLM description generation failed:', e);
|
|
35886
|
+
this.markLlmError(llm);
|
|
35827
35887
|
return null;
|
|
35828
35888
|
}
|
|
35829
35889
|
}
|
|
@@ -35892,6 +35952,8 @@ Examples of good descriptions:
|
|
|
35892
35952
|
- "Landscaper with leaf blower heading to work truck"
|
|
35893
35953
|
|
|
35894
35954
|
Generate ONLY the description, nothing else:`;
|
|
35955
|
+
// Mark LLM as used for load balancing
|
|
35956
|
+
this.markLlmUsed(llm);
|
|
35895
35957
|
// Try multimodal format first, fall back to text-only if it fails
|
|
35896
35958
|
let result;
|
|
35897
35959
|
let usedVision = false;
|
|
@@ -35942,6 +36004,7 @@ Generate ONLY the description, nothing else:`;
|
|
|
35942
36004
|
const content = result?.choices?.[0]?.message?.content;
|
|
35943
36005
|
if (content && typeof content === 'string') {
|
|
35944
36006
|
this.console.log(`[LLM] Got ${eventType} description (vision=${usedVision}): ${content.trim().substring(0, 50)}...`);
|
|
36007
|
+
this.markLlmSuccess(llm);
|
|
35945
36008
|
return content.trim();
|
|
35946
36009
|
}
|
|
35947
36010
|
this.console.warn(`[LLM] No content in response for ${eventType}`);
|
|
@@ -35949,6 +36012,7 @@ Generate ONLY the description, nothing else:`;
|
|
|
35949
36012
|
}
|
|
35950
36013
|
catch (e) {
|
|
35951
36014
|
this.console.warn(`[LLM] ${eventType} description generation failed:`, e);
|
|
36015
|
+
this.markLlmError(llm);
|
|
35952
36016
|
return null;
|
|
35953
36017
|
}
|
|
35954
36018
|
}
|
|
@@ -36386,12 +36450,12 @@ class TopologyDiscoveryEngine {
|
|
|
36386
36450
|
isEnabled() {
|
|
36387
36451
|
return this.config.discoveryIntervalHours > 0;
|
|
36388
36452
|
}
|
|
36389
|
-
|
|
36390
|
-
|
|
36391
|
-
|
|
36392
|
-
|
|
36453
|
+
// Load balancing for multiple LLMs
|
|
36454
|
+
llmDevices = [];
|
|
36455
|
+
/** Find ALL LLM devices for load balancing */
|
|
36456
|
+
async findAllLlmDevices() {
|
|
36393
36457
|
if (this.llmSearched)
|
|
36394
|
-
return
|
|
36458
|
+
return;
|
|
36395
36459
|
this.llmSearched = true;
|
|
36396
36460
|
try {
|
|
36397
36461
|
for (const id of Object.keys(systemManager.getSystemState())) {
|
|
@@ -36400,33 +36464,77 @@ class TopologyDiscoveryEngine {
|
|
|
36400
36464
|
continue;
|
|
36401
36465
|
if (device.interfaces?.includes('ChatCompletion')) {
|
|
36402
36466
|
const deviceName = device.name?.toLowerCase() || '';
|
|
36403
|
-
|
|
36467
|
+
let providerType = 'unknown';
|
|
36404
36468
|
if (deviceName.includes('openai') || deviceName.includes('gpt')) {
|
|
36405
|
-
|
|
36469
|
+
providerType = 'openai';
|
|
36406
36470
|
}
|
|
36407
36471
|
else if (deviceName.includes('anthropic') || deviceName.includes('claude')) {
|
|
36408
|
-
|
|
36472
|
+
providerType = 'anthropic';
|
|
36409
36473
|
}
|
|
36410
36474
|
else if (deviceName.includes('ollama') || deviceName.includes('gemini') ||
|
|
36411
36475
|
deviceName.includes('google') || deviceName.includes('llama')) {
|
|
36412
|
-
|
|
36413
|
-
this.llmProviderType = 'openai';
|
|
36476
|
+
providerType = 'openai';
|
|
36414
36477
|
}
|
|
36415
|
-
|
|
36416
|
-
|
|
36417
|
-
|
|
36418
|
-
|
|
36419
|
-
|
|
36420
|
-
|
|
36421
|
-
|
|
36478
|
+
this.llmDevices.push({
|
|
36479
|
+
device: device,
|
|
36480
|
+
id,
|
|
36481
|
+
name: device.name || id,
|
|
36482
|
+
providerType,
|
|
36483
|
+
lastUsed: 0,
|
|
36484
|
+
errorCount: 0,
|
|
36485
|
+
});
|
|
36486
|
+
this.console.log(`[Discovery] Found LLM: ${device.name}`);
|
|
36422
36487
|
}
|
|
36423
36488
|
}
|
|
36424
|
-
this.
|
|
36489
|
+
if (this.llmDevices.length === 0) {
|
|
36490
|
+
this.console.warn('[Discovery] No ChatCompletion devices found. Vision-based discovery unavailable.');
|
|
36491
|
+
}
|
|
36492
|
+
else {
|
|
36493
|
+
this.console.log(`[Discovery] Load balancing across ${this.llmDevices.length} LLM device(s)`);
|
|
36494
|
+
}
|
|
36425
36495
|
}
|
|
36426
36496
|
catch (e) {
|
|
36427
|
-
this.console.error('[Discovery] Error finding LLM
|
|
36497
|
+
this.console.error('[Discovery] Error finding LLM devices:', e);
|
|
36498
|
+
}
|
|
36499
|
+
}
|
|
36500
|
+
/** Find LLM device with ChatCompletion interface - uses load balancing */
|
|
36501
|
+
async findLlmDevice() {
|
|
36502
|
+
await this.findAllLlmDevices();
|
|
36503
|
+
if (this.llmDevices.length === 0)
|
|
36504
|
+
return null;
|
|
36505
|
+
// If only one LLM, just use it
|
|
36506
|
+
if (this.llmDevices.length === 1) {
|
|
36507
|
+
const llm = this.llmDevices[0];
|
|
36508
|
+
this.llmDevice = llm.device;
|
|
36509
|
+
this.llmProviderType = llm.providerType;
|
|
36510
|
+
return llm.device;
|
|
36511
|
+
}
|
|
36512
|
+
// Find the LLM with oldest lastUsed time (least recently used)
|
|
36513
|
+
let bestIndex = 0;
|
|
36514
|
+
let bestScore = Infinity;
|
|
36515
|
+
for (let i = 0; i < this.llmDevices.length; i++) {
|
|
36516
|
+
const llm = this.llmDevices[i];
|
|
36517
|
+
const score = llm.lastUsed + (llm.errorCount * 60000);
|
|
36518
|
+
if (score < bestScore) {
|
|
36519
|
+
bestScore = score;
|
|
36520
|
+
bestIndex = i;
|
|
36521
|
+
}
|
|
36522
|
+
}
|
|
36523
|
+
const selected = this.llmDevices[bestIndex];
|
|
36524
|
+
this.llmDevice = selected.device;
|
|
36525
|
+
this.llmProviderType = selected.providerType;
|
|
36526
|
+
// Mark as used
|
|
36527
|
+
selected.lastUsed = Date.now();
|
|
36528
|
+
this.console.log(`[Discovery] Selected LLM: ${selected.name}`);
|
|
36529
|
+
return selected.device;
|
|
36530
|
+
}
|
|
36531
|
+
/** Mark an LLM as having an error */
|
|
36532
|
+
markLlmError(device) {
|
|
36533
|
+
const llm = this.llmDevices.find(l => l.device === device);
|
|
36534
|
+
if (llm) {
|
|
36535
|
+
llm.errorCount++;
|
|
36536
|
+
this.console.log(`[Discovery] ${llm.name} error count: ${llm.errorCount}`);
|
|
36428
36537
|
}
|
|
36429
|
-
return null;
|
|
36430
36538
|
}
|
|
36431
36539
|
/** Get camera snapshot as ImageData */
|
|
36432
36540
|
async getCameraSnapshot(cameraId) {
|
|
@@ -36602,6 +36710,10 @@ Use the mount height to help estimate distances - objects at ground level will a
|
|
|
36602
36710
|
}
|
|
36603
36711
|
// All formats failed
|
|
36604
36712
|
if (lastError) {
|
|
36713
|
+
// Track error for load balancing
|
|
36714
|
+
if (llm) {
|
|
36715
|
+
this.markLlmError(llm);
|
|
36716
|
+
}
|
|
36605
36717
|
const errorStr = String(lastError);
|
|
36606
36718
|
if ((0, spatial_reasoning_1.isVisionFormatError)(lastError)) {
|
|
36607
36719
|
analysis.error = 'Vision/image analysis failed with all formats. Ensure you have a vision-capable model (e.g., gpt-4o, gpt-4-turbo, claude-3-sonnet) configured and the @scrypted/llm plugin supports vision.';
|
|
@@ -37351,14 +37463,23 @@ class TrackingEngine {
|
|
|
37351
37463
|
recordLlmCall() {
|
|
37352
37464
|
this.lastLlmCallTime = Date.now();
|
|
37353
37465
|
}
|
|
37466
|
+
/** Check and record LLM call - returns false if rate limited */
|
|
37467
|
+
tryLlmCall() {
|
|
37468
|
+
if (!this.isLlmCallAllowed()) {
|
|
37469
|
+
this.console.log('[LLM] Rate limited, skipping LLM call');
|
|
37470
|
+
return false;
|
|
37471
|
+
}
|
|
37472
|
+
this.recordLlmCall();
|
|
37473
|
+
return true;
|
|
37474
|
+
}
|
|
37354
37475
|
/** Get spatial reasoning result for movement (uses RAG + LLM) with debouncing and fallback */
|
|
37355
37476
|
async getSpatialDescription(tracked, fromCameraId, toCameraId, transitTime, currentCameraId) {
|
|
37356
37477
|
const fallbackEnabled = this.config.llmFallbackEnabled ?? true;
|
|
37357
37478
|
const fallbackTimeout = this.config.llmFallbackTimeout ?? 3000;
|
|
37358
37479
|
try {
|
|
37359
37480
|
// Check rate limiting - if not allowed, return null to use basic description
|
|
37360
|
-
if (!this.
|
|
37361
|
-
this.console.log('LLM rate-limited, using basic notification');
|
|
37481
|
+
if (!this.tryLlmCall()) {
|
|
37482
|
+
this.console.log('[Movement] LLM rate-limited, using basic notification');
|
|
37362
37483
|
return null;
|
|
37363
37484
|
}
|
|
37364
37485
|
// Get snapshot from camera for LLM analysis (if LLM is enabled)
|
|
@@ -37369,8 +37490,6 @@ class TrackingEngine {
|
|
|
37369
37490
|
mediaObject = await camera.takePicture();
|
|
37370
37491
|
}
|
|
37371
37492
|
}
|
|
37372
|
-
// Record that we're making an LLM call
|
|
37373
|
-
this.recordLlmCall();
|
|
37374
37493
|
// Use spatial reasoning engine for rich context-aware description
|
|
37375
37494
|
// Apply timeout if fallback is enabled
|
|
37376
37495
|
let result;
|
|
@@ -37524,17 +37643,25 @@ class TrackingEngine {
|
|
|
37524
37643
|
this.console.log(`[Entry Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
37525
37644
|
}
|
|
37526
37645
|
catch (e) {
|
|
37527
|
-
this.console.warn(`[Entry Alert] Prefetch failed,
|
|
37646
|
+
this.console.warn(`[Entry Alert] Prefetch failed, using basic description: ${e}`);
|
|
37647
|
+
// Don't make another LLM call - use basic description (no mediaObject = no LLM)
|
|
37528
37648
|
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
|
|
37529
37649
|
}
|
|
37530
37650
|
this.pendingDescriptions.delete(globalId);
|
|
37531
37651
|
}
|
|
37532
37652
|
else {
|
|
37533
|
-
//
|
|
37534
|
-
this.
|
|
37535
|
-
|
|
37536
|
-
|
|
37537
|
-
|
|
37653
|
+
// No prefetch available - only call LLM if rate limit allows
|
|
37654
|
+
if (this.tryLlmCall()) {
|
|
37655
|
+
this.console.log(`[Entry Alert] No prefetch, generating with LLM`);
|
|
37656
|
+
const mediaObject = this.snapshotCache.get(globalId);
|
|
37657
|
+
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId, mediaObject);
|
|
37658
|
+
this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
37659
|
+
}
|
|
37660
|
+
else {
|
|
37661
|
+
// Rate limited - use basic description (no LLM)
|
|
37662
|
+
this.console.log(`[Entry Alert] Rate limited, using basic description`);
|
|
37663
|
+
spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
|
|
37664
|
+
}
|
|
37538
37665
|
}
|
|
37539
37666
|
// Always use movement alert type for smart notifications with LLM descriptions
|
|
37540
37667
|
// The property_entry/property_exit types are legacy and disabled by default
|
|
@@ -37561,9 +37688,9 @@ class TrackingEngine {
|
|
|
37561
37688
|
if (mediaObject) {
|
|
37562
37689
|
this.snapshotCache.set(globalId, mediaObject);
|
|
37563
37690
|
this.console.log(`[Snapshot] Cached snapshot for ${globalId.slice(0, 8)} from ${cameraId}`);
|
|
37564
|
-
// Start LLM analysis immediately in parallel (don't await)
|
|
37691
|
+
// Start LLM analysis immediately in parallel (don't await) - but respect rate limits
|
|
37565
37692
|
const tracked = this.state.getObject(globalId);
|
|
37566
|
-
if (tracked && this.config.useLlmDescriptions) {
|
|
37693
|
+
if (tracked && this.config.useLlmDescriptions && this.tryLlmCall()) {
|
|
37567
37694
|
this.console.log(`[LLM Prefetch] Starting ${eventType} analysis for ${globalId.slice(0, 8)}`);
|
|
37568
37695
|
const descriptionPromise = eventType === 'exit'
|
|
37569
37696
|
? this.spatialReasoning.generateExitDescription(tracked, cameraId, mediaObject)
|
|
@@ -37576,6 +37703,9 @@ class TrackingEngine {
|
|
|
37576
37703
|
this.console.warn(`[LLM Prefetch] Failed for ${globalId.slice(0, 8)}: ${e}`);
|
|
37577
37704
|
});
|
|
37578
37705
|
}
|
|
37706
|
+
else if (tracked && this.config.useLlmDescriptions) {
|
|
37707
|
+
this.console.log(`[LLM Prefetch] Skipped for ${globalId.slice(0, 8)} - rate limited`);
|
|
37708
|
+
}
|
|
37579
37709
|
}
|
|
37580
37710
|
}
|
|
37581
37711
|
}
|
|
@@ -37644,17 +37774,25 @@ class TrackingEngine {
|
|
|
37644
37774
|
this.console.log(`[Exit Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
37645
37775
|
}
|
|
37646
37776
|
catch (e) {
|
|
37647
|
-
this.console.warn(`[Exit Alert] Prefetch failed,
|
|
37777
|
+
this.console.warn(`[Exit Alert] Prefetch failed, using basic description: ${e}`);
|
|
37778
|
+
// Don't make another LLM call - use basic description
|
|
37648
37779
|
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
|
|
37649
37780
|
}
|
|
37650
37781
|
this.pendingDescriptions.delete(tracked.globalId);
|
|
37651
37782
|
}
|
|
37652
37783
|
else {
|
|
37653
|
-
//
|
|
37654
|
-
this.
|
|
37655
|
-
|
|
37656
|
-
|
|
37657
|
-
|
|
37784
|
+
// No prefetch available - only call LLM if rate limit allows
|
|
37785
|
+
if (this.tryLlmCall()) {
|
|
37786
|
+
this.console.log(`[Exit Alert] No prefetch, generating with LLM`);
|
|
37787
|
+
const mediaObject = this.snapshotCache.get(tracked.globalId);
|
|
37788
|
+
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId, mediaObject);
|
|
37789
|
+
this.console.log(`[Exit Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
|
|
37790
|
+
}
|
|
37791
|
+
else {
|
|
37792
|
+
// Rate limited - use basic description (no LLM)
|
|
37793
|
+
this.console.log(`[Exit Alert] Rate limited, using basic description`);
|
|
37794
|
+
spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
|
|
37795
|
+
}
|
|
37658
37796
|
}
|
|
37659
37797
|
// Use movement alert for exit too - smart notifications with LLM descriptions
|
|
37660
37798
|
await this.alertManager.checkAndAlert('movement', current, {
|
|
@@ -39197,8 +39335,8 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
39197
39335
|
llmDebounceInterval: {
|
|
39198
39336
|
title: 'LLM Rate Limit (seconds)',
|
|
39199
39337
|
type: 'number',
|
|
39200
|
-
defaultValue:
|
|
39201
|
-
description: 'Minimum time between LLM calls to prevent API
|
|
39338
|
+
defaultValue: 30,
|
|
39339
|
+
description: 'Minimum time between LLM calls to prevent API rate limiting. Increase if you get rate limit errors. (0 = no limit)',
|
|
39202
39340
|
group: 'AI & Spatial Reasoning',
|
|
39203
39341
|
},
|
|
39204
39342
|
llmFallbackEnabled: {
|