@blueharford/scrypted-spatial-awareness 0.2.1 → 0.4.0
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/README.md +175 -10
- 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 +2585 -60
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/tracking-engine.ts +963 -10
- package/src/main.ts +492 -19
- package/src/models/training.ts +300 -0
- package/src/ui/editor-html.ts +256 -0
- package/src/ui/training-html.ts +1007 -0
package/src/main.ts
CHANGED
|
@@ -32,6 +32,8 @@ import { GlobalTrackerSensor } from './devices/global-tracker-sensor';
|
|
|
32
32
|
import { TrackingZone } from './devices/tracking-zone';
|
|
33
33
|
import { MqttPublisher, MqttConfig } from './integrations/mqtt-publisher';
|
|
34
34
|
import { EDITOR_HTML } from './ui/editor-html';
|
|
35
|
+
import { TRAINING_HTML } from './ui/training-html';
|
|
36
|
+
import { TrainingConfig, TrainingLandmark } from './models/training';
|
|
35
37
|
|
|
36
38
|
const { deviceManager, systemManager } = sdk;
|
|
37
39
|
|
|
@@ -115,6 +117,41 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
115
117
|
description: 'Use LLM plugin (if installed) to generate descriptive alerts like "Man walking from garage towards front door"',
|
|
116
118
|
group: 'AI & Spatial Reasoning',
|
|
117
119
|
},
|
|
120
|
+
llmDebounceInterval: {
|
|
121
|
+
title: 'LLM Rate Limit (seconds)',
|
|
122
|
+
type: 'number',
|
|
123
|
+
defaultValue: 10,
|
|
124
|
+
description: 'Minimum time between LLM calls to prevent API overload (0 = no limit)',
|
|
125
|
+
group: 'AI & Spatial Reasoning',
|
|
126
|
+
},
|
|
127
|
+
llmFallbackEnabled: {
|
|
128
|
+
title: 'Fallback to Basic Notifications',
|
|
129
|
+
type: 'boolean',
|
|
130
|
+
defaultValue: true,
|
|
131
|
+
description: 'When LLM is rate-limited or slow, fall back to basic notifications immediately',
|
|
132
|
+
group: 'AI & Spatial Reasoning',
|
|
133
|
+
},
|
|
134
|
+
llmFallbackTimeout: {
|
|
135
|
+
title: 'LLM Timeout (seconds)',
|
|
136
|
+
type: 'number',
|
|
137
|
+
defaultValue: 3,
|
|
138
|
+
description: 'Maximum time to wait for LLM response before falling back to basic notification',
|
|
139
|
+
group: 'AI & Spatial Reasoning',
|
|
140
|
+
},
|
|
141
|
+
enableTransitTimeLearning: {
|
|
142
|
+
title: 'Learn Transit Times',
|
|
143
|
+
type: 'boolean',
|
|
144
|
+
defaultValue: true,
|
|
145
|
+
description: 'Automatically adjust connection transit times based on observed movement patterns',
|
|
146
|
+
group: 'AI & Spatial Reasoning',
|
|
147
|
+
},
|
|
148
|
+
enableConnectionSuggestions: {
|
|
149
|
+
title: 'Suggest Camera Connections',
|
|
150
|
+
type: 'boolean',
|
|
151
|
+
defaultValue: true,
|
|
152
|
+
description: 'Automatically suggest new camera connections based on observed movement patterns',
|
|
153
|
+
group: 'AI & Spatial Reasoning',
|
|
154
|
+
},
|
|
118
155
|
enableLandmarkLearning: {
|
|
119
156
|
title: 'Learn Landmarks from AI',
|
|
120
157
|
type: 'boolean',
|
|
@@ -291,6 +328,11 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
291
328
|
loiteringThreshold: (this.storageSettings.values.loiteringThreshold as number || 3) * 1000,
|
|
292
329
|
objectAlertCooldown: (this.storageSettings.values.objectAlertCooldown as number || 30) * 1000,
|
|
293
330
|
useLlmDescriptions: this.storageSettings.values.useLlmDescriptions as boolean ?? true,
|
|
331
|
+
llmDebounceInterval: (this.storageSettings.values.llmDebounceInterval as number || 10) * 1000,
|
|
332
|
+
llmFallbackEnabled: this.storageSettings.values.llmFallbackEnabled as boolean ?? true,
|
|
333
|
+
llmFallbackTimeout: (this.storageSettings.values.llmFallbackTimeout as number || 3) * 1000,
|
|
334
|
+
enableTransitTimeLearning: this.storageSettings.values.enableTransitTimeLearning as boolean ?? true,
|
|
335
|
+
enableConnectionSuggestions: this.storageSettings.values.enableConnectionSuggestions as boolean ?? true,
|
|
294
336
|
enableLandmarkLearning: this.storageSettings.values.enableLandmarkLearning as boolean ?? true,
|
|
295
337
|
landmarkConfidenceThreshold: this.storageSettings.values.landmarkConfidenceThreshold as number ?? 0.7,
|
|
296
338
|
};
|
|
@@ -392,8 +434,91 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
392
434
|
async getSettings(): Promise<Setting[]> {
|
|
393
435
|
const settings = await this.storageSettings.getSettings();
|
|
394
436
|
|
|
437
|
+
// Training Mode button that opens mobile-friendly training UI in modal
|
|
438
|
+
const trainingOnclickCode = `(function(){var e=document.getElementById('sa-training-modal');if(e)e.remove();var m=document.createElement('div');m.id='sa-training-modal';m.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:2147483647;display:flex;align-items:center;justify-content:center;';var c=document.createElement('div');c.style.cssText='width:min(420px,95vw);height:92vh;max-height:900px;background:#121212;border-radius:8px;overflow:hidden;position:relative;box-shadow:0 8px 32px rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.1);';var b=document.createElement('button');b.innerHTML='×';b.style.cssText='position:absolute;top:8px;right:8px;z-index:2147483647;background:rgba(255,255,255,0.1);color:white;border:none;width:32px;height:32px;border-radius:4px;font-size:18px;cursor:pointer;line-height:1;';b.onclick=function(){m.remove();};var f=document.createElement('iframe');f.src='/endpoint/@blueharford/scrypted-spatial-awareness/ui/training';f.style.cssText='width:100%;height:100%;border:none;';c.appendChild(b);c.appendChild(f);m.appendChild(c);m.onclick=function(ev){if(ev.target===m)m.remove();};document.body.appendChild(m);})()`;
|
|
439
|
+
|
|
440
|
+
settings.push({
|
|
441
|
+
key: 'trainingMode',
|
|
442
|
+
title: 'Training Mode',
|
|
443
|
+
type: 'html' as any,
|
|
444
|
+
value: `
|
|
445
|
+
<style>
|
|
446
|
+
.sa-training-container {
|
|
447
|
+
padding: 16px;
|
|
448
|
+
background: rgba(255,255,255,0.03);
|
|
449
|
+
border-radius: 4px;
|
|
450
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
451
|
+
}
|
|
452
|
+
.sa-training-title {
|
|
453
|
+
color: #4fc3f7;
|
|
454
|
+
font-size: 14px;
|
|
455
|
+
font-weight: 500;
|
|
456
|
+
margin-bottom: 8px;
|
|
457
|
+
font-family: inherit;
|
|
458
|
+
}
|
|
459
|
+
.sa-training-desc {
|
|
460
|
+
color: rgba(255,255,255,0.6);
|
|
461
|
+
margin-bottom: 12px;
|
|
462
|
+
font-size: 13px;
|
|
463
|
+
line-height: 1.5;
|
|
464
|
+
font-family: inherit;
|
|
465
|
+
}
|
|
466
|
+
.sa-training-btn {
|
|
467
|
+
background: #4fc3f7;
|
|
468
|
+
color: #000;
|
|
469
|
+
border: none;
|
|
470
|
+
padding: 10px 20px;
|
|
471
|
+
border-radius: 4px;
|
|
472
|
+
font-size: 14px;
|
|
473
|
+
font-weight: 500;
|
|
474
|
+
cursor: pointer;
|
|
475
|
+
display: inline-flex;
|
|
476
|
+
align-items: center;
|
|
477
|
+
gap: 8px;
|
|
478
|
+
transition: background 0.2s;
|
|
479
|
+
font-family: inherit;
|
|
480
|
+
}
|
|
481
|
+
.sa-training-btn:hover {
|
|
482
|
+
background: #81d4fa;
|
|
483
|
+
}
|
|
484
|
+
.sa-training-steps {
|
|
485
|
+
color: rgba(255,255,255,0.5);
|
|
486
|
+
font-size: 12px;
|
|
487
|
+
margin-top: 12px;
|
|
488
|
+
padding-top: 12px;
|
|
489
|
+
border-top: 1px solid rgba(255,255,255,0.05);
|
|
490
|
+
font-family: inherit;
|
|
491
|
+
}
|
|
492
|
+
.sa-training-steps ol {
|
|
493
|
+
margin: 6px 0 0 16px;
|
|
494
|
+
padding: 0;
|
|
495
|
+
}
|
|
496
|
+
.sa-training-steps li {
|
|
497
|
+
margin-bottom: 2px;
|
|
498
|
+
}
|
|
499
|
+
</style>
|
|
500
|
+
<div class="sa-training-container">
|
|
501
|
+
<div class="sa-training-title">Guided Property Training</div>
|
|
502
|
+
<p class="sa-training-desc">Walk your property while the system learns your camera layout, transit times, and landmarks automatically.</p>
|
|
503
|
+
<button class="sa-training-btn" onclick="${trainingOnclickCode}">
|
|
504
|
+
Start Training Mode
|
|
505
|
+
</button>
|
|
506
|
+
<div class="sa-training-steps">
|
|
507
|
+
<strong>How it works:</strong>
|
|
508
|
+
<ol>
|
|
509
|
+
<li>Start training and walk to each camera</li>
|
|
510
|
+
<li>System auto-detects you and records transit times</li>
|
|
511
|
+
<li>Mark landmarks as you encounter them</li>
|
|
512
|
+
<li>Apply results to generate your topology</li>
|
|
513
|
+
</ol>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
`,
|
|
517
|
+
group: 'Getting Started',
|
|
518
|
+
});
|
|
519
|
+
|
|
395
520
|
// Topology editor button that opens modal overlay (appended to body for proper z-index)
|
|
396
|
-
const onclickCode = `(function(){var e=document.getElementById('sa-topology-modal');if(e)e.remove();var m=document.createElement('div');m.id='sa-topology-modal';m.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.
|
|
521
|
+
const onclickCode = `(function(){var e=document.getElementById('sa-topology-modal');if(e)e.remove();var m=document.createElement('div');m.id='sa-topology-modal';m.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:2147483647;display:flex;align-items:center;justify-content:center;';var c=document.createElement('div');c.style.cssText='width:95vw;height:92vh;max-width:1800px;background:#121212;border-radius:8px;overflow:hidden;position:relative;box-shadow:0 8px 32px rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.1);';var b=document.createElement('button');b.innerHTML='×';b.style.cssText='position:absolute;top:8px;right:8px;z-index:2147483647;background:rgba(255,255,255,0.1);color:white;border:none;width:32px;height:32px;border-radius:4px;font-size:18px;cursor:pointer;line-height:1;';b.onclick=function(){m.remove();};var f=document.createElement('iframe');f.src='/endpoint/@blueharford/scrypted-spatial-awareness/ui/editor';f.style.cssText='width:100%;height:100%;border:none;';c.appendChild(b);c.appendChild(f);m.appendChild(c);m.onclick=function(ev){if(ev.target===m)m.remove();};document.body.appendChild(m);})()`;
|
|
397
522
|
|
|
398
523
|
settings.push({
|
|
399
524
|
key: 'topologyEditor',
|
|
@@ -402,39 +527,41 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
402
527
|
value: `
|
|
403
528
|
<style>
|
|
404
529
|
.sa-open-btn {
|
|
405
|
-
background:
|
|
406
|
-
color:
|
|
530
|
+
background: #4fc3f7;
|
|
531
|
+
color: #000;
|
|
407
532
|
border: none;
|
|
408
|
-
padding:
|
|
409
|
-
border-radius:
|
|
410
|
-
font-size:
|
|
411
|
-
font-weight:
|
|
533
|
+
padding: 10px 20px;
|
|
534
|
+
border-radius: 4px;
|
|
535
|
+
font-size: 14px;
|
|
536
|
+
font-weight: 500;
|
|
412
537
|
cursor: pointer;
|
|
413
538
|
display: inline-flex;
|
|
414
539
|
align-items: center;
|
|
415
|
-
gap:
|
|
416
|
-
transition:
|
|
540
|
+
gap: 8px;
|
|
541
|
+
transition: background 0.2s;
|
|
542
|
+
font-family: inherit;
|
|
417
543
|
}
|
|
418
544
|
.sa-open-btn:hover {
|
|
419
|
-
|
|
420
|
-
box-shadow: 0 8px 20px rgba(233, 69, 96, 0.4);
|
|
545
|
+
background: #81d4fa;
|
|
421
546
|
}
|
|
422
547
|
.sa-btn-container {
|
|
423
|
-
padding:
|
|
424
|
-
background:
|
|
425
|
-
border-radius:
|
|
548
|
+
padding: 16px;
|
|
549
|
+
background: rgba(255,255,255,0.03);
|
|
550
|
+
border-radius: 4px;
|
|
426
551
|
text-align: center;
|
|
552
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
427
553
|
}
|
|
428
554
|
.sa-btn-desc {
|
|
429
|
-
color:
|
|
430
|
-
margin-bottom:
|
|
431
|
-
font-size:
|
|
555
|
+
color: rgba(255,255,255,0.6);
|
|
556
|
+
margin-bottom: 12px;
|
|
557
|
+
font-size: 13px;
|
|
558
|
+
font-family: inherit;
|
|
432
559
|
}
|
|
433
560
|
</style>
|
|
434
561
|
<div class="sa-btn-container">
|
|
435
562
|
<p class="sa-btn-desc">Configure camera positions, connections, and transit times</p>
|
|
436
563
|
<button class="sa-open-btn" onclick="${onclickCode}">
|
|
437
|
-
|
|
564
|
+
Open Topology Editor
|
|
438
565
|
</button>
|
|
439
566
|
</div>
|
|
440
567
|
`,
|
|
@@ -576,6 +703,11 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
576
703
|
key === 'loiteringThreshold' ||
|
|
577
704
|
key === 'objectAlertCooldown' ||
|
|
578
705
|
key === 'useLlmDescriptions' ||
|
|
706
|
+
key === 'llmDebounceInterval' ||
|
|
707
|
+
key === 'llmFallbackEnabled' ||
|
|
708
|
+
key === 'llmFallbackTimeout' ||
|
|
709
|
+
key === 'enableTransitTimeLearning' ||
|
|
710
|
+
key === 'enableConnectionSuggestions' ||
|
|
579
711
|
key === 'enableLandmarkLearning' ||
|
|
580
712
|
key === 'landmarkConfidenceThreshold'
|
|
581
713
|
) {
|
|
@@ -668,11 +800,61 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
668
800
|
return this.handleInferRelationshipsRequest(response);
|
|
669
801
|
}
|
|
670
802
|
|
|
803
|
+
// Connection suggestions
|
|
804
|
+
if (path.endsWith('/api/connection-suggestions')) {
|
|
805
|
+
return this.handleConnectionSuggestionsRequest(request, response);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (path.match(/\/api\/connection-suggestions\/[\w->]+\/(accept|reject)$/)) {
|
|
809
|
+
const parts = path.split('/');
|
|
810
|
+
const action = parts.pop()!;
|
|
811
|
+
const suggestionId = parts.pop()!;
|
|
812
|
+
return this.handleConnectionSuggestionActionRequest(suggestionId, action, response);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Live tracking state
|
|
816
|
+
if (path.endsWith('/api/live-tracking')) {
|
|
817
|
+
return this.handleLiveTrackingRequest(response);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Journey visualization
|
|
821
|
+
if (path.match(/\/api\/journey-path\/[\w-]+$/)) {
|
|
822
|
+
const globalId = path.split('/').pop()!;
|
|
823
|
+
return this.handleJourneyPathRequest(globalId, response);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Training Mode endpoints
|
|
827
|
+
if (path.endsWith('/api/training/start')) {
|
|
828
|
+
return this.handleTrainingStartRequest(request, response);
|
|
829
|
+
}
|
|
830
|
+
if (path.endsWith('/api/training/pause')) {
|
|
831
|
+
return this.handleTrainingPauseRequest(response);
|
|
832
|
+
}
|
|
833
|
+
if (path.endsWith('/api/training/resume')) {
|
|
834
|
+
return this.handleTrainingResumeRequest(response);
|
|
835
|
+
}
|
|
836
|
+
if (path.endsWith('/api/training/end')) {
|
|
837
|
+
return this.handleTrainingEndRequest(response);
|
|
838
|
+
}
|
|
839
|
+
if (path.endsWith('/api/training/status')) {
|
|
840
|
+
return this.handleTrainingStatusRequest(response);
|
|
841
|
+
}
|
|
842
|
+
if (path.endsWith('/api/training/landmark')) {
|
|
843
|
+
return this.handleTrainingLandmarkRequest(request, response);
|
|
844
|
+
}
|
|
845
|
+
if (path.endsWith('/api/training/apply')) {
|
|
846
|
+
return this.handleTrainingApplyRequest(response);
|
|
847
|
+
}
|
|
848
|
+
|
|
671
849
|
// UI Routes
|
|
672
850
|
if (path.endsWith('/ui/editor') || path.endsWith('/ui/editor/')) {
|
|
673
851
|
return this.serveEditorUI(response);
|
|
674
852
|
}
|
|
675
853
|
|
|
854
|
+
if (path.endsWith('/ui/training') || path.endsWith('/ui/training/')) {
|
|
855
|
+
return this.serveTrainingUI(response);
|
|
856
|
+
}
|
|
857
|
+
|
|
676
858
|
if (path.includes('/ui/')) {
|
|
677
859
|
return this.serveStaticFile(path, response);
|
|
678
860
|
}
|
|
@@ -680,17 +862,31 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
680
862
|
// Default: return info page
|
|
681
863
|
response.send(JSON.stringify({
|
|
682
864
|
name: 'Spatial Awareness Plugin',
|
|
683
|
-
version: '0.
|
|
865
|
+
version: '0.4.0',
|
|
684
866
|
endpoints: {
|
|
685
867
|
api: {
|
|
686
868
|
trackedObjects: '/api/tracked-objects',
|
|
687
869
|
journey: '/api/journey/{globalId}',
|
|
870
|
+
journeyPath: '/api/journey-path/{globalId}',
|
|
688
871
|
topology: '/api/topology',
|
|
689
872
|
alerts: '/api/alerts',
|
|
690
873
|
floorPlan: '/api/floor-plan',
|
|
874
|
+
liveTracking: '/api/live-tracking',
|
|
875
|
+
connectionSuggestions: '/api/connection-suggestions',
|
|
876
|
+
landmarkSuggestions: '/api/landmark-suggestions',
|
|
877
|
+
training: {
|
|
878
|
+
start: '/api/training/start',
|
|
879
|
+
pause: '/api/training/pause',
|
|
880
|
+
resume: '/api/training/resume',
|
|
881
|
+
end: '/api/training/end',
|
|
882
|
+
status: '/api/training/status',
|
|
883
|
+
landmark: '/api/training/landmark',
|
|
884
|
+
apply: '/api/training/apply',
|
|
885
|
+
},
|
|
691
886
|
},
|
|
692
887
|
ui: {
|
|
693
888
|
editor: '/ui/editor',
|
|
889
|
+
training: '/ui/training',
|
|
694
890
|
},
|
|
695
891
|
},
|
|
696
892
|
}), {
|
|
@@ -1054,12 +1250,289 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
1054
1250
|
});
|
|
1055
1251
|
}
|
|
1056
1252
|
|
|
1253
|
+
private handleConnectionSuggestionsRequest(request: HttpRequest, response: HttpResponse): void {
|
|
1254
|
+
if (!this.trackingEngine) {
|
|
1255
|
+
response.send(JSON.stringify({ suggestions: [] }), {
|
|
1256
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1257
|
+
});
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const suggestions = this.trackingEngine.getConnectionSuggestions();
|
|
1262
|
+
response.send(JSON.stringify({
|
|
1263
|
+
suggestions,
|
|
1264
|
+
count: suggestions.length,
|
|
1265
|
+
}), {
|
|
1266
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
private handleConnectionSuggestionActionRequest(
|
|
1271
|
+
suggestionId: string,
|
|
1272
|
+
action: string,
|
|
1273
|
+
response: HttpResponse
|
|
1274
|
+
): void {
|
|
1275
|
+
if (!this.trackingEngine) {
|
|
1276
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1277
|
+
code: 500,
|
|
1278
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1279
|
+
});
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (action === 'accept') {
|
|
1284
|
+
const connection = this.trackingEngine.acceptConnectionSuggestion(suggestionId);
|
|
1285
|
+
if (connection) {
|
|
1286
|
+
// Save updated topology
|
|
1287
|
+
const topology = this.trackingEngine.getTopology();
|
|
1288
|
+
this.storage.setItem('topology', JSON.stringify(topology));
|
|
1289
|
+
|
|
1290
|
+
response.send(JSON.stringify({ success: true, connection }), {
|
|
1291
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1292
|
+
});
|
|
1293
|
+
} else {
|
|
1294
|
+
response.send(JSON.stringify({ error: 'Suggestion not found' }), {
|
|
1295
|
+
code: 404,
|
|
1296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
} else if (action === 'reject') {
|
|
1300
|
+
const success = this.trackingEngine.rejectConnectionSuggestion(suggestionId);
|
|
1301
|
+
if (success) {
|
|
1302
|
+
response.send(JSON.stringify({ success: true }), {
|
|
1303
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1304
|
+
});
|
|
1305
|
+
} else {
|
|
1306
|
+
response.send(JSON.stringify({ error: 'Suggestion not found' }), {
|
|
1307
|
+
code: 404,
|
|
1308
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
} else {
|
|
1312
|
+
response.send(JSON.stringify({ error: 'Invalid action' }), {
|
|
1313
|
+
code: 400,
|
|
1314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
private handleLiveTrackingRequest(response: HttpResponse): void {
|
|
1320
|
+
if (!this.trackingEngine) {
|
|
1321
|
+
response.send(JSON.stringify({ objects: [], timestamp: Date.now() }), {
|
|
1322
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1323
|
+
});
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const liveState = this.trackingEngine.getLiveTrackingState();
|
|
1328
|
+
response.send(JSON.stringify(liveState), {
|
|
1329
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
private handleJourneyPathRequest(globalId: string, response: HttpResponse): void {
|
|
1334
|
+
if (!this.trackingEngine) {
|
|
1335
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1336
|
+
code: 500,
|
|
1337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1338
|
+
});
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const journeyPath = this.trackingEngine.getJourneyPath(globalId);
|
|
1343
|
+
if (journeyPath) {
|
|
1344
|
+
response.send(JSON.stringify(journeyPath), {
|
|
1345
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1346
|
+
});
|
|
1347
|
+
} else {
|
|
1348
|
+
response.send(JSON.stringify({ error: 'Object not found' }), {
|
|
1349
|
+
code: 404,
|
|
1350
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// ==================== Training Mode Handlers ====================
|
|
1356
|
+
|
|
1357
|
+
private handleTrainingStartRequest(request: HttpRequest, response: HttpResponse): void {
|
|
1358
|
+
if (!this.trackingEngine) {
|
|
1359
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running. Configure topology first.' }), {
|
|
1360
|
+
code: 500,
|
|
1361
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1362
|
+
});
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
try {
|
|
1367
|
+
let config: Partial<TrainingConfig> | undefined;
|
|
1368
|
+
let trainerName: string | undefined;
|
|
1369
|
+
|
|
1370
|
+
if (request.body) {
|
|
1371
|
+
const body = JSON.parse(request.body);
|
|
1372
|
+
trainerName = body.trainerName;
|
|
1373
|
+
config = body.config;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const session = this.trackingEngine.startTrainingSession(trainerName, config);
|
|
1377
|
+
response.send(JSON.stringify(session), {
|
|
1378
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1379
|
+
});
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
response.send(JSON.stringify({ error: (e as Error).message }), {
|
|
1382
|
+
code: 500,
|
|
1383
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
private handleTrainingPauseRequest(response: HttpResponse): void {
|
|
1389
|
+
if (!this.trackingEngine) {
|
|
1390
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1391
|
+
code: 500,
|
|
1392
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1393
|
+
});
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const success = this.trackingEngine.pauseTrainingSession();
|
|
1398
|
+
if (success) {
|
|
1399
|
+
response.send(JSON.stringify({ success: true }), {
|
|
1400
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1401
|
+
});
|
|
1402
|
+
} else {
|
|
1403
|
+
response.send(JSON.stringify({ error: 'No active training session to pause' }), {
|
|
1404
|
+
code: 400,
|
|
1405
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
private handleTrainingResumeRequest(response: HttpResponse): void {
|
|
1411
|
+
if (!this.trackingEngine) {
|
|
1412
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1413
|
+
code: 500,
|
|
1414
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1415
|
+
});
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const success = this.trackingEngine.resumeTrainingSession();
|
|
1420
|
+
if (success) {
|
|
1421
|
+
response.send(JSON.stringify({ success: true }), {
|
|
1422
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1423
|
+
});
|
|
1424
|
+
} else {
|
|
1425
|
+
response.send(JSON.stringify({ error: 'No paused training session to resume' }), {
|
|
1426
|
+
code: 400,
|
|
1427
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
private handleTrainingEndRequest(response: HttpResponse): void {
|
|
1433
|
+
if (!this.trackingEngine) {
|
|
1434
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1435
|
+
code: 500,
|
|
1436
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1437
|
+
});
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const session = this.trackingEngine.endTrainingSession();
|
|
1442
|
+
if (session) {
|
|
1443
|
+
response.send(JSON.stringify(session), {
|
|
1444
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1445
|
+
});
|
|
1446
|
+
} else {
|
|
1447
|
+
response.send(JSON.stringify({ error: 'No training session to end' }), {
|
|
1448
|
+
code: 400,
|
|
1449
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private handleTrainingStatusRequest(response: HttpResponse): void {
|
|
1455
|
+
if (!this.trackingEngine) {
|
|
1456
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
1457
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1458
|
+
});
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const status = this.trackingEngine.getTrainingStatus();
|
|
1463
|
+
if (status) {
|
|
1464
|
+
response.send(JSON.stringify(status), {
|
|
1465
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1466
|
+
});
|
|
1467
|
+
} else {
|
|
1468
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
1469
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
private handleTrainingLandmarkRequest(request: HttpRequest, response: HttpResponse): void {
|
|
1475
|
+
if (!this.trackingEngine) {
|
|
1476
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1477
|
+
code: 500,
|
|
1478
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1479
|
+
});
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
try {
|
|
1484
|
+
const body = JSON.parse(request.body!) as Omit<TrainingLandmark, 'id' | 'markedAt'>;
|
|
1485
|
+
const landmark = this.trackingEngine.markTrainingLandmark(body);
|
|
1486
|
+
if (landmark) {
|
|
1487
|
+
response.send(JSON.stringify({ success: true, landmark }), {
|
|
1488
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1489
|
+
});
|
|
1490
|
+
} else {
|
|
1491
|
+
response.send(JSON.stringify({ error: 'No active training session' }), {
|
|
1492
|
+
code: 400,
|
|
1493
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
response.send(JSON.stringify({ error: 'Invalid request body' }), {
|
|
1498
|
+
code: 400,
|
|
1499
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
private handleTrainingApplyRequest(response: HttpResponse): void {
|
|
1505
|
+
if (!this.trackingEngine) {
|
|
1506
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1507
|
+
code: 500,
|
|
1508
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1509
|
+
});
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const result = this.trackingEngine.applyTrainingToTopology();
|
|
1514
|
+
if (result.success) {
|
|
1515
|
+
// Save the updated topology
|
|
1516
|
+
const topology = this.trackingEngine.getTopology();
|
|
1517
|
+
this.storage.setItem('topology', JSON.stringify(topology));
|
|
1518
|
+
}
|
|
1519
|
+
response.send(JSON.stringify(result), {
|
|
1520
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1057
1524
|
private serveEditorUI(response: HttpResponse): void {
|
|
1058
1525
|
response.send(EDITOR_HTML, {
|
|
1059
1526
|
headers: { 'Content-Type': 'text/html' },
|
|
1060
1527
|
});
|
|
1061
1528
|
}
|
|
1062
1529
|
|
|
1530
|
+
private serveTrainingUI(response: HttpResponse): void {
|
|
1531
|
+
response.send(TRAINING_HTML, {
|
|
1532
|
+
headers: { 'Content-Type': 'text/html' },
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1063
1536
|
private serveStaticFile(path: string, response: HttpResponse): void {
|
|
1064
1537
|
// Serve static files for the UI
|
|
1065
1538
|
response.send('Not found', { code: 404 });
|