@blueharford/scrypted-spatial-awareness 0.3.0 → 0.4.1
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 +94 -3
- 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 +1889 -20
- package/out/main.nodejs.js.map +1 -1
- package/package.json +1 -1
- package/src/core/tracking-engine.ts +588 -1
- package/src/main.ts +345 -20
- package/src/models/training.ts +300 -0
- package/src/ui/training-html.ts +1007 -0
- package/out/plugin.zip +0 -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
|
|
|
@@ -430,10 +432,103 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
430
432
|
// ==================== Settings Implementation ====================
|
|
431
433
|
|
|
432
434
|
async getSettings(): Promise<Setting[]> {
|
|
433
|
-
const
|
|
435
|
+
const baseSettings = await this.storageSettings.getSettings();
|
|
434
436
|
|
|
437
|
+
// Build settings in desired order
|
|
438
|
+
const settings: Setting[] = [];
|
|
439
|
+
|
|
440
|
+
// Helper to find and add settings from baseSettings by group
|
|
441
|
+
const addGroup = (group: string) => {
|
|
442
|
+
baseSettings.filter(s => s.group === group).forEach(s => settings.push(s));
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// ==================== 1. Getting Started ====================
|
|
446
|
+
// Training Mode button that opens mobile-friendly training UI in modal
|
|
447
|
+
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);})()`;
|
|
448
|
+
|
|
449
|
+
settings.push({
|
|
450
|
+
key: 'trainingMode',
|
|
451
|
+
title: 'Training Mode',
|
|
452
|
+
type: 'html' as any,
|
|
453
|
+
value: `
|
|
454
|
+
<style>
|
|
455
|
+
.sa-training-container {
|
|
456
|
+
padding: 16px;
|
|
457
|
+
background: rgba(255,255,255,0.03);
|
|
458
|
+
border-radius: 4px;
|
|
459
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
460
|
+
}
|
|
461
|
+
.sa-training-title {
|
|
462
|
+
color: #4fc3f7;
|
|
463
|
+
font-size: 14px;
|
|
464
|
+
font-weight: 500;
|
|
465
|
+
margin-bottom: 8px;
|
|
466
|
+
font-family: inherit;
|
|
467
|
+
}
|
|
468
|
+
.sa-training-desc {
|
|
469
|
+
color: rgba(255,255,255,0.6);
|
|
470
|
+
margin-bottom: 12px;
|
|
471
|
+
font-size: 13px;
|
|
472
|
+
line-height: 1.5;
|
|
473
|
+
font-family: inherit;
|
|
474
|
+
}
|
|
475
|
+
.sa-training-btn {
|
|
476
|
+
background: #4fc3f7;
|
|
477
|
+
color: #000;
|
|
478
|
+
border: none;
|
|
479
|
+
padding: 10px 20px;
|
|
480
|
+
border-radius: 4px;
|
|
481
|
+
font-size: 14px;
|
|
482
|
+
font-weight: 500;
|
|
483
|
+
cursor: pointer;
|
|
484
|
+
display: inline-flex;
|
|
485
|
+
align-items: center;
|
|
486
|
+
gap: 8px;
|
|
487
|
+
transition: background 0.2s;
|
|
488
|
+
font-family: inherit;
|
|
489
|
+
}
|
|
490
|
+
.sa-training-btn:hover {
|
|
491
|
+
background: #81d4fa;
|
|
492
|
+
}
|
|
493
|
+
.sa-training-steps {
|
|
494
|
+
color: rgba(255,255,255,0.5);
|
|
495
|
+
font-size: 12px;
|
|
496
|
+
margin-top: 12px;
|
|
497
|
+
padding-top: 12px;
|
|
498
|
+
border-top: 1px solid rgba(255,255,255,0.05);
|
|
499
|
+
font-family: inherit;
|
|
500
|
+
}
|
|
501
|
+
.sa-training-steps ol {
|
|
502
|
+
margin: 6px 0 0 16px;
|
|
503
|
+
padding: 0;
|
|
504
|
+
}
|
|
505
|
+
.sa-training-steps li {
|
|
506
|
+
margin-bottom: 2px;
|
|
507
|
+
}
|
|
508
|
+
</style>
|
|
509
|
+
<div class="sa-training-container">
|
|
510
|
+
<div class="sa-training-title">Guided Property Training</div>
|
|
511
|
+
<p class="sa-training-desc">Walk your property while the system learns your camera layout, transit times, and landmarks automatically.</p>
|
|
512
|
+
<button class="sa-training-btn" onclick="${trainingOnclickCode}">
|
|
513
|
+
Start Training Mode
|
|
514
|
+
</button>
|
|
515
|
+
<div class="sa-training-steps">
|
|
516
|
+
<strong>How it works:</strong>
|
|
517
|
+
<ol>
|
|
518
|
+
<li>Start training and walk to each camera</li>
|
|
519
|
+
<li>System auto-detects you and records transit times</li>
|
|
520
|
+
<li>Mark landmarks as you encounter them</li>
|
|
521
|
+
<li>Apply results to generate your topology</li>
|
|
522
|
+
</ol>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
`,
|
|
526
|
+
group: 'Getting Started',
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// ==================== 2. Topology ====================
|
|
435
530
|
// Topology editor button that opens modal overlay (appended to body for proper z-index)
|
|
436
|
-
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.
|
|
531
|
+
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);})()`;
|
|
437
532
|
|
|
438
533
|
settings.push({
|
|
439
534
|
key: 'topologyEditor',
|
|
@@ -442,45 +537,51 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
442
537
|
value: `
|
|
443
538
|
<style>
|
|
444
539
|
.sa-open-btn {
|
|
445
|
-
background:
|
|
446
|
-
color:
|
|
540
|
+
background: #4fc3f7;
|
|
541
|
+
color: #000;
|
|
447
542
|
border: none;
|
|
448
|
-
padding:
|
|
449
|
-
border-radius:
|
|
450
|
-
font-size:
|
|
451
|
-
font-weight:
|
|
543
|
+
padding: 10px 20px;
|
|
544
|
+
border-radius: 4px;
|
|
545
|
+
font-size: 14px;
|
|
546
|
+
font-weight: 500;
|
|
452
547
|
cursor: pointer;
|
|
453
548
|
display: inline-flex;
|
|
454
549
|
align-items: center;
|
|
455
|
-
gap:
|
|
456
|
-
transition:
|
|
550
|
+
gap: 8px;
|
|
551
|
+
transition: background 0.2s;
|
|
552
|
+
font-family: inherit;
|
|
457
553
|
}
|
|
458
554
|
.sa-open-btn:hover {
|
|
459
|
-
|
|
460
|
-
box-shadow: 0 8px 20px rgba(233, 69, 96, 0.4);
|
|
555
|
+
background: #81d4fa;
|
|
461
556
|
}
|
|
462
557
|
.sa-btn-container {
|
|
463
|
-
padding:
|
|
464
|
-
background:
|
|
465
|
-
border-radius:
|
|
558
|
+
padding: 16px;
|
|
559
|
+
background: rgba(255,255,255,0.03);
|
|
560
|
+
border-radius: 4px;
|
|
466
561
|
text-align: center;
|
|
562
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
467
563
|
}
|
|
468
564
|
.sa-btn-desc {
|
|
469
|
-
color:
|
|
470
|
-
margin-bottom:
|
|
471
|
-
font-size:
|
|
565
|
+
color: rgba(255,255,255,0.6);
|
|
566
|
+
margin-bottom: 12px;
|
|
567
|
+
font-size: 13px;
|
|
568
|
+
font-family: inherit;
|
|
472
569
|
}
|
|
473
570
|
</style>
|
|
474
571
|
<div class="sa-btn-container">
|
|
475
572
|
<p class="sa-btn-desc">Configure camera positions, connections, and transit times</p>
|
|
476
573
|
<button class="sa-open-btn" onclick="${onclickCode}">
|
|
477
|
-
|
|
574
|
+
Open Topology Editor
|
|
478
575
|
</button>
|
|
479
576
|
</div>
|
|
480
577
|
`,
|
|
481
578
|
group: 'Topology',
|
|
482
579
|
});
|
|
483
580
|
|
|
581
|
+
// ==================== 3. Cameras ====================
|
|
582
|
+
addGroup('Cameras');
|
|
583
|
+
|
|
584
|
+
// ==================== 4. Status ====================
|
|
484
585
|
// Add status display
|
|
485
586
|
const activeCount = this.trackingState.getActiveCount();
|
|
486
587
|
const topologyJson = this.storage.getItem('topology');
|
|
@@ -526,6 +627,15 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
526
627
|
});
|
|
527
628
|
}
|
|
528
629
|
|
|
630
|
+
// ==================== 5. Tracking ====================
|
|
631
|
+
addGroup('Tracking');
|
|
632
|
+
|
|
633
|
+
// ==================== 6. AI & Spatial Reasoning ====================
|
|
634
|
+
addGroup('AI & Spatial Reasoning');
|
|
635
|
+
|
|
636
|
+
// ==================== 7. Alerts ====================
|
|
637
|
+
addGroup('Alerts');
|
|
638
|
+
|
|
529
639
|
// Add alert rules configuration UI
|
|
530
640
|
const alertRules = this.alertManager.getRules();
|
|
531
641
|
const rulesHtml = this.generateAlertRulesHtml(alertRules);
|
|
@@ -537,6 +647,9 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
537
647
|
group: 'Alerts',
|
|
538
648
|
});
|
|
539
649
|
|
|
650
|
+
// ==================== 8. MQTT Integration ====================
|
|
651
|
+
addGroup('MQTT Integration');
|
|
652
|
+
|
|
540
653
|
return settings;
|
|
541
654
|
}
|
|
542
655
|
|
|
@@ -736,11 +849,38 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
736
849
|
return this.handleJourneyPathRequest(globalId, response);
|
|
737
850
|
}
|
|
738
851
|
|
|
852
|
+
// Training Mode endpoints
|
|
853
|
+
if (path.endsWith('/api/training/start')) {
|
|
854
|
+
return this.handleTrainingStartRequest(request, response);
|
|
855
|
+
}
|
|
856
|
+
if (path.endsWith('/api/training/pause')) {
|
|
857
|
+
return this.handleTrainingPauseRequest(response);
|
|
858
|
+
}
|
|
859
|
+
if (path.endsWith('/api/training/resume')) {
|
|
860
|
+
return this.handleTrainingResumeRequest(response);
|
|
861
|
+
}
|
|
862
|
+
if (path.endsWith('/api/training/end')) {
|
|
863
|
+
return this.handleTrainingEndRequest(response);
|
|
864
|
+
}
|
|
865
|
+
if (path.endsWith('/api/training/status')) {
|
|
866
|
+
return this.handleTrainingStatusRequest(response);
|
|
867
|
+
}
|
|
868
|
+
if (path.endsWith('/api/training/landmark')) {
|
|
869
|
+
return this.handleTrainingLandmarkRequest(request, response);
|
|
870
|
+
}
|
|
871
|
+
if (path.endsWith('/api/training/apply')) {
|
|
872
|
+
return this.handleTrainingApplyRequest(response);
|
|
873
|
+
}
|
|
874
|
+
|
|
739
875
|
// UI Routes
|
|
740
876
|
if (path.endsWith('/ui/editor') || path.endsWith('/ui/editor/')) {
|
|
741
877
|
return this.serveEditorUI(response);
|
|
742
878
|
}
|
|
743
879
|
|
|
880
|
+
if (path.endsWith('/ui/training') || path.endsWith('/ui/training/')) {
|
|
881
|
+
return this.serveTrainingUI(response);
|
|
882
|
+
}
|
|
883
|
+
|
|
744
884
|
if (path.includes('/ui/')) {
|
|
745
885
|
return this.serveStaticFile(path, response);
|
|
746
886
|
}
|
|
@@ -748,7 +888,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
748
888
|
// Default: return info page
|
|
749
889
|
response.send(JSON.stringify({
|
|
750
890
|
name: 'Spatial Awareness Plugin',
|
|
751
|
-
version: '0.
|
|
891
|
+
version: '0.4.0',
|
|
752
892
|
endpoints: {
|
|
753
893
|
api: {
|
|
754
894
|
trackedObjects: '/api/tracked-objects',
|
|
@@ -760,9 +900,19 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
760
900
|
liveTracking: '/api/live-tracking',
|
|
761
901
|
connectionSuggestions: '/api/connection-suggestions',
|
|
762
902
|
landmarkSuggestions: '/api/landmark-suggestions',
|
|
903
|
+
training: {
|
|
904
|
+
start: '/api/training/start',
|
|
905
|
+
pause: '/api/training/pause',
|
|
906
|
+
resume: '/api/training/resume',
|
|
907
|
+
end: '/api/training/end',
|
|
908
|
+
status: '/api/training/status',
|
|
909
|
+
landmark: '/api/training/landmark',
|
|
910
|
+
apply: '/api/training/apply',
|
|
911
|
+
},
|
|
763
912
|
},
|
|
764
913
|
ui: {
|
|
765
914
|
editor: '/ui/editor',
|
|
915
|
+
training: '/ui/training',
|
|
766
916
|
},
|
|
767
917
|
},
|
|
768
918
|
}), {
|
|
@@ -1228,12 +1378,187 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
1228
1378
|
}
|
|
1229
1379
|
}
|
|
1230
1380
|
|
|
1381
|
+
// ==================== Training Mode Handlers ====================
|
|
1382
|
+
|
|
1383
|
+
private handleTrainingStartRequest(request: HttpRequest, response: HttpResponse): void {
|
|
1384
|
+
if (!this.trackingEngine) {
|
|
1385
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running. Configure topology first.' }), {
|
|
1386
|
+
code: 500,
|
|
1387
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1388
|
+
});
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
try {
|
|
1393
|
+
let config: Partial<TrainingConfig> | undefined;
|
|
1394
|
+
let trainerName: string | undefined;
|
|
1395
|
+
|
|
1396
|
+
if (request.body) {
|
|
1397
|
+
const body = JSON.parse(request.body);
|
|
1398
|
+
trainerName = body.trainerName;
|
|
1399
|
+
config = body.config;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const session = this.trackingEngine.startTrainingSession(trainerName, config);
|
|
1403
|
+
response.send(JSON.stringify(session), {
|
|
1404
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1405
|
+
});
|
|
1406
|
+
} catch (e) {
|
|
1407
|
+
response.send(JSON.stringify({ error: (e as Error).message }), {
|
|
1408
|
+
code: 500,
|
|
1409
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
private handleTrainingPauseRequest(response: HttpResponse): void {
|
|
1415
|
+
if (!this.trackingEngine) {
|
|
1416
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1417
|
+
code: 500,
|
|
1418
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1419
|
+
});
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const success = this.trackingEngine.pauseTrainingSession();
|
|
1424
|
+
if (success) {
|
|
1425
|
+
response.send(JSON.stringify({ success: true }), {
|
|
1426
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1427
|
+
});
|
|
1428
|
+
} else {
|
|
1429
|
+
response.send(JSON.stringify({ error: 'No active training session to pause' }), {
|
|
1430
|
+
code: 400,
|
|
1431
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
private handleTrainingResumeRequest(response: HttpResponse): void {
|
|
1437
|
+
if (!this.trackingEngine) {
|
|
1438
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1439
|
+
code: 500,
|
|
1440
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1441
|
+
});
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const success = this.trackingEngine.resumeTrainingSession();
|
|
1446
|
+
if (success) {
|
|
1447
|
+
response.send(JSON.stringify({ success: true }), {
|
|
1448
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1449
|
+
});
|
|
1450
|
+
} else {
|
|
1451
|
+
response.send(JSON.stringify({ error: 'No paused training session to resume' }), {
|
|
1452
|
+
code: 400,
|
|
1453
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
private handleTrainingEndRequest(response: HttpResponse): void {
|
|
1459
|
+
if (!this.trackingEngine) {
|
|
1460
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1461
|
+
code: 500,
|
|
1462
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1463
|
+
});
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const session = this.trackingEngine.endTrainingSession();
|
|
1468
|
+
if (session) {
|
|
1469
|
+
response.send(JSON.stringify(session), {
|
|
1470
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1471
|
+
});
|
|
1472
|
+
} else {
|
|
1473
|
+
response.send(JSON.stringify({ error: 'No training session to end' }), {
|
|
1474
|
+
code: 400,
|
|
1475
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private handleTrainingStatusRequest(response: HttpResponse): void {
|
|
1481
|
+
if (!this.trackingEngine) {
|
|
1482
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
1483
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1484
|
+
});
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const status = this.trackingEngine.getTrainingStatus();
|
|
1489
|
+
if (status) {
|
|
1490
|
+
response.send(JSON.stringify(status), {
|
|
1491
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1492
|
+
});
|
|
1493
|
+
} else {
|
|
1494
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
1495
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
private handleTrainingLandmarkRequest(request: HttpRequest, response: HttpResponse): void {
|
|
1501
|
+
if (!this.trackingEngine) {
|
|
1502
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1503
|
+
code: 500,
|
|
1504
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1505
|
+
});
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
try {
|
|
1510
|
+
const body = JSON.parse(request.body!) as Omit<TrainingLandmark, 'id' | 'markedAt'>;
|
|
1511
|
+
const landmark = this.trackingEngine.markTrainingLandmark(body);
|
|
1512
|
+
if (landmark) {
|
|
1513
|
+
response.send(JSON.stringify({ success: true, landmark }), {
|
|
1514
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1515
|
+
});
|
|
1516
|
+
} else {
|
|
1517
|
+
response.send(JSON.stringify({ error: 'No active training session' }), {
|
|
1518
|
+
code: 400,
|
|
1519
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
} catch (e) {
|
|
1523
|
+
response.send(JSON.stringify({ error: 'Invalid request body' }), {
|
|
1524
|
+
code: 400,
|
|
1525
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
private handleTrainingApplyRequest(response: HttpResponse): void {
|
|
1531
|
+
if (!this.trackingEngine) {
|
|
1532
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1533
|
+
code: 500,
|
|
1534
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1535
|
+
});
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const result = this.trackingEngine.applyTrainingToTopology();
|
|
1540
|
+
if (result.success) {
|
|
1541
|
+
// Save the updated topology
|
|
1542
|
+
const topology = this.trackingEngine.getTopology();
|
|
1543
|
+
this.storage.setItem('topology', JSON.stringify(topology));
|
|
1544
|
+
}
|
|
1545
|
+
response.send(JSON.stringify(result), {
|
|
1546
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1231
1550
|
private serveEditorUI(response: HttpResponse): void {
|
|
1232
1551
|
response.send(EDITOR_HTML, {
|
|
1233
1552
|
headers: { 'Content-Type': 'text/html' },
|
|
1234
1553
|
});
|
|
1235
1554
|
}
|
|
1236
1555
|
|
|
1556
|
+
private serveTrainingUI(response: HttpResponse): void {
|
|
1557
|
+
response.send(TRAINING_HTML, {
|
|
1558
|
+
headers: { 'Content-Type': 'text/html' },
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1237
1562
|
private serveStaticFile(path: string, response: HttpResponse): void {
|
|
1238
1563
|
// Serve static files for the UI
|
|
1239
1564
|
response.send('Not found', { code: 404 });
|