scripts/auto-find-loop.jsjavascript
/**
* @module auto-find-loop
* @description
* Silent auto-find loop for the Ghostmaxxing lab page.
*
* Every few seconds this add-on repeats the "find face" pipeline without
* writing user-facing log text: it detects the live face descriptor, composites
* and re-detects when a 2D or 3D Ghostyle is active, computes the nearest
* distances/similarities against the local databases, and dispatches a unified
* `matchStateChanged` payload.
*
* Textual logs remain reserved for explicit user actions such as the "find
* face" button. Live feedback is rendered by `bbox-overlay.js` and `lab-ui.js`,
* which listen for the auto payload and update the box/readout metrics.
*
* Auto-pause behavior: when models/video/database inputs are not ready or no
* face is detected, the current tick exits without changing the UI. The loop
* also allows one interval of startup stabilization before the first scan.
*/
import { compositeAndDetect, evaluateMatch, detectFaceInCam, hasActivePlugin, seekFaceInDb } from './engine.js';
import { findFace3d, evaluateMatch3d } from './engine-3d.js';
import { state } from './state.js';
import { t } from './i18n.js';
window.addEventListener('ghostatiReady', autoFindLoop, { once: true });
/**
* Starts the silent polling loop after the main app has initialized `state`,
* `window.gstmxx`, the webcam, and plugin infrastructure. The first interval is
* skipped to give face-api, MediaPipe, and camera state a short stabilization
* window before auto metrics begin updating the overlay.
*
* @returns {void}
* @see main - Dispatches `ghostatiReady` once the lab environment is ready.
* @see tick - Performs each silent 2D/3D matching pass.
* @see renderReadout - Receives the auto `matchStateChanged` payload through the event bus.
* @see onMatchStateChanged - Updates the bbox overlay from auto match-state events.
*/
function autoFindLoop() {
const INTERVAL_MS = 2000;
let tickCount = 0;
setInterval(async () => {
tickCount++;
if (tickCount < 2) return; // allow brief stabilization
await tick();
}, INTERVAL_MS);
}
/**
* Runs one silent auto-find pass. It detects the live face, builds the 2D
* face-api section when the 2D database has records, builds the 3D MediaPipe
* ImageEmbedder section, derives the combined overall state, and dispatches
* `matchStateChanged` with `source: 'auto'` so overlays update without applying
* the suppression used for explicit scan/save/find actions.
*
* Errors are logged only to the developer console to avoid spamming the lab log;
* the UI keeps showing the last known match state until a later tick succeeds.
*
* @returns {Promise<void>} Resolves after the pass dispatches or exits early.
* @see detectFaceInCam - Provides the live face-api detection and descriptor.
* @see evaluateMatch - Converts live and composited 2D metrics into payload detail.
* @see findFace3d - Produces live and composited 3D ImageEmbedder metrics.
* @see evaluateMatch3d - Converts 3D metrics into the `mediapipe` payload section.
* @see renderReadout - Displays auto distance/readout metrics in the lab UI.
* @see onMatchStateChanged - Colors and labels the bbox overlay from this payload.
*/
async function tick() {
try {
const liveResult = await detectFaceInCam(false);
if (!liveResult) return;
// ── 2D (face-api) ───────────────────────────────────────────────────────
let faceapiSection = null;
const composite = hasActivePlugin() ? await compositeAndDetect(liveResult) : null;
if (state.db.faces.length > 0) {
const liveInfo = seekFaceInDb(liveResult);
const { detail } = evaluateMatch(liveInfo, composite);
faceapiSection = {
detectionState: detail.detectionState,
distance: detail.distance,
matchedId: detail.matchedId,
liveMinDist: liveInfo.liveMinDist,
liveMinId: liveInfo.liveMinId,
obfMinDist: detail.obfMinDist ?? null,
obfMinId: detail.obfMinId ?? null,
};
}
// ── 3D (MobileNet) ──────────────────────────────────────────────────────
// Embed the *2D-composited* frame (video + active 2D Ghostyle) so a 2D
// overlay such as the Face Brush actually shifts the ImageEmbedder score.
// Falls back to the raw video inside compositeAndDetect3d when there is no
// 2D composite (e.g. only a 3D/UV plugin is active).
const result3d = await findFace3d(composite?.canvas ?? null);
const mediapipeSection = evaluateMatch3d(result3d);
// ── overall ─────────────────────────────────────────────────────────────
const f = faceapiSection?.detectionState;
const m = mediapipeSection?.detectionState;
const overall = (!f && !m) ? 'unknown'
: (!f || !m) ? 'unknown'
: (f === m) ? f
: 'partial-elusion';
state.gstmxxEvents.dispatchEvent(new CustomEvent('matchStateChanged', {
detail: {
source: 'auto',
ghostylePresent: hasActivePlugin(),
faceapi: faceapiSection,
mediapipe: mediapipeSection,
overall,
}
}));
} catch (err) {
console.error(t('console_auto_find_tick_error'), err);
}
}