@blueharford/scrypted-spatial-awareness 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +1869 -19
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/tracking-engine.ts +588 -1
- package/src/main.ts +318 -19
- package/src/models/training.ts +300 -0
- package/src/ui/training-html.ts +1007 -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';
|
|
@@ -37340,8 +37811,89 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37340
37811
|
// ==================== Settings Implementation ====================
|
|
37341
37812
|
async getSettings() {
|
|
37342
37813
|
const settings = await this.storageSettings.getSettings();
|
|
37814
|
+
// Training Mode button that opens mobile-friendly training UI in modal
|
|
37815
|
+
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);})()`;
|
|
37816
|
+
settings.push({
|
|
37817
|
+
key: 'trainingMode',
|
|
37818
|
+
title: 'Training Mode',
|
|
37819
|
+
type: 'html',
|
|
37820
|
+
value: `
|
|
37821
|
+
<style>
|
|
37822
|
+
.sa-training-container {
|
|
37823
|
+
padding: 16px;
|
|
37824
|
+
background: rgba(255,255,255,0.03);
|
|
37825
|
+
border-radius: 4px;
|
|
37826
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
37827
|
+
}
|
|
37828
|
+
.sa-training-title {
|
|
37829
|
+
color: #4fc3f7;
|
|
37830
|
+
font-size: 14px;
|
|
37831
|
+
font-weight: 500;
|
|
37832
|
+
margin-bottom: 8px;
|
|
37833
|
+
font-family: inherit;
|
|
37834
|
+
}
|
|
37835
|
+
.sa-training-desc {
|
|
37836
|
+
color: rgba(255,255,255,0.6);
|
|
37837
|
+
margin-bottom: 12px;
|
|
37838
|
+
font-size: 13px;
|
|
37839
|
+
line-height: 1.5;
|
|
37840
|
+
font-family: inherit;
|
|
37841
|
+
}
|
|
37842
|
+
.sa-training-btn {
|
|
37843
|
+
background: #4fc3f7;
|
|
37844
|
+
color: #000;
|
|
37845
|
+
border: none;
|
|
37846
|
+
padding: 10px 20px;
|
|
37847
|
+
border-radius: 4px;
|
|
37848
|
+
font-size: 14px;
|
|
37849
|
+
font-weight: 500;
|
|
37850
|
+
cursor: pointer;
|
|
37851
|
+
display: inline-flex;
|
|
37852
|
+
align-items: center;
|
|
37853
|
+
gap: 8px;
|
|
37854
|
+
transition: background 0.2s;
|
|
37855
|
+
font-family: inherit;
|
|
37856
|
+
}
|
|
37857
|
+
.sa-training-btn:hover {
|
|
37858
|
+
background: #81d4fa;
|
|
37859
|
+
}
|
|
37860
|
+
.sa-training-steps {
|
|
37861
|
+
color: rgba(255,255,255,0.5);
|
|
37862
|
+
font-size: 12px;
|
|
37863
|
+
margin-top: 12px;
|
|
37864
|
+
padding-top: 12px;
|
|
37865
|
+
border-top: 1px solid rgba(255,255,255,0.05);
|
|
37866
|
+
font-family: inherit;
|
|
37867
|
+
}
|
|
37868
|
+
.sa-training-steps ol {
|
|
37869
|
+
margin: 6px 0 0 16px;
|
|
37870
|
+
padding: 0;
|
|
37871
|
+
}
|
|
37872
|
+
.sa-training-steps li {
|
|
37873
|
+
margin-bottom: 2px;
|
|
37874
|
+
}
|
|
37875
|
+
</style>
|
|
37876
|
+
<div class="sa-training-container">
|
|
37877
|
+
<div class="sa-training-title">Guided Property Training</div>
|
|
37878
|
+
<p class="sa-training-desc">Walk your property while the system learns your camera layout, transit times, and landmarks automatically.</p>
|
|
37879
|
+
<button class="sa-training-btn" onclick="${trainingOnclickCode}">
|
|
37880
|
+
Start Training Mode
|
|
37881
|
+
</button>
|
|
37882
|
+
<div class="sa-training-steps">
|
|
37883
|
+
<strong>How it works:</strong>
|
|
37884
|
+
<ol>
|
|
37885
|
+
<li>Start training and walk to each camera</li>
|
|
37886
|
+
<li>System auto-detects you and records transit times</li>
|
|
37887
|
+
<li>Mark landmarks as you encounter them</li>
|
|
37888
|
+
<li>Apply results to generate your topology</li>
|
|
37889
|
+
</ol>
|
|
37890
|
+
</div>
|
|
37891
|
+
</div>
|
|
37892
|
+
`,
|
|
37893
|
+
group: 'Getting Started',
|
|
37894
|
+
});
|
|
37343
37895
|
// 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.
|
|
37896
|
+
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
37897
|
settings.push({
|
|
37346
37898
|
key: 'topologyEditor',
|
|
37347
37899
|
title: 'Topology Editor',
|
|
@@ -37349,39 +37901,41 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37349
37901
|
value: `
|
|
37350
37902
|
<style>
|
|
37351
37903
|
.sa-open-btn {
|
|
37352
|
-
background:
|
|
37353
|
-
color:
|
|
37904
|
+
background: #4fc3f7;
|
|
37905
|
+
color: #000;
|
|
37354
37906
|
border: none;
|
|
37355
|
-
padding:
|
|
37356
|
-
border-radius:
|
|
37357
|
-
font-size:
|
|
37358
|
-
font-weight:
|
|
37907
|
+
padding: 10px 20px;
|
|
37908
|
+
border-radius: 4px;
|
|
37909
|
+
font-size: 14px;
|
|
37910
|
+
font-weight: 500;
|
|
37359
37911
|
cursor: pointer;
|
|
37360
37912
|
display: inline-flex;
|
|
37361
37913
|
align-items: center;
|
|
37362
|
-
gap:
|
|
37363
|
-
transition:
|
|
37914
|
+
gap: 8px;
|
|
37915
|
+
transition: background 0.2s;
|
|
37916
|
+
font-family: inherit;
|
|
37364
37917
|
}
|
|
37365
37918
|
.sa-open-btn:hover {
|
|
37366
|
-
|
|
37367
|
-
box-shadow: 0 8px 20px rgba(233, 69, 96, 0.4);
|
|
37919
|
+
background: #81d4fa;
|
|
37368
37920
|
}
|
|
37369
37921
|
.sa-btn-container {
|
|
37370
|
-
padding:
|
|
37371
|
-
background:
|
|
37372
|
-
border-radius:
|
|
37922
|
+
padding: 16px;
|
|
37923
|
+
background: rgba(255,255,255,0.03);
|
|
37924
|
+
border-radius: 4px;
|
|
37373
37925
|
text-align: center;
|
|
37926
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
37374
37927
|
}
|
|
37375
37928
|
.sa-btn-desc {
|
|
37376
|
-
color:
|
|
37377
|
-
margin-bottom:
|
|
37378
|
-
font-size:
|
|
37929
|
+
color: rgba(255,255,255,0.6);
|
|
37930
|
+
margin-bottom: 12px;
|
|
37931
|
+
font-size: 13px;
|
|
37932
|
+
font-family: inherit;
|
|
37379
37933
|
}
|
|
37380
37934
|
</style>
|
|
37381
37935
|
<div class="sa-btn-container">
|
|
37382
37936
|
<p class="sa-btn-desc">Configure camera positions, connections, and transit times</p>
|
|
37383
37937
|
<button class="sa-open-btn" onclick="${onclickCode}">
|
|
37384
|
-
|
|
37938
|
+
Open Topology Editor
|
|
37385
37939
|
</button>
|
|
37386
37940
|
</div>
|
|
37387
37941
|
`,
|
|
@@ -37610,17 +38164,42 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37610
38164
|
const globalId = path.split('/').pop();
|
|
37611
38165
|
return this.handleJourneyPathRequest(globalId, response);
|
|
37612
38166
|
}
|
|
38167
|
+
// Training Mode endpoints
|
|
38168
|
+
if (path.endsWith('/api/training/start')) {
|
|
38169
|
+
return this.handleTrainingStartRequest(request, response);
|
|
38170
|
+
}
|
|
38171
|
+
if (path.endsWith('/api/training/pause')) {
|
|
38172
|
+
return this.handleTrainingPauseRequest(response);
|
|
38173
|
+
}
|
|
38174
|
+
if (path.endsWith('/api/training/resume')) {
|
|
38175
|
+
return this.handleTrainingResumeRequest(response);
|
|
38176
|
+
}
|
|
38177
|
+
if (path.endsWith('/api/training/end')) {
|
|
38178
|
+
return this.handleTrainingEndRequest(response);
|
|
38179
|
+
}
|
|
38180
|
+
if (path.endsWith('/api/training/status')) {
|
|
38181
|
+
return this.handleTrainingStatusRequest(response);
|
|
38182
|
+
}
|
|
38183
|
+
if (path.endsWith('/api/training/landmark')) {
|
|
38184
|
+
return this.handleTrainingLandmarkRequest(request, response);
|
|
38185
|
+
}
|
|
38186
|
+
if (path.endsWith('/api/training/apply')) {
|
|
38187
|
+
return this.handleTrainingApplyRequest(response);
|
|
38188
|
+
}
|
|
37613
38189
|
// UI Routes
|
|
37614
38190
|
if (path.endsWith('/ui/editor') || path.endsWith('/ui/editor/')) {
|
|
37615
38191
|
return this.serveEditorUI(response);
|
|
37616
38192
|
}
|
|
38193
|
+
if (path.endsWith('/ui/training') || path.endsWith('/ui/training/')) {
|
|
38194
|
+
return this.serveTrainingUI(response);
|
|
38195
|
+
}
|
|
37617
38196
|
if (path.includes('/ui/')) {
|
|
37618
38197
|
return this.serveStaticFile(path, response);
|
|
37619
38198
|
}
|
|
37620
38199
|
// Default: return info page
|
|
37621
38200
|
response.send(JSON.stringify({
|
|
37622
38201
|
name: 'Spatial Awareness Plugin',
|
|
37623
|
-
version: '0.
|
|
38202
|
+
version: '0.4.0',
|
|
37624
38203
|
endpoints: {
|
|
37625
38204
|
api: {
|
|
37626
38205
|
trackedObjects: '/api/tracked-objects',
|
|
@@ -37632,9 +38211,19 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
37632
38211
|
liveTracking: '/api/live-tracking',
|
|
37633
38212
|
connectionSuggestions: '/api/connection-suggestions',
|
|
37634
38213
|
landmarkSuggestions: '/api/landmark-suggestions',
|
|
38214
|
+
training: {
|
|
38215
|
+
start: '/api/training/start',
|
|
38216
|
+
pause: '/api/training/pause',
|
|
38217
|
+
resume: '/api/training/resume',
|
|
38218
|
+
end: '/api/training/end',
|
|
38219
|
+
status: '/api/training/status',
|
|
38220
|
+
landmark: '/api/training/landmark',
|
|
38221
|
+
apply: '/api/training/apply',
|
|
38222
|
+
},
|
|
37635
38223
|
},
|
|
37636
38224
|
ui: {
|
|
37637
38225
|
editor: '/ui/editor',
|
|
38226
|
+
training: '/ui/training',
|
|
37638
38227
|
},
|
|
37639
38228
|
},
|
|
37640
38229
|
}), {
|
|
@@ -38085,11 +38674,175 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
38085
38674
|
});
|
|
38086
38675
|
}
|
|
38087
38676
|
}
|
|
38677
|
+
// ==================== Training Mode Handlers ====================
|
|
38678
|
+
handleTrainingStartRequest(request, response) {
|
|
38679
|
+
if (!this.trackingEngine) {
|
|
38680
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running. Configure topology first.' }), {
|
|
38681
|
+
code: 500,
|
|
38682
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38683
|
+
});
|
|
38684
|
+
return;
|
|
38685
|
+
}
|
|
38686
|
+
try {
|
|
38687
|
+
let config;
|
|
38688
|
+
let trainerName;
|
|
38689
|
+
if (request.body) {
|
|
38690
|
+
const body = JSON.parse(request.body);
|
|
38691
|
+
trainerName = body.trainerName;
|
|
38692
|
+
config = body.config;
|
|
38693
|
+
}
|
|
38694
|
+
const session = this.trackingEngine.startTrainingSession(trainerName, config);
|
|
38695
|
+
response.send(JSON.stringify(session), {
|
|
38696
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38697
|
+
});
|
|
38698
|
+
}
|
|
38699
|
+
catch (e) {
|
|
38700
|
+
response.send(JSON.stringify({ error: e.message }), {
|
|
38701
|
+
code: 500,
|
|
38702
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38703
|
+
});
|
|
38704
|
+
}
|
|
38705
|
+
}
|
|
38706
|
+
handleTrainingPauseRequest(response) {
|
|
38707
|
+
if (!this.trackingEngine) {
|
|
38708
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38709
|
+
code: 500,
|
|
38710
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38711
|
+
});
|
|
38712
|
+
return;
|
|
38713
|
+
}
|
|
38714
|
+
const success = this.trackingEngine.pauseTrainingSession();
|
|
38715
|
+
if (success) {
|
|
38716
|
+
response.send(JSON.stringify({ success: true }), {
|
|
38717
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38718
|
+
});
|
|
38719
|
+
}
|
|
38720
|
+
else {
|
|
38721
|
+
response.send(JSON.stringify({ error: 'No active training session to pause' }), {
|
|
38722
|
+
code: 400,
|
|
38723
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38724
|
+
});
|
|
38725
|
+
}
|
|
38726
|
+
}
|
|
38727
|
+
handleTrainingResumeRequest(response) {
|
|
38728
|
+
if (!this.trackingEngine) {
|
|
38729
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38730
|
+
code: 500,
|
|
38731
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38732
|
+
});
|
|
38733
|
+
return;
|
|
38734
|
+
}
|
|
38735
|
+
const success = this.trackingEngine.resumeTrainingSession();
|
|
38736
|
+
if (success) {
|
|
38737
|
+
response.send(JSON.stringify({ success: true }), {
|
|
38738
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38739
|
+
});
|
|
38740
|
+
}
|
|
38741
|
+
else {
|
|
38742
|
+
response.send(JSON.stringify({ error: 'No paused training session to resume' }), {
|
|
38743
|
+
code: 400,
|
|
38744
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38745
|
+
});
|
|
38746
|
+
}
|
|
38747
|
+
}
|
|
38748
|
+
handleTrainingEndRequest(response) {
|
|
38749
|
+
if (!this.trackingEngine) {
|
|
38750
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38751
|
+
code: 500,
|
|
38752
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38753
|
+
});
|
|
38754
|
+
return;
|
|
38755
|
+
}
|
|
38756
|
+
const session = this.trackingEngine.endTrainingSession();
|
|
38757
|
+
if (session) {
|
|
38758
|
+
response.send(JSON.stringify(session), {
|
|
38759
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38760
|
+
});
|
|
38761
|
+
}
|
|
38762
|
+
else {
|
|
38763
|
+
response.send(JSON.stringify({ error: 'No training session to end' }), {
|
|
38764
|
+
code: 400,
|
|
38765
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38766
|
+
});
|
|
38767
|
+
}
|
|
38768
|
+
}
|
|
38769
|
+
handleTrainingStatusRequest(response) {
|
|
38770
|
+
if (!this.trackingEngine) {
|
|
38771
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
38772
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38773
|
+
});
|
|
38774
|
+
return;
|
|
38775
|
+
}
|
|
38776
|
+
const status = this.trackingEngine.getTrainingStatus();
|
|
38777
|
+
if (status) {
|
|
38778
|
+
response.send(JSON.stringify(status), {
|
|
38779
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38780
|
+
});
|
|
38781
|
+
}
|
|
38782
|
+
else {
|
|
38783
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
38784
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38785
|
+
});
|
|
38786
|
+
}
|
|
38787
|
+
}
|
|
38788
|
+
handleTrainingLandmarkRequest(request, response) {
|
|
38789
|
+
if (!this.trackingEngine) {
|
|
38790
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38791
|
+
code: 500,
|
|
38792
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38793
|
+
});
|
|
38794
|
+
return;
|
|
38795
|
+
}
|
|
38796
|
+
try {
|
|
38797
|
+
const body = JSON.parse(request.body);
|
|
38798
|
+
const landmark = this.trackingEngine.markTrainingLandmark(body);
|
|
38799
|
+
if (landmark) {
|
|
38800
|
+
response.send(JSON.stringify({ success: true, landmark }), {
|
|
38801
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38802
|
+
});
|
|
38803
|
+
}
|
|
38804
|
+
else {
|
|
38805
|
+
response.send(JSON.stringify({ error: 'No active training session' }), {
|
|
38806
|
+
code: 400,
|
|
38807
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38808
|
+
});
|
|
38809
|
+
}
|
|
38810
|
+
}
|
|
38811
|
+
catch (e) {
|
|
38812
|
+
response.send(JSON.stringify({ error: 'Invalid request body' }), {
|
|
38813
|
+
code: 400,
|
|
38814
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38815
|
+
});
|
|
38816
|
+
}
|
|
38817
|
+
}
|
|
38818
|
+
handleTrainingApplyRequest(response) {
|
|
38819
|
+
if (!this.trackingEngine) {
|
|
38820
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
38821
|
+
code: 500,
|
|
38822
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38823
|
+
});
|
|
38824
|
+
return;
|
|
38825
|
+
}
|
|
38826
|
+
const result = this.trackingEngine.applyTrainingToTopology();
|
|
38827
|
+
if (result.success) {
|
|
38828
|
+
// Save the updated topology
|
|
38829
|
+
const topology = this.trackingEngine.getTopology();
|
|
38830
|
+
this.storage.setItem('topology', JSON.stringify(topology));
|
|
38831
|
+
}
|
|
38832
|
+
response.send(JSON.stringify(result), {
|
|
38833
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38834
|
+
});
|
|
38835
|
+
}
|
|
38088
38836
|
serveEditorUI(response) {
|
|
38089
38837
|
response.send(editor_html_1.EDITOR_HTML, {
|
|
38090
38838
|
headers: { 'Content-Type': 'text/html' },
|
|
38091
38839
|
});
|
|
38092
38840
|
}
|
|
38841
|
+
serveTrainingUI(response) {
|
|
38842
|
+
response.send(training_html_1.TRAINING_HTML, {
|
|
38843
|
+
headers: { 'Content-Type': 'text/html' },
|
|
38844
|
+
});
|
|
38845
|
+
}
|
|
38093
38846
|
serveStaticFile(path, response) {
|
|
38094
38847
|
// Serve static files for the UI
|
|
38095
38848
|
response.send('Not found', { code: 404 });
|
|
@@ -38668,6 +39421,83 @@ function getJourneySummary(tracked) {
|
|
|
38668
39421
|
}
|
|
38669
39422
|
|
|
38670
39423
|
|
|
39424
|
+
/***/ },
|
|
39425
|
+
|
|
39426
|
+
/***/ "./src/models/training.ts"
|
|
39427
|
+
/*!********************************!*\
|
|
39428
|
+
!*** ./src/models/training.ts ***!
|
|
39429
|
+
\********************************/
|
|
39430
|
+
(__unused_webpack_module, exports) {
|
|
39431
|
+
|
|
39432
|
+
"use strict";
|
|
39433
|
+
|
|
39434
|
+
/**
|
|
39435
|
+
* Training Mode Types
|
|
39436
|
+
*
|
|
39437
|
+
* These types support the guided training system where a user physically
|
|
39438
|
+
* walks around their property to train the system on camera positions,
|
|
39439
|
+
* transit times, overlaps, landmarks, and structures.
|
|
39440
|
+
*/
|
|
39441
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
39442
|
+
exports.DEFAULT_TRAINING_CONFIG = void 0;
|
|
39443
|
+
exports.createTrainingSession = createTrainingSession;
|
|
39444
|
+
exports.calculateTrainingStats = calculateTrainingStats;
|
|
39445
|
+
/** Default training configuration */
|
|
39446
|
+
exports.DEFAULT_TRAINING_CONFIG = {
|
|
39447
|
+
minDetectionConfidence: 0.7,
|
|
39448
|
+
maxTransitWait: 120, // 2 minutes
|
|
39449
|
+
autoDetectOverlaps: true,
|
|
39450
|
+
autoSuggestLandmarks: true,
|
|
39451
|
+
minOverlapDuration: 2, // 2 seconds
|
|
39452
|
+
};
|
|
39453
|
+
/** Create a new empty training session */
|
|
39454
|
+
function createTrainingSession(trainerName) {
|
|
39455
|
+
const now = Date.now();
|
|
39456
|
+
return {
|
|
39457
|
+
id: `training-${now}-${Math.random().toString(36).substr(2, 9)}`,
|
|
39458
|
+
state: 'idle',
|
|
39459
|
+
startedAt: now,
|
|
39460
|
+
updatedAt: now,
|
|
39461
|
+
trainerName,
|
|
39462
|
+
visits: [],
|
|
39463
|
+
transits: [],
|
|
39464
|
+
landmarks: [],
|
|
39465
|
+
overlaps: [],
|
|
39466
|
+
structures: [],
|
|
39467
|
+
stats: {
|
|
39468
|
+
totalDuration: 0,
|
|
39469
|
+
camerasVisited: 0,
|
|
39470
|
+
transitsRecorded: 0,
|
|
39471
|
+
landmarksMarked: 0,
|
|
39472
|
+
overlapsDetected: 0,
|
|
39473
|
+
structuresMarked: 0,
|
|
39474
|
+
averageTransitTime: 0,
|
|
39475
|
+
coveragePercentage: 0,
|
|
39476
|
+
},
|
|
39477
|
+
};
|
|
39478
|
+
}
|
|
39479
|
+
/** Calculate session statistics */
|
|
39480
|
+
function calculateTrainingStats(session, totalCameras) {
|
|
39481
|
+
const uniqueCameras = new Set(session.visits.map(v => v.cameraId));
|
|
39482
|
+
const transitTimes = session.transits.map(t => t.transitSeconds);
|
|
39483
|
+
const avgTransit = transitTimes.length > 0
|
|
39484
|
+
? transitTimes.reduce((a, b) => a + b, 0) / transitTimes.length
|
|
39485
|
+
: 0;
|
|
39486
|
+
return {
|
|
39487
|
+
totalDuration: (session.completedAt || Date.now()) - session.startedAt,
|
|
39488
|
+
camerasVisited: uniqueCameras.size,
|
|
39489
|
+
transitsRecorded: session.transits.length,
|
|
39490
|
+
landmarksMarked: session.landmarks.length,
|
|
39491
|
+
overlapsDetected: session.overlaps.length,
|
|
39492
|
+
structuresMarked: session.structures.length,
|
|
39493
|
+
averageTransitTime: Math.round(avgTransit),
|
|
39494
|
+
coveragePercentage: totalCameras > 0
|
|
39495
|
+
? Math.round((uniqueCameras.size / totalCameras) * 100)
|
|
39496
|
+
: 0,
|
|
39497
|
+
};
|
|
39498
|
+
}
|
|
39499
|
+
|
|
39500
|
+
|
|
38671
39501
|
/***/ },
|
|
38672
39502
|
|
|
38673
39503
|
/***/ "./src/state/tracking-state.ts"
|
|
@@ -40200,6 +41030,1026 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
40200
41030
|
</html>`;
|
|
40201
41031
|
|
|
40202
41032
|
|
|
41033
|
+
/***/ },
|
|
41034
|
+
|
|
41035
|
+
/***/ "./src/ui/training-html.ts"
|
|
41036
|
+
/*!*********************************!*\
|
|
41037
|
+
!*** ./src/ui/training-html.ts ***!
|
|
41038
|
+
\*********************************/
|
|
41039
|
+
(__unused_webpack_module, exports) {
|
|
41040
|
+
|
|
41041
|
+
"use strict";
|
|
41042
|
+
|
|
41043
|
+
/**
|
|
41044
|
+
* Training Mode UI - Mobile-optimized walkthrough interface
|
|
41045
|
+
* Designed for phone use while walking around property
|
|
41046
|
+
*/
|
|
41047
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
41048
|
+
exports.TRAINING_HTML = void 0;
|
|
41049
|
+
exports.TRAINING_HTML = `<!DOCTYPE html>
|
|
41050
|
+
<html lang="en">
|
|
41051
|
+
<head>
|
|
41052
|
+
<meta charset="UTF-8">
|
|
41053
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
41054
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
41055
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
41056
|
+
<title>Spatial Awareness - Training Mode</title>
|
|
41057
|
+
<style>
|
|
41058
|
+
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
|
41059
|
+
html, body { height: 100%; overflow: hidden; }
|
|
41060
|
+
body {
|
|
41061
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
41062
|
+
background: #121212;
|
|
41063
|
+
color: rgba(255,255,255,0.87);
|
|
41064
|
+
min-height: 100vh;
|
|
41065
|
+
display: flex;
|
|
41066
|
+
flex-direction: column;
|
|
41067
|
+
}
|
|
41068
|
+
|
|
41069
|
+
/* Header */
|
|
41070
|
+
.header {
|
|
41071
|
+
background: rgba(255,255,255,0.03);
|
|
41072
|
+
padding: 12px 16px;
|
|
41073
|
+
display: flex;
|
|
41074
|
+
justify-content: space-between;
|
|
41075
|
+
align-items: center;
|
|
41076
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
41077
|
+
}
|
|
41078
|
+
.header h1 { font-size: 16px; font-weight: 500; }
|
|
41079
|
+
.header-status {
|
|
41080
|
+
display: flex;
|
|
41081
|
+
align-items: center;
|
|
41082
|
+
gap: 8px;
|
|
41083
|
+
font-size: 13px;
|
|
41084
|
+
}
|
|
41085
|
+
.status-badge {
|
|
41086
|
+
padding: 4px 10px;
|
|
41087
|
+
border-radius: 4px;
|
|
41088
|
+
font-size: 11px;
|
|
41089
|
+
font-weight: 500;
|
|
41090
|
+
text-transform: uppercase;
|
|
41091
|
+
}
|
|
41092
|
+
.status-badge.idle { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); }
|
|
41093
|
+
.status-badge.active { background: #4fc3f7; color: #000; animation: pulse 2s infinite; }
|
|
41094
|
+
.status-badge.paused { background: #ffb74d; color: #000; }
|
|
41095
|
+
.status-badge.completed { background: #81c784; color: #000; }
|
|
41096
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
|
41097
|
+
|
|
41098
|
+
/* Main content area */
|
|
41099
|
+
.main-content {
|
|
41100
|
+
flex: 1;
|
|
41101
|
+
display: flex;
|
|
41102
|
+
flex-direction: column;
|
|
41103
|
+
padding: 12px;
|
|
41104
|
+
overflow-y: auto;
|
|
41105
|
+
gap: 12px;
|
|
41106
|
+
}
|
|
41107
|
+
|
|
41108
|
+
/* Camera detection card */
|
|
41109
|
+
.detection-card {
|
|
41110
|
+
background: rgba(255,255,255,0.03);
|
|
41111
|
+
border-radius: 8px;
|
|
41112
|
+
padding: 16px;
|
|
41113
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
41114
|
+
transition: all 0.3s;
|
|
41115
|
+
}
|
|
41116
|
+
.detection-card.detecting {
|
|
41117
|
+
border-color: #4fc3f7;
|
|
41118
|
+
background: rgba(79, 195, 247, 0.1);
|
|
41119
|
+
}
|
|
41120
|
+
.detection-card.in-transit {
|
|
41121
|
+
border-color: #ffb74d;
|
|
41122
|
+
background: rgba(255, 183, 77, 0.1);
|
|
41123
|
+
}
|
|
41124
|
+
|
|
41125
|
+
.detection-icon {
|
|
41126
|
+
width: 64px;
|
|
41127
|
+
height: 64px;
|
|
41128
|
+
border-radius: 8px;
|
|
41129
|
+
background: rgba(255,255,255,0.05);
|
|
41130
|
+
display: flex;
|
|
41131
|
+
align-items: center;
|
|
41132
|
+
justify-content: center;
|
|
41133
|
+
margin: 0 auto 12px;
|
|
41134
|
+
font-size: 28px;
|
|
41135
|
+
}
|
|
41136
|
+
.detection-card.detecting .detection-icon {
|
|
41137
|
+
background: #4fc3f7;
|
|
41138
|
+
animation: detectPulse 1.5s infinite;
|
|
41139
|
+
}
|
|
41140
|
+
@keyframes detectPulse {
|
|
41141
|
+
0%, 100% { transform: scale(1); }
|
|
41142
|
+
50% { transform: scale(1.03); }
|
|
41143
|
+
}
|
|
41144
|
+
|
|
41145
|
+
.detection-title {
|
|
41146
|
+
font-size: 18px;
|
|
41147
|
+
font-weight: 500;
|
|
41148
|
+
text-align: center;
|
|
41149
|
+
margin-bottom: 4px;
|
|
41150
|
+
}
|
|
41151
|
+
.detection-subtitle {
|
|
41152
|
+
font-size: 13px;
|
|
41153
|
+
color: rgba(255,255,255,0.5);
|
|
41154
|
+
text-align: center;
|
|
41155
|
+
}
|
|
41156
|
+
.detection-confidence {
|
|
41157
|
+
margin-top: 8px;
|
|
41158
|
+
text-align: center;
|
|
41159
|
+
font-size: 12px;
|
|
41160
|
+
color: rgba(255,255,255,0.4);
|
|
41161
|
+
}
|
|
41162
|
+
|
|
41163
|
+
/* Transit timer */
|
|
41164
|
+
.transit-timer {
|
|
41165
|
+
display: flex;
|
|
41166
|
+
align-items: center;
|
|
41167
|
+
justify-content: center;
|
|
41168
|
+
gap: 8px;
|
|
41169
|
+
margin-top: 12px;
|
|
41170
|
+
padding: 10px;
|
|
41171
|
+
background: rgba(0,0,0,0.2);
|
|
41172
|
+
border-radius: 6px;
|
|
41173
|
+
}
|
|
41174
|
+
.transit-timer-icon { font-size: 18px; }
|
|
41175
|
+
.transit-timer-text { font-size: 16px; font-weight: 500; }
|
|
41176
|
+
.transit-timer-from { font-size: 12px; color: rgba(255,255,255,0.5); }
|
|
41177
|
+
|
|
41178
|
+
/* Stats grid */
|
|
41179
|
+
.stats-grid {
|
|
41180
|
+
display: grid;
|
|
41181
|
+
grid-template-columns: repeat(3, 1fr);
|
|
41182
|
+
gap: 8px;
|
|
41183
|
+
}
|
|
41184
|
+
.stat-item {
|
|
41185
|
+
background: rgba(255,255,255,0.03);
|
|
41186
|
+
border-radius: 6px;
|
|
41187
|
+
padding: 12px 8px;
|
|
41188
|
+
text-align: center;
|
|
41189
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41190
|
+
}
|
|
41191
|
+
.stat-value {
|
|
41192
|
+
font-size: 24px;
|
|
41193
|
+
font-weight: 500;
|
|
41194
|
+
color: #4fc3f7;
|
|
41195
|
+
}
|
|
41196
|
+
.stat-label {
|
|
41197
|
+
font-size: 10px;
|
|
41198
|
+
color: rgba(255,255,255,0.4);
|
|
41199
|
+
text-transform: uppercase;
|
|
41200
|
+
margin-top: 2px;
|
|
41201
|
+
}
|
|
41202
|
+
|
|
41203
|
+
/* Progress bar */
|
|
41204
|
+
.progress-section {
|
|
41205
|
+
background: rgba(255,255,255,0.03);
|
|
41206
|
+
border-radius: 6px;
|
|
41207
|
+
padding: 12px;
|
|
41208
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41209
|
+
}
|
|
41210
|
+
.progress-header {
|
|
41211
|
+
display: flex;
|
|
41212
|
+
justify-content: space-between;
|
|
41213
|
+
margin-bottom: 8px;
|
|
41214
|
+
font-size: 13px;
|
|
41215
|
+
}
|
|
41216
|
+
.progress-bar {
|
|
41217
|
+
height: 6px;
|
|
41218
|
+
background: rgba(255,255,255,0.1);
|
|
41219
|
+
border-radius: 3px;
|
|
41220
|
+
overflow: hidden;
|
|
41221
|
+
}
|
|
41222
|
+
.progress-fill {
|
|
41223
|
+
height: 100%;
|
|
41224
|
+
background: #4fc3f7;
|
|
41225
|
+
border-radius: 3px;
|
|
41226
|
+
transition: width 0.5s;
|
|
41227
|
+
}
|
|
41228
|
+
|
|
41229
|
+
/* Suggestions */
|
|
41230
|
+
.suggestions-section {
|
|
41231
|
+
background: rgba(79, 195, 247, 0.08);
|
|
41232
|
+
border: 1px solid rgba(79, 195, 247, 0.2);
|
|
41233
|
+
border-radius: 6px;
|
|
41234
|
+
padding: 12px;
|
|
41235
|
+
}
|
|
41236
|
+
.suggestions-title {
|
|
41237
|
+
font-size: 11px;
|
|
41238
|
+
text-transform: uppercase;
|
|
41239
|
+
color: #4fc3f7;
|
|
41240
|
+
margin-bottom: 6px;
|
|
41241
|
+
font-weight: 500;
|
|
41242
|
+
}
|
|
41243
|
+
.suggestion-item {
|
|
41244
|
+
font-size: 13px;
|
|
41245
|
+
padding: 6px 0;
|
|
41246
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
41247
|
+
color: rgba(255,255,255,0.7);
|
|
41248
|
+
}
|
|
41249
|
+
.suggestion-item:last-child { border-bottom: none; }
|
|
41250
|
+
.suggestion-item::before {
|
|
41251
|
+
content: "→ ";
|
|
41252
|
+
color: #4fc3f7;
|
|
41253
|
+
}
|
|
41254
|
+
|
|
41255
|
+
/* Action buttons */
|
|
41256
|
+
.action-buttons {
|
|
41257
|
+
display: flex;
|
|
41258
|
+
gap: 8px;
|
|
41259
|
+
padding: 8px 0;
|
|
41260
|
+
}
|
|
41261
|
+
.btn {
|
|
41262
|
+
flex: 1;
|
|
41263
|
+
padding: 14px 16px;
|
|
41264
|
+
border: none;
|
|
41265
|
+
border-radius: 6px;
|
|
41266
|
+
font-size: 14px;
|
|
41267
|
+
font-weight: 500;
|
|
41268
|
+
cursor: pointer;
|
|
41269
|
+
transition: all 0.2s;
|
|
41270
|
+
display: flex;
|
|
41271
|
+
align-items: center;
|
|
41272
|
+
justify-content: center;
|
|
41273
|
+
gap: 6px;
|
|
41274
|
+
}
|
|
41275
|
+
.btn:active { transform: scale(0.98); }
|
|
41276
|
+
.btn-primary { background: #4fc3f7; color: #000; }
|
|
41277
|
+
.btn-secondary { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.87); }
|
|
41278
|
+
.btn-danger { background: #ef5350; color: #fff; }
|
|
41279
|
+
.btn-warning { background: #ffb74d; color: #000; }
|
|
41280
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
41281
|
+
|
|
41282
|
+
/* Full-width button */
|
|
41283
|
+
.btn-full { flex: none; width: 100%; }
|
|
41284
|
+
|
|
41285
|
+
/* Mark landmark/structure panel */
|
|
41286
|
+
.mark-panel {
|
|
41287
|
+
background: rgba(255,255,255,0.03);
|
|
41288
|
+
border-radius: 6px;
|
|
41289
|
+
padding: 16px;
|
|
41290
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41291
|
+
}
|
|
41292
|
+
.mark-panel-title {
|
|
41293
|
+
font-size: 14px;
|
|
41294
|
+
font-weight: 500;
|
|
41295
|
+
margin-bottom: 12px;
|
|
41296
|
+
}
|
|
41297
|
+
.mark-type-grid {
|
|
41298
|
+
display: grid;
|
|
41299
|
+
grid-template-columns: repeat(4, 1fr);
|
|
41300
|
+
gap: 6px;
|
|
41301
|
+
margin-bottom: 12px;
|
|
41302
|
+
}
|
|
41303
|
+
.mark-type-btn {
|
|
41304
|
+
padding: 10px 6px;
|
|
41305
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
41306
|
+
border-radius: 6px;
|
|
41307
|
+
background: transparent;
|
|
41308
|
+
color: rgba(255,255,255,0.7);
|
|
41309
|
+
font-size: 10px;
|
|
41310
|
+
text-align: center;
|
|
41311
|
+
cursor: pointer;
|
|
41312
|
+
transition: all 0.2s;
|
|
41313
|
+
}
|
|
41314
|
+
.mark-type-btn.selected {
|
|
41315
|
+
border-color: #4fc3f7;
|
|
41316
|
+
background: rgba(79, 195, 247, 0.15);
|
|
41317
|
+
color: #fff;
|
|
41318
|
+
}
|
|
41319
|
+
.mark-type-btn .icon { font-size: 18px; margin-bottom: 2px; display: block; }
|
|
41320
|
+
|
|
41321
|
+
/* Input field */
|
|
41322
|
+
.input-group { margin-bottom: 12px; }
|
|
41323
|
+
.input-group label {
|
|
41324
|
+
display: block;
|
|
41325
|
+
font-size: 12px;
|
|
41326
|
+
color: rgba(255,255,255,0.5);
|
|
41327
|
+
margin-bottom: 4px;
|
|
41328
|
+
}
|
|
41329
|
+
.input-group input {
|
|
41330
|
+
width: 100%;
|
|
41331
|
+
padding: 12px;
|
|
41332
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
41333
|
+
border-radius: 6px;
|
|
41334
|
+
background: rgba(0,0,0,0.2);
|
|
41335
|
+
color: #fff;
|
|
41336
|
+
font-size: 14px;
|
|
41337
|
+
}
|
|
41338
|
+
.input-group input:focus {
|
|
41339
|
+
outline: none;
|
|
41340
|
+
border-color: #4fc3f7;
|
|
41341
|
+
}
|
|
41342
|
+
|
|
41343
|
+
/* History list */
|
|
41344
|
+
.history-section {
|
|
41345
|
+
background: rgba(255,255,255,0.03);
|
|
41346
|
+
border-radius: 6px;
|
|
41347
|
+
padding: 12px;
|
|
41348
|
+
max-height: 180px;
|
|
41349
|
+
overflow-y: auto;
|
|
41350
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41351
|
+
}
|
|
41352
|
+
.history-title {
|
|
41353
|
+
font-size: 13px;
|
|
41354
|
+
font-weight: 500;
|
|
41355
|
+
margin-bottom: 8px;
|
|
41356
|
+
display: flex;
|
|
41357
|
+
justify-content: space-between;
|
|
41358
|
+
}
|
|
41359
|
+
.history-item {
|
|
41360
|
+
padding: 8px;
|
|
41361
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
41362
|
+
font-size: 13px;
|
|
41363
|
+
}
|
|
41364
|
+
.history-item:last-child { border-bottom: none; }
|
|
41365
|
+
.history-item-time {
|
|
41366
|
+
font-size: 10px;
|
|
41367
|
+
color: rgba(255,255,255,0.4);
|
|
41368
|
+
}
|
|
41369
|
+
.history-item-camera { color: #4fc3f7; font-weight: 500; }
|
|
41370
|
+
.history-item-transit { color: #ffb74d; }
|
|
41371
|
+
|
|
41372
|
+
/* Bottom action bar */
|
|
41373
|
+
.bottom-bar {
|
|
41374
|
+
background: rgba(255,255,255,0.03);
|
|
41375
|
+
padding: 12px 16px;
|
|
41376
|
+
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
|
41377
|
+
border-top: 1px solid rgba(255,255,255,0.08);
|
|
41378
|
+
}
|
|
41379
|
+
|
|
41380
|
+
/* Tabs */
|
|
41381
|
+
.tabs {
|
|
41382
|
+
display: flex;
|
|
41383
|
+
background: rgba(0,0,0,0.2);
|
|
41384
|
+
border-radius: 6px;
|
|
41385
|
+
padding: 3px;
|
|
41386
|
+
margin-bottom: 12px;
|
|
41387
|
+
}
|
|
41388
|
+
.tab {
|
|
41389
|
+
flex: 1;
|
|
41390
|
+
padding: 10px;
|
|
41391
|
+
border: none;
|
|
41392
|
+
background: transparent;
|
|
41393
|
+
color: rgba(255,255,255,0.5);
|
|
41394
|
+
font-size: 13px;
|
|
41395
|
+
font-weight: 500;
|
|
41396
|
+
cursor: pointer;
|
|
41397
|
+
border-radius: 4px;
|
|
41398
|
+
transition: all 0.2s;
|
|
41399
|
+
}
|
|
41400
|
+
.tab.active {
|
|
41401
|
+
background: rgba(255,255,255,0.08);
|
|
41402
|
+
color: rgba(255,255,255,0.87);
|
|
41403
|
+
}
|
|
41404
|
+
|
|
41405
|
+
/* Tab content */
|
|
41406
|
+
.tab-content { display: none; }
|
|
41407
|
+
.tab-content.active { display: block; }
|
|
41408
|
+
|
|
41409
|
+
/* Apply results modal */
|
|
41410
|
+
.modal-overlay {
|
|
41411
|
+
display: none;
|
|
41412
|
+
position: fixed;
|
|
41413
|
+
top: 0;
|
|
41414
|
+
left: 0;
|
|
41415
|
+
right: 0;
|
|
41416
|
+
bottom: 0;
|
|
41417
|
+
background: rgba(0,0,0,0.85);
|
|
41418
|
+
z-index: 100;
|
|
41419
|
+
align-items: center;
|
|
41420
|
+
justify-content: center;
|
|
41421
|
+
padding: 16px;
|
|
41422
|
+
}
|
|
41423
|
+
.modal-overlay.active { display: flex; }
|
|
41424
|
+
.modal {
|
|
41425
|
+
background: #1e1e1e;
|
|
41426
|
+
border-radius: 8px;
|
|
41427
|
+
padding: 20px;
|
|
41428
|
+
max-width: 360px;
|
|
41429
|
+
width: 100%;
|
|
41430
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
41431
|
+
}
|
|
41432
|
+
.modal h2 { font-size: 18px; margin-bottom: 12px; font-weight: 500; }
|
|
41433
|
+
.modal-result-item {
|
|
41434
|
+
display: flex;
|
|
41435
|
+
justify-content: space-between;
|
|
41436
|
+
padding: 8px 0;
|
|
41437
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
41438
|
+
font-size: 13px;
|
|
41439
|
+
}
|
|
41440
|
+
.modal-result-value { color: #4fc3f7; font-weight: 500; }
|
|
41441
|
+
.modal-buttons { display: flex; gap: 8px; margin-top: 16px; }
|
|
41442
|
+
|
|
41443
|
+
/* Idle state */
|
|
41444
|
+
.idle-content {
|
|
41445
|
+
flex: 1;
|
|
41446
|
+
display: flex;
|
|
41447
|
+
flex-direction: column;
|
|
41448
|
+
align-items: center;
|
|
41449
|
+
justify-content: center;
|
|
41450
|
+
text-align: center;
|
|
41451
|
+
padding: 32px 16px;
|
|
41452
|
+
}
|
|
41453
|
+
.idle-icon {
|
|
41454
|
+
font-size: 56px;
|
|
41455
|
+
margin-bottom: 16px;
|
|
41456
|
+
opacity: 0.7;
|
|
41457
|
+
}
|
|
41458
|
+
.idle-title {
|
|
41459
|
+
font-size: 20px;
|
|
41460
|
+
font-weight: 500;
|
|
41461
|
+
margin-bottom: 8px;
|
|
41462
|
+
}
|
|
41463
|
+
.idle-desc {
|
|
41464
|
+
font-size: 14px;
|
|
41465
|
+
color: rgba(255,255,255,0.5);
|
|
41466
|
+
max-width: 280px;
|
|
41467
|
+
line-height: 1.5;
|
|
41468
|
+
}
|
|
41469
|
+
.idle-instructions {
|
|
41470
|
+
margin-top: 24px;
|
|
41471
|
+
text-align: left;
|
|
41472
|
+
background: rgba(255,255,255,0.03);
|
|
41473
|
+
border-radius: 6px;
|
|
41474
|
+
padding: 16px;
|
|
41475
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
41476
|
+
}
|
|
41477
|
+
.idle-instructions h3 {
|
|
41478
|
+
font-size: 12px;
|
|
41479
|
+
margin-bottom: 10px;
|
|
41480
|
+
color: #4fc3f7;
|
|
41481
|
+
font-weight: 500;
|
|
41482
|
+
}
|
|
41483
|
+
.idle-instructions ol {
|
|
41484
|
+
padding-left: 18px;
|
|
41485
|
+
font-size: 13px;
|
|
41486
|
+
line-height: 1.8;
|
|
41487
|
+
color: rgba(255,255,255,0.6);
|
|
41488
|
+
}
|
|
41489
|
+
</style>
|
|
41490
|
+
</head>
|
|
41491
|
+
<body>
|
|
41492
|
+
<div class="header">
|
|
41493
|
+
<h1>Training Mode</h1>
|
|
41494
|
+
<div class="header-status">
|
|
41495
|
+
<span class="status-badge" id="status-badge">Idle</span>
|
|
41496
|
+
</div>
|
|
41497
|
+
</div>
|
|
41498
|
+
|
|
41499
|
+
<!-- Idle State -->
|
|
41500
|
+
<div class="main-content" id="idle-content">
|
|
41501
|
+
<div class="idle-content">
|
|
41502
|
+
<div class="idle-icon">🚶</div>
|
|
41503
|
+
<div class="idle-title">Train Your System</div>
|
|
41504
|
+
<div class="idle-desc">Walk around your property to teach the system about camera positions, transit times, and landmarks.</div>
|
|
41505
|
+
|
|
41506
|
+
<div class="idle-instructions">
|
|
41507
|
+
<h3>How it works:</h3>
|
|
41508
|
+
<ol>
|
|
41509
|
+
<li>Tap <strong>Start Training</strong> below</li>
|
|
41510
|
+
<li>Walk to each camera on your property</li>
|
|
41511
|
+
<li>The system detects you automatically</li>
|
|
41512
|
+
<li>Mark landmarks as you encounter them</li>
|
|
41513
|
+
<li>End training when you're done</li>
|
|
41514
|
+
</ol>
|
|
41515
|
+
</div>
|
|
41516
|
+
</div>
|
|
41517
|
+
|
|
41518
|
+
<div class="bottom-bar">
|
|
41519
|
+
<button class="btn btn-primary btn-full" onclick="startTraining()">
|
|
41520
|
+
▶ Start Training
|
|
41521
|
+
</button>
|
|
41522
|
+
</div>
|
|
41523
|
+
</div>
|
|
41524
|
+
|
|
41525
|
+
<!-- Active Training State -->
|
|
41526
|
+
<div class="main-content" id="active-content" style="display: none;">
|
|
41527
|
+
<!-- Detection Card -->
|
|
41528
|
+
<div class="detection-card" id="detection-card">
|
|
41529
|
+
<div class="detection-icon" id="detection-icon">👤</div>
|
|
41530
|
+
<div class="detection-title" id="detection-title">Waiting for detection...</div>
|
|
41531
|
+
<div class="detection-subtitle" id="detection-subtitle">Walk to any camera to begin</div>
|
|
41532
|
+
<div class="detection-confidence" id="detection-confidence"></div>
|
|
41533
|
+
|
|
41534
|
+
<div class="transit-timer" id="transit-timer" style="display: none;">
|
|
41535
|
+
<span class="transit-timer-icon">⏱</span>
|
|
41536
|
+
<span class="transit-timer-text" id="transit-time">0s</span>
|
|
41537
|
+
<span class="transit-timer-from" id="transit-from"></span>
|
|
41538
|
+
</div>
|
|
41539
|
+
</div>
|
|
41540
|
+
|
|
41541
|
+
<!-- Tabs -->
|
|
41542
|
+
<div class="tabs">
|
|
41543
|
+
<button class="tab active" onclick="switchTab('status')">Status</button>
|
|
41544
|
+
<button class="tab" onclick="switchTab('mark')">Mark</button>
|
|
41545
|
+
<button class="tab" onclick="switchTab('history')">History</button>
|
|
41546
|
+
</div>
|
|
41547
|
+
|
|
41548
|
+
<!-- Status Tab -->
|
|
41549
|
+
<div class="tab-content active" id="tab-status">
|
|
41550
|
+
<!-- Stats -->
|
|
41551
|
+
<div class="stats-grid">
|
|
41552
|
+
<div class="stat-item">
|
|
41553
|
+
<div class="stat-value" id="stat-cameras">0</div>
|
|
41554
|
+
<div class="stat-label">Cameras</div>
|
|
41555
|
+
</div>
|
|
41556
|
+
<div class="stat-item">
|
|
41557
|
+
<div class="stat-value" id="stat-transits">0</div>
|
|
41558
|
+
<div class="stat-label">Transits</div>
|
|
41559
|
+
</div>
|
|
41560
|
+
<div class="stat-item">
|
|
41561
|
+
<div class="stat-value" id="stat-landmarks">0</div>
|
|
41562
|
+
<div class="stat-label">Landmarks</div>
|
|
41563
|
+
</div>
|
|
41564
|
+
</div>
|
|
41565
|
+
|
|
41566
|
+
<!-- Progress -->
|
|
41567
|
+
<div class="progress-section" style="margin-top: 15px;">
|
|
41568
|
+
<div class="progress-header">
|
|
41569
|
+
<span>Coverage</span>
|
|
41570
|
+
<span id="progress-percent">0%</span>
|
|
41571
|
+
</div>
|
|
41572
|
+
<div class="progress-bar">
|
|
41573
|
+
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
|
|
41574
|
+
</div>
|
|
41575
|
+
</div>
|
|
41576
|
+
|
|
41577
|
+
<!-- Suggestions -->
|
|
41578
|
+
<div class="suggestions-section" style="margin-top: 15px;" id="suggestions-section">
|
|
41579
|
+
<div class="suggestions-title">Suggestions</div>
|
|
41580
|
+
<div id="suggestions-list">
|
|
41581
|
+
<div class="suggestion-item">Start walking to a camera</div>
|
|
41582
|
+
</div>
|
|
41583
|
+
</div>
|
|
41584
|
+
</div>
|
|
41585
|
+
|
|
41586
|
+
<!-- Mark Tab -->
|
|
41587
|
+
<div class="tab-content" id="tab-mark">
|
|
41588
|
+
<div class="mark-panel">
|
|
41589
|
+
<div class="mark-panel-title">Mark a Landmark</div>
|
|
41590
|
+
<div class="mark-type-grid" id="landmark-type-grid">
|
|
41591
|
+
<button class="mark-type-btn selected" data-type="mailbox" onclick="selectLandmarkType('mailbox')">
|
|
41592
|
+
<span class="icon">📬</span>
|
|
41593
|
+
Mailbox
|
|
41594
|
+
</button>
|
|
41595
|
+
<button class="mark-type-btn" data-type="garage" onclick="selectLandmarkType('garage')">
|
|
41596
|
+
<span class="icon">🏠</span>
|
|
41597
|
+
Garage
|
|
41598
|
+
</button>
|
|
41599
|
+
<button class="mark-type-btn" data-type="shed" onclick="selectLandmarkType('shed')">
|
|
41600
|
+
<span class="icon">🏚</span>
|
|
41601
|
+
Shed
|
|
41602
|
+
</button>
|
|
41603
|
+
<button class="mark-type-btn" data-type="tree" onclick="selectLandmarkType('tree')">
|
|
41604
|
+
<span class="icon">🌳</span>
|
|
41605
|
+
Tree
|
|
41606
|
+
</button>
|
|
41607
|
+
<button class="mark-type-btn" data-type="gate" onclick="selectLandmarkType('gate')">
|
|
41608
|
+
<span class="icon">🚪</span>
|
|
41609
|
+
Gate
|
|
41610
|
+
</button>
|
|
41611
|
+
<button class="mark-type-btn" data-type="driveway" onclick="selectLandmarkType('driveway')">
|
|
41612
|
+
<span class="icon">🛣</span>
|
|
41613
|
+
Driveway
|
|
41614
|
+
</button>
|
|
41615
|
+
<button class="mark-type-btn" data-type="pool" onclick="selectLandmarkType('pool')">
|
|
41616
|
+
<span class="icon">🏊</span>
|
|
41617
|
+
Pool
|
|
41618
|
+
</button>
|
|
41619
|
+
<button class="mark-type-btn" data-type="other" onclick="selectLandmarkType('other')">
|
|
41620
|
+
<span class="icon">📍</span>
|
|
41621
|
+
Other
|
|
41622
|
+
</button>
|
|
41623
|
+
</div>
|
|
41624
|
+
|
|
41625
|
+
<div class="input-group">
|
|
41626
|
+
<label>Landmark Name</label>
|
|
41627
|
+
<input type="text" id="landmark-name" placeholder="e.g., Front Mailbox">
|
|
41628
|
+
</div>
|
|
41629
|
+
|
|
41630
|
+
<button class="btn btn-primary btn-full" onclick="markLandmark()">
|
|
41631
|
+
📍 Mark Landmark Here
|
|
41632
|
+
</button>
|
|
41633
|
+
</div>
|
|
41634
|
+
</div>
|
|
41635
|
+
|
|
41636
|
+
<!-- History Tab -->
|
|
41637
|
+
<div class="tab-content" id="tab-history">
|
|
41638
|
+
<div class="history-section">
|
|
41639
|
+
<div class="history-title">
|
|
41640
|
+
<span>Recent Activity</span>
|
|
41641
|
+
<span id="history-count">0 events</span>
|
|
41642
|
+
</div>
|
|
41643
|
+
<div id="history-list">
|
|
41644
|
+
<div class="history-item" style="color: rgba(255,255,255,0.4); text-align: center;">
|
|
41645
|
+
No activity yet
|
|
41646
|
+
</div>
|
|
41647
|
+
</div>
|
|
41648
|
+
</div>
|
|
41649
|
+
</div>
|
|
41650
|
+
|
|
41651
|
+
<!-- Bottom Actions -->
|
|
41652
|
+
<div class="bottom-bar">
|
|
41653
|
+
<div class="action-buttons">
|
|
41654
|
+
<button class="btn btn-warning" id="pause-btn" onclick="togglePause()">
|
|
41655
|
+
⏸ Pause
|
|
41656
|
+
</button>
|
|
41657
|
+
<button class="btn btn-danger" onclick="endTraining()">
|
|
41658
|
+
⏹ End
|
|
41659
|
+
</button>
|
|
41660
|
+
</div>
|
|
41661
|
+
</div>
|
|
41662
|
+
</div>
|
|
41663
|
+
|
|
41664
|
+
<!-- Completed State -->
|
|
41665
|
+
<div class="main-content" id="completed-content" style="display: none;">
|
|
41666
|
+
<div class="idle-content">
|
|
41667
|
+
<div class="idle-icon">✅</div>
|
|
41668
|
+
<div class="idle-title">Training Complete!</div>
|
|
41669
|
+
<div class="idle-desc">Review the results and apply them to your topology.</div>
|
|
41670
|
+
</div>
|
|
41671
|
+
|
|
41672
|
+
<!-- Final Stats -->
|
|
41673
|
+
<div class="stats-grid">
|
|
41674
|
+
<div class="stat-item">
|
|
41675
|
+
<div class="stat-value" id="final-cameras">0</div>
|
|
41676
|
+
<div class="stat-label">Cameras</div>
|
|
41677
|
+
</div>
|
|
41678
|
+
<div class="stat-item">
|
|
41679
|
+
<div class="stat-value" id="final-transits">0</div>
|
|
41680
|
+
<div class="stat-label">Transits</div>
|
|
41681
|
+
</div>
|
|
41682
|
+
<div class="stat-item">
|
|
41683
|
+
<div class="stat-value" id="final-landmarks">0</div>
|
|
41684
|
+
<div class="stat-label">Landmarks</div>
|
|
41685
|
+
</div>
|
|
41686
|
+
</div>
|
|
41687
|
+
|
|
41688
|
+
<div class="stats-grid" style="margin-top: 10px;">
|
|
41689
|
+
<div class="stat-item">
|
|
41690
|
+
<div class="stat-value" id="final-overlaps">0</div>
|
|
41691
|
+
<div class="stat-label">Overlaps</div>
|
|
41692
|
+
</div>
|
|
41693
|
+
<div class="stat-item">
|
|
41694
|
+
<div class="stat-value" id="final-avg-transit">0s</div>
|
|
41695
|
+
<div class="stat-label">Avg Transit</div>
|
|
41696
|
+
</div>
|
|
41697
|
+
<div class="stat-item">
|
|
41698
|
+
<div class="stat-value" id="final-coverage">0%</div>
|
|
41699
|
+
<div class="stat-label">Coverage</div>
|
|
41700
|
+
</div>
|
|
41701
|
+
</div>
|
|
41702
|
+
|
|
41703
|
+
<div class="bottom-bar" style="margin-top: auto;">
|
|
41704
|
+
<div class="action-buttons">
|
|
41705
|
+
<button class="btn btn-secondary" onclick="resetTraining()">
|
|
41706
|
+
↻ Start Over
|
|
41707
|
+
</button>
|
|
41708
|
+
<button class="btn btn-primary" onclick="applyTraining()">
|
|
41709
|
+
✓ Apply Results
|
|
41710
|
+
</button>
|
|
41711
|
+
</div>
|
|
41712
|
+
</div>
|
|
41713
|
+
</div>
|
|
41714
|
+
|
|
41715
|
+
<!-- Apply Results Modal -->
|
|
41716
|
+
<div class="modal-overlay" id="results-modal">
|
|
41717
|
+
<div class="modal">
|
|
41718
|
+
<h2>Training Applied!</h2>
|
|
41719
|
+
<div id="results-content">
|
|
41720
|
+
<div class="modal-result-item">
|
|
41721
|
+
<span>Connections Created</span>
|
|
41722
|
+
<span class="modal-result-value" id="result-connections">0</span>
|
|
41723
|
+
</div>
|
|
41724
|
+
<div class="modal-result-item">
|
|
41725
|
+
<span>Connections Updated</span>
|
|
41726
|
+
<span class="modal-result-value" id="result-updated">0</span>
|
|
41727
|
+
</div>
|
|
41728
|
+
<div class="modal-result-item">
|
|
41729
|
+
<span>Landmarks Added</span>
|
|
41730
|
+
<span class="modal-result-value" id="result-landmarks">0</span>
|
|
41731
|
+
</div>
|
|
41732
|
+
<div class="modal-result-item">
|
|
41733
|
+
<span>Zones Created</span>
|
|
41734
|
+
<span class="modal-result-value" id="result-zones">0</span>
|
|
41735
|
+
</div>
|
|
41736
|
+
</div>
|
|
41737
|
+
<div class="modal-buttons">
|
|
41738
|
+
<button class="btn btn-secondary" style="flex: 1;" onclick="closeResultsModal()">Close</button>
|
|
41739
|
+
<button class="btn btn-primary" style="flex: 1;" onclick="openEditor()">Open Editor</button>
|
|
41740
|
+
</div>
|
|
41741
|
+
</div>
|
|
41742
|
+
</div>
|
|
41743
|
+
|
|
41744
|
+
<script>
|
|
41745
|
+
let trainingState = 'idle'; // idle, active, paused, completed
|
|
41746
|
+
let session = null;
|
|
41747
|
+
let pollInterval = null;
|
|
41748
|
+
let transitInterval = null;
|
|
41749
|
+
let selectedLandmarkType = 'mailbox';
|
|
41750
|
+
let historyItems = [];
|
|
41751
|
+
|
|
41752
|
+
// Initialize
|
|
41753
|
+
async function init() {
|
|
41754
|
+
// Check if there's an existing session
|
|
41755
|
+
const status = await fetchTrainingStatus();
|
|
41756
|
+
if (status && (status.state === 'active' || status.state === 'paused')) {
|
|
41757
|
+
session = status;
|
|
41758
|
+
trainingState = status.state;
|
|
41759
|
+
updateUI();
|
|
41760
|
+
startPolling();
|
|
41761
|
+
}
|
|
41762
|
+
}
|
|
41763
|
+
|
|
41764
|
+
// API calls
|
|
41765
|
+
async function fetchTrainingStatus() {
|
|
41766
|
+
try {
|
|
41767
|
+
const response = await fetch('../api/training/status');
|
|
41768
|
+
if (response.ok) {
|
|
41769
|
+
return await response.json();
|
|
41770
|
+
}
|
|
41771
|
+
} catch (e) { console.error('Failed to fetch status:', e); }
|
|
41772
|
+
return null;
|
|
41773
|
+
}
|
|
41774
|
+
|
|
41775
|
+
async function startTraining() {
|
|
41776
|
+
try {
|
|
41777
|
+
const response = await fetch('../api/training/start', { method: 'POST' });
|
|
41778
|
+
if (response.ok) {
|
|
41779
|
+
session = await response.json();
|
|
41780
|
+
trainingState = 'active';
|
|
41781
|
+
updateUI();
|
|
41782
|
+
startPolling();
|
|
41783
|
+
addHistoryItem('Training started', 'start');
|
|
41784
|
+
}
|
|
41785
|
+
} catch (e) {
|
|
41786
|
+
console.error('Failed to start training:', e);
|
|
41787
|
+
alert('Failed to start training. Please try again.');
|
|
41788
|
+
}
|
|
41789
|
+
}
|
|
41790
|
+
|
|
41791
|
+
async function togglePause() {
|
|
41792
|
+
const endpoint = trainingState === 'active' ? 'pause' : 'resume';
|
|
41793
|
+
try {
|
|
41794
|
+
const response = await fetch('../api/training/' + endpoint, { method: 'POST' });
|
|
41795
|
+
if (response.ok) {
|
|
41796
|
+
trainingState = trainingState === 'active' ? 'paused' : 'active';
|
|
41797
|
+
updateUI();
|
|
41798
|
+
addHistoryItem('Training ' + (trainingState === 'paused' ? 'paused' : 'resumed'), 'control');
|
|
41799
|
+
}
|
|
41800
|
+
} catch (e) { console.error('Failed to toggle pause:', e); }
|
|
41801
|
+
}
|
|
41802
|
+
|
|
41803
|
+
async function endTraining() {
|
|
41804
|
+
if (!confirm('End training session?')) return;
|
|
41805
|
+
try {
|
|
41806
|
+
const response = await fetch('../api/training/end', { method: 'POST' });
|
|
41807
|
+
if (response.ok) {
|
|
41808
|
+
session = await response.json();
|
|
41809
|
+
trainingState = 'completed';
|
|
41810
|
+
stopPolling();
|
|
41811
|
+
updateUI();
|
|
41812
|
+
}
|
|
41813
|
+
} catch (e) { console.error('Failed to end training:', e); }
|
|
41814
|
+
}
|
|
41815
|
+
|
|
41816
|
+
async function applyTraining() {
|
|
41817
|
+
try {
|
|
41818
|
+
const response = await fetch('../api/training/apply', { method: 'POST' });
|
|
41819
|
+
if (response.ok) {
|
|
41820
|
+
const result = await response.json();
|
|
41821
|
+
document.getElementById('result-connections').textContent = result.connectionsCreated;
|
|
41822
|
+
document.getElementById('result-updated').textContent = result.connectionsUpdated;
|
|
41823
|
+
document.getElementById('result-landmarks').textContent = result.landmarksAdded;
|
|
41824
|
+
document.getElementById('result-zones').textContent = result.zonesCreated;
|
|
41825
|
+
document.getElementById('results-modal').classList.add('active');
|
|
41826
|
+
}
|
|
41827
|
+
} catch (e) {
|
|
41828
|
+
console.error('Failed to apply training:', e);
|
|
41829
|
+
alert('Failed to apply training results.');
|
|
41830
|
+
}
|
|
41831
|
+
}
|
|
41832
|
+
|
|
41833
|
+
function closeResultsModal() {
|
|
41834
|
+
document.getElementById('results-modal').classList.remove('active');
|
|
41835
|
+
}
|
|
41836
|
+
|
|
41837
|
+
function openEditor() {
|
|
41838
|
+
window.location.href = '../ui/editor';
|
|
41839
|
+
}
|
|
41840
|
+
|
|
41841
|
+
function resetTraining() {
|
|
41842
|
+
trainingState = 'idle';
|
|
41843
|
+
session = null;
|
|
41844
|
+
historyItems = [];
|
|
41845
|
+
updateUI();
|
|
41846
|
+
}
|
|
41847
|
+
|
|
41848
|
+
async function markLandmark() {
|
|
41849
|
+
const name = document.getElementById('landmark-name').value.trim();
|
|
41850
|
+
if (!name) {
|
|
41851
|
+
alert('Please enter a landmark name');
|
|
41852
|
+
return;
|
|
41853
|
+
}
|
|
41854
|
+
|
|
41855
|
+
const currentCameraId = session?.currentCamera?.id;
|
|
41856
|
+
const visibleFromCameras = currentCameraId ? [currentCameraId] : [];
|
|
41857
|
+
|
|
41858
|
+
try {
|
|
41859
|
+
const response = await fetch('../api/training/landmark', {
|
|
41860
|
+
method: 'POST',
|
|
41861
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41862
|
+
body: JSON.stringify({
|
|
41863
|
+
name,
|
|
41864
|
+
type: selectedLandmarkType,
|
|
41865
|
+
visibleFromCameras,
|
|
41866
|
+
position: { x: 50, y: 50 }, // Will be refined when applied
|
|
41867
|
+
})
|
|
41868
|
+
});
|
|
41869
|
+
if (response.ok) {
|
|
41870
|
+
document.getElementById('landmark-name').value = '';
|
|
41871
|
+
addHistoryItem('Marked: ' + name + ' (' + selectedLandmarkType + ')', 'landmark');
|
|
41872
|
+
// Refresh status
|
|
41873
|
+
const status = await fetchTrainingStatus();
|
|
41874
|
+
if (status) {
|
|
41875
|
+
session = status;
|
|
41876
|
+
updateStatsUI();
|
|
41877
|
+
}
|
|
41878
|
+
}
|
|
41879
|
+
} catch (e) { console.error('Failed to mark landmark:', e); }
|
|
41880
|
+
}
|
|
41881
|
+
|
|
41882
|
+
function selectLandmarkType(type) {
|
|
41883
|
+
selectedLandmarkType = type;
|
|
41884
|
+
document.querySelectorAll('.mark-type-btn').forEach(btn => {
|
|
41885
|
+
btn.classList.toggle('selected', btn.dataset.type === type);
|
|
41886
|
+
});
|
|
41887
|
+
}
|
|
41888
|
+
|
|
41889
|
+
// Polling
|
|
41890
|
+
function startPolling() {
|
|
41891
|
+
if (pollInterval) clearInterval(pollInterval);
|
|
41892
|
+
pollInterval = setInterval(async () => {
|
|
41893
|
+
const status = await fetchTrainingStatus();
|
|
41894
|
+
if (status) {
|
|
41895
|
+
session = status;
|
|
41896
|
+
updateDetectionUI();
|
|
41897
|
+
updateStatsUI();
|
|
41898
|
+
updateSuggestionsUI();
|
|
41899
|
+
}
|
|
41900
|
+
}, 1000);
|
|
41901
|
+
}
|
|
41902
|
+
|
|
41903
|
+
function stopPolling() {
|
|
41904
|
+
if (pollInterval) {
|
|
41905
|
+
clearInterval(pollInterval);
|
|
41906
|
+
pollInterval = null;
|
|
41907
|
+
}
|
|
41908
|
+
if (transitInterval) {
|
|
41909
|
+
clearInterval(transitInterval);
|
|
41910
|
+
transitInterval = null;
|
|
41911
|
+
}
|
|
41912
|
+
}
|
|
41913
|
+
|
|
41914
|
+
// UI Updates
|
|
41915
|
+
function updateUI() {
|
|
41916
|
+
// Show/hide content sections
|
|
41917
|
+
document.getElementById('idle-content').style.display = trainingState === 'idle' ? 'flex' : 'none';
|
|
41918
|
+
document.getElementById('active-content').style.display = (trainingState === 'active' || trainingState === 'paused') ? 'flex' : 'none';
|
|
41919
|
+
document.getElementById('completed-content').style.display = trainingState === 'completed' ? 'flex' : 'none';
|
|
41920
|
+
|
|
41921
|
+
// Update status badge
|
|
41922
|
+
const badge = document.getElementById('status-badge');
|
|
41923
|
+
badge.textContent = trainingState.charAt(0).toUpperCase() + trainingState.slice(1);
|
|
41924
|
+
badge.className = 'status-badge ' + trainingState;
|
|
41925
|
+
|
|
41926
|
+
// Update pause button
|
|
41927
|
+
const pauseBtn = document.getElementById('pause-btn');
|
|
41928
|
+
if (pauseBtn) {
|
|
41929
|
+
pauseBtn.innerHTML = trainingState === 'paused' ? '▶ Resume' : '⏸ Pause';
|
|
41930
|
+
}
|
|
41931
|
+
|
|
41932
|
+
// Update completed stats
|
|
41933
|
+
if (trainingState === 'completed' && session) {
|
|
41934
|
+
document.getElementById('final-cameras').textContent = session.stats?.camerasVisited || 0;
|
|
41935
|
+
document.getElementById('final-transits').textContent = session.stats?.transitsRecorded || 0;
|
|
41936
|
+
document.getElementById('final-landmarks').textContent = session.stats?.landmarksMarked || 0;
|
|
41937
|
+
document.getElementById('final-overlaps').textContent = session.stats?.overlapsDetected || 0;
|
|
41938
|
+
document.getElementById('final-avg-transit').textContent = (session.stats?.averageTransitTime || 0) + 's';
|
|
41939
|
+
document.getElementById('final-coverage').textContent = (session.stats?.coveragePercentage || 0) + '%';
|
|
41940
|
+
}
|
|
41941
|
+
}
|
|
41942
|
+
|
|
41943
|
+
function updateDetectionUI() {
|
|
41944
|
+
if (!session) return;
|
|
41945
|
+
|
|
41946
|
+
const card = document.getElementById('detection-card');
|
|
41947
|
+
const icon = document.getElementById('detection-icon');
|
|
41948
|
+
const title = document.getElementById('detection-title');
|
|
41949
|
+
const subtitle = document.getElementById('detection-subtitle');
|
|
41950
|
+
const confidence = document.getElementById('detection-confidence');
|
|
41951
|
+
const transitTimer = document.getElementById('transit-timer');
|
|
41952
|
+
|
|
41953
|
+
if (session.currentCamera) {
|
|
41954
|
+
// Detected on a camera
|
|
41955
|
+
card.className = 'detection-card detecting';
|
|
41956
|
+
icon.textContent = '📷';
|
|
41957
|
+
title.textContent = session.currentCamera.name;
|
|
41958
|
+
subtitle.textContent = 'You are visible on this camera';
|
|
41959
|
+
confidence.textContent = 'Confidence: ' + Math.round(session.currentCamera.confidence * 100) + '%';
|
|
41960
|
+
transitTimer.style.display = 'none';
|
|
41961
|
+
|
|
41962
|
+
// Check for new camera detection to add to history
|
|
41963
|
+
const lastHistoryCamera = historyItems.find(h => h.type === 'camera');
|
|
41964
|
+
if (!lastHistoryCamera || lastHistoryCamera.cameraId !== session.currentCamera.id) {
|
|
41965
|
+
addHistoryItem('Detected on: ' + session.currentCamera.name, 'camera', session.currentCamera.id);
|
|
41966
|
+
}
|
|
41967
|
+
} else if (session.activeTransit) {
|
|
41968
|
+
// In transit
|
|
41969
|
+
card.className = 'detection-card in-transit';
|
|
41970
|
+
icon.textContent = '🚶';
|
|
41971
|
+
title.textContent = 'In Transit';
|
|
41972
|
+
subtitle.textContent = 'Walking to next camera...';
|
|
41973
|
+
confidence.textContent = '';
|
|
41974
|
+
transitTimer.style.display = 'flex';
|
|
41975
|
+
document.getElementById('transit-from').textContent = 'from ' + session.activeTransit.fromCameraName;
|
|
41976
|
+
|
|
41977
|
+
// Start transit timer if not already running
|
|
41978
|
+
if (!transitInterval) {
|
|
41979
|
+
transitInterval = setInterval(() => {
|
|
41980
|
+
if (session?.activeTransit) {
|
|
41981
|
+
document.getElementById('transit-time').textContent = session.activeTransit.elapsedSeconds + 's';
|
|
41982
|
+
}
|
|
41983
|
+
}, 1000);
|
|
41984
|
+
}
|
|
41985
|
+
} else {
|
|
41986
|
+
// Waiting
|
|
41987
|
+
card.className = 'detection-card';
|
|
41988
|
+
icon.textContent = '👤';
|
|
41989
|
+
title.textContent = 'Waiting for detection...';
|
|
41990
|
+
subtitle.textContent = 'Walk to any camera to begin';
|
|
41991
|
+
confidence.textContent = '';
|
|
41992
|
+
transitTimer.style.display = 'none';
|
|
41993
|
+
|
|
41994
|
+
if (transitInterval) {
|
|
41995
|
+
clearInterval(transitInterval);
|
|
41996
|
+
transitInterval = null;
|
|
41997
|
+
}
|
|
41998
|
+
}
|
|
41999
|
+
}
|
|
42000
|
+
|
|
42001
|
+
function updateStatsUI() {
|
|
42002
|
+
if (!session?.stats) return;
|
|
42003
|
+
|
|
42004
|
+
document.getElementById('stat-cameras').textContent = session.stats.camerasVisited;
|
|
42005
|
+
document.getElementById('stat-transits').textContent = session.stats.transitsRecorded;
|
|
42006
|
+
document.getElementById('stat-landmarks').textContent = session.stats.landmarksMarked;
|
|
42007
|
+
document.getElementById('progress-percent').textContent = session.stats.coveragePercentage + '%';
|
|
42008
|
+
document.getElementById('progress-fill').style.width = session.stats.coveragePercentage + '%';
|
|
42009
|
+
}
|
|
42010
|
+
|
|
42011
|
+
function updateSuggestionsUI() {
|
|
42012
|
+
if (!session?.suggestions || session.suggestions.length === 0) return;
|
|
42013
|
+
|
|
42014
|
+
const list = document.getElementById('suggestions-list');
|
|
42015
|
+
list.innerHTML = session.suggestions.map(s =>
|
|
42016
|
+
'<div class="suggestion-item">' + s + '</div>'
|
|
42017
|
+
).join('');
|
|
42018
|
+
}
|
|
42019
|
+
|
|
42020
|
+
function addHistoryItem(text, type, cameraId) {
|
|
42021
|
+
const time = new Date().toLocaleTimeString();
|
|
42022
|
+
historyItems.unshift({ text, type, time, cameraId });
|
|
42023
|
+
if (historyItems.length > 50) historyItems.pop();
|
|
42024
|
+
|
|
42025
|
+
const list = document.getElementById('history-list');
|
|
42026
|
+
document.getElementById('history-count').textContent = historyItems.length + ' events';
|
|
42027
|
+
|
|
42028
|
+
list.innerHTML = historyItems.map(item => {
|
|
42029
|
+
let className = '';
|
|
42030
|
+
if (item.type === 'camera') className = 'history-item-camera';
|
|
42031
|
+
if (item.type === 'transit') className = 'history-item-transit';
|
|
42032
|
+
return '<div class="history-item"><span class="' + className + '">' + item.text + '</span>' +
|
|
42033
|
+
'<div class="history-item-time">' + item.time + '</div></div>';
|
|
42034
|
+
}).join('');
|
|
42035
|
+
}
|
|
42036
|
+
|
|
42037
|
+
function switchTab(tabName) {
|
|
42038
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
42039
|
+
tab.classList.toggle('active', tab.textContent.toLowerCase() === tabName);
|
|
42040
|
+
});
|
|
42041
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
42042
|
+
content.classList.toggle('active', content.id === 'tab-' + tabName);
|
|
42043
|
+
});
|
|
42044
|
+
}
|
|
42045
|
+
|
|
42046
|
+
// Initialize on load
|
|
42047
|
+
init();
|
|
42048
|
+
</script>
|
|
42049
|
+
</body>
|
|
42050
|
+
</html>`;
|
|
42051
|
+
|
|
42052
|
+
|
|
40203
42053
|
/***/ },
|
|
40204
42054
|
|
|
40205
42055
|
/***/ "assert"
|