/** * Deep camera verification: capture a fresh photo from the candidate's * webcam, run face detection on it, and pass / fail based on the face * count. * * A face detector is mandatory. The check has exactly one pass * condition — the detector returns a face count inside the allowed * band. There is no brightness fallback: a covered or dark lens simply * yields zero faces and fails as `no-face`, which is both simpler and * stricter than the old "is the frame bright enough?" heuristic. * * Improvements over the teq-ts equivalent: * - Reuses the camera stream across retries instead of reopening it. * teq-ts does up to 3 × `getUserMedia` + 700ms warmup for a covered- * webcam case (~3s of camera light flickering). * - Warmup duration is a parameter, not a hardcoded 700ms. */ import type { CheckRow } from "./checks.js"; import { type HiddenCameraOptions } from "./hidden-camera.js"; export interface DeepCameraFaceResult { faceCount: number; /** * Backend-owned camera-check helpers can include the persisted * evidence row id. The SDK does not need it for pass/fail, but * Vue uses it to avoid uploading the same frame a second time. */ photoId?: string; /** * Server-side verdict for the exact stored JPEG. When present it * wins over the local face-count classification for detector * failures, so the UI can fail closed while still preserving the * photo on the dashboard. */ verdict?: "passed" | "no-face" | "multiple-faces" | "detector-error"; attempt?: number; byteSize?: number; inferenceMs?: number; } export interface DeepCameraCheckOptions { /** * Customer-provided face detector. The SDK can't ship a face model — * each customer's compliance / model preferences differ — so the * host must wire one. The detector is mandatory: omitting it fails * the check as `deep-check-failed` (a misconfiguration, not a free * pass). The contract is strict in three ways: * - `faceCount === 1` is the only pass (proctoring needs exactly * the candidate in frame, not zero and not more than one). * - `faceCount >= 2` fails as `multiple-faces`. * - An error thrown from the callback fails as `deep-check-failed`. * * The strict-error stance is deliberate. A host wiring this is opting * into face verification -- "no face = no pass." Silently passing * when the detector errors would turn every operational failure of * the detector (server down, network blip, model crashed) into a * free pass exactly when verification matters most. If you want * softer semantics (e.g. accept the candidate during a known * outage), handle it in your callback by returning a synthetic * `{ faceCount: 1 }` rather than throwing. */ detectFace?: (jpegDataUrl: string) => Promise; /** * Optional pre-acquired stream to reuse instead of opening a fresh * hidden camera. Use this during in-test rechecks where the * proctoring stream is already live — keeps the camera light from * flickering. * * When provided, the check captures from this stream and skips * warmup (the stream is already producing real frames). */ existingStream?: { videoElement: HTMLVideoElement; }; /** * Sensor warmup window after the camera opens, in ms. Default: 700. * Skipped when `existingStream` is provided. */ warmupMs?: number; /** Number of capture+detect attempts before giving up. Default: 3. */ maxAttempts?: number; /** * Maximum number of faces allowed in the captured frame for a pass. * - 1 (default) -- exactly one face required (typical proctoring; * zero faces → `no-face`, two-plus → `multiple-faces`). * - 2+ -- allows that many faces, useful for assistive-companion * scenarios. Anything above still emits `multiple-faces` if * exceeded. */ maxFacesAllowed?: number; /** Hidden camera options. Forwarded as-is. */ cameraOptions?: HiddenCameraOptions; /** * Optional logger. Receives one-line strings tagged for the check. * Off by default so production runs stay quiet. */ log?: (message: string) => void; } /** * Per-attempt record returned alongside the final verdict. Lets * consumers (the Vue wizard's camera step, ultimately the * dashboard) persist every frame the check saw, not just the * final one. Useful for forensics: did the detector return a * different face count each attempt? Was the candidate off-camera * on attempt 1 then in frame for 2 and 3? The intermediate frames * are the evidence. */ export interface DeepCameraAttempt { /** 1-based attempt index within the loop. */ attempt: number; /** JPEG data URL of the frame captured for this attempt. */ photo: string; /** Verdict the loop assigned to this specific attempt: * "passed" -- the frame that ended the loop with a pass * "no-face" -- detector returned 0 faces (off-camera or * covered lens) * "multiple-faces" -- detector returned 2+ faces * "detector-error" -- detectFace callback threw */ verdict: "passed" | "no-face" | "multiple-faces" | "detector-error"; /** Face count returned by detectFace, if a detector was wired * and the call succeeded for this attempt. */ faceCount?: number; /** Persisted evidence id when the detector stores the frame server-side. */ photoId?: string; /** Server attempt index when it differs from the local retry index. */ serverAttempt?: number; /** Stored image size when the backend owns persistence. */ byteSize?: number; /** Detector runtime when returned by the backend. */ inferenceMs?: number; } export interface DeepCameraCheckResult { state: CheckRow["state"]; /** Final attempt's JPEG. Stays at the top level so existing * consumers (engine row's `photo` field, the dashboard's * "captured photo" panel) work unchanged. */ photo?: string; /** All attempts the loop saw, in order. Includes the final * attempt (which is also exposed at top-level `photo`). */ attempts: DeepCameraAttempt[]; } /** * Run the deep camera verification. * * Stream lifecycle: * - If `existingStream` is provided, the function only takes snapshots * and never opens a stream — the caller owns the lifecycle. * - Otherwise, the function opens a hidden camera once, retries * snapshots from the same stream, and tears it down on the way out * (success or failure). */ export declare const runDeepCameraCheck: (options?: DeepCameraCheckOptions) => Promise; //# sourceMappingURL=deep-camera-check.d.ts.map