lab-js/realtime/realtime.jsjavascript
/**
* @module realtime
* @description
* Entry point for the latent space visualizer. Owns the detection loop, the
* descriptor equalizer, the presence state machine and the page lifecycle;
* everything else lives in the sibling realtime-* modules.
*
* The page measures one thing: how far a face has moved from a reference
* captured before the makeup session. Two lines on the plot carry claims and
* they are different kinds of claim — the noise floor is measured here, on this
* camera, from this face; 0.60 is dlib's LFW default and is a property of a
* benchmark, not of anyone's face. Nothing is drawn between them, because
* nothing between them has been measured.
*/
import { MODEL_URLS, DETECTOR_OPTIONS } from '../config.js';
import { createGraph, formatDuration } from './realtime-graph.js';
import {
createQualityGate,
createBaselineCollector,
createSpikeDetector,
createFrameBuffer,
compareConditions,
euclidean,
} from './realtime-capture.js';
import { createGallery } from './realtime-gallery.js';
import { createRadialGraph } from './realtime-radial.js';
import { canUseGuideShortcut, createBaselineGuide } from './realtime-guide.js';
import {
createSettings,
guardDestructive,
loadBaseline,
saveBaseline,
clearBaseline,
} from './realtime-settings.js';
const APP_STATES = {
NO_BASELINE: 'NO BASELINE',
CAPTURING: 'CAPTURING',
TRACKING: 'TRACKING',
};
const SESSION_PHASES = {
IDLE: 'idle',
READY: 'ready',
APPLYING: 'applying',
MEASURING: 'measuring',
};
const DESCRIPTOR_SIZE = 128;
const CELLS_PER_ROW = 10;
const MAX_DELTA = 0.15;
/** dlib's documented default, measured on LFW. Cited, not tuned, not movable. */
const MATCH_THRESHOLD = 0.6;
/** Impostor pairs live below this; the ceiling exists so success has headroom. */
const GRAPH_MAX_DISTANCE = 1.25;
const GRAPH_WINDOW_SECONDS = 60;
const SAMPLES_PER_SECOND = 3;
/** Long enough that a blink does not put controls into a paused UI state. */
const NO_FACE_DEBOUNCE_MS = 1500;
const BASELINE_TARGET_SAMPLES = 5;
const BASELINE_MIN_MS = 2000;
const BASELINE_TIMEOUT_MS = 30000;
const BASELINE_MAX_SPREAD = 0.2;
const MEASUREMENT_TARGET_SAMPLES = 8;
const MEASUREMENT_MIN_MS = 3000;
const MEASUREMENT_TIMEOUT_MS = 30000;
const MEASUREMENT_MAX_SPREAD = 0.2;
const SPIKE_SUSTAIN_MS = 1500;
const SPIKE_BEST_MARGIN = 0.05;
const SPIKE_COOLDOWN_MS = 8000;
const FRAME_BUFFER_SLOTS = 5;
const CAPTURE_MAX_WIDTH = 960;
const STATUS_MIN_DWELL_MS = 1500;
/** Grace period before a hidden tab loses the camera entirely. */
const HIDDEN_STOP_DELAY_MS = 45000;
const RING_RADIUS = 54;
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
const runtime = {
appState: APP_STATES.NO_BASELINE,
baseline: null,
equalizerRows: [],
equalizerLit: new Int16Array(DESCRIPTOR_SIZE).fill(-1),
distanceBuffer: [],
frameRequestId: null,
flushTimerId: null,
hiddenStopTimerId: null,
detectorBusy: false,
initialized: false,
running: false,
streamStopped: false,
faceMissing: false,
timelineNoFace: false,
lastFaceAt: 0,
stream: null,
sourceKind: 'camera',
sourceEpoch: 0,
sourceObjectUrl: null,
lastDetectionScore: null,
lastDescriptorLength: 0,
comparisonExcluded: false,
sessionPhase: SESSION_PHASES.IDLE,
sessionStep: 0,
lastMeasurement: null,
hasValidDescriptorView: false,
};
const els = {
layer: document.getElementById('ui-layer'),
video: document.getElementById('webcam'),
centerView: document.getElementById('center-view'),
embeddingBar: document.getElementById('embedding-bar'),
graphScroll: document.getElementById('graph-scroll'),
graphSpacer: document.getElementById('graph-spacer'),
graphCanvas: document.getElementById('distance-graph'),
graphFollow: document.getElementById('graph-follow'),
radialCanvas: document.getElementById('radial-graph'),
radialValidity: document.getElementById('radial-validity'),
guideCanvas: document.getElementById('baseline-guide'),
legendNoise: document.getElementById('legend-noise'),
calibrationUi: document.getElementById('calibration-ui'),
captureRing: document.getElementById('capture-ring'),
captureRingArc: document.getElementById('capture-ring-arc'),
captureRingCount: document.getElementById('capture-ring-count'),
captureHint: document.getElementById('capture-hint'),
captureProgressDetail: document.getElementById('capture-progress-detail'),
btnBaselineStart: document.getElementById('btn-baseline-start'),
btnBaseline: document.getElementById('btn-baseline'),
btnShutter: document.getElementById('btn-shutter'),
btnDownloadAll: document.getElementById('btn-download-all'),
btnDeleteAll: document.getElementById('btn-delete-all'),
galleryCount: document.getElementById('gallery-count'),
brightness: document.getElementById('brightness-input'),
brightnessValue: document.getElementById('brightness-value'),
toggleEqualizer: document.getElementById('toggle-equalizer'),
sessionBar: document.getElementById('session-bar'),
sessionStep: document.getElementById('session-step'),
sessionResult: document.getElementById('session-result'),
btnStartChange: document.getElementById('btn-start-change'),
btnMeasureChange: document.getElementById('btn-measure-change'),
btnShowGuide: document.getElementById('btn-show-guide'),
videoUpload: document.getElementById('video-upload'),
btnOpenVideo: document.getElementById('btn-open-video'),
btnUseCamera: document.getElementById('btn-use-camera'),
sourceState: document.getElementById('source-state'),
statusPill: document.getElementById('status-pill'),
statusText: document.getElementById('status-text'),
statusSwatch: document.getElementById('status-swatch'),
thresholdHelp: document.getElementById('threshold-help'),
thresholdTooltip: document.getElementById('threshold-tooltip'),
distanceValue: document.getElementById('distance-value'),
distanceQuality: document.getElementById('distance-quality'),
distanceQualityMain: document.getElementById('distance-quality-main'),
distanceQualityDetail: document.getElementById('distance-quality-detail'),
trackingState: document.getElementById('tracking-state'),
viewer: document.getElementById('photo-viewer'),
viewerImage: document.getElementById('viewer-image'),
viewerMeta: document.getElementById('viewer-meta'),
viewerCopy: document.getElementById('viewer-copy'),
viewerDownload: document.getElementById('viewer-download'),
viewerDelete: document.getElementById('viewer-delete'),
viewerClose: document.getElementById('viewer-close'),
};
let graph = null;
let radialGraph = null;
let gallery = null;
let qualityGate = null;
let baselineCollector = null;
let measurementCollector = null;
let spikeDetector = null;
let frameBuffer = null;
let baselineGuide = null;
const statusDisplay = {
key: '',
quality: 'neutral',
shownAt: 0,
pending: null,
timer: null,
};
/**
* Writes the floating status message over the video stage.
*
* @param {string} message Message to display.
* @param {boolean} [warn] Whether the message needs emphasis.
* @param {'neutral'|'valid'|'outside-guide'|'lighting'|'no-face'} [quality] Timeline state.
* @param {string} [key] Stable state identity; numeric details may vary per frame.
* @param {string} [details] Full text exposed as a native tooltip.
* @returns {void}
*/
function setStatus(message, warn = false, quality = 'neutral', key = message, details = message) {
if (!els.statusPill || !els.statusText || !els.statusSwatch) return;
const render = (next) => {
statusDisplay.key = next.key;
statusDisplay.quality = next.quality;
statusDisplay.shownAt = Date.now();
statusDisplay.pending = null;
statusDisplay.timer = null;
els.statusText.textContent = next.message;
els.statusPill.title = next.details;
els.statusPill.classList.toggle('is-warn', next.warn);
els.statusSwatch.dataset.quality = next.quality;
};
const next = { message, warn, quality, key, details };
if (quality === 'neutral' || !statusDisplay.key || statusDisplay.quality === 'neutral') {
if (statusDisplay.timer) window.clearTimeout(statusDisplay.timer);
render(next);
return;
}
if (statusDisplay.key === key && statusDisplay.quality === quality) {
return;
}
const remaining = STATUS_MIN_DWELL_MS - (Date.now() - statusDisplay.shownAt);
if (remaining <= 0) {
if (statusDisplay.timer) window.clearTimeout(statusDisplay.timer);
render(next);
return;
}
statusDisplay.pending = next;
if (statusDisplay.timer) window.clearTimeout(statusDisplay.timer);
statusDisplay.timer = window.setTimeout(() => {
if (statusDisplay.pending) render(statusDisplay.pending);
}, remaining);
}
/** Shows or hides the threshold explainer without occupying persistent space. */
function setThresholdTooltipVisible(visible) {
if (!els.thresholdHelp || !els.thresholdTooltip) return;
els.thresholdTooltip.hidden = !visible;
els.thresholdHelp.setAttribute('aria-expanded', String(visible));
if (visible) els.thresholdTooltip.focus();
}
/**
* Writes the right-panel state label.
*
* @param {string} state Current app state.
* @returns {void}
*/
function setTrackingState(state) {
if (els.trackingState) els.trackingState.textContent = state;
}
/**
* Writes the numeric distance readout.
*
* @param {number} value Distance to display.
* @returns {void}
*/
function setDistanceValue(value) {
if (!els.distanceValue) return;
els.distanceValue.textContent = Number.isFinite(value) ? value.toFixed(3) : '—';
}
/** Labels whether the numeric readout is measurement-grade or raw context. */
function setDistanceQuality(status) {
if (!els.distanceQuality || !els.distanceQualityMain || !els.distanceQualityDetail) return;
const labels = {
valid: { main: 'VALID DISTANCE', detail: '' },
'outside-guide': { main: 'NO DISTANCE', detail: 'OUTSIDE GUIDE' },
lighting: { main: 'NO DISTANCE', detail: 'LIGHTING CHANGED' },
'no-face': { main: 'NO DISTANCE', detail: 'NO FACE' },
};
const label = labels[status] || labels.valid;
els.distanceQualityMain.textContent = label.main;
els.distanceQualityDetail.textContent = label.detail;
els.distanceQualityDetail.hidden = !label.detail;
els.distanceQuality.dataset.quality = status;
}
/** Marks whether the descriptor panels show a current or frozen valid frame. */
function setDescriptorValidity(valid) {
if (!els.radialValidity) return;
if (valid) {
runtime.hasValidDescriptorView = true;
els.radialValidity.textContent = 'LIVE VALID';
els.radialValidity.dataset.validity = 'live';
} else if (runtime.hasValidDescriptorView) {
els.radialValidity.textContent = 'LAST VALID FRAME';
els.radialValidity.dataset.validity = 'stale';
} else {
els.radialValidity.textContent = 'WAITING FOR VALID FRAME';
delete els.radialValidity.dataset.validity;
}
}
/**
* Computes signed per-coordinate descriptor deltas.
*
* @param {Float32Array|number[]} baseline Baseline descriptor.
* @param {Float32Array|number[]} current Current descriptor.
* @returns {Float32Array} Signed deltas in descriptor order.
*/
function descriptorDelta(baseline, current) {
const delta = new Float32Array(DESCRIPTOR_SIZE);
for (let index = 0; index < DESCRIPTOR_SIZE; index += 1) {
delta[index] = current[index] - baseline[index];
}
return delta;
}
/** Returns a compact local-video time suffix for timeline events. */
function mediaTimeSuffix() {
if (runtime.sourceKind !== 'video' || !Number.isFinite(els.video.currentTime)) return '';
const total = Math.max(0, els.video.currentTime);
const minutes = Math.floor(total / 60);
const seconds = (total % 60).toFixed(1).padStart(4, '0');
return ` · ${minutes}:${seconds}`;
}
/** Reflects the declared experiment phase in the workflow controls. */
function syncSessionControls() {
const hasBaseline = Boolean(runtime.baseline);
const applying = runtime.sessionPhase === SESSION_PHASES.APPLYING;
const measuring = runtime.sessionPhase === SESSION_PHASES.MEASURING;
els.sessionBar.hidden = !hasBaseline;
els.btnStartChange.disabled = !hasBaseline || applying || measuring;
els.btnStartChange.classList.toggle('is-status', applying);
els.btnMeasureChange.disabled = !applying;
if (!hasBaseline) {
els.sessionStep.textContent = 'Ready';
els.sessionResult.textContent = 'Capture a clean baseline first.';
return;
}
if (runtime.sessionPhase === SESSION_PHASES.APPLYING) {
els.sessionStep.textContent = `Step ${runtime.sessionStep + 1} · applying`;
els.sessionResult.textContent = 'Live readings continue, but this interval is marked as application time.';
els.btnStartChange.textContent = 'Applying…';
} else if (runtime.sessionPhase === SESSION_PHASES.MEASURING) {
els.sessionStep.textContent = `Step ${runtime.sessionStep + 1} · measuring`;
els.sessionResult.textContent = 'Collecting a comparable temporal window…';
els.btnStartChange.textContent = 'Start makeup step';
} else {
els.sessionStep.textContent = runtime.sessionStep ? `Step ${runtime.sessionStep} measured` : 'Ready';
els.sessionResult.textContent = runtime.lastMeasurement
? `Snapshot d=${runtime.lastMeasurement.distance.toFixed(3)} · spread ±${runtime.lastMeasurement.spread.toFixed(3)}`
: 'Mark when you begin applying a change.';
els.btnStartChange.textContent = runtime.sessionStep ? 'Start next step' : 'Start makeup step';
}
}
/** Changes the declared experiment phase and redraws its controls. */
function setSessionPhase(phase) {
runtime.sessionPhase = phase;
syncSessionControls();
}
/**
* Builds the descriptor equalizer.
*
* Level is baked into each cell at build time so the runtime loop only toggles
* .is-on for cells that actually changed. The prototype rewrote all 1,280
* className strings on every detection frame.
*
* @returns {void}
*/
function initEqualizer() {
if (!els.embeddingBar) return;
els.embeddingBar.textContent = '';
runtime.equalizerRows = [];
runtime.equalizerLit = new Int16Array(DESCRIPTOR_SIZE).fill(0);
for (let rowIndex = 0; rowIndex < DESCRIPTOR_SIZE; rowIndex += 1) {
const row = document.createElement('div');
row.className = 'descriptor-row';
for (let cellIndex = 0; cellIndex < CELLS_PER_ROW; cellIndex += 1) {
const cell = document.createElement('div');
let level = 'lvl-high';
if (cellIndex < 3) level = 'lvl-low';
else if (cellIndex < 7) level = 'lvl-med';
cell.className = `cell ${level}`;
row.appendChild(cell);
}
els.embeddingBar.appendChild(row);
runtime.equalizerRows.push(row);
}
}
/**
* Updates the equalizer from per-index baseline deltas.
*
* @param {number[]} baseline Baseline descriptor.
* @param {Float32Array|number[]} current Live descriptor.
* @returns {void}
*/
function updateEqualizer(baseline, current) {
if (!runtime.equalizerRows.length) return;
for (let i = 0; i < DESCRIPTOR_SIZE; i += 1) {
const delta = Math.abs(baseline[i] - current[i]);
const scaled = Math.floor((delta / MAX_DELTA) * CELLS_PER_ROW);
const lit = Math.max(0, Math.min(CELLS_PER_ROW, scaled));
const previous = runtime.equalizerLit[i];
if (lit === previous) continue;
const cells = runtime.equalizerRows[i].children;
if (lit > previous) {
for (let c = previous; c < lit; c += 1) cells[c].classList.add('is-on');
} else {
for (let c = lit; c < previous; c += 1) cells[c].classList.remove('is-on');
}
runtime.equalizerLit[i] = lit;
}
if (radialGraph) radialGraph.setLive(descriptorDelta(baseline, current));
}
/**
* Returns the equalizer to its unlit state.
*
* @returns {void}
*/
function resetEqualizer() {
for (let i = 0; i < runtime.equalizerRows.length; i += 1) {
const cells = runtime.equalizerRows[i].children;
for (const cell of cells) cell.classList.remove('is-on');
runtime.equalizerLit[i] = 0;
}
if (radialGraph) radialGraph.setLive(null);
}
/**
* Updates the baseline capture ring.
*
* @param {number} accepted Accepted samples so far.
* @param {number} target Samples required.
* @returns {void}
*/
function setRingProgress(accepted, target) {
if (!els.captureRingArc) return;
const ratio = Math.max(0, Math.min(1, accepted / target));
els.captureRingArc.style.strokeDasharray = String(RING_CIRCUMFERENCE);
els.captureRingArc.style.strokeDashoffset = String(RING_CIRCUMFERENCE * (1 - ratio));
if (els.captureRingCount) els.captureRingCount.textContent = `${accepted}/${target}`;
if (els.captureRing) {
const previous = Number(els.captureRing.dataset.accepted || 0);
els.captureRing.setAttribute('aria-valuenow', String(accepted));
els.captureRing.setAttribute('aria-valuemax', String(target));
els.captureRing.dataset.accepted = String(accepted);
if (accepted > previous) {
els.captureRing.classList.remove('is-sampled');
void els.captureRing.offsetWidth;
els.captureRing.classList.add('is-sampled');
}
}
}
/** Returns source-relative timing so a paused video cannot be sampled twice. */
function currentObservation() {
if (runtime.sourceKind !== 'video' || !Number.isFinite(els.video.currentTime)) return {};
const sourceTimeMs = els.video.currentTime * 1000;
return {
sourceTimeMs,
id: `${runtime.sourceEpoch}:${sourceTimeMs.toFixed(3)}`,
};
}
/** Updates the diagnostic line below the baseline target. */
function setCaptureProgressDetail(progress, faceDetected = true) {
if (!els.captureProgressDetail || !progress) return;
els.captureProgressDetail.hidden = false;
const state = faceDetected ? 'face detected' : 'no face';
const excluded = progress.rejected || 0;
const duplicate = progress.duplicates ? ` · ${progress.duplicates} repeated video frames ignored` : '';
els.captureProgressDetail.textContent =
`${state} · ${progress.accepted}/${progress.target} clean · ${excluded} excluded${duplicate}`;
}
/** Joins every failed quality check instead of hiding all but the first one. */
function qualityMessage(progress) {
const reasons = progress?.reasons?.filter(Boolean) || [];
if (!reasons.length) return progress?.reason || 'This frame is not usable.';
return reasons.join(' Also: ');
}
/**
* Shows or hides the baseline capture overlay.
*
* @param {boolean} visible Whether the overlay should show.
* @returns {void}
*/
function setCalibrationVisible(visible) {
if (els.calibrationUi) els.calibrationUi.hidden = !visible;
}
/**
* Reflects baseline presence across the controls that depend on it.
*
* @returns {void}
*/
function syncBaselineControls() {
const hasBaseline = Boolean(runtime.baseline);
if (els.btnBaseline) els.btnBaseline.hidden = !hasBaseline;
if (els.btnShutter) els.btnShutter.disabled = !hasBaseline;
if (els.btnShowGuide) els.btnShowGuide.disabled = !hasBaseline || !baselineGuide?.hasGuide();
setCalibrationVisible(!hasBaseline);
if (els.btnBaselineStart) els.btnBaselineStart.hidden = false;
if (els.legendNoise) {
els.legendNoise.textContent = hasBaseline ? runtime.baseline.noiseFloor.toFixed(3) : '—';
}
if (graph) graph.setNoiseFloor(hasBaseline ? runtime.baseline.noiseFloor : null);
syncSessionControls();
}
/**
* Starts or resumes a selected local video before a declared session action.
*
* @param {string} failureMessage Status shown when autoplay is rejected.
* @returns {Promise<boolean>} Whether the action may continue.
*/
async function ensureLocalVideoPlaying(failureMessage) {
if (runtime.sourceKind !== 'video' || !els.video.paused) return true;
try {
if (els.video.ended) els.video.currentTime = 0;
await els.video.play();
return true;
} catch {
setStatus(failureMessage, true);
return false;
}
}
/**
* Begins a baseline capture run.
*
* @returns {Promise<void>}
*/
async function startBaselineCapture() {
const playing = await ensureLocalVideoPlaying(
'Play the local video before capturing a baseline.',
);
if (!playing) return;
runtime.appState = APP_STATES.CAPTURING;
runtime.baseline = null;
runtime.distanceBuffer = [];
runtime.comparisonExcluded = false;
runtime.sessionStep = 0;
runtime.lastMeasurement = null;
runtime.hasValidDescriptorView = false;
setSessionPhase(SESSION_PHASES.IDLE);
clearBaseline();
setTrackingState(APP_STATES.CAPTURING);
setDistanceValue(0);
resetEqualizer();
graph.reset();
graph.setNoiseFloor(null);
spikeDetector.reset();
frameBuffer.clear();
if (radialGraph) radialGraph.reset();
if (baselineGuide) baselineGuide.setGuide(null);
setDistanceQuality('valid');
setDescriptorValidity(false);
setCalibrationVisible(true);
if (els.captureRing) els.captureRing.classList.add('is-active');
if (els.btnBaselineStart) els.btnBaselineStart.hidden = true;
if (els.btnBaseline) els.btnBaseline.hidden = true;
if (els.btnShutter) els.btnShutter.disabled = true;
if (els.legendNoise) els.legendNoise.textContent = '—';
setRingProgress(0, BASELINE_TARGET_SAMPLES);
if (els.captureProgressDetail) {
els.captureProgressDetail.hidden = false;
els.captureProgressDetail.textContent = `face not sampled yet · 0/${BASELINE_TARGET_SAMPLES} clean · 0 excluded`;
}
setHint('Keep your face still and look at the centre dot for 2 seconds.', true);
setStatus('Capturing baseline…');
baselineCollector.start();
}
/**
* Writes the capture overlay hint line.
*
* @param {string} text Hint text.
* @param {boolean} [blocking] Whether the hint is an instruction to act on.
* @returns {void}
*/
function setHint(text, blocking = false) {
if (!els.captureHint) return;
els.captureHint.textContent = text;
els.captureHint.classList.toggle('is-blocking', blocking);
}
/**
* Adopts a captured or restored baseline and enters tracking.
*
* @param {object} baseline Baseline record.
* @param {boolean} [restored] Whether the baseline came from storage.
* @returns {void}
*/
function adoptBaseline(baseline, restored = false) {
runtime.baseline = baseline;
runtime.appState = APP_STATES.TRACKING;
runtime.distanceBuffer = [];
runtime.faceMissing = false;
runtime.timelineNoFace = false;
runtime.comparisonExcluded = false;
runtime.sessionStep = 0;
runtime.lastMeasurement = null;
runtime.hasValidDescriptorView = false;
setSessionPhase(SESSION_PHASES.READY);
setTrackingState(APP_STATES.TRACKING);
setDistanceValue(0);
resetEqualizer();
spikeDetector.reset();
frameBuffer.clear();
if (radialGraph) radialGraph.reset();
if (baselineGuide) {
baselineGuide.setGuide(baseline.guide);
baselineGuide.show();
}
setDistanceQuality('valid');
setDescriptorValidity(false);
if (els.captureRing) els.captureRing.classList.remove('is-active');
if (els.captureProgressDetail) els.captureProgressDetail.hidden = true;
syncBaselineControls();
if (restored) {
const age = formatDuration(Date.now() - baseline.capturedAt);
setStatus(`Baseline restored, captured ${age} ago. Noise floor ${baseline.noiseFloor.toFixed(3)}.`);
} else {
setStatus(`Baseline set. Noise floor ${baseline.noiseFloor.toFixed(3)} — readings below it are within baseline variation.`);
}
}
/**
* Discards the current baseline and returns to the capture prompt.
*
* @returns {void}
*/
function deleteBaseline() {
runtime.baseline = null;
runtime.appState = APP_STATES.NO_BASELINE;
runtime.comparisonExcluded = false;
runtime.sessionStep = 0;
runtime.lastMeasurement = null;
runtime.hasValidDescriptorView = false;
setSessionPhase(SESSION_PHASES.IDLE);
clearBaseline();
baselineCollector.cancel();
measurementCollector.cancel();
setTrackingState(APP_STATES.NO_BASELINE);
setDistanceValue(0);
resetEqualizer();
graph.reset();
spikeDetector.reset();
frameBuffer.clear();
if (radialGraph) radialGraph.reset();
if (baselineGuide) baselineGuide.setGuide(null);
setDistanceQuality('valid');
setDescriptorValidity(false);
if (els.captureRing) els.captureRing.classList.remove('is-active');
if (els.captureProgressDetail) els.captureProgressDetail.hidden = true;
syncBaselineControls();
setHint('Capture a reference of your bare face before you start.');
setStatus('Baseline deleted. Saved captures are kept.');
}
/**
* Feeds one detection into the baseline collector.
*
* @param {*} detection face-api detection result.
* @returns {void}
*/
function processBaselineFrame(detection) {
const verdict = qualityGate.evaluate(detection);
const progress = baselineCollector.offer(verdict, detection.descriptor, currentObservation());
setRingProgress(progress.accepted, progress.target);
setCaptureProgressDetail(progress, true);
if (progress.status === 'blocked') {
setHint(qualityMessage(progress), true);
return;
}
if (progress.status === 'waiting') {
setHint(progress.reason, true);
return;
}
if (progress.status === 'collecting') {
setHint('Keep your face still and look at the centre dot.', true);
return;
}
if (progress.status === 'failed') {
if (els.captureRing) els.captureRing.classList.remove('is-active');
if (els.btnBaselineStart) els.btnBaselineStart.hidden = false;
runtime.appState = APP_STATES.NO_BASELINE;
setTrackingState(APP_STATES.NO_BASELINE);
setHint(`Could not get a clean read. ${progress.reason}`, true);
setStatus('Baseline capture abandoned.', true);
return;
}
if (progress.status === 'done') {
// Camera baselines can be restored against the same physical source. A
// local video's blob URL disappears on reload, so persisting that template
// would silently compare the next camera session with the wrong source.
if (runtime.sourceKind === 'camera') {
saveBaseline({ ...progress.baseline, sourceKind: 'camera' });
} else {
clearBaseline();
}
adoptBaseline(progress.baseline, false);
}
}
/** Marks the beginning of one operator-declared makeup change. */
async function startMakeupStep() {
if (!runtime.baseline || runtime.appState !== APP_STATES.TRACKING) return;
const playing = await ensureLocalVideoPlaying(
'Play the local video before marking the makeup step.',
);
if (!playing) return;
measurementCollector.cancel();
frameBuffer.clear();
setSessionPhase(SESSION_PHASES.APPLYING);
graph.addEvent(
Date.now(),
SESSION_PHASES.APPLYING,
`step ${runtime.sessionStep + 1}: applying${mediaTimeSuffix()}`,
);
setStatus('Application interval marked. When this piece is finished, measure the result.');
}
/** Starts a temporal measurement window for the current declared change. */
async function startStepMeasurement() {
if (!runtime.baseline || runtime.sessionPhase !== SESSION_PHASES.APPLYING) return;
const playing = await ensureLocalVideoPlaying(
'Play the local video before measuring this step.',
);
if (!playing) return;
measurementCollector.start();
frameBuffer.clear();
setSessionPhase(SESSION_PHASES.MEASURING);
graph.addEvent(
Date.now(),
SESSION_PHASES.MEASURING,
`step ${runtime.sessionStep + 1}: measuring${mediaTimeSuffix()}`,
);
setStatus('Measuring this step — hold the baseline pose and lighting.');
}
/**
* Publishes an aggregated step measurement and saves its sharpest frame.
*
* @param {object} snapshot Temporal-window result returned by the collector.
* @returns {Promise<void>}
*/
async function completeStepMeasurement(snapshot) {
const distance = euclidean(runtime.baseline.descriptor, snapshot.descriptor);
const delta = descriptorDelta(runtime.baseline.descriptor, snapshot.descriptor);
runtime.sessionStep += 1;
runtime.lastMeasurement = {
distance,
spread: snapshot.noiseFloor,
descriptor: snapshot.descriptor,
conditions: snapshot.conditions,
capturedAt: snapshot.capturedAt,
};
if (radialGraph) radialGraph.setMeasured(delta);
updateEqualizer(runtime.baseline.descriptor, snapshot.descriptor);
graph.addEvent(
Date.now(),
SESSION_PHASES.MEASURING,
`step ${runtime.sessionStep}: snapshot d=${distance.toFixed(3)}${mediaTimeSuffix()}`,
);
const best = frameBuffer.best();
if (best) await storeCapture(best.canvas, distance, `step-${runtime.sessionStep}`);
frameBuffer.clear();
setSessionPhase(SESSION_PHASES.READY);
setStatus(
`Step ${runtime.sessionStep} measured: d=${distance.toFixed(3)}, temporal spread ±${snapshot.noiseFloor.toFixed(3)}.`,
);
}
/**
* Measures one tracking frame and evaluates it for a spike capture.
*
* @param {*} detection face-api detection result.
* @returns {Promise<void>}
*/
async function processTrackingFrame(detection) {
// Makeup may legitimately change detector confidence and the exposure inside
// the face crop. Comparability is based on pose, framing and ambient light so
// the intervention itself is not filtered away.
const quality = qualityGate.evaluate(detection, { allowAppearanceChanges: true });
const comparable = quality.ok
? compareConditions(runtime.baseline.conditions, quality.metrics)
: quality;
const distance = euclidean(runtime.baseline.descriptor, detection.descriptor);
if (!comparable.ok) {
const exclusionReason = qualityMessage(comparable);
const hasLightDifference = comparable.reasons?.some((reason) =>
/light|luma/i.test(reason),
);
const hasGeometryDifference = comparable.reasons?.some((reason) =>
!/light|luma/i.test(reason),
);
const timelineStatus = hasLightDifference && !hasGeometryDifference
? 'lighting'
: 'outside-guide';
runtime.distanceBuffer.push({ value: distance, status: timelineStatus });
runtime.comparisonExcluded = true;
setDistanceValue(distance);
setDistanceQuality(timelineStatus);
setDescriptorValidity(false);
els.btnShutter.disabled = true;
setStatus(
comparable.reason || exclusionReason,
true,
timelineStatus,
`excluded:${timelineStatus}`,
`${exclusionReason} Raw distance is shown, but this frame is excluded from measurements.`,
);
if (runtime.sessionPhase === SESSION_PHASES.MEASURING) {
const progress = measurementCollector.offer(
{ ok: false, reason: comparable.reason, reasons: comparable.reasons },
detection.descriptor,
currentObservation(),
);
els.sessionResult.textContent = `Waiting for comparable frames: ${exclusionReason}`;
if (progress.status === 'failed') {
measurementCollector.cancel();
setSessionPhase(SESSION_PHASES.APPLYING);
setStatus(`Measurement paused. ${progress.reason}`, true);
}
}
return;
}
runtime.comparisonExcluded = false;
els.btnShutter.disabled = false;
runtime.distanceBuffer.push({ value: distance, status: 'valid' });
setDistanceValue(distance);
setDistanceQuality('valid');
updateEqualizer(runtime.baseline.descriptor, detection.descriptor);
setDescriptorValidity(true);
if (runtime.sessionPhase === SESSION_PHASES.MEASURING) {
const progress = measurementCollector.offer(quality, detection.descriptor, currentObservation());
if (quality.ok) frameBuffer.push(quality.sharpness, distance);
if (progress.status === 'collecting') {
els.sessionResult.textContent = `Comparable samples ${progress.accepted}/${progress.target}`;
} else if (progress.status === 'done') {
await completeStepMeasurement(progress.baseline);
} else if (progress.status === 'failed') {
setSessionPhase(SESSION_PHASES.APPLYING);
setStatus(`Could not measure this step. ${progress.reason}`, true);
}
return;
}
if (distance > MATCH_THRESHOLD) {
setStatus(
'Past the 0.60 match threshold — reads as a different person.',
false,
'valid',
'valid:threshold',
);
} else if (distance <= runtime.baseline.noiseFloor) {
setStatus(
'Inside baseline variation — no change is distinguishable from the baseline noise.',
false,
'valid',
'valid:noise',
);
} else {
setStatus(
'Above the noise floor, below 0.60 — still reads as a match.',
false,
'valid',
'valid:between',
);
}
// Declared application intervals use an explicit temporal snapshot. Automatic
// "best" captures remain available only while the experiment is otherwise idle.
if (runtime.sessionPhase === SESSION_PHASES.APPLYING) return;
const verdict = spikeDetector.update(distance);
if (!verdict.candidate) return;
if (quality.ok) frameBuffer.push(quality.sharpness, distance);
if (!verdict.capture) return;
const best = frameBuffer.best();
if (!best) return;
await storeCapture(best.canvas, best.distance, verdict.kind);
spikeDetector.noteCapture(best.distance);
frameBuffer.clear();
}
/**
* Watermarks and stores one frame, then marks it on the timeline.
*
* @param {HTMLCanvasElement} frame Frame to store.
* @param {number} distance Distance at capture time.
* @param {string} kind Capture trigger.
* @returns {Promise<void>}
*/
async function storeCapture(frame, distance, kind) {
try {
const id = await gallery.save(frame, {
t: Date.now(),
distance,
noiseFloor: runtime.baseline ? runtime.baseline.noiseFloor : 0,
threshold: MATCH_THRESHOLD,
kind,
});
graph.markLatestCapture(id);
setStatus(`Capture saved at ${distance.toFixed(3)}.`);
} catch (err) {
console.error('capture save failed:', err);
setStatus('Could not save the capture.', true);
}
}
/**
* Captures the current frame on demand.
*
* @returns {Promise<void>}
*/
async function captureNow() {
if (!runtime.baseline || !els.video || els.video.readyState < 2) return;
const videoW = els.video.videoWidth;
const scale = Math.min(1, CAPTURE_MAX_WIDTH / videoW);
const canvas = document.createElement('canvas');
canvas.width = Math.round(videoW * scale);
canvas.height = Math.round(els.video.videoHeight * scale);
canvas.getContext('2d').drawImage(els.video, 0, 0, canvas.width, canvas.height);
const distance = Number(els.distanceValue.textContent) || 0;
await storeCapture(canvas, distance, 'manual');
}
/**
* Handles a frame in which no face was found.
*
* The timeline marks absence immediately with a no-value quality tick. The UI
* waits through a short debounce before disabling controls or announcing a
* pause, so a blink remains harmless without erasing elapsed time.
*
* @returns {void}
*/
function processNoFace() {
const now = Date.now();
runtime.timelineNoFace = true;
runtime.distanceBuffer = [];
if (!runtime.lastFaceAt) runtime.lastFaceAt = now;
if (now - runtime.lastFaceAt < NO_FACE_DEBOUNCE_MS) return;
if (!runtime.faceMissing) {
runtime.faceMissing = true;
setDistanceValue(Number.NaN);
setDistanceQuality('no-face');
setDescriptorValidity(false);
if (els.btnShutter) els.btnShutter.disabled = true;
if (runtime.appState === APP_STATES.CAPTURING) {
setStatus('No face detected — baseline capture is waiting.');
} else if (runtime.appState === APP_STATES.TRACKING) {
setStatus('No face — measurement paused.', false, 'no-face', 'no-face');
} else {
setStatus('No face detected. Enter the frame, then set a baseline.');
}
}
if (runtime.sessionPhase === SESSION_PHASES.MEASURING) {
els.sessionResult.textContent = 'Waiting for the face to return.';
}
if (runtime.appState === APP_STATES.CAPTURING) {
const progress = baselineCollector.offer(
{ ok: false, reason: 'No face detected.', reasons: ['No face detected.'] },
null,
currentObservation(),
);
setRingProgress(progress.accepted, progress.target);
setCaptureProgressDetail(progress, false);
if (progress.status === 'failed') {
if (els.captureRing) els.captureRing.classList.remove('is-active');
if (els.btnBaselineStart) els.btnBaselineStart.hidden = false;
runtime.appState = APP_STATES.NO_BASELINE;
setTrackingState(APP_STATES.NO_BASELINE);
setHint(`Could not get a clean read. ${progress.reason}`, true);
setStatus('Baseline capture abandoned.', true);
} else {
setHint('No face detected. Centre your face on the target and hold still.', true);
}
}
}
/**
* Handles a face reappearing after an absence.
*
* @returns {void}
*/
function processFaceReturned() {
const now = Date.now();
runtime.timelineNoFace = false;
if (runtime.faceMissing) {
runtime.faceMissing = false;
setStatus('Face back — reading resuming.', false, 'valid', 'face-returned');
}
runtime.lastFaceAt = now;
}
/**
* Runs one detection pass and schedules the next frame.
*
* @returns {Promise<void>}
*/
async function onFrame() {
if (!runtime.running) return;
runtime.frameRequestId = window.requestAnimationFrame(onFrame);
if (runtime.detectorBusy) return;
if (!els.video || els.video.readyState < 2) return;
runtime.detectorBusy = true;
const sourceEpoch = runtime.sourceEpoch;
try {
const detection = await faceapi
.detectSingleFace(els.video, DETECTOR_OPTIONS)
.withFaceLandmarks()
.withFaceDescriptor();
// A camera detection can still be resolving while a local file is being
// selected. Never publish results produced for the previous source.
if (sourceEpoch !== runtime.sourceEpoch) return;
if (!detection || !detection.descriptor) {
runtime.lastDetectionScore = null;
runtime.lastDescriptorLength = 0;
processNoFace();
return;
}
runtime.lastDetectionScore = detection.detection?.score ?? null;
runtime.lastDescriptorLength = detection.descriptor.length;
processFaceReturned();
if (runtime.appState === APP_STATES.CAPTURING) {
processBaselineFrame(detection);
} else if (runtime.appState === APP_STATES.TRACKING && runtime.baseline) {
await processTrackingFrame(detection);
}
} catch (err) {
console.error('realtime detection loop error:', err);
setStatus('Detection error. Check the console.', true);
} finally {
runtime.detectorBusy = false;
}
}
/**
* Averages buffered readings into one timeline sample.
*
* @returns {void}
*/
function flushDistanceBuffer() {
if (runtime.appState !== APP_STATES.TRACKING) return;
if (runtime.timelineNoFace) {
graph.pushSample(Date.now(), null, runtime.sessionPhase, 'no-face');
return;
}
if (!runtime.distanceBuffer.length) return;
const status = runtime.distanceBuffer[runtime.distanceBuffer.length - 1].status;
const matching = runtime.distanceBuffer.filter((reading) => reading.status === status);
const sum = matching.reduce((acc, reading) => acc + reading.value, 0);
const average = sum / matching.length;
runtime.distanceBuffer = [];
graph.pushSample(Date.now(), average, runtime.sessionPhase, status);
}
/**
* Starts the detection and sampling loops.
*
* @returns {void}
*/
function startLoops() {
if (runtime.running) return;
runtime.running = true;
if (runtime.flushTimerId) window.clearInterval(runtime.flushTimerId);
runtime.flushTimerId = window.setInterval(flushDistanceBuffer, 1000 / SAMPLES_PER_SECOND);
onFrame();
}
/**
* Stops the detection and sampling loops without touching the camera.
*
* @returns {void}
*/
function stopLoops() {
runtime.running = false;
if (runtime.frameRequestId) window.cancelAnimationFrame(runtime.frameRequestId);
runtime.frameRequestId = null;
if (runtime.flushTimerId) window.clearInterval(runtime.flushTimerId);
runtime.flushTimerId = null;
}
/**
* Releases the camera.
*
* Only stopping the tracks turns the hardware indicator off. Pausing the video
* element leaves the light on, which on this project is exactly the wrong
* impression to give.
*
* @returns {void}
*/
function stopStream() {
if (runtime.stream) {
for (const track of runtime.stream.getTracks()) track.stop();
}
runtime.stream = null;
runtime.streamStopped = true;
if (els.video?.srcObject) els.video.srcObject = null;
}
/** Releases the object URL used by a local-video source. */
function releaseLocalVideo() {
if (runtime.sourceObjectUrl) URL.revokeObjectURL(runtime.sourceObjectUrl);
runtime.sourceObjectUrl = null;
}
/**
* Requests the camera and binds it to the video element.
*
* @returns {Promise<void>}
*/
async function initWebcam() {
if (!els.video) throw new Error('Missing #webcam element');
runtime.sourceEpoch += 1;
runtime.lastDetectionScore = null;
runtime.lastDescriptorLength = 0;
releaseLocalVideo();
els.video.pause();
els.video.removeAttribute('src');
els.video.load();
els.video.controls = false;
els.video.classList.remove('source-video');
els.video.classList.add('source-camera');
runtime.stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user',
width: { ideal: 1080 },
height: { ideal: 1920 }
},
audio: false,
});
runtime.streamStopped = false;
runtime.sourceKind = 'camera';
els.video.srcObject = runtime.stream;
await els.video.play();
els.sourceState.textContent = 'camera';
els.btnUseCamera.hidden = true;
}
/**
* Replaces the camera with a local, seekable video. The file never leaves the
* browser; changing source invalidates the source-specific baseline.
*
* @param {File} file Local video chosen by the operator.
* @returns {Promise<void>}
*/
async function useLocalVideo(file) {
if (!file) return;
runtime.sourceEpoch += 1;
runtime.lastDetectionScore = null;
runtime.lastDescriptorLength = 0;
stopLoops();
stopStream();
releaseLocalVideo();
deleteBaseline();
runtime.sourceKind = 'video';
runtime.sourceObjectUrl = URL.createObjectURL(file);
els.video.srcObject = null;
els.video.src = runtime.sourceObjectUrl;
els.video.controls = true;
els.video.loop = false;
els.video.classList.remove('source-camera');
els.video.classList.add('source-video');
els.sourceState.textContent = `local video · ${file.name}`;
els.btnUseCamera.hidden = false;
await new Promise((resolve, reject) => {
const ready = () => {
cleanup();
resolve();
};
const failed = () => {
cleanup();
reject(new Error('The selected video could not be decoded.'));
};
const cleanup = () => {
els.video.removeEventListener('loadeddata', ready);
els.video.removeEventListener('error', failed);
};
els.video.addEventListener('loadeddata', ready, { once: true });
els.video.addEventListener('error', failed, { once: true });
els.video.load();
});
startLoops();
setStatus('Local video ready. Play or seek it, then capture a clean baseline.');
}
/** Returns from a local-video experiment to the live camera. */
async function useCameraSource() {
stopLoops();
deleteBaseline();
setStatus('Connecting the camera…');
try {
await initWebcam();
startLoops();
setStatus('Camera ready. Capture a baseline to start measuring.');
} catch (err) {
console.error('failed to switch to camera:', err);
setStatus('Camera unavailable. You can still open a local video.', true);
}
}
/**
* Loads the vendored face-api models.
*
* Every shard lives under /lab-js/vendor, so the page makes no third-party
* request and runs with the network unplugged. MODEL_URLS.tiny, .landmarks and
* .recognition all resolve to that one directory.
*
* @returns {Promise<void>}
*/
async function loadModels() {
const vendorRoot = MODEL_URLS.recognition;
await Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri(vendorRoot),
faceapi.nets.faceLandmark68Net.loadFromUri(vendorRoot),
faceapi.nets.faceRecognitionNet.loadFromUri(vendorRoot),
]);
setStatus('Models loaded. Preparing camera…');
}
/**
* Suspends work while the tab is hidden.
*
* Window blur is deliberately not the trigger: it fires when another window is
* clicked while this tab is still on screen, which would kill the camera while
* the person is looking straight at it.
*
* @returns {void}
*/
function handleHidden() {
stopLoops();
if (els.video) els.video.pause();
if (runtime.hiddenStopTimerId) window.clearTimeout(runtime.hiddenStopTimerId);
runtime.hiddenStopTimerId = window.setTimeout(stopStream, HIDDEN_STOP_DELAY_MS);
}
/**
* Resumes work when the tab becomes visible again.
*
* @returns {Promise<void>}
*/
async function handleVisible() {
if (runtime.hiddenStopTimerId) {
window.clearTimeout(runtime.hiddenStopTimerId);
runtime.hiddenStopTimerId = null;
}
try {
if (runtime.sourceKind === 'camera' && runtime.streamStopped) {
setStatus('Reconnecting the camera…');
await initWebcam();
} else if (runtime.sourceKind === 'camera' && els.video) {
await els.video.play();
}
if (runtime.appState === APP_STATES.TRACKING) runtime.lastFaceAt = Date.now();
startLoops();
setStatus(runtime.sourceKind === 'camera' ? 'Camera live.' : 'Local video ready.');
} catch (err) {
console.error('failed to resume camera:', err);
setStatus('Could not reconnect the camera.', true);
}
}
/**
* Fully releases every resource held by the page.
*
* @returns {void}
*/
function teardown() {
stopLoops();
stopStream();
if (statusDisplay.timer) window.clearTimeout(statusDisplay.timer);
statusDisplay.timer = null;
statusDisplay.pending = null;
if (runtime.hiddenStopTimerId) window.clearTimeout(runtime.hiddenStopTimerId);
runtime.hiddenStopTimerId = null;
if (graph) graph.destroy();
if (radialGraph) radialGraph.destroy();
if (baselineGuide) baselineGuide.destroy();
releaseLocalVideo();
}
/**
* Binds interaction and lifecycle listeners.
*
* @returns {void}
*/
function bindListeners() {
els.btnBaselineStart.addEventListener('click', startBaselineCapture);
els.btnStartChange.addEventListener('click', startMakeupStep);
els.btnMeasureChange.addEventListener('click', startStepMeasurement);
els.btnOpenVideo.addEventListener('click', () => els.videoUpload.click());
els.btnShowGuide.addEventListener('click', () => baselineGuide?.show());
for (const toggle of document.querySelectorAll('.quality-toggle')) {
toggle.addEventListener('click', () => {
const visible = toggle.getAttribute('aria-pressed') !== 'true';
toggle.setAttribute('aria-pressed', String(visible));
graph.setQualityVisible(toggle.dataset.quality, visible);
});
}
els.videoUpload.addEventListener('change', async () => {
const [file] = els.videoUpload.files || [];
if (!file) return;
try {
await useLocalVideo(file);
} catch (err) {
console.error('failed to open local video:', err);
setStatus(err.message || 'Could not open the selected video.', true);
} finally {
els.videoUpload.value = '';
}
});
els.btnUseCamera.addEventListener('click', useCameraSource);
guardDestructive(els.btnBaseline, 'Reset baseline', 'Reset — sure?', deleteBaseline);
guardDestructive(els.btnDeleteAll, 'Delete all', 'Delete all — sure?', () => gallery.deleteAll());
els.thresholdHelp.addEventListener('click', () => {
setThresholdTooltipVisible(els.thresholdTooltip.hidden);
});
els.thresholdTooltip.addEventListener('click', () => setThresholdTooltipVisible(false));
els.thresholdTooltip.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') setThresholdTooltipVisible(false);
});
els.btnShutter.addEventListener('click', captureNow);
els.btnDownloadAll.addEventListener('click', () => gallery.downloadAll());
els.viewerClose.addEventListener('click', () => gallery.close());
els.viewerCopy.addEventListener('click', () => gallery.copyOpen());
els.viewerDownload.addEventListener('click', () => gallery.downloadOpen());
els.viewerDelete.addEventListener('click', () => gallery.deleteOpen());
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && gallery.isOpen()) gallery.close();
if (event.key === 'Escape') setThresholdTooltipVisible(false);
if (
event.code === 'Space' &&
!event.repeat &&
runtime.baseline &&
canUseGuideShortcut(event.target)
) {
event.preventDefault();
baselineGuide?.show();
}
});
document.addEventListener('visibilitychange', () => {
if (document.hidden) handleHidden();
else handleVisible();
});
window.addEventListener('pagehide', teardown);
}
/**
* Runtime bridge for diagnostics and Playwright checks.
*
* @type {object}
*/
window.gstmxxRealtime = {
/**
* Returns a serialisable snapshot of runtime state.
*
* @returns {object} State snapshot.
*/
getState() {
const stats = graph ? graph.stats() : { samples: 0, gaps: 0, events: 0, following: true };
return {
appState: runtime.appState,
hasBaseline: Boolean(runtime.baseline),
noiseFloor: runtime.baseline ? runtime.baseline.noiseFloor : null,
threshold: MATCH_THRESHOLD,
graphMaxDistance: GRAPH_MAX_DISTANCE,
windowSeconds: GRAPH_WINDOW_SECONDS,
faceMissing: runtime.faceMissing,
distanceBufferLength: runtime.distanceBuffer.length,
sampleCount: stats.samples,
visibleSampleCount: stats.visibleSamples,
timelineVirtualHeight: stats.virtualHeight,
gapCount: stats.gaps,
eventCount: stats.events,
qualityCounts: stats.quality,
qualityVisible: stats.qualityVisible,
statusQuality: statusDisplay.quality,
statusKey: statusDisplay.key,
following: stats.following,
captureCount: gallery ? gallery.size() : 0,
running: runtime.running,
streamStopped: runtime.streamStopped,
sourceKind: runtime.sourceKind,
lastDetectionScore: runtime.lastDetectionScore,
lastDescriptorLength: runtime.lastDescriptorLength,
sessionPhase: runtime.sessionPhase,
sessionStep: runtime.sessionStep,
lastMeasurement: runtime.lastMeasurement
? {
distance: runtime.lastMeasurement.distance,
spread: runtime.lastMeasurement.spread,
capturedAt: runtime.lastMeasurement.capturedAt,
}
: null,
};
},
/**
* Starts a baseline capture run without pressing UI controls.
*
* @returns {void}
*/
triggerCalibration() {
startBaselineCapture();
},
/** Marks the beginning of a declared makeup step. */
startMakeupStep,
/** Starts the temporal snapshot for the declared makeup step. */
measureStep: startStepMeasurement,
/**
* Releases camera and loops.
*
* @returns {void}
*/
teardown,
/**
* Restarts camera and loops after a teardown.
*
* @returns {Promise<void>}
*/
resume: handleVisible,
};
/**
* Initialises the visualizer and starts its loops.
*
* @returns {Promise<void>}
*/
export async function initRealtime() {
if (runtime.initialized) return;
runtime.initialized = true;
setStatus('Loading models…');
setTrackingState(APP_STATES.NO_BASELINE);
initEqualizer();
graph = createGraph({
scrollEl: els.graphScroll,
spacerEl: els.graphSpacer,
canvasEl: els.graphCanvas,
followEl: els.graphFollow,
windowSeconds: GRAPH_WINDOW_SECONDS,
samplesPerSecond: SAMPLES_PER_SECOND,
maxDistance: GRAPH_MAX_DISTANCE,
threshold: MATCH_THRESHOLD,
onMarkerClick: (captureId) => gallery.open(captureId),
});
radialGraph = createRadialGraph({
canvasEl: els.radialCanvas,
maxDelta: MAX_DELTA,
});
baselineGuide = createBaselineGuide({
canvasEl: els.guideCanvas,
videoEl: els.video,
stageEl: els.centerView,
});
gallery = createGallery({
viewerEl: els.viewer,
imageEl: els.viewerImage,
metaEl: els.viewerMeta,
countEl: els.galleryCount,
onCountChange: (count) => {
els.btnDownloadAll.disabled = count === 0;
els.btnDeleteAll.disabled = count === 0;
},
onDelete: (captureId) => {
if (captureId) graph.clearCapture(captureId);
else graph.reset();
},
onStatus: (message) => setStatus(message),
});
createSettings({
rootEl: els.layer,
brightnessEl: els.brightness,
brightnessValueEl: els.brightnessValue,
equalizerEl: els.toggleEqualizer,
layoutEl: els.layer,
onLayoutChange: () => graph.resize(),
});
qualityGate = createQualityGate(els.video);
baselineCollector = createBaselineCollector({
targetSamples: BASELINE_TARGET_SAMPLES,
minDurationMs: BASELINE_MIN_MS,
timeoutMs: BASELINE_TIMEOUT_MS,
maxSpread: BASELINE_MAX_SPREAD,
});
measurementCollector = createBaselineCollector({
targetSamples: MEASUREMENT_TARGET_SAMPLES,
minDurationMs: MEASUREMENT_MIN_MS,
timeoutMs: MEASUREMENT_TIMEOUT_MS,
maxSpread: MEASUREMENT_MAX_SPREAD,
});
spikeDetector = createSpikeDetector({
sustainMs: SPIKE_SUSTAIN_MS,
bestMargin: SPIKE_BEST_MARGIN,
threshold: MATCH_THRESHOLD,
cooldownMs: SPIKE_COOLDOWN_MS,
});
frameBuffer = createFrameBuffer(els.video, FRAME_BUFFER_SLOTS, CAPTURE_MAX_WIDTH);
bindListeners();
graph.resize();
try {
await gallery.init();
} catch (err) {
console.error('capture store unavailable:', err);
setStatus('Capture storage unavailable — measurement still works.', true);
}
await loadModels();
let sourceReady = false;
try {
await initWebcam();
sourceReady = true;
} catch (err) {
runtime.streamStopped = true;
console.error('camera unavailable:', err);
els.sourceState.textContent = 'camera unavailable';
setStatus('Camera unavailable. Open a local video from Source.', true);
}
// A stored baseline skips straight to tracking. The trace itself is not
// restored: replaying old history and appending new samples would draw one
// continuous line across a discontinuity that could be days wide.
const stored = sourceReady ? loadBaseline() : null;
if (stored) adoptBaseline(stored, true);
else {
syncBaselineControls();
if (sourceReady) setStatus('Camera ready. Capture a baseline to start measuring.');
}
startLoops();
}
/**
* Bootstraps the page and reports startup failures in the status pill.
*
* @returns {Promise<void>}
*/
async function boot() {
try {
await initRealtime();
} catch (err) {
console.error('Failed to initialize realtime visualizer:', err);
setStatus(`Initialization failed: ${err.message || String(err)}`, true);
}
}
boot();