@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/out/main.nodejs.js
CHANGED
|
@@ -35627,6 +35627,7 @@ exports.TrackingEngine = void 0;
|
|
|
35627
35627
|
const sdk_1 = __importStar(__webpack_require__(/*! @scrypted/sdk */ "./node_modules/@scrypted/sdk/dist/src/index.js"));
|
|
35628
35628
|
const topology_1 = __webpack_require__(/*! ../models/topology */ "./src/models/topology.ts");
|
|
35629
35629
|
const tracked_object_1 = __webpack_require__(/*! ../models/tracked-object */ "./src/models/tracked-object.ts");
|
|
35630
|
+
const training_1 = __webpack_require__(/*! ../models/training */ "./src/models/training.ts");
|
|
35630
35631
|
const object_correlator_1 = __webpack_require__(/*! ./object-correlator */ "./src/core/object-correlator.ts");
|
|
35631
35632
|
const spatial_reasoning_1 = __webpack_require__(/*! ./spatial-reasoning */ "./src/core/spatial-reasoning.ts");
|
|
35632
35633
|
const { systemManager } = sdk_1.default;
|
|
@@ -35657,6 +35658,13 @@ class TrackingEngine {
|
|
|
35657
35658
|
connectionSuggestions = new Map();
|
|
35658
35659
|
/** Minimum observations before suggesting a connection */
|
|
35659
35660
|
MIN_OBSERVATIONS_FOR_SUGGESTION = 3;
|
|
35661
|
+
// ==================== Training Mode ====================
|
|
35662
|
+
/** Current training session (null if not training) */
|
|
35663
|
+
trainingSession = null;
|
|
35664
|
+
/** Training configuration */
|
|
35665
|
+
trainingConfig = training_1.DEFAULT_TRAINING_CONFIG;
|
|
35666
|
+
/** Callback for training status updates */
|
|
35667
|
+
onTrainingStatusUpdate;
|
|
35660
35668
|
constructor(topology, state, alertManager, config, console) {
|
|
35661
35669
|
this.topology = topology;
|
|
35662
35670
|
this.state = state;
|
|
@@ -35750,6 +35758,10 @@ class TrackingEngine {
|
|
|
35750
35758
|
// Skip low-confidence detections
|
|
35751
35759
|
if (detection.score < 0.5)
|
|
35752
35760
|
continue;
|
|
35761
|
+
// If in training mode, record trainer detections
|
|
35762
|
+
if (this.isTrainingActive() && detection.className === 'person') {
|
|
35763
|
+
this.recordTrainerDetection(cameraId, detection, detection.score);
|
|
35764
|
+
}
|
|
35753
35765
|
// Skip classes we're not tracking on this camera
|
|
35754
35766
|
if (camera.trackClasses.length > 0 &&
|
|
35755
35767
|
!camera.trackClasses.includes(detection.className)) {
|
|
@@ -36282,6 +36294,464 @@ class TrackingEngine {
|
|
|
36282
36294
|
}
|
|
36283
36295
|
return { segments, currentLocation };
|
|
36284
36296
|
}
|
|
36297
|
+
// ==================== Training Mode Methods ====================
|
|
36298
|
+
/** Set callback for training status updates */
|
|
36299
|
+
setTrainingStatusCallback(callback) {
|
|
36300
|
+
this.onTrainingStatusUpdate = callback;
|
|
36301
|
+
}
|
|
36302
|
+
/** Get current training session (if any) */
|
|
36303
|
+
getTrainingSession() {
|
|
36304
|
+
return this.trainingSession;
|
|
36305
|
+
}
|
|
36306
|
+
/** Check if training mode is active */
|
|
36307
|
+
isTrainingActive() {
|
|
36308
|
+
return this.trainingSession !== null && this.trainingSession.state === 'active';
|
|
36309
|
+
}
|
|
36310
|
+
/** Start a new training session */
|
|
36311
|
+
startTrainingSession(trainerName, config) {
|
|
36312
|
+
// End any existing session
|
|
36313
|
+
if (this.trainingSession && this.trainingSession.state === 'active') {
|
|
36314
|
+
this.endTrainingSession();
|
|
36315
|
+
}
|
|
36316
|
+
// Apply custom config
|
|
36317
|
+
if (config) {
|
|
36318
|
+
this.trainingConfig = { ...training_1.DEFAULT_TRAINING_CONFIG, ...config };
|
|
36319
|
+
}
|
|
36320
|
+
// Create new session
|
|
36321
|
+
this.trainingSession = (0, training_1.createTrainingSession)(trainerName);
|
|
36322
|
+
this.trainingSession.state = 'active';
|
|
36323
|
+
this.console.log(`Training session started: ${this.trainingSession.id}`);
|
|
36324
|
+
this.emitTrainingStatus();
|
|
36325
|
+
return this.trainingSession;
|
|
36326
|
+
}
|
|
36327
|
+
/** Pause the current training session */
|
|
36328
|
+
pauseTrainingSession() {
|
|
36329
|
+
if (!this.trainingSession || this.trainingSession.state !== 'active') {
|
|
36330
|
+
return false;
|
|
36331
|
+
}
|
|
36332
|
+
this.trainingSession.state = 'paused';
|
|
36333
|
+
this.trainingSession.updatedAt = Date.now();
|
|
36334
|
+
this.console.log('Training session paused');
|
|
36335
|
+
this.emitTrainingStatus();
|
|
36336
|
+
return true;
|
|
36337
|
+
}
|
|
36338
|
+
/** Resume a paused training session */
|
|
36339
|
+
resumeTrainingSession() {
|
|
36340
|
+
if (!this.trainingSession || this.trainingSession.state !== 'paused') {
|
|
36341
|
+
return false;
|
|
36342
|
+
}
|
|
36343
|
+
this.trainingSession.state = 'active';
|
|
36344
|
+
this.trainingSession.updatedAt = Date.now();
|
|
36345
|
+
this.console.log('Training session resumed');
|
|
36346
|
+
this.emitTrainingStatus();
|
|
36347
|
+
return true;
|
|
36348
|
+
}
|
|
36349
|
+
/** End the current training session */
|
|
36350
|
+
endTrainingSession() {
|
|
36351
|
+
if (!this.trainingSession) {
|
|
36352
|
+
return null;
|
|
36353
|
+
}
|
|
36354
|
+
this.trainingSession.state = 'completed';
|
|
36355
|
+
this.trainingSession.completedAt = Date.now();
|
|
36356
|
+
this.trainingSession.updatedAt = Date.now();
|
|
36357
|
+
this.trainingSession.stats = (0, training_1.calculateTrainingStats)(this.trainingSession, this.topology.cameras.length);
|
|
36358
|
+
this.console.log(`Training session completed: ${this.trainingSession.stats.camerasVisited} cameras, ` +
|
|
36359
|
+
`${this.trainingSession.stats.transitsRecorded} transits, ` +
|
|
36360
|
+
`${this.trainingSession.stats.landmarksMarked} landmarks`);
|
|
36361
|
+
const session = this.trainingSession;
|
|
36362
|
+
this.emitTrainingStatus();
|
|
36363
|
+
return session;
|
|
36364
|
+
}
|
|
36365
|
+
/** Record that trainer was detected on a camera */
|
|
36366
|
+
recordTrainerDetection(cameraId, detection, detectionConfidence) {
|
|
36367
|
+
if (!this.trainingSession || this.trainingSession.state !== 'active') {
|
|
36368
|
+
return;
|
|
36369
|
+
}
|
|
36370
|
+
// Only process person detections during training
|
|
36371
|
+
if (detection.className !== 'person') {
|
|
36372
|
+
return;
|
|
36373
|
+
}
|
|
36374
|
+
// Check confidence threshold
|
|
36375
|
+
if (detectionConfidence < this.trainingConfig.minDetectionConfidence) {
|
|
36376
|
+
return;
|
|
36377
|
+
}
|
|
36378
|
+
const camera = (0, topology_1.findCamera)(this.topology, cameraId);
|
|
36379
|
+
const cameraName = camera?.name || cameraId;
|
|
36380
|
+
const now = Date.now();
|
|
36381
|
+
// Check if this is a new camera or same camera
|
|
36382
|
+
if (this.trainingSession.currentCameraId === cameraId) {
|
|
36383
|
+
// Update existing visit
|
|
36384
|
+
const currentVisit = this.trainingSession.visits.find(v => v.cameraId === cameraId && v.departedAt === null);
|
|
36385
|
+
if (currentVisit) {
|
|
36386
|
+
currentVisit.detectionConfidence = Math.max(currentVisit.detectionConfidence, detectionConfidence);
|
|
36387
|
+
if (detection.boundingBox) {
|
|
36388
|
+
currentVisit.boundingBox = detection.boundingBox;
|
|
36389
|
+
}
|
|
36390
|
+
}
|
|
36391
|
+
}
|
|
36392
|
+
else {
|
|
36393
|
+
// This is a new camera - check for transition
|
|
36394
|
+
if (this.trainingSession.currentCameraId && this.trainingSession.transitStartTime) {
|
|
36395
|
+
// Complete the transit
|
|
36396
|
+
const transitDuration = now - this.trainingSession.transitStartTime;
|
|
36397
|
+
const fromCameraId = this.trainingSession.previousCameraId || this.trainingSession.currentCameraId;
|
|
36398
|
+
const fromCamera = (0, topology_1.findCamera)(this.topology, fromCameraId);
|
|
36399
|
+
// Mark departure from previous camera
|
|
36400
|
+
const prevVisit = this.trainingSession.visits.find(v => v.cameraId === fromCameraId && v.departedAt === null);
|
|
36401
|
+
if (prevVisit) {
|
|
36402
|
+
prevVisit.departedAt = this.trainingSession.transitStartTime;
|
|
36403
|
+
}
|
|
36404
|
+
// Check for overlap (both cameras detecting at same time)
|
|
36405
|
+
const hasOverlap = this.checkTrainingOverlap(fromCameraId, cameraId, now);
|
|
36406
|
+
// Record the transit
|
|
36407
|
+
const transit = {
|
|
36408
|
+
id: `transit-${now}`,
|
|
36409
|
+
fromCameraId,
|
|
36410
|
+
toCameraId: cameraId,
|
|
36411
|
+
startTime: this.trainingSession.transitStartTime,
|
|
36412
|
+
endTime: now,
|
|
36413
|
+
transitSeconds: Math.round(transitDuration / 1000),
|
|
36414
|
+
hasOverlap,
|
|
36415
|
+
};
|
|
36416
|
+
this.trainingSession.transits.push(transit);
|
|
36417
|
+
this.console.log(`Training transit: ${fromCamera?.name || fromCameraId} → ${cameraName} ` +
|
|
36418
|
+
`(${transit.transitSeconds}s${hasOverlap ? ', overlap detected' : ''})`);
|
|
36419
|
+
// If overlap detected, record it
|
|
36420
|
+
if (hasOverlap && this.trainingConfig.autoDetectOverlaps) {
|
|
36421
|
+
this.recordTrainingOverlap(fromCameraId, cameraId);
|
|
36422
|
+
}
|
|
36423
|
+
}
|
|
36424
|
+
// Record new camera visit
|
|
36425
|
+
const visit = {
|
|
36426
|
+
cameraId,
|
|
36427
|
+
cameraName,
|
|
36428
|
+
arrivedAt: now,
|
|
36429
|
+
departedAt: null,
|
|
36430
|
+
trainerEmbedding: detection.embedding,
|
|
36431
|
+
detectionConfidence,
|
|
36432
|
+
boundingBox: detection.boundingBox,
|
|
36433
|
+
floorPlanPosition: camera?.floorPlanPosition,
|
|
36434
|
+
};
|
|
36435
|
+
this.trainingSession.visits.push(visit);
|
|
36436
|
+
// Update session state
|
|
36437
|
+
this.trainingSession.previousCameraId = this.trainingSession.currentCameraId;
|
|
36438
|
+
this.trainingSession.currentCameraId = cameraId;
|
|
36439
|
+
this.trainingSession.transitStartTime = now;
|
|
36440
|
+
// Store trainer embedding if not already captured
|
|
36441
|
+
if (!this.trainingSession.trainerEmbedding && detection.embedding) {
|
|
36442
|
+
this.trainingSession.trainerEmbedding = detection.embedding;
|
|
36443
|
+
}
|
|
36444
|
+
}
|
|
36445
|
+
this.trainingSession.updatedAt = now;
|
|
36446
|
+
this.trainingSession.stats = (0, training_1.calculateTrainingStats)(this.trainingSession, this.topology.cameras.length);
|
|
36447
|
+
this.emitTrainingStatus();
|
|
36448
|
+
}
|
|
36449
|
+
/** Check if there's overlap between two cameras during training */
|
|
36450
|
+
checkTrainingOverlap(fromCameraId, toCameraId, now) {
|
|
36451
|
+
// Check if both cameras have recent visits overlapping in time
|
|
36452
|
+
const fromVisit = this.trainingSession?.visits.find(v => v.cameraId === fromCameraId &&
|
|
36453
|
+
(v.departedAt === null || v.departedAt > now - 5000) // Within 5 seconds
|
|
36454
|
+
);
|
|
36455
|
+
const toVisit = this.trainingSession?.visits.find(v => v.cameraId === toCameraId &&
|
|
36456
|
+
v.arrivedAt <= now &&
|
|
36457
|
+
v.arrivedAt >= now - 5000 // Arrived within last 5 seconds
|
|
36458
|
+
);
|
|
36459
|
+
return !!(fromVisit && toVisit);
|
|
36460
|
+
}
|
|
36461
|
+
/** Record a camera overlap detected during training */
|
|
36462
|
+
recordTrainingOverlap(camera1Id, camera2Id) {
|
|
36463
|
+
if (!this.trainingSession)
|
|
36464
|
+
return;
|
|
36465
|
+
// Check if we already have this overlap
|
|
36466
|
+
const existingOverlap = this.trainingSession.overlaps.find(o => (o.camera1Id === camera1Id && o.camera2Id === camera2Id) ||
|
|
36467
|
+
(o.camera1Id === camera2Id && o.camera2Id === camera1Id));
|
|
36468
|
+
if (existingOverlap)
|
|
36469
|
+
return;
|
|
36470
|
+
const camera1 = (0, topology_1.findCamera)(this.topology, camera1Id);
|
|
36471
|
+
const camera2 = (0, topology_1.findCamera)(this.topology, camera2Id);
|
|
36472
|
+
// Calculate approximate position (midpoint of both camera positions)
|
|
36473
|
+
let position = { x: 50, y: 50 };
|
|
36474
|
+
if (camera1?.floorPlanPosition && camera2?.floorPlanPosition) {
|
|
36475
|
+
position = {
|
|
36476
|
+
x: (camera1.floorPlanPosition.x + camera2.floorPlanPosition.x) / 2,
|
|
36477
|
+
y: (camera1.floorPlanPosition.y + camera2.floorPlanPosition.y) / 2,
|
|
36478
|
+
};
|
|
36479
|
+
}
|
|
36480
|
+
const overlap = {
|
|
36481
|
+
id: `overlap-${Date.now()}`,
|
|
36482
|
+
camera1Id,
|
|
36483
|
+
camera2Id,
|
|
36484
|
+
position,
|
|
36485
|
+
radius: 30, // Default radius
|
|
36486
|
+
markedAt: Date.now(),
|
|
36487
|
+
};
|
|
36488
|
+
this.trainingSession.overlaps.push(overlap);
|
|
36489
|
+
this.console.log(`Camera overlap detected: ${camera1?.name} ↔ ${camera2?.name}`);
|
|
36490
|
+
}
|
|
36491
|
+
/** Manually mark a landmark during training */
|
|
36492
|
+
markTrainingLandmark(landmark) {
|
|
36493
|
+
if (!this.trainingSession)
|
|
36494
|
+
return null;
|
|
36495
|
+
const newLandmark = {
|
|
36496
|
+
...landmark,
|
|
36497
|
+
id: `landmark-${Date.now()}`,
|
|
36498
|
+
markedAt: Date.now(),
|
|
36499
|
+
};
|
|
36500
|
+
this.trainingSession.landmarks.push(newLandmark);
|
|
36501
|
+
this.trainingSession.updatedAt = Date.now();
|
|
36502
|
+
this.trainingSession.stats = (0, training_1.calculateTrainingStats)(this.trainingSession, this.topology.cameras.length);
|
|
36503
|
+
this.console.log(`Landmark marked: ${newLandmark.name} (${newLandmark.type})`);
|
|
36504
|
+
this.emitTrainingStatus();
|
|
36505
|
+
return newLandmark;
|
|
36506
|
+
}
|
|
36507
|
+
/** Manually mark a structure during training */
|
|
36508
|
+
markTrainingStructure(structure) {
|
|
36509
|
+
if (!this.trainingSession)
|
|
36510
|
+
return null;
|
|
36511
|
+
const newStructure = {
|
|
36512
|
+
...structure,
|
|
36513
|
+
id: `structure-${Date.now()}`,
|
|
36514
|
+
markedAt: Date.now(),
|
|
36515
|
+
};
|
|
36516
|
+
this.trainingSession.structures.push(newStructure);
|
|
36517
|
+
this.trainingSession.updatedAt = Date.now();
|
|
36518
|
+
this.trainingSession.stats = (0, training_1.calculateTrainingStats)(this.trainingSession, this.topology.cameras.length);
|
|
36519
|
+
this.console.log(`Structure marked: ${newStructure.name} (${newStructure.type})`);
|
|
36520
|
+
this.emitTrainingStatus();
|
|
36521
|
+
return newStructure;
|
|
36522
|
+
}
|
|
36523
|
+
/** Confirm camera position on floor plan during training */
|
|
36524
|
+
confirmCameraPosition(cameraId, position) {
|
|
36525
|
+
if (!this.trainingSession)
|
|
36526
|
+
return false;
|
|
36527
|
+
// Update in current session
|
|
36528
|
+
const visit = this.trainingSession.visits.find(v => v.cameraId === cameraId);
|
|
36529
|
+
if (visit) {
|
|
36530
|
+
visit.floorPlanPosition = position;
|
|
36531
|
+
}
|
|
36532
|
+
// Update in topology
|
|
36533
|
+
const camera = (0, topology_1.findCamera)(this.topology, cameraId);
|
|
36534
|
+
if (camera) {
|
|
36535
|
+
camera.floorPlanPosition = position;
|
|
36536
|
+
if (this.onTopologyChange) {
|
|
36537
|
+
this.onTopologyChange(this.topology);
|
|
36538
|
+
}
|
|
36539
|
+
}
|
|
36540
|
+
this.trainingSession.updatedAt = Date.now();
|
|
36541
|
+
this.emitTrainingStatus();
|
|
36542
|
+
return true;
|
|
36543
|
+
}
|
|
36544
|
+
/** Get training status for UI updates */
|
|
36545
|
+
getTrainingStatus() {
|
|
36546
|
+
if (!this.trainingSession)
|
|
36547
|
+
return null;
|
|
36548
|
+
const currentCamera = this.trainingSession.currentCameraId
|
|
36549
|
+
? (0, topology_1.findCamera)(this.topology, this.trainingSession.currentCameraId)
|
|
36550
|
+
: null;
|
|
36551
|
+
const previousCamera = this.trainingSession.previousCameraId
|
|
36552
|
+
? (0, topology_1.findCamera)(this.topology, this.trainingSession.previousCameraId)
|
|
36553
|
+
: null;
|
|
36554
|
+
// Generate suggestions for next actions
|
|
36555
|
+
const suggestions = [];
|
|
36556
|
+
const visitedCameras = new Set(this.trainingSession.visits.map(v => v.cameraId));
|
|
36557
|
+
const unvisitedCameras = this.topology.cameras.filter(c => !visitedCameras.has(c.deviceId));
|
|
36558
|
+
if (unvisitedCameras.length > 0) {
|
|
36559
|
+
// Suggest nearest unvisited camera based on connections
|
|
36560
|
+
const currentConnections = currentCamera
|
|
36561
|
+
? (0, topology_1.findConnectionsFrom)(this.topology, currentCamera.deviceId)
|
|
36562
|
+
: [];
|
|
36563
|
+
const connectedUnvisited = currentConnections
|
|
36564
|
+
.map(c => c.toCameraId)
|
|
36565
|
+
.filter(id => !visitedCameras.has(id));
|
|
36566
|
+
if (connectedUnvisited.length > 0) {
|
|
36567
|
+
const nextCam = (0, topology_1.findCamera)(this.topology, connectedUnvisited[0]);
|
|
36568
|
+
if (nextCam) {
|
|
36569
|
+
suggestions.push(`Walk to ${nextCam.name}`);
|
|
36570
|
+
}
|
|
36571
|
+
}
|
|
36572
|
+
else {
|
|
36573
|
+
suggestions.push(`${unvisitedCameras.length} cameras not yet visited`);
|
|
36574
|
+
}
|
|
36575
|
+
}
|
|
36576
|
+
if (this.trainingSession.visits.length >= 2 && this.trainingSession.landmarks.length === 0) {
|
|
36577
|
+
suggestions.push('Consider marking some landmarks');
|
|
36578
|
+
}
|
|
36579
|
+
if (visitedCameras.size >= this.topology.cameras.length) {
|
|
36580
|
+
suggestions.push('All cameras visited! You can end training.');
|
|
36581
|
+
}
|
|
36582
|
+
const status = {
|
|
36583
|
+
sessionId: this.trainingSession.id,
|
|
36584
|
+
state: this.trainingSession.state,
|
|
36585
|
+
currentCamera: currentCamera ? {
|
|
36586
|
+
id: currentCamera.deviceId,
|
|
36587
|
+
name: currentCamera.name,
|
|
36588
|
+
detectedAt: this.trainingSession.visits.find(v => v.cameraId === currentCamera.deviceId && !v.departedAt)?.arrivedAt || Date.now(),
|
|
36589
|
+
confidence: this.trainingSession.visits.find(v => v.cameraId === currentCamera.deviceId && !v.departedAt)?.detectionConfidence || 0,
|
|
36590
|
+
} : undefined,
|
|
36591
|
+
activeTransit: this.trainingSession.transitStartTime && previousCamera ? {
|
|
36592
|
+
fromCameraId: previousCamera.deviceId,
|
|
36593
|
+
fromCameraName: previousCamera.name,
|
|
36594
|
+
startTime: this.trainingSession.transitStartTime,
|
|
36595
|
+
elapsedSeconds: Math.round((Date.now() - this.trainingSession.transitStartTime) / 1000),
|
|
36596
|
+
} : undefined,
|
|
36597
|
+
stats: this.trainingSession.stats,
|
|
36598
|
+
suggestions,
|
|
36599
|
+
};
|
|
36600
|
+
return status;
|
|
36601
|
+
}
|
|
36602
|
+
/** Emit training status update to callback */
|
|
36603
|
+
emitTrainingStatus() {
|
|
36604
|
+
if (this.onTrainingStatusUpdate) {
|
|
36605
|
+
const status = this.getTrainingStatus();
|
|
36606
|
+
if (status) {
|
|
36607
|
+
this.onTrainingStatusUpdate(status);
|
|
36608
|
+
}
|
|
36609
|
+
}
|
|
36610
|
+
}
|
|
36611
|
+
/** Apply training results to topology */
|
|
36612
|
+
applyTrainingToTopology() {
|
|
36613
|
+
const result = {
|
|
36614
|
+
camerasAdded: 0,
|
|
36615
|
+
connectionsCreated: 0,
|
|
36616
|
+
connectionsUpdated: 0,
|
|
36617
|
+
landmarksAdded: 0,
|
|
36618
|
+
zonesCreated: 0,
|
|
36619
|
+
warnings: [],
|
|
36620
|
+
success: false,
|
|
36621
|
+
};
|
|
36622
|
+
if (!this.trainingSession) {
|
|
36623
|
+
result.warnings.push('No training session to apply');
|
|
36624
|
+
return result;
|
|
36625
|
+
}
|
|
36626
|
+
try {
|
|
36627
|
+
// 1. Update camera positions from training visits
|
|
36628
|
+
for (const visit of this.trainingSession.visits) {
|
|
36629
|
+
const camera = (0, topology_1.findCamera)(this.topology, visit.cameraId);
|
|
36630
|
+
if (camera && visit.floorPlanPosition) {
|
|
36631
|
+
if (!camera.floorPlanPosition) {
|
|
36632
|
+
camera.floorPlanPosition = visit.floorPlanPosition;
|
|
36633
|
+
result.camerasAdded++;
|
|
36634
|
+
}
|
|
36635
|
+
}
|
|
36636
|
+
}
|
|
36637
|
+
// 2. Create or update connections from training transits
|
|
36638
|
+
for (const transit of this.trainingSession.transits) {
|
|
36639
|
+
const existingConnection = (0, topology_1.findConnection)(this.topology, transit.fromCameraId, transit.toCameraId);
|
|
36640
|
+
if (existingConnection) {
|
|
36641
|
+
// Update existing connection with observed transit time
|
|
36642
|
+
const transitMs = transit.transitSeconds * 1000;
|
|
36643
|
+
existingConnection.transitTime = {
|
|
36644
|
+
min: Math.min(existingConnection.transitTime.min, transitMs * 0.7),
|
|
36645
|
+
typical: transitMs,
|
|
36646
|
+
max: Math.max(existingConnection.transitTime.max, transitMs * 1.3),
|
|
36647
|
+
};
|
|
36648
|
+
result.connectionsUpdated++;
|
|
36649
|
+
}
|
|
36650
|
+
else {
|
|
36651
|
+
// Create new connection
|
|
36652
|
+
const fromCamera = (0, topology_1.findCamera)(this.topology, transit.fromCameraId);
|
|
36653
|
+
const toCamera = (0, topology_1.findCamera)(this.topology, transit.toCameraId);
|
|
36654
|
+
if (fromCamera && toCamera) {
|
|
36655
|
+
const transitMs = transit.transitSeconds * 1000;
|
|
36656
|
+
const newConnection = {
|
|
36657
|
+
id: `conn-training-${Date.now()}-${result.connectionsCreated}`,
|
|
36658
|
+
fromCameraId: transit.fromCameraId,
|
|
36659
|
+
toCameraId: transit.toCameraId,
|
|
36660
|
+
name: `${fromCamera.name} to ${toCamera.name}`,
|
|
36661
|
+
exitZone: [], // Will be refined in topology editor
|
|
36662
|
+
entryZone: [], // Will be refined in topology editor
|
|
36663
|
+
transitTime: {
|
|
36664
|
+
min: transitMs * 0.7,
|
|
36665
|
+
typical: transitMs,
|
|
36666
|
+
max: transitMs * 1.3,
|
|
36667
|
+
},
|
|
36668
|
+
bidirectional: true,
|
|
36669
|
+
};
|
|
36670
|
+
this.topology.connections.push(newConnection);
|
|
36671
|
+
result.connectionsCreated++;
|
|
36672
|
+
}
|
|
36673
|
+
}
|
|
36674
|
+
}
|
|
36675
|
+
// 3. Add landmarks from training
|
|
36676
|
+
for (const trainLandmark of this.trainingSession.landmarks) {
|
|
36677
|
+
// Map training landmark type to topology landmark type
|
|
36678
|
+
const typeMapping = {
|
|
36679
|
+
mailbox: 'feature',
|
|
36680
|
+
garage: 'structure',
|
|
36681
|
+
shed: 'structure',
|
|
36682
|
+
tree: 'feature',
|
|
36683
|
+
gate: 'access',
|
|
36684
|
+
door: 'access',
|
|
36685
|
+
driveway: 'access',
|
|
36686
|
+
pathway: 'access',
|
|
36687
|
+
garden: 'feature',
|
|
36688
|
+
pool: 'feature',
|
|
36689
|
+
deck: 'structure',
|
|
36690
|
+
patio: 'structure',
|
|
36691
|
+
other: 'feature',
|
|
36692
|
+
};
|
|
36693
|
+
// Convert training landmark to topology landmark
|
|
36694
|
+
const landmark = {
|
|
36695
|
+
id: trainLandmark.id,
|
|
36696
|
+
name: trainLandmark.name,
|
|
36697
|
+
type: typeMapping[trainLandmark.type] || 'feature',
|
|
36698
|
+
position: trainLandmark.position,
|
|
36699
|
+
visibleFromCameras: trainLandmark.visibleFromCameras.length > 0
|
|
36700
|
+
? trainLandmark.visibleFromCameras
|
|
36701
|
+
: undefined,
|
|
36702
|
+
description: trainLandmark.description,
|
|
36703
|
+
};
|
|
36704
|
+
if (!this.topology.landmarks) {
|
|
36705
|
+
this.topology.landmarks = [];
|
|
36706
|
+
}
|
|
36707
|
+
this.topology.landmarks.push(landmark);
|
|
36708
|
+
result.landmarksAdded++;
|
|
36709
|
+
}
|
|
36710
|
+
// 4. Create zones from overlaps
|
|
36711
|
+
for (const overlap of this.trainingSession.overlaps) {
|
|
36712
|
+
const camera1 = (0, topology_1.findCamera)(this.topology, overlap.camera1Id);
|
|
36713
|
+
const camera2 = (0, topology_1.findCamera)(this.topology, overlap.camera2Id);
|
|
36714
|
+
if (camera1 && camera2) {
|
|
36715
|
+
// Create global zone for overlap area
|
|
36716
|
+
const zoneName = `${camera1.name}/${camera2.name} Overlap`;
|
|
36717
|
+
const existingZone = this.topology.globalZones?.find(z => z.name === zoneName);
|
|
36718
|
+
if (!existingZone) {
|
|
36719
|
+
if (!this.topology.globalZones) {
|
|
36720
|
+
this.topology.globalZones = [];
|
|
36721
|
+
}
|
|
36722
|
+
// Create camera zone mappings (placeholder zones to be refined in editor)
|
|
36723
|
+
const cameraZones = [
|
|
36724
|
+
{ cameraId: overlap.camera1Id, zone: [] },
|
|
36725
|
+
{ cameraId: overlap.camera2Id, zone: [] },
|
|
36726
|
+
];
|
|
36727
|
+
this.topology.globalZones.push({
|
|
36728
|
+
id: `zone-overlap-${overlap.id}`,
|
|
36729
|
+
name: zoneName,
|
|
36730
|
+
type: 'dwell', // Overlap zones are good for tracking dwell time
|
|
36731
|
+
cameraZones,
|
|
36732
|
+
});
|
|
36733
|
+
result.zonesCreated++;
|
|
36734
|
+
}
|
|
36735
|
+
}
|
|
36736
|
+
}
|
|
36737
|
+
// Notify about topology change
|
|
36738
|
+
if (this.onTopologyChange) {
|
|
36739
|
+
this.onTopologyChange(this.topology);
|
|
36740
|
+
}
|
|
36741
|
+
result.success = true;
|
|
36742
|
+
this.console.log(`Training applied: ${result.connectionsCreated} connections created, ` +
|
|
36743
|
+
`${result.connectionsUpdated} updated, ${result.landmarksAdded} landmarks added`);
|
|
36744
|
+
}
|
|
36745
|
+
catch (e) {
|
|
36746
|
+
result.warnings.push(`Error applying training: ${e}`);
|
|
36747
|
+
}
|
|
36748
|
+
return result;
|
|
36749
|
+
}
|
|
36750
|
+
/** Clear the current training session without applying */
|
|
36751
|
+
clearTrainingSession() {
|
|
36752
|
+
this.trainingSession = null;
|
|
36753
|
+
this.emitTrainingStatus();
|
|
36754
|
+
}
|
|
36285
36755
|
}
|
|
36286
36756
|
exports.TrackingEngine = TrackingEngine;
|
|
36287
36757
|
|
|
@@ -36989,6 +37459,7 @@ const global_tracker_sensor_1 = __webpack_require__(/*! ./devices/global-tracker
|
|
|
36989
37459
|
const tracking_zone_1 = __webpack_require__(/*! ./devices/tracking-zone */ "./src/devices/tracking-zone.ts");
|
|
36990
37460
|
const mqtt_publisher_1 = __webpack_require__(/*! ./integrations/mqtt-publisher */ "./src/integrations/mqtt-publisher.ts");
|
|
36991
37461
|
const editor_html_1 = __webpack_require__(/*! ./ui/editor-html */ "./src/ui/editor-html.ts");
|
|
37462
|
+
const training_html_1 = __webpack_require__(/*! ./ui/training-html */ "./src/ui/training-html.ts");
|
|
36992
37463
|
const { deviceManager, systemManager } = sdk_1.default;
|
|
36993
37464
|
const TRACKING_ZONE_PREFIX = 'tracking-zone:';
|
|
36994
37465
|
const GLOBAL_TRACKER_ID = 'global-tracker';
|
|
@@ -37339,9 +37810,98 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37339
37810
|
}
|
|
37340
37811
|
// ==================== Settings Implementation ====================
|
|
37341
37812
|
async getSettings() {
|
|
37342
|
-
const
|
|
37813
|
+
const baseSettings = await this.storageSettings.getSettings();
|
|
37814
|
+
// Build settings in desired order
|
|
37815
|
+
const settings = [];
|
|
37816
|
+
// Helper to find and add settings from baseSettings by group
|
|
37817
|
+
const addGroup = (group) => {
|
|
37818
|
+
baseSettings.filter(s => s.group === group).forEach(s => settings.push(s));
|
|
37819
|
+
};
|
|
37820
|
+
// ==================== 1. Getting Started ====================
|
|
37821
|
+
// Training Mode button that opens mobile-friendly training UI in modal
|
|
37822
|
+
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);})()`;
|
|
37823
|
+
settings.push({
|
|
37824
|
+
key: 'trainingMode',
|
|
37825
|
+
title: 'Training Mode',
|
|
37826
|
+
type: 'html',
|
|
37827
|
+
value: `
|
|
37828
|
+
<style>
|
|
37829
|
+
.sa-training-container {
|
|
37830
|
+
padding: 16px;
|
|
37831
|
+
background: rgba(255,255,255,0.03);
|
|
37832
|
+
border-radius: 4px;
|
|
37833
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
37834
|
+
}
|
|
37835
|
+
.sa-training-title {
|
|
37836
|
+
color: #4fc3f7;
|
|
37837
|
+
font-size: 14px;
|
|
37838
|
+
font-weight: 500;
|
|
37839
|
+
margin-bottom: 8px;
|
|
37840
|
+
font-family: inherit;
|
|
37841
|
+
}
|
|
37842
|
+
.sa-training-desc {
|
|
37843
|
+
color: rgba(255,255,255,0.6);
|
|
37844
|
+
margin-bottom: 12px;
|
|
37845
|
+
font-size: 13px;
|
|
37846
|
+
line-height: 1.5;
|
|
37847
|
+
font-family: inherit;
|
|
37848
|
+
}
|
|
37849
|
+
.sa-training-btn {
|
|
37850
|
+
background: #4fc3f7;
|
|
37851
|
+
color: #000;
|
|
37852
|
+
border: none;
|
|
37853
|
+
padding: 10px 20px;
|
|
37854
|
+
border-radius: 4px;
|
|
37855
|
+
font-size: 14px;
|
|
37856
|
+
font-weight: 500;
|
|
37857
|
+
cursor: pointer;
|
|
37858
|
+
display: inline-flex;
|
|
37859
|
+
align-items: center;
|
|
37860
|
+
gap: 8px;
|
|
37861
|
+
transition: background 0.2s;
|
|
37862
|
+
font-family: inherit;
|
|
37863
|
+
}
|
|
37864
|
+
.sa-training-btn:hover {
|
|
37865
|
+
background: #81d4fa;
|
|
37866
|
+
}
|
|
37867
|
+
.sa-training-steps {
|
|
37868
|
+
color: rgba(255,255,255,0.5);
|
|
37869
|
+
font-size: 12px;
|
|
37870
|
+
margin-top: 12px;
|
|
37871
|
+
padding-top: 12px;
|
|
37872
|
+
border-top: 1px solid rgba(255,255,255,0.05);
|
|
37873
|
+
font-family: inherit;
|
|
37874
|
+
}
|
|
37875
|
+
.sa-training-steps ol {
|
|
37876
|
+
margin: 6px 0 0 16px;
|
|
37877
|
+
padding: 0;
|
|
37878
|
+
}
|
|
37879
|
+
.sa-training-steps li {
|
|
37880
|
+
margin-bottom: 2px;
|
|
37881
|
+
}
|
|
37882
|
+
</style>
|
|
37883
|
+
<div class="sa-training-container">
|
|
37884
|
+
<div class="sa-training-title">Guided Property Training</div>
|
|
37885
|
+
<p class="sa-training-desc">Walk your property while the system learns your camera layout, transit times, and landmarks automatically.</p>
|
|
37886
|
+
<button class="sa-training-btn" onclick="${trainingOnclickCode}">
|
|
37887
|
+
Start Training Mode
|
|
37888
|
+
</button>
|
|
37889
|
+
<div class="sa-training-steps">
|
|
37890
|
+
<strong>How it works:</strong>
|
|
37891
|
+
<ol>
|
|
37892
|
+
<li>Start training and walk to each camera</li>
|
|
37893
|
+
<li>System auto-detects you and records transit times</li>
|
|
37894
|
+
<li>Mark landmarks as you encounter them</li>
|
|
37895
|
+
<li>Apply results to generate your topology</li>
|
|
37896
|
+
</ol>
|
|
37897
|
+
</div>
|
|
37898
|
+
</div>
|
|
37899
|
+
`,
|
|
37900
|
+
group: 'Getting Started',
|
|
37901
|
+
});
|
|
37902
|
+
// ==================== 2. Topology ====================
|
|
37343
37903
|
// Topology editor button that opens modal overlay (appended to body for proper z-index)
|
|
37344
|
-
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.
|
|
37904
|
+
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);})()`;
|
|
37345
37905
|
settings.push({
|
|
37346
37906
|
key: 'topologyEditor',
|
|
37347
37907
|
title: 'Topology Editor',
|
|
@@ -37349,44 +37909,49 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37349
37909
|
value: `
|
|
37350
37910
|
<style>
|
|
37351
37911
|
.sa-open-btn {
|
|
37352
|
-
background:
|
|
37353
|
-
color:
|
|
37912
|
+
background: #4fc3f7;
|
|
37913
|
+
color: #000;
|
|
37354
37914
|
border: none;
|
|
37355
|
-
padding:
|
|
37356
|
-
border-radius:
|
|
37357
|
-
font-size:
|
|
37358
|
-
font-weight:
|
|
37915
|
+
padding: 10px 20px;
|
|
37916
|
+
border-radius: 4px;
|
|
37917
|
+
font-size: 14px;
|
|
37918
|
+
font-weight: 500;
|
|
37359
37919
|
cursor: pointer;
|
|
37360
37920
|
display: inline-flex;
|
|
37361
37921
|
align-items: center;
|
|
37362
|
-
gap:
|
|
37363
|
-
transition:
|
|
37922
|
+
gap: 8px;
|
|
37923
|
+
transition: background 0.2s;
|
|
37924
|
+
font-family: inherit;
|
|
37364
37925
|
}
|
|
37365
37926
|
.sa-open-btn:hover {
|
|
37366
|
-
|
|
37367
|
-
box-shadow: 0 8px 20px rgba(233, 69, 96, 0.4);
|
|
37927
|
+
background: #81d4fa;
|
|
37368
37928
|
}
|
|
37369
37929
|
.sa-btn-container {
|
|
37370
|
-
padding:
|
|
37371
|
-
background:
|
|
37372
|
-
border-radius:
|
|
37930
|
+
padding: 16px;
|
|
37931
|
+
background: rgba(255,255,255,0.03);
|
|
37932
|
+
border-radius: 4px;
|
|
37373
37933
|
text-align: center;
|
|
37934
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
37374
37935
|
}
|
|
37375
37936
|
.sa-btn-desc {
|
|
37376
|
-
color:
|
|
37377
|
-
margin-bottom:
|
|
37378
|
-
font-size:
|
|
37937
|
+
color: rgba(255,255,255,0.6);
|
|
37938
|
+
margin-bottom: 12px;
|
|
37939
|
+
font-size: 13px;
|
|
37940
|
+
font-family: inherit;
|
|
37379
37941
|
}
|
|
37380
37942
|
</style>
|
|
37381
37943
|
<div class="sa-btn-container">
|
|
37382
37944
|
<p class="sa-btn-desc">Configure camera positions, connections, and transit times</p>
|
|
37383
37945
|
<button class="sa-open-btn" onclick="${onclickCode}">
|
|
37384
|
-
|
|
37946
|
+
Open Topology Editor
|
|
37385
37947
|
</button>
|
|
37386
37948
|
</div>
|
|
37387
37949
|
`,
|
|
37388
37950
|
group: 'Topology',
|
|
37389
37951
|
});
|
|
37952
|
+
// ==================== 3. Cameras ====================
|
|
37953
|
+
addGroup('Cameras');
|
|
37954
|
+
// ==================== 4. Status ====================
|
|
37390
37955
|
// Add status display
|
|
37391
37956
|
const activeCount = this.trackingState.getActiveCount();
|
|
37392
37957
|
const topologyJson = this.storage.getItem('topology');
|
|
@@ -37430,6 +37995,12 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37430
37995
|
group: 'Status',
|
|
37431
37996
|
});
|
|
37432
37997
|
}
|
|
37998
|
+
// ==================== 5. Tracking ====================
|
|
37999
|
+
addGroup('Tracking');
|
|
38000
|
+
// ==================== 6. AI & Spatial Reasoning ====================
|
|
38001
|
+
addGroup('AI & Spatial Reasoning');
|
|
38002
|
+
// ==================== 7. Alerts ====================
|
|
38003
|
+
addGroup('Alerts');
|
|
37433
38004
|
// Add alert rules configuration UI
|
|
37434
38005
|
const alertRules = this.alertManager.getRules();
|
|
37435
38006
|
const rulesHtml = this.generateAlertRulesHtml(alertRules);
|
|
@@ -37440,6 +38011,8 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37440
38011
|
value: rulesHtml,
|
|
37441
38012
|
group: 'Alerts',
|
|
37442
38013
|
});
|
|
38014
|
+
// ==================== 8. MQTT Integration ====================
|
|
38015
|
+
addGroup('MQTT Integration');
|
|
37443
38016
|
return settings;
|
|
37444
38017
|
}
|
|
37445
38018
|
generateAlertRulesHtml(rules) {
|
|
@@ -37610,17 +38183,42 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37610
38183
|
const globalId = path.split('/').pop();
|
|
37611
38184
|
return this.handleJourneyPathRequest(globalId, response);
|
|
37612
38185
|
}
|
|
38186
|
+
// Training Mode endpoints
|
|
38187
|
+
if (path.endsWith('/api/training/start')) {
|
|
38188
|
+
return this.handleTrainingStartRequest(request, response);
|
|
38189
|
+
}
|
|
38190
|
+
if (path.endsWith('/api/training/pause')) {
|
|
38191
|
+
return this.handleTrainingPauseRequest(response);
|
|
38192
|
+
}
|
|
38193
|
+
if (path.endsWith('/api/training/resume')) {
|
|
38194
|
+
return this.handleTrainingResumeRequest(response);
|
|
38195
|
+
}
|
|
38196
|
+
if (path.endsWith('/api/training/end')) {
|
|
38197
|
+
return this.handleTrainingEndRequest(response);
|
|
38198
|
+
}
|
|
38199
|
+
if (path.endsWith('/api/training/status')) {
|
|
38200
|
+
return this.handleTrainingStatusRequest(response);
|
|
38201
|
+
}
|
|
38202
|
+
if (path.endsWith('/api/training/landmark')) {
|
|
38203
|
+
return this.handleTrainingLandmarkRequest(request, response);
|
|
38204
|
+
}
|
|
38205
|
+
if (path.endsWith('/api/training/apply')) {
|
|
38206
|
+
return this.handleTrainingApplyRequest(response);
|
|
38207
|
+
}
|
|
37613
38208
|
// UI Routes
|
|
37614
38209
|
if (path.endsWith('/ui/editor') || path.endsWith('/ui/editor/')) {
|
|
37615
38210
|
return this.serveEditorUI(response);
|
|
37616
38211
|
}
|
|
38212
|
+
if (path.endsWith('/ui/training') || path.endsWith('/ui/training/')) {
|
|
38213
|
+
return this.serveTrainingUI(response);
|
|
38214
|
+
}
|
|
37617
38215
|
if (path.includes('/ui/')) {
|
|
37618
38216
|
return this.serveStaticFile(path, response);
|
|
37619
38217
|
}
|
|
37620
38218
|
// Default: return info page
|
|
37621
38219
|
response.send(JSON.stringify({
|
|
37622
38220
|
name: 'Spatial Awareness Plugin',
|
|
37623
|
-
version: '0.
|
|
38221
|
+
version: '0.4.0',
|
|
37624
38222
|
endpoints: {
|
|
37625
38223
|
api: {
|
|
37626
38224
|
trackedObjects: '/api/tracked-objects',
|
|
@@ -37632,9 +38230,19 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37632
38230
|
liveTracking: '/api/live-tracking',
|
|
37633
38231
|
connectionSuggestions: '/api/connection-suggestions',
|
|
37634
38232
|
landmarkSuggestions: '/api/landmark-suggestions',
|
|
38233
|
+
training: {
|
|
38234
|
+
start: '/api/training/start',
|
|
38235
|
+
pause: '/api/training/pause',
|
|
38236
|
+
resume: '/api/training/resume',
|
|
38237
|
+
end: '/api/training/end',
|
|
38238
|
+
status: '/api/training/status',
|
|
38239
|
+
landmark: '/api/training/landmark',
|
|
38240
|
+
apply: '/api/training/apply',
|
|
38241
|
+
},
|
|
37635
38242
|
},
|
|
37636
38243
|
ui: {
|
|
37637
38244
|
editor: '/ui/editor',
|
|
38245
|
+
training: '/ui/training',
|
|
37638
38246
|
},
|
|
37639
38247
|
},
|
|
37640
38248
|
}), {
|
|
@@ -38085,11 +38693,175 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
38085
38693
|
});
|
|
38086
38694
|
}
|
|
38087
38695
|
}
|
|
38696
|
+
// ==================== Training Mode Handlers ====================
|
|
38697
|
+
handleTrainingStartRequest(request, response) {
|
|
38698
|
+
if (!this.trackingEngine) {
|
|
38699
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running. Configure topology first.' }), {
|
|
38700
|
+
code: 500,
|
|
38701
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38702
|
+
});
|
|
38703
|
+
return;
|
|
38704
|
+
}
|
|
38705
|
+
try {
|
|
38706
|
+
let config;
|
|
38707
|
+
let trainerName;
|
|
38708
|
+
if (request.body) {
|
|
38709
|
+
const body = JSON.parse(request.body);
|
|
38710
|
+
trainerName = body.trainerName;
|
|
38711
|
+
config = body.config;
|
|
38712
|
+
}
|
|
38713
|
+
const session = this.trackingEngine.startTrainingSession(trainerName, config);
|
|
38714
|
+
response.send(JSON.stringify(session), {
|
|
38715
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38716
|
+
});
|
|
38717
|
+
}
|
|
38718
|
+
catch (e) {
|
|
38719
|
+
response.send(JSON.stringify({ error: e.message }), {
|
|
38720
|
+
code: 500,
|
|
38721
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38722
|
+
});
|
|
38723
|
+
}
|
|
38724
|
+
}
|
|
38725
|
+
handleTrainingPauseRequest(response) {
|
|
38726
|
+
if (!this.trackingEngine) {
|
|
38727
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38728
|
+
code: 500,
|
|
38729
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38730
|
+
});
|
|
38731
|
+
return;
|
|
38732
|
+
}
|
|
38733
|
+
const success = this.trackingEngine.pauseTrainingSession();
|
|
38734
|
+
if (success) {
|
|
38735
|
+
response.send(JSON.stringify({ success: true }), {
|
|
38736
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38737
|
+
});
|
|
38738
|
+
}
|
|
38739
|
+
else {
|
|
38740
|
+
response.send(JSON.stringify({ error: 'No active training session to pause' }), {
|
|
38741
|
+
code: 400,
|
|
38742
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38743
|
+
});
|
|
38744
|
+
}
|
|
38745
|
+
}
|
|
38746
|
+
handleTrainingResumeRequest(response) {
|
|
38747
|
+
if (!this.trackingEngine) {
|
|
38748
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38749
|
+
code: 500,
|
|
38750
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38751
|
+
});
|
|
38752
|
+
return;
|
|
38753
|
+
}
|
|
38754
|
+
const success = this.trackingEngine.resumeTrainingSession();
|
|
38755
|
+
if (success) {
|
|
38756
|
+
response.send(JSON.stringify({ success: true }), {
|
|
38757
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38758
|
+
});
|
|
38759
|
+
}
|
|
38760
|
+
else {
|
|
38761
|
+
response.send(JSON.stringify({ error: 'No paused training session to resume' }), {
|
|
38762
|
+
code: 400,
|
|
38763
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38764
|
+
});
|
|
38765
|
+
}
|
|
38766
|
+
}
|
|
38767
|
+
handleTrainingEndRequest(response) {
|
|
38768
|
+
if (!this.trackingEngine) {
|
|
38769
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38770
|
+
code: 500,
|
|
38771
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38772
|
+
});
|
|
38773
|
+
return;
|
|
38774
|
+
}
|
|
38775
|
+
const session = this.trackingEngine.endTrainingSession();
|
|
38776
|
+
if (session) {
|
|
38777
|
+
response.send(JSON.stringify(session), {
|
|
38778
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38779
|
+
});
|
|
38780
|
+
}
|
|
38781
|
+
else {
|
|
38782
|
+
response.send(JSON.stringify({ error: 'No training session to end' }), {
|
|
38783
|
+
code: 400,
|
|
38784
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38785
|
+
});
|
|
38786
|
+
}
|
|
38787
|
+
}
|
|
38788
|
+
handleTrainingStatusRequest(response) {
|
|
38789
|
+
if (!this.trackingEngine) {
|
|
38790
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
38791
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38792
|
+
});
|
|
38793
|
+
return;
|
|
38794
|
+
}
|
|
38795
|
+
const status = this.trackingEngine.getTrainingStatus();
|
|
38796
|
+
if (status) {
|
|
38797
|
+
response.send(JSON.stringify(status), {
|
|
38798
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38799
|
+
});
|
|
38800
|
+
}
|
|
38801
|
+
else {
|
|
38802
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
38803
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38804
|
+
});
|
|
38805
|
+
}
|
|
38806
|
+
}
|
|
38807
|
+
handleTrainingLandmarkRequest(request, response) {
|
|
38808
|
+
if (!this.trackingEngine) {
|
|
38809
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38810
|
+
code: 500,
|
|
38811
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38812
|
+
});
|
|
38813
|
+
return;
|
|
38814
|
+
}
|
|
38815
|
+
try {
|
|
38816
|
+
const body = JSON.parse(request.body);
|
|
38817
|
+
const landmark = this.trackingEngine.markTrainingLandmark(body);
|
|
38818
|
+
if (landmark) {
|
|
38819
|
+
response.send(JSON.stringify({ success: true, landmark }), {
|
|
38820
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38821
|
+
});
|
|
38822
|
+
}
|
|
38823
|
+
else {
|
|
38824
|
+
response.send(JSON.stringify({ error: 'No active training session' }), {
|
|
38825
|
+
code: 400,
|
|
38826
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38827
|
+
});
|
|
38828
|
+
}
|
|
38829
|
+
}
|
|
38830
|
+
catch (e) {
|
|
38831
|
+
response.send(JSON.stringify({ error: 'Invalid request body' }), {
|
|
38832
|
+
code: 400,
|
|
38833
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38834
|
+
});
|
|
38835
|
+
}
|
|
38836
|
+
}
|
|
38837
|
+
handleTrainingApplyRequest(response) {
|
|
38838
|
+
if (!this.trackingEngine) {
|
|
38839
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38840
|
+
code: 500,
|
|
38841
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38842
|
+
});
|
|
38843
|
+
return;
|
|
38844
|
+
}
|
|
38845
|
+
const result = this.trackingEngine.applyTrainingToTopology();
|
|
38846
|
+
if (result.success) {
|
|
38847
|
+
// Save the updated topology
|
|
38848
|
+
const topology = this.trackingEngine.getTopology();
|
|
38849
|
+
this.storage.setItem('topology', JSON.stringify(topology));
|
|
38850
|
+
}
|
|
38851
|
+
response.send(JSON.stringify(result), {
|
|
38852
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38853
|
+
});
|
|
38854
|
+
}
|
|
38088
38855
|
serveEditorUI(response) {
|
|
38089
38856
|
response.send(editor_html_1.EDITOR_HTML, {
|
|
38090
38857
|
headers: { 'Content-Type': 'text/html' },
|
|
38091
38858
|
});
|
|
38092
38859
|
}
|
|
38860
|
+
serveTrainingUI(response) {
|
|
38861
|
+
response.send(training_html_1.TRAINING_HTML, {
|
|
38862
|
+
headers: { 'Content-Type': 'text/html' },
|
|
38863
|
+
});
|
|
38864
|
+
}
|
|
38093
38865
|
serveStaticFile(path, response) {
|
|
38094
38866
|
// Serve static files for the UI
|
|
38095
38867
|
response.send('Not found', { code: 404 });
|
|
@@ -38668,6 +39440,83 @@ function getJourneySummary(tracked) {
|
|
|
38668
39440
|
}
|
|
38669
39441
|
|
|
38670
39442
|
|
|
39443
|
+
/***/ },
|
|
39444
|
+
|
|
39445
|
+
/***/ "./src/models/training.ts"
|
|
39446
|
+
/*!********************************!*\
|
|
39447
|
+
!*** ./src/models/training.ts ***!
|
|
39448
|
+
\********************************/
|
|
39449
|
+
(__unused_webpack_module, exports) {
|
|
39450
|
+
|
|
39451
|
+
"use strict";
|
|
39452
|
+
|
|
39453
|
+
/**
|
|
39454
|
+
* Training Mode Types
|
|
39455
|
+
*
|
|
39456
|
+
* These types support the guided training system where a user physically
|
|
39457
|
+
* walks around their property to train the system on camera positions,
|
|
39458
|
+
* transit times, overlaps, landmarks, and structures.
|
|
39459
|
+
*/
|
|
39460
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
39461
|
+
exports.DEFAULT_TRAINING_CONFIG = void 0;
|
|
39462
|
+
exports.createTrainingSession = createTrainingSession;
|
|
39463
|
+
exports.calculateTrainingStats = calculateTrainingStats;
|
|
39464
|
+
/** Default training configuration */
|
|
39465
|
+
exports.DEFAULT_TRAINING_CONFIG = {
|
|
39466
|
+
minDetectionConfidence: 0.7,
|
|
39467
|
+
maxTransitWait: 120, // 2 minutes
|
|
39468
|
+
autoDetectOverlaps: true,
|
|
39469
|
+
autoSuggestLandmarks: true,
|
|
39470
|
+
minOverlapDuration: 2, // 2 seconds
|
|
39471
|
+
};
|
|
39472
|
+
/** Create a new empty training session */
|
|
39473
|
+
function createTrainingSession(trainerName) {
|
|
39474
|
+
const now = Date.now();
|
|
39475
|
+
return {
|
|
39476
|
+
id: `training-${now}-${Math.random().toString(36).substr(2, 9)}`,
|
|
39477
|
+
state: 'idle',
|
|
39478
|
+
startedAt: now,
|
|
39479
|
+
updatedAt: now,
|
|
39480
|
+
trainerName,
|
|
39481
|
+
visits: [],
|
|
39482
|
+
transits: [],
|
|
39483
|
+
landmarks: [],
|
|
39484
|
+
overlaps: [],
|
|
39485
|
+
structures: [],
|
|
39486
|
+
stats: {
|
|
39487
|
+
totalDuration: 0,
|
|
39488
|
+
camerasVisited: 0,
|
|
39489
|
+
transitsRecorded: 0,
|
|
39490
|
+
landmarksMarked: 0,
|
|
39491
|
+
overlapsDetected: 0,
|
|
39492
|
+
structuresMarked: 0,
|
|
39493
|
+
averageTransitTime: 0,
|
|
39494
|
+
coveragePercentage: 0,
|
|
39495
|
+
},
|
|
39496
|
+
};
|
|
39497
|
+
}
|
|
39498
|
+
/** Calculate session statistics */
|
|
39499
|
+
function calculateTrainingStats(session, totalCameras) {
|
|
39500
|
+
const uniqueCameras = new Set(session.visits.map(v => v.cameraId));
|
|
39501
|
+
const transitTimes = session.transits.map(t => t.transitSeconds);
|
|
39502
|
+
const avgTransit = transitTimes.length > 0
|
|
39503
|
+
? transitTimes.reduce((a, b) => a + b, 0) / transitTimes.length
|
|
39504
|
+
: 0;
|
|
39505
|
+
return {
|
|
39506
|
+
totalDuration: (session.completedAt || Date.now()) - session.startedAt,
|
|
39507
|
+
camerasVisited: uniqueCameras.size,
|
|
39508
|
+
transitsRecorded: session.transits.length,
|
|
39509
|
+
landmarksMarked: session.landmarks.length,
|
|
39510
|
+
overlapsDetected: session.overlaps.length,
|
|
39511
|
+
structuresMarked: session.structures.length,
|
|
39512
|
+
averageTransitTime: Math.round(avgTransit),
|
|
39513
|
+
coveragePercentage: totalCameras > 0
|
|
39514
|
+
? Math.round((uniqueCameras.size / totalCameras) * 100)
|
|
39515
|
+
: 0,
|
|
39516
|
+
};
|
|
39517
|
+
}
|
|
39518
|
+
|
|
39519
|
+
|
|
38671
39520
|
/***/ },
|
|
38672
39521
|
|
|
38673
39522
|
/***/ "./src/state/tracking-state.ts"
|
|
@@ -40200,6 +41049,1026 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
40200
41049
|
</html>`;
|
|
40201
41050
|
|
|
40202
41051
|
|
|
41052
|
+
/***/ },
|
|
41053
|
+
|
|
41054
|
+
/***/ "./src/ui/training-html.ts"
|
|
41055
|
+
/*!*********************************!*\
|
|
41056
|
+
!*** ./src/ui/training-html.ts ***!
|
|
41057
|
+
\*********************************/
|
|
41058
|
+
(__unused_webpack_module, exports) {
|
|
41059
|
+
|
|
41060
|
+
"use strict";
|
|
41061
|
+
|
|
41062
|
+
/**
|
|
41063
|
+
* Training Mode UI - Mobile-optimized walkthrough interface
|
|
41064
|
+
* Designed for phone use while walking around property
|
|
41065
|
+
*/
|
|
41066
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
41067
|
+
exports.TRAINING_HTML = void 0;
|
|
41068
|
+
exports.TRAINING_HTML = `<!DOCTYPE html>
|
|
41069
|
+
<html lang="en">
|
|
41070
|
+
<head>
|
|
41071
|
+
<meta charset="UTF-8">
|
|
41072
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
41073
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
41074
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
41075
|
+
<title>Spatial Awareness - Training Mode</title>
|
|
41076
|
+
<style>
|
|
41077
|
+
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
|
41078
|
+
html, body { height: 100%; overflow: hidden; }
|
|
41079
|
+
body {
|
|
41080
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
41081
|
+
background: #121212;
|
|
41082
|
+
color: rgba(255,255,255,0.87);
|
|
41083
|
+
min-height: 100vh;
|
|
41084
|
+
display: flex;
|
|
41085
|
+
flex-direction: column;
|
|
41086
|
+
}
|
|
41087
|
+
|
|
41088
|
+
/* Header */
|
|
41089
|
+
.header {
|
|
41090
|
+
background: rgba(255,255,255,0.03);
|
|
41091
|
+
padding: 12px 16px;
|
|
41092
|
+
display: flex;
|
|
41093
|
+
justify-content: space-between;
|
|
41094
|
+
align-items: center;
|
|
41095
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
41096
|
+
}
|
|
41097
|
+
.header h1 { font-size: 16px; font-weight: 500; }
|
|
41098
|
+
.header-status {
|
|
41099
|
+
display: flex;
|
|
41100
|
+
align-items: center;
|
|
41101
|
+
gap: 8px;
|
|
41102
|
+
font-size: 13px;
|
|
41103
|
+
}
|
|
41104
|
+
.status-badge {
|
|
41105
|
+
padding: 4px 10px;
|
|
41106
|
+
border-radius: 4px;
|
|
41107
|
+
font-size: 11px;
|
|
41108
|
+
font-weight: 500;
|
|
41109
|
+
text-transform: uppercase;
|
|
41110
|
+
}
|
|
41111
|
+
.status-badge.idle { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); }
|
|
41112
|
+
.status-badge.active { background: #4fc3f7; color: #000; animation: pulse 2s infinite; }
|
|
41113
|
+
.status-badge.paused { background: #ffb74d; color: #000; }
|
|
41114
|
+
.status-badge.completed { background: #81c784; color: #000; }
|
|
41115
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
|
41116
|
+
|
|
41117
|
+
/* Main content area */
|
|
41118
|
+
.main-content {
|
|
41119
|
+
flex: 1;
|
|
41120
|
+
display: flex;
|
|
41121
|
+
flex-direction: column;
|
|
41122
|
+
padding: 12px;
|
|
41123
|
+
overflow-y: auto;
|
|
41124
|
+
gap: 12px;
|
|
41125
|
+
}
|
|
41126
|
+
|
|
41127
|
+
/* Camera detection card */
|
|
41128
|
+
.detection-card {
|
|
41129
|
+
background: rgba(255,255,255,0.03);
|
|
41130
|
+
border-radius: 8px;
|
|
41131
|
+
padding: 16px;
|
|
41132
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
41133
|
+
transition: all 0.3s;
|
|
41134
|
+
}
|
|
41135
|
+
.detection-card.detecting {
|
|
41136
|
+
border-color: #4fc3f7;
|
|
41137
|
+
background: rgba(79, 195, 247, 0.1);
|
|
41138
|
+
}
|
|
41139
|
+
.detection-card.in-transit {
|
|
41140
|
+
border-color: #ffb74d;
|
|
41141
|
+
background: rgba(255, 183, 77, 0.1);
|
|
41142
|
+
}
|
|
41143
|
+
|
|
41144
|
+
.detection-icon {
|
|
41145
|
+
width: 64px;
|
|
41146
|
+
height: 64px;
|
|
41147
|
+
border-radius: 8px;
|
|
41148
|
+
background: rgba(255,255,255,0.05);
|
|
41149
|
+
display: flex;
|
|
41150
|
+
align-items: center;
|
|
41151
|
+
justify-content: center;
|
|
41152
|
+
margin: 0 auto 12px;
|
|
41153
|
+
font-size: 28px;
|
|
41154
|
+
}
|
|
41155
|
+
.detection-card.detecting .detection-icon {
|
|
41156
|
+
background: #4fc3f7;
|
|
41157
|
+
animation: detectPulse 1.5s infinite;
|
|
41158
|
+
}
|
|
41159
|
+
@keyframes detectPulse {
|
|
41160
|
+
0%, 100% { transform: scale(1); }
|
|
41161
|
+
50% { transform: scale(1.03); }
|
|
41162
|
+
}
|
|
41163
|
+
|
|
41164
|
+
.detection-title {
|
|
41165
|
+
font-size: 18px;
|
|
41166
|
+
font-weight: 500;
|
|
41167
|
+
text-align: center;
|
|
41168
|
+
margin-bottom: 4px;
|
|
41169
|
+
}
|
|
41170
|
+
.detection-subtitle {
|
|
41171
|
+
font-size: 13px;
|
|
41172
|
+
color: rgba(255,255,255,0.5);
|
|
41173
|
+
text-align: center;
|
|
41174
|
+
}
|
|
41175
|
+
.detection-confidence {
|
|
41176
|
+
margin-top: 8px;
|
|
41177
|
+
text-align: center;
|
|
41178
|
+
font-size: 12px;
|
|
41179
|
+
color: rgba(255,255,255,0.4);
|
|
41180
|
+
}
|
|
41181
|
+
|
|
41182
|
+
/* Transit timer */
|
|
41183
|
+
.transit-timer {
|
|
41184
|
+
display: flex;
|
|
41185
|
+
align-items: center;
|
|
41186
|
+
justify-content: center;
|
|
41187
|
+
gap: 8px;
|
|
41188
|
+
margin-top: 12px;
|
|
41189
|
+
padding: 10px;
|
|
41190
|
+
background: rgba(0,0,0,0.2);
|
|
41191
|
+
border-radius: 6px;
|
|
41192
|
+
}
|
|
41193
|
+
.transit-timer-icon { font-size: 18px; }
|
|
41194
|
+
.transit-timer-text { font-size: 16px; font-weight: 500; }
|
|
41195
|
+
.transit-timer-from { font-size: 12px; color: rgba(255,255,255,0.5); }
|
|
41196
|
+
|
|
41197
|
+
/* Stats grid */
|
|
41198
|
+
.stats-grid {
|
|
41199
|
+
display: grid;
|
|
41200
|
+
grid-template-columns: repeat(3, 1fr);
|
|
41201
|
+
gap: 8px;
|
|
41202
|
+
}
|
|
41203
|
+
.stat-item {
|
|
41204
|
+
background: rgba(255,255,255,0.03);
|
|
41205
|
+
border-radius: 6px;
|
|
41206
|
+
padding: 12px 8px;
|
|
41207
|
+
text-align: center;
|
|
41208
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41209
|
+
}
|
|
41210
|
+
.stat-value {
|
|
41211
|
+
font-size: 24px;
|
|
41212
|
+
font-weight: 500;
|
|
41213
|
+
color: #4fc3f7;
|
|
41214
|
+
}
|
|
41215
|
+
.stat-label {
|
|
41216
|
+
font-size: 10px;
|
|
41217
|
+
color: rgba(255,255,255,0.4);
|
|
41218
|
+
text-transform: uppercase;
|
|
41219
|
+
margin-top: 2px;
|
|
41220
|
+
}
|
|
41221
|
+
|
|
41222
|
+
/* Progress bar */
|
|
41223
|
+
.progress-section {
|
|
41224
|
+
background: rgba(255,255,255,0.03);
|
|
41225
|
+
border-radius: 6px;
|
|
41226
|
+
padding: 12px;
|
|
41227
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41228
|
+
}
|
|
41229
|
+
.progress-header {
|
|
41230
|
+
display: flex;
|
|
41231
|
+
justify-content: space-between;
|
|
41232
|
+
margin-bottom: 8px;
|
|
41233
|
+
font-size: 13px;
|
|
41234
|
+
}
|
|
41235
|
+
.progress-bar {
|
|
41236
|
+
height: 6px;
|
|
41237
|
+
background: rgba(255,255,255,0.1);
|
|
41238
|
+
border-radius: 3px;
|
|
41239
|
+
overflow: hidden;
|
|
41240
|
+
}
|
|
41241
|
+
.progress-fill {
|
|
41242
|
+
height: 100%;
|
|
41243
|
+
background: #4fc3f7;
|
|
41244
|
+
border-radius: 3px;
|
|
41245
|
+
transition: width 0.5s;
|
|
41246
|
+
}
|
|
41247
|
+
|
|
41248
|
+
/* Suggestions */
|
|
41249
|
+
.suggestions-section {
|
|
41250
|
+
background: rgba(79, 195, 247, 0.08);
|
|
41251
|
+
border: 1px solid rgba(79, 195, 247, 0.2);
|
|
41252
|
+
border-radius: 6px;
|
|
41253
|
+
padding: 12px;
|
|
41254
|
+
}
|
|
41255
|
+
.suggestions-title {
|
|
41256
|
+
font-size: 11px;
|
|
41257
|
+
text-transform: uppercase;
|
|
41258
|
+
color: #4fc3f7;
|
|
41259
|
+
margin-bottom: 6px;
|
|
41260
|
+
font-weight: 500;
|
|
41261
|
+
}
|
|
41262
|
+
.suggestion-item {
|
|
41263
|
+
font-size: 13px;
|
|
41264
|
+
padding: 6px 0;
|
|
41265
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
41266
|
+
color: rgba(255,255,255,0.7);
|
|
41267
|
+
}
|
|
41268
|
+
.suggestion-item:last-child { border-bottom: none; }
|
|
41269
|
+
.suggestion-item::before {
|
|
41270
|
+
content: "→ ";
|
|
41271
|
+
color: #4fc3f7;
|
|
41272
|
+
}
|
|
41273
|
+
|
|
41274
|
+
/* Action buttons */
|
|
41275
|
+
.action-buttons {
|
|
41276
|
+
display: flex;
|
|
41277
|
+
gap: 8px;
|
|
41278
|
+
padding: 8px 0;
|
|
41279
|
+
}
|
|
41280
|
+
.btn {
|
|
41281
|
+
flex: 1;
|
|
41282
|
+
padding: 14px 16px;
|
|
41283
|
+
border: none;
|
|
41284
|
+
border-radius: 6px;
|
|
41285
|
+
font-size: 14px;
|
|
41286
|
+
font-weight: 500;
|
|
41287
|
+
cursor: pointer;
|
|
41288
|
+
transition: all 0.2s;
|
|
41289
|
+
display: flex;
|
|
41290
|
+
align-items: center;
|
|
41291
|
+
justify-content: center;
|
|
41292
|
+
gap: 6px;
|
|
41293
|
+
}
|
|
41294
|
+
.btn:active { transform: scale(0.98); }
|
|
41295
|
+
.btn-primary { background: #4fc3f7; color: #000; }
|
|
41296
|
+
.btn-secondary { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.87); }
|
|
41297
|
+
.btn-danger { background: #ef5350; color: #fff; }
|
|
41298
|
+
.btn-warning { background: #ffb74d; color: #000; }
|
|
41299
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
41300
|
+
|
|
41301
|
+
/* Full-width button */
|
|
41302
|
+
.btn-full { flex: none; width: 100%; }
|
|
41303
|
+
|
|
41304
|
+
/* Mark landmark/structure panel */
|
|
41305
|
+
.mark-panel {
|
|
41306
|
+
background: rgba(255,255,255,0.03);
|
|
41307
|
+
border-radius: 6px;
|
|
41308
|
+
padding: 16px;
|
|
41309
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41310
|
+
}
|
|
41311
|
+
.mark-panel-title {
|
|
41312
|
+
font-size: 14px;
|
|
41313
|
+
font-weight: 500;
|
|
41314
|
+
margin-bottom: 12px;
|
|
41315
|
+
}
|
|
41316
|
+
.mark-type-grid {
|
|
41317
|
+
display: grid;
|
|
41318
|
+
grid-template-columns: repeat(4, 1fr);
|
|
41319
|
+
gap: 6px;
|
|
41320
|
+
margin-bottom: 12px;
|
|
41321
|
+
}
|
|
41322
|
+
.mark-type-btn {
|
|
41323
|
+
padding: 10px 6px;
|
|
41324
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
41325
|
+
border-radius: 6px;
|
|
41326
|
+
background: transparent;
|
|
41327
|
+
color: rgba(255,255,255,0.7);
|
|
41328
|
+
font-size: 10px;
|
|
41329
|
+
text-align: center;
|
|
41330
|
+
cursor: pointer;
|
|
41331
|
+
transition: all 0.2s;
|
|
41332
|
+
}
|
|
41333
|
+
.mark-type-btn.selected {
|
|
41334
|
+
border-color: #4fc3f7;
|
|
41335
|
+
background: rgba(79, 195, 247, 0.15);
|
|
41336
|
+
color: #fff;
|
|
41337
|
+
}
|
|
41338
|
+
.mark-type-btn .icon { font-size: 18px; margin-bottom: 2px; display: block; }
|
|
41339
|
+
|
|
41340
|
+
/* Input field */
|
|
41341
|
+
.input-group { margin-bottom: 12px; }
|
|
41342
|
+
.input-group label {
|
|
41343
|
+
display: block;
|
|
41344
|
+
font-size: 12px;
|
|
41345
|
+
color: rgba(255,255,255,0.5);
|
|
41346
|
+
margin-bottom: 4px;
|
|
41347
|
+
}
|
|
41348
|
+
.input-group input {
|
|
41349
|
+
width: 100%;
|
|
41350
|
+
padding: 12px;
|
|
41351
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
41352
|
+
border-radius: 6px;
|
|
41353
|
+
background: rgba(0,0,0,0.2);
|
|
41354
|
+
color: #fff;
|
|
41355
|
+
font-size: 14px;
|
|
41356
|
+
}
|
|
41357
|
+
.input-group input:focus {
|
|
41358
|
+
outline: none;
|
|
41359
|
+
border-color: #4fc3f7;
|
|
41360
|
+
}
|
|
41361
|
+
|
|
41362
|
+
/* History list */
|
|
41363
|
+
.history-section {
|
|
41364
|
+
background: rgba(255,255,255,0.03);
|
|
41365
|
+
border-radius: 6px;
|
|
41366
|
+
padding: 12px;
|
|
41367
|
+
max-height: 180px;
|
|
41368
|
+
overflow-y: auto;
|
|
41369
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41370
|
+
}
|
|
41371
|
+
.history-title {
|
|
41372
|
+
font-size: 13px;
|
|
41373
|
+
font-weight: 500;
|
|
41374
|
+
margin-bottom: 8px;
|
|
41375
|
+
display: flex;
|
|
41376
|
+
justify-content: space-between;
|
|
41377
|
+
}
|
|
41378
|
+
.history-item {
|
|
41379
|
+
padding: 8px;
|
|
41380
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
41381
|
+
font-size: 13px;
|
|
41382
|
+
}
|
|
41383
|
+
.history-item:last-child { border-bottom: none; }
|
|
41384
|
+
.history-item-time {
|
|
41385
|
+
font-size: 10px;
|
|
41386
|
+
color: rgba(255,255,255,0.4);
|
|
41387
|
+
}
|
|
41388
|
+
.history-item-camera { color: #4fc3f7; font-weight: 500; }
|
|
41389
|
+
.history-item-transit { color: #ffb74d; }
|
|
41390
|
+
|
|
41391
|
+
/* Bottom action bar */
|
|
41392
|
+
.bottom-bar {
|
|
41393
|
+
background: rgba(255,255,255,0.03);
|
|
41394
|
+
padding: 12px 16px;
|
|
41395
|
+
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
|
41396
|
+
border-top: 1px solid rgba(255,255,255,0.08);
|
|
41397
|
+
}
|
|
41398
|
+
|
|
41399
|
+
/* Tabs */
|
|
41400
|
+
.tabs {
|
|
41401
|
+
display: flex;
|
|
41402
|
+
background: rgba(0,0,0,0.2);
|
|
41403
|
+
border-radius: 6px;
|
|
41404
|
+
padding: 3px;
|
|
41405
|
+
margin-bottom: 12px;
|
|
41406
|
+
}
|
|
41407
|
+
.tab {
|
|
41408
|
+
flex: 1;
|
|
41409
|
+
padding: 10px;
|
|
41410
|
+
border: none;
|
|
41411
|
+
background: transparent;
|
|
41412
|
+
color: rgba(255,255,255,0.5);
|
|
41413
|
+
font-size: 13px;
|
|
41414
|
+
font-weight: 500;
|
|
41415
|
+
cursor: pointer;
|
|
41416
|
+
border-radius: 4px;
|
|
41417
|
+
transition: all 0.2s;
|
|
41418
|
+
}
|
|
41419
|
+
.tab.active {
|
|
41420
|
+
background: rgba(255,255,255,0.08);
|
|
41421
|
+
color: rgba(255,255,255,0.87);
|
|
41422
|
+
}
|
|
41423
|
+
|
|
41424
|
+
/* Tab content */
|
|
41425
|
+
.tab-content { display: none; }
|
|
41426
|
+
.tab-content.active { display: block; }
|
|
41427
|
+
|
|
41428
|
+
/* Apply results modal */
|
|
41429
|
+
.modal-overlay {
|
|
41430
|
+
display: none;
|
|
41431
|
+
position: fixed;
|
|
41432
|
+
top: 0;
|
|
41433
|
+
left: 0;
|
|
41434
|
+
right: 0;
|
|
41435
|
+
bottom: 0;
|
|
41436
|
+
background: rgba(0,0,0,0.85);
|
|
41437
|
+
z-index: 100;
|
|
41438
|
+
align-items: center;
|
|
41439
|
+
justify-content: center;
|
|
41440
|
+
padding: 16px;
|
|
41441
|
+
}
|
|
41442
|
+
.modal-overlay.active { display: flex; }
|
|
41443
|
+
.modal {
|
|
41444
|
+
background: #1e1e1e;
|
|
41445
|
+
border-radius: 8px;
|
|
41446
|
+
padding: 20px;
|
|
41447
|
+
max-width: 360px;
|
|
41448
|
+
width: 100%;
|
|
41449
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
41450
|
+
}
|
|
41451
|
+
.modal h2 { font-size: 18px; margin-bottom: 12px; font-weight: 500; }
|
|
41452
|
+
.modal-result-item {
|
|
41453
|
+
display: flex;
|
|
41454
|
+
justify-content: space-between;
|
|
41455
|
+
padding: 8px 0;
|
|
41456
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
41457
|
+
font-size: 13px;
|
|
41458
|
+
}
|
|
41459
|
+
.modal-result-value { color: #4fc3f7; font-weight: 500; }
|
|
41460
|
+
.modal-buttons { display: flex; gap: 8px; margin-top: 16px; }
|
|
41461
|
+
|
|
41462
|
+
/* Idle state */
|
|
41463
|
+
.idle-content {
|
|
41464
|
+
flex: 1;
|
|
41465
|
+
display: flex;
|
|
41466
|
+
flex-direction: column;
|
|
41467
|
+
align-items: center;
|
|
41468
|
+
justify-content: center;
|
|
41469
|
+
text-align: center;
|
|
41470
|
+
padding: 32px 16px;
|
|
41471
|
+
}
|
|
41472
|
+
.idle-icon {
|
|
41473
|
+
font-size: 56px;
|
|
41474
|
+
margin-bottom: 16px;
|
|
41475
|
+
opacity: 0.7;
|
|
41476
|
+
}
|
|
41477
|
+
.idle-title {
|
|
41478
|
+
font-size: 20px;
|
|
41479
|
+
font-weight: 500;
|
|
41480
|
+
margin-bottom: 8px;
|
|
41481
|
+
}
|
|
41482
|
+
.idle-desc {
|
|
41483
|
+
font-size: 14px;
|
|
41484
|
+
color: rgba(255,255,255,0.5);
|
|
41485
|
+
max-width: 280px;
|
|
41486
|
+
line-height: 1.5;
|
|
41487
|
+
}
|
|
41488
|
+
.idle-instructions {
|
|
41489
|
+
margin-top: 24px;
|
|
41490
|
+
text-align: left;
|
|
41491
|
+
background: rgba(255,255,255,0.03);
|
|
41492
|
+
border-radius: 6px;
|
|
41493
|
+
padding: 16px;
|
|
41494
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41495
|
+
}
|
|
41496
|
+
.idle-instructions h3 {
|
|
41497
|
+
font-size: 12px;
|
|
41498
|
+
margin-bottom: 10px;
|
|
41499
|
+
color: #4fc3f7;
|
|
41500
|
+
font-weight: 500;
|
|
41501
|
+
}
|
|
41502
|
+
.idle-instructions ol {
|
|
41503
|
+
padding-left: 18px;
|
|
41504
|
+
font-size: 13px;
|
|
41505
|
+
line-height: 1.8;
|
|
41506
|
+
color: rgba(255,255,255,0.6);
|
|
41507
|
+
}
|
|
41508
|
+
</style>
|
|
41509
|
+
</head>
|
|
41510
|
+
<body>
|
|
41511
|
+
<div class="header">
|
|
41512
|
+
<h1>Training Mode</h1>
|
|
41513
|
+
<div class="header-status">
|
|
41514
|
+
<span class="status-badge" id="status-badge">Idle</span>
|
|
41515
|
+
</div>
|
|
41516
|
+
</div>
|
|
41517
|
+
|
|
41518
|
+
<!-- Idle State -->
|
|
41519
|
+
<div class="main-content" id="idle-content">
|
|
41520
|
+
<div class="idle-content">
|
|
41521
|
+
<div class="idle-icon">🚶</div>
|
|
41522
|
+
<div class="idle-title">Train Your System</div>
|
|
41523
|
+
<div class="idle-desc">Walk around your property to teach the system about camera positions, transit times, and landmarks.</div>
|
|
41524
|
+
|
|
41525
|
+
<div class="idle-instructions">
|
|
41526
|
+
<h3>How it works:</h3>
|
|
41527
|
+
<ol>
|
|
41528
|
+
<li>Tap <strong>Start Training</strong> below</li>
|
|
41529
|
+
<li>Walk to each camera on your property</li>
|
|
41530
|
+
<li>The system detects you automatically</li>
|
|
41531
|
+
<li>Mark landmarks as you encounter them</li>
|
|
41532
|
+
<li>End training when you're done</li>
|
|
41533
|
+
</ol>
|
|
41534
|
+
</div>
|
|
41535
|
+
</div>
|
|
41536
|
+
|
|
41537
|
+
<div class="bottom-bar">
|
|
41538
|
+
<button class="btn btn-primary btn-full" onclick="startTraining()">
|
|
41539
|
+
▶ Start Training
|
|
41540
|
+
</button>
|
|
41541
|
+
</div>
|
|
41542
|
+
</div>
|
|
41543
|
+
|
|
41544
|
+
<!-- Active Training State -->
|
|
41545
|
+
<div class="main-content" id="active-content" style="display: none;">
|
|
41546
|
+
<!-- Detection Card -->
|
|
41547
|
+
<div class="detection-card" id="detection-card">
|
|
41548
|
+
<div class="detection-icon" id="detection-icon">👤</div>
|
|
41549
|
+
<div class="detection-title" id="detection-title">Waiting for detection...</div>
|
|
41550
|
+
<div class="detection-subtitle" id="detection-subtitle">Walk to any camera to begin</div>
|
|
41551
|
+
<div class="detection-confidence" id="detection-confidence"></div>
|
|
41552
|
+
|
|
41553
|
+
<div class="transit-timer" id="transit-timer" style="display: none;">
|
|
41554
|
+
<span class="transit-timer-icon">⏱</span>
|
|
41555
|
+
<span class="transit-timer-text" id="transit-time">0s</span>
|
|
41556
|
+
<span class="transit-timer-from" id="transit-from"></span>
|
|
41557
|
+
</div>
|
|
41558
|
+
</div>
|
|
41559
|
+
|
|
41560
|
+
<!-- Tabs -->
|
|
41561
|
+
<div class="tabs">
|
|
41562
|
+
<button class="tab active" onclick="switchTab('status')">Status</button>
|
|
41563
|
+
<button class="tab" onclick="switchTab('mark')">Mark</button>
|
|
41564
|
+
<button class="tab" onclick="switchTab('history')">History</button>
|
|
41565
|
+
</div>
|
|
41566
|
+
|
|
41567
|
+
<!-- Status Tab -->
|
|
41568
|
+
<div class="tab-content active" id="tab-status">
|
|
41569
|
+
<!-- Stats -->
|
|
41570
|
+
<div class="stats-grid">
|
|
41571
|
+
<div class="stat-item">
|
|
41572
|
+
<div class="stat-value" id="stat-cameras">0</div>
|
|
41573
|
+
<div class="stat-label">Cameras</div>
|
|
41574
|
+
</div>
|
|
41575
|
+
<div class="stat-item">
|
|
41576
|
+
<div class="stat-value" id="stat-transits">0</div>
|
|
41577
|
+
<div class="stat-label">Transits</div>
|
|
41578
|
+
</div>
|
|
41579
|
+
<div class="stat-item">
|
|
41580
|
+
<div class="stat-value" id="stat-landmarks">0</div>
|
|
41581
|
+
<div class="stat-label">Landmarks</div>
|
|
41582
|
+
</div>
|
|
41583
|
+
</div>
|
|
41584
|
+
|
|
41585
|
+
<!-- Progress -->
|
|
41586
|
+
<div class="progress-section" style="margin-top: 15px;">
|
|
41587
|
+
<div class="progress-header">
|
|
41588
|
+
<span>Coverage</span>
|
|
41589
|
+
<span id="progress-percent">0%</span>
|
|
41590
|
+
</div>
|
|
41591
|
+
<div class="progress-bar">
|
|
41592
|
+
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
|
|
41593
|
+
</div>
|
|
41594
|
+
</div>
|
|
41595
|
+
|
|
41596
|
+
<!-- Suggestions -->
|
|
41597
|
+
<div class="suggestions-section" style="margin-top: 15px;" id="suggestions-section">
|
|
41598
|
+
<div class="suggestions-title">Suggestions</div>
|
|
41599
|
+
<div id="suggestions-list">
|
|
41600
|
+
<div class="suggestion-item">Start walking to a camera</div>
|
|
41601
|
+
</div>
|
|
41602
|
+
</div>
|
|
41603
|
+
</div>
|
|
41604
|
+
|
|
41605
|
+
<!-- Mark Tab -->
|
|
41606
|
+
<div class="tab-content" id="tab-mark">
|
|
41607
|
+
<div class="mark-panel">
|
|
41608
|
+
<div class="mark-panel-title">Mark a Landmark</div>
|
|
41609
|
+
<div class="mark-type-grid" id="landmark-type-grid">
|
|
41610
|
+
<button class="mark-type-btn selected" data-type="mailbox" onclick="selectLandmarkType('mailbox')">
|
|
41611
|
+
<span class="icon">📬</span>
|
|
41612
|
+
Mailbox
|
|
41613
|
+
</button>
|
|
41614
|
+
<button class="mark-type-btn" data-type="garage" onclick="selectLandmarkType('garage')">
|
|
41615
|
+
<span class="icon">🏠</span>
|
|
41616
|
+
Garage
|
|
41617
|
+
</button>
|
|
41618
|
+
<button class="mark-type-btn" data-type="shed" onclick="selectLandmarkType('shed')">
|
|
41619
|
+
<span class="icon">🏚</span>
|
|
41620
|
+
Shed
|
|
41621
|
+
</button>
|
|
41622
|
+
<button class="mark-type-btn" data-type="tree" onclick="selectLandmarkType('tree')">
|
|
41623
|
+
<span class="icon">🌳</span>
|
|
41624
|
+
Tree
|
|
41625
|
+
</button>
|
|
41626
|
+
<button class="mark-type-btn" data-type="gate" onclick="selectLandmarkType('gate')">
|
|
41627
|
+
<span class="icon">🚪</span>
|
|
41628
|
+
Gate
|
|
41629
|
+
</button>
|
|
41630
|
+
<button class="mark-type-btn" data-type="driveway" onclick="selectLandmarkType('driveway')">
|
|
41631
|
+
<span class="icon">🛣</span>
|
|
41632
|
+
Driveway
|
|
41633
|
+
</button>
|
|
41634
|
+
<button class="mark-type-btn" data-type="pool" onclick="selectLandmarkType('pool')">
|
|
41635
|
+
<span class="icon">🏊</span>
|
|
41636
|
+
Pool
|
|
41637
|
+
</button>
|
|
41638
|
+
<button class="mark-type-btn" data-type="other" onclick="selectLandmarkType('other')">
|
|
41639
|
+
<span class="icon">📍</span>
|
|
41640
|
+
Other
|
|
41641
|
+
</button>
|
|
41642
|
+
</div>
|
|
41643
|
+
|
|
41644
|
+
<div class="input-group">
|
|
41645
|
+
<label>Landmark Name</label>
|
|
41646
|
+
<input type="text" id="landmark-name" placeholder="e.g., Front Mailbox">
|
|
41647
|
+
</div>
|
|
41648
|
+
|
|
41649
|
+
<button class="btn btn-primary btn-full" onclick="markLandmark()">
|
|
41650
|
+
📍 Mark Landmark Here
|
|
41651
|
+
</button>
|
|
41652
|
+
</div>
|
|
41653
|
+
</div>
|
|
41654
|
+
|
|
41655
|
+
<!-- History Tab -->
|
|
41656
|
+
<div class="tab-content" id="tab-history">
|
|
41657
|
+
<div class="history-section">
|
|
41658
|
+
<div class="history-title">
|
|
41659
|
+
<span>Recent Activity</span>
|
|
41660
|
+
<span id="history-count">0 events</span>
|
|
41661
|
+
</div>
|
|
41662
|
+
<div id="history-list">
|
|
41663
|
+
<div class="history-item" style="color: rgba(255,255,255,0.4); text-align: center;">
|
|
41664
|
+
No activity yet
|
|
41665
|
+
</div>
|
|
41666
|
+
</div>
|
|
41667
|
+
</div>
|
|
41668
|
+
</div>
|
|
41669
|
+
|
|
41670
|
+
<!-- Bottom Actions -->
|
|
41671
|
+
<div class="bottom-bar">
|
|
41672
|
+
<div class="action-buttons">
|
|
41673
|
+
<button class="btn btn-warning" id="pause-btn" onclick="togglePause()">
|
|
41674
|
+
⏸ Pause
|
|
41675
|
+
</button>
|
|
41676
|
+
<button class="btn btn-danger" onclick="endTraining()">
|
|
41677
|
+
⏹ End
|
|
41678
|
+
</button>
|
|
41679
|
+
</div>
|
|
41680
|
+
</div>
|
|
41681
|
+
</div>
|
|
41682
|
+
|
|
41683
|
+
<!-- Completed State -->
|
|
41684
|
+
<div class="main-content" id="completed-content" style="display: none;">
|
|
41685
|
+
<div class="idle-content">
|
|
41686
|
+
<div class="idle-icon">✅</div>
|
|
41687
|
+
<div class="idle-title">Training Complete!</div>
|
|
41688
|
+
<div class="idle-desc">Review the results and apply them to your topology.</div>
|
|
41689
|
+
</div>
|
|
41690
|
+
|
|
41691
|
+
<!-- Final Stats -->
|
|
41692
|
+
<div class="stats-grid">
|
|
41693
|
+
<div class="stat-item">
|
|
41694
|
+
<div class="stat-value" id="final-cameras">0</div>
|
|
41695
|
+
<div class="stat-label">Cameras</div>
|
|
41696
|
+
</div>
|
|
41697
|
+
<div class="stat-item">
|
|
41698
|
+
<div class="stat-value" id="final-transits">0</div>
|
|
41699
|
+
<div class="stat-label">Transits</div>
|
|
41700
|
+
</div>
|
|
41701
|
+
<div class="stat-item">
|
|
41702
|
+
<div class="stat-value" id="final-landmarks">0</div>
|
|
41703
|
+
<div class="stat-label">Landmarks</div>
|
|
41704
|
+
</div>
|
|
41705
|
+
</div>
|
|
41706
|
+
|
|
41707
|
+
<div class="stats-grid" style="margin-top: 10px;">
|
|
41708
|
+
<div class="stat-item">
|
|
41709
|
+
<div class="stat-value" id="final-overlaps">0</div>
|
|
41710
|
+
<div class="stat-label">Overlaps</div>
|
|
41711
|
+
</div>
|
|
41712
|
+
<div class="stat-item">
|
|
41713
|
+
<div class="stat-value" id="final-avg-transit">0s</div>
|
|
41714
|
+
<div class="stat-label">Avg Transit</div>
|
|
41715
|
+
</div>
|
|
41716
|
+
<div class="stat-item">
|
|
41717
|
+
<div class="stat-value" id="final-coverage">0%</div>
|
|
41718
|
+
<div class="stat-label">Coverage</div>
|
|
41719
|
+
</div>
|
|
41720
|
+
</div>
|
|
41721
|
+
|
|
41722
|
+
<div class="bottom-bar" style="margin-top: auto;">
|
|
41723
|
+
<div class="action-buttons">
|
|
41724
|
+
<button class="btn btn-secondary" onclick="resetTraining()">
|
|
41725
|
+
↻ Start Over
|
|
41726
|
+
</button>
|
|
41727
|
+
<button class="btn btn-primary" onclick="applyTraining()">
|
|
41728
|
+
✓ Apply Results
|
|
41729
|
+
</button>
|
|
41730
|
+
</div>
|
|
41731
|
+
</div>
|
|
41732
|
+
</div>
|
|
41733
|
+
|
|
41734
|
+
<!-- Apply Results Modal -->
|
|
41735
|
+
<div class="modal-overlay" id="results-modal">
|
|
41736
|
+
<div class="modal">
|
|
41737
|
+
<h2>Training Applied!</h2>
|
|
41738
|
+
<div id="results-content">
|
|
41739
|
+
<div class="modal-result-item">
|
|
41740
|
+
<span>Connections Created</span>
|
|
41741
|
+
<span class="modal-result-value" id="result-connections">0</span>
|
|
41742
|
+
</div>
|
|
41743
|
+
<div class="modal-result-item">
|
|
41744
|
+
<span>Connections Updated</span>
|
|
41745
|
+
<span class="modal-result-value" id="result-updated">0</span>
|
|
41746
|
+
</div>
|
|
41747
|
+
<div class="modal-result-item">
|
|
41748
|
+
<span>Landmarks Added</span>
|
|
41749
|
+
<span class="modal-result-value" id="result-landmarks">0</span>
|
|
41750
|
+
</div>
|
|
41751
|
+
<div class="modal-result-item">
|
|
41752
|
+
<span>Zones Created</span>
|
|
41753
|
+
<span class="modal-result-value" id="result-zones">0</span>
|
|
41754
|
+
</div>
|
|
41755
|
+
</div>
|
|
41756
|
+
<div class="modal-buttons">
|
|
41757
|
+
<button class="btn btn-secondary" style="flex: 1;" onclick="closeResultsModal()">Close</button>
|
|
41758
|
+
<button class="btn btn-primary" style="flex: 1;" onclick="openEditor()">Open Editor</button>
|
|
41759
|
+
</div>
|
|
41760
|
+
</div>
|
|
41761
|
+
</div>
|
|
41762
|
+
|
|
41763
|
+
<script>
|
|
41764
|
+
let trainingState = 'idle'; // idle, active, paused, completed
|
|
41765
|
+
let session = null;
|
|
41766
|
+
let pollInterval = null;
|
|
41767
|
+
let transitInterval = null;
|
|
41768
|
+
let selectedLandmarkType = 'mailbox';
|
|
41769
|
+
let historyItems = [];
|
|
41770
|
+
|
|
41771
|
+
// Initialize
|
|
41772
|
+
async function init() {
|
|
41773
|
+
// Check if there's an existing session
|
|
41774
|
+
const status = await fetchTrainingStatus();
|
|
41775
|
+
if (status && (status.state === 'active' || status.state === 'paused')) {
|
|
41776
|
+
session = status;
|
|
41777
|
+
trainingState = status.state;
|
|
41778
|
+
updateUI();
|
|
41779
|
+
startPolling();
|
|
41780
|
+
}
|
|
41781
|
+
}
|
|
41782
|
+
|
|
41783
|
+
// API calls
|
|
41784
|
+
async function fetchTrainingStatus() {
|
|
41785
|
+
try {
|
|
41786
|
+
const response = await fetch('../api/training/status');
|
|
41787
|
+
if (response.ok) {
|
|
41788
|
+
return await response.json();
|
|
41789
|
+
}
|
|
41790
|
+
} catch (e) { console.error('Failed to fetch status:', e); }
|
|
41791
|
+
return null;
|
|
41792
|
+
}
|
|
41793
|
+
|
|
41794
|
+
async function startTraining() {
|
|
41795
|
+
try {
|
|
41796
|
+
const response = await fetch('../api/training/start', { method: 'POST' });
|
|
41797
|
+
if (response.ok) {
|
|
41798
|
+
session = await response.json();
|
|
41799
|
+
trainingState = 'active';
|
|
41800
|
+
updateUI();
|
|
41801
|
+
startPolling();
|
|
41802
|
+
addHistoryItem('Training started', 'start');
|
|
41803
|
+
}
|
|
41804
|
+
} catch (e) {
|
|
41805
|
+
console.error('Failed to start training:', e);
|
|
41806
|
+
alert('Failed to start training. Please try again.');
|
|
41807
|
+
}
|
|
41808
|
+
}
|
|
41809
|
+
|
|
41810
|
+
async function togglePause() {
|
|
41811
|
+
const endpoint = trainingState === 'active' ? 'pause' : 'resume';
|
|
41812
|
+
try {
|
|
41813
|
+
const response = await fetch('../api/training/' + endpoint, { method: 'POST' });
|
|
41814
|
+
if (response.ok) {
|
|
41815
|
+
trainingState = trainingState === 'active' ? 'paused' : 'active';
|
|
41816
|
+
updateUI();
|
|
41817
|
+
addHistoryItem('Training ' + (trainingState === 'paused' ? 'paused' : 'resumed'), 'control');
|
|
41818
|
+
}
|
|
41819
|
+
} catch (e) { console.error('Failed to toggle pause:', e); }
|
|
41820
|
+
}
|
|
41821
|
+
|
|
41822
|
+
async function endTraining() {
|
|
41823
|
+
if (!confirm('End training session?')) return;
|
|
41824
|
+
try {
|
|
41825
|
+
const response = await fetch('../api/training/end', { method: 'POST' });
|
|
41826
|
+
if (response.ok) {
|
|
41827
|
+
session = await response.json();
|
|
41828
|
+
trainingState = 'completed';
|
|
41829
|
+
stopPolling();
|
|
41830
|
+
updateUI();
|
|
41831
|
+
}
|
|
41832
|
+
} catch (e) { console.error('Failed to end training:', e); }
|
|
41833
|
+
}
|
|
41834
|
+
|
|
41835
|
+
async function applyTraining() {
|
|
41836
|
+
try {
|
|
41837
|
+
const response = await fetch('../api/training/apply', { method: 'POST' });
|
|
41838
|
+
if (response.ok) {
|
|
41839
|
+
const result = await response.json();
|
|
41840
|
+
document.getElementById('result-connections').textContent = result.connectionsCreated;
|
|
41841
|
+
document.getElementById('result-updated').textContent = result.connectionsUpdated;
|
|
41842
|
+
document.getElementById('result-landmarks').textContent = result.landmarksAdded;
|
|
41843
|
+
document.getElementById('result-zones').textContent = result.zonesCreated;
|
|
41844
|
+
document.getElementById('results-modal').classList.add('active');
|
|
41845
|
+
}
|
|
41846
|
+
} catch (e) {
|
|
41847
|
+
console.error('Failed to apply training:', e);
|
|
41848
|
+
alert('Failed to apply training results.');
|
|
41849
|
+
}
|
|
41850
|
+
}
|
|
41851
|
+
|
|
41852
|
+
function closeResultsModal() {
|
|
41853
|
+
document.getElementById('results-modal').classList.remove('active');
|
|
41854
|
+
}
|
|
41855
|
+
|
|
41856
|
+
function openEditor() {
|
|
41857
|
+
window.location.href = '../ui/editor';
|
|
41858
|
+
}
|
|
41859
|
+
|
|
41860
|
+
function resetTraining() {
|
|
41861
|
+
trainingState = 'idle';
|
|
41862
|
+
session = null;
|
|
41863
|
+
historyItems = [];
|
|
41864
|
+
updateUI();
|
|
41865
|
+
}
|
|
41866
|
+
|
|
41867
|
+
async function markLandmark() {
|
|
41868
|
+
const name = document.getElementById('landmark-name').value.trim();
|
|
41869
|
+
if (!name) {
|
|
41870
|
+
alert('Please enter a landmark name');
|
|
41871
|
+
return;
|
|
41872
|
+
}
|
|
41873
|
+
|
|
41874
|
+
const currentCameraId = session?.currentCamera?.id;
|
|
41875
|
+
const visibleFromCameras = currentCameraId ? [currentCameraId] : [];
|
|
41876
|
+
|
|
41877
|
+
try {
|
|
41878
|
+
const response = await fetch('../api/training/landmark', {
|
|
41879
|
+
method: 'POST',
|
|
41880
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41881
|
+
body: JSON.stringify({
|
|
41882
|
+
name,
|
|
41883
|
+
type: selectedLandmarkType,
|
|
41884
|
+
visibleFromCameras,
|
|
41885
|
+
position: { x: 50, y: 50 }, // Will be refined when applied
|
|
41886
|
+
})
|
|
41887
|
+
});
|
|
41888
|
+
if (response.ok) {
|
|
41889
|
+
document.getElementById('landmark-name').value = '';
|
|
41890
|
+
addHistoryItem('Marked: ' + name + ' (' + selectedLandmarkType + ')', 'landmark');
|
|
41891
|
+
// Refresh status
|
|
41892
|
+
const status = await fetchTrainingStatus();
|
|
41893
|
+
if (status) {
|
|
41894
|
+
session = status;
|
|
41895
|
+
updateStatsUI();
|
|
41896
|
+
}
|
|
41897
|
+
}
|
|
41898
|
+
} catch (e) { console.error('Failed to mark landmark:', e); }
|
|
41899
|
+
}
|
|
41900
|
+
|
|
41901
|
+
function selectLandmarkType(type) {
|
|
41902
|
+
selectedLandmarkType = type;
|
|
41903
|
+
document.querySelectorAll('.mark-type-btn').forEach(btn => {
|
|
41904
|
+
btn.classList.toggle('selected', btn.dataset.type === type);
|
|
41905
|
+
});
|
|
41906
|
+
}
|
|
41907
|
+
|
|
41908
|
+
// Polling
|
|
41909
|
+
function startPolling() {
|
|
41910
|
+
if (pollInterval) clearInterval(pollInterval);
|
|
41911
|
+
pollInterval = setInterval(async () => {
|
|
41912
|
+
const status = await fetchTrainingStatus();
|
|
41913
|
+
if (status) {
|
|
41914
|
+
session = status;
|
|
41915
|
+
updateDetectionUI();
|
|
41916
|
+
updateStatsUI();
|
|
41917
|
+
updateSuggestionsUI();
|
|
41918
|
+
}
|
|
41919
|
+
}, 1000);
|
|
41920
|
+
}
|
|
41921
|
+
|
|
41922
|
+
function stopPolling() {
|
|
41923
|
+
if (pollInterval) {
|
|
41924
|
+
clearInterval(pollInterval);
|
|
41925
|
+
pollInterval = null;
|
|
41926
|
+
}
|
|
41927
|
+
if (transitInterval) {
|
|
41928
|
+
clearInterval(transitInterval);
|
|
41929
|
+
transitInterval = null;
|
|
41930
|
+
}
|
|
41931
|
+
}
|
|
41932
|
+
|
|
41933
|
+
// UI Updates
|
|
41934
|
+
function updateUI() {
|
|
41935
|
+
// Show/hide content sections
|
|
41936
|
+
document.getElementById('idle-content').style.display = trainingState === 'idle' ? 'flex' : 'none';
|
|
41937
|
+
document.getElementById('active-content').style.display = (trainingState === 'active' || trainingState === 'paused') ? 'flex' : 'none';
|
|
41938
|
+
document.getElementById('completed-content').style.display = trainingState === 'completed' ? 'flex' : 'none';
|
|
41939
|
+
|
|
41940
|
+
// Update status badge
|
|
41941
|
+
const badge = document.getElementById('status-badge');
|
|
41942
|
+
badge.textContent = trainingState.charAt(0).toUpperCase() + trainingState.slice(1);
|
|
41943
|
+
badge.className = 'status-badge ' + trainingState;
|
|
41944
|
+
|
|
41945
|
+
// Update pause button
|
|
41946
|
+
const pauseBtn = document.getElementById('pause-btn');
|
|
41947
|
+
if (pauseBtn) {
|
|
41948
|
+
pauseBtn.innerHTML = trainingState === 'paused' ? '▶ Resume' : '⏸ Pause';
|
|
41949
|
+
}
|
|
41950
|
+
|
|
41951
|
+
// Update completed stats
|
|
41952
|
+
if (trainingState === 'completed' && session) {
|
|
41953
|
+
document.getElementById('final-cameras').textContent = session.stats?.camerasVisited || 0;
|
|
41954
|
+
document.getElementById('final-transits').textContent = session.stats?.transitsRecorded || 0;
|
|
41955
|
+
document.getElementById('final-landmarks').textContent = session.stats?.landmarksMarked || 0;
|
|
41956
|
+
document.getElementById('final-overlaps').textContent = session.stats?.overlapsDetected || 0;
|
|
41957
|
+
document.getElementById('final-avg-transit').textContent = (session.stats?.averageTransitTime || 0) + 's';
|
|
41958
|
+
document.getElementById('final-coverage').textContent = (session.stats?.coveragePercentage || 0) + '%';
|
|
41959
|
+
}
|
|
41960
|
+
}
|
|
41961
|
+
|
|
41962
|
+
function updateDetectionUI() {
|
|
41963
|
+
if (!session) return;
|
|
41964
|
+
|
|
41965
|
+
const card = document.getElementById('detection-card');
|
|
41966
|
+
const icon = document.getElementById('detection-icon');
|
|
41967
|
+
const title = document.getElementById('detection-title');
|
|
41968
|
+
const subtitle = document.getElementById('detection-subtitle');
|
|
41969
|
+
const confidence = document.getElementById('detection-confidence');
|
|
41970
|
+
const transitTimer = document.getElementById('transit-timer');
|
|
41971
|
+
|
|
41972
|
+
if (session.currentCamera) {
|
|
41973
|
+
// Detected on a camera
|
|
41974
|
+
card.className = 'detection-card detecting';
|
|
41975
|
+
icon.textContent = '📷';
|
|
41976
|
+
title.textContent = session.currentCamera.name;
|
|
41977
|
+
subtitle.textContent = 'You are visible on this camera';
|
|
41978
|
+
confidence.textContent = 'Confidence: ' + Math.round(session.currentCamera.confidence * 100) + '%';
|
|
41979
|
+
transitTimer.style.display = 'none';
|
|
41980
|
+
|
|
41981
|
+
// Check for new camera detection to add to history
|
|
41982
|
+
const lastHistoryCamera = historyItems.find(h => h.type === 'camera');
|
|
41983
|
+
if (!lastHistoryCamera || lastHistoryCamera.cameraId !== session.currentCamera.id) {
|
|
41984
|
+
addHistoryItem('Detected on: ' + session.currentCamera.name, 'camera', session.currentCamera.id);
|
|
41985
|
+
}
|
|
41986
|
+
} else if (session.activeTransit) {
|
|
41987
|
+
// In transit
|
|
41988
|
+
card.className = 'detection-card in-transit';
|
|
41989
|
+
icon.textContent = '🚶';
|
|
41990
|
+
title.textContent = 'In Transit';
|
|
41991
|
+
subtitle.textContent = 'Walking to next camera...';
|
|
41992
|
+
confidence.textContent = '';
|
|
41993
|
+
transitTimer.style.display = 'flex';
|
|
41994
|
+
document.getElementById('transit-from').textContent = 'from ' + session.activeTransit.fromCameraName;
|
|
41995
|
+
|
|
41996
|
+
// Start transit timer if not already running
|
|
41997
|
+
if (!transitInterval) {
|
|
41998
|
+
transitInterval = setInterval(() => {
|
|
41999
|
+
if (session?.activeTransit) {
|
|
42000
|
+
document.getElementById('transit-time').textContent = session.activeTransit.elapsedSeconds + 's';
|
|
42001
|
+
}
|
|
42002
|
+
}, 1000);
|
|
42003
|
+
}
|
|
42004
|
+
} else {
|
|
42005
|
+
// Waiting
|
|
42006
|
+
card.className = 'detection-card';
|
|
42007
|
+
icon.textContent = '👤';
|
|
42008
|
+
title.textContent = 'Waiting for detection...';
|
|
42009
|
+
subtitle.textContent = 'Walk to any camera to begin';
|
|
42010
|
+
confidence.textContent = '';
|
|
42011
|
+
transitTimer.style.display = 'none';
|
|
42012
|
+
|
|
42013
|
+
if (transitInterval) {
|
|
42014
|
+
clearInterval(transitInterval);
|
|
42015
|
+
transitInterval = null;
|
|
42016
|
+
}
|
|
42017
|
+
}
|
|
42018
|
+
}
|
|
42019
|
+
|
|
42020
|
+
function updateStatsUI() {
|
|
42021
|
+
if (!session?.stats) return;
|
|
42022
|
+
|
|
42023
|
+
document.getElementById('stat-cameras').textContent = session.stats.camerasVisited;
|
|
42024
|
+
document.getElementById('stat-transits').textContent = session.stats.transitsRecorded;
|
|
42025
|
+
document.getElementById('stat-landmarks').textContent = session.stats.landmarksMarked;
|
|
42026
|
+
document.getElementById('progress-percent').textContent = session.stats.coveragePercentage + '%';
|
|
42027
|
+
document.getElementById('progress-fill').style.width = session.stats.coveragePercentage + '%';
|
|
42028
|
+
}
|
|
42029
|
+
|
|
42030
|
+
function updateSuggestionsUI() {
|
|
42031
|
+
if (!session?.suggestions || session.suggestions.length === 0) return;
|
|
42032
|
+
|
|
42033
|
+
const list = document.getElementById('suggestions-list');
|
|
42034
|
+
list.innerHTML = session.suggestions.map(s =>
|
|
42035
|
+
'<div class="suggestion-item">' + s + '</div>'
|
|
42036
|
+
).join('');
|
|
42037
|
+
}
|
|
42038
|
+
|
|
42039
|
+
function addHistoryItem(text, type, cameraId) {
|
|
42040
|
+
const time = new Date().toLocaleTimeString();
|
|
42041
|
+
historyItems.unshift({ text, type, time, cameraId });
|
|
42042
|
+
if (historyItems.length > 50) historyItems.pop();
|
|
42043
|
+
|
|
42044
|
+
const list = document.getElementById('history-list');
|
|
42045
|
+
document.getElementById('history-count').textContent = historyItems.length + ' events';
|
|
42046
|
+
|
|
42047
|
+
list.innerHTML = historyItems.map(item => {
|
|
42048
|
+
let className = '';
|
|
42049
|
+
if (item.type === 'camera') className = 'history-item-camera';
|
|
42050
|
+
if (item.type === 'transit') className = 'history-item-transit';
|
|
42051
|
+
return '<div class="history-item"><span class="' + className + '">' + item.text + '</span>' +
|
|
42052
|
+
'<div class="history-item-time">' + item.time + '</div></div>';
|
|
42053
|
+
}).join('');
|
|
42054
|
+
}
|
|
42055
|
+
|
|
42056
|
+
function switchTab(tabName) {
|
|
42057
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
42058
|
+
tab.classList.toggle('active', tab.textContent.toLowerCase() === tabName);
|
|
42059
|
+
});
|
|
42060
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
42061
|
+
content.classList.toggle('active', content.id === 'tab-' + tabName);
|
|
42062
|
+
});
|
|
42063
|
+
}
|
|
42064
|
+
|
|
42065
|
+
// Initialize on load
|
|
42066
|
+
init();
|
|
42067
|
+
</script>
|
|
42068
|
+
</body>
|
|
42069
|
+
</html>`;
|
|
42070
|
+
|
|
42071
|
+
|
|
40203
42072
|
/***/ },
|
|
40204
42073
|
|
|
40205
42074
|
/***/ "assert"
|