@sekyro/web 0.1.0-rc.2 → 0.1.0-rc.4

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/{capture-session-v3-CAoWM8iT.d.cts → capture-session-v3-DEInCOUg.d.cts} +37 -7
  3. package/dist/{capture-session-v3-BJ8ciaWl.d.ts → capture-session-v3-uXJr1Wlm.d.ts} +37 -7
  4. package/dist/{chunk-LOTAJDRE.js → chunk-EWRNPKTR.js} +38 -9
  5. package/dist/chunk-EWRNPKTR.js.map +1 -0
  6. package/dist/{chunk-BPYVZUJA.js → chunk-TBANUT3X.js} +39 -6
  7. package/dist/chunk-TBANUT3X.js.map +1 -0
  8. package/dist/client/index.cjs.map +1 -1
  9. package/dist/client/index.d.cts +1 -1
  10. package/dist/client/index.d.ts +1 -1
  11. package/dist/core/index.cjs +37 -8
  12. package/dist/core/index.cjs.map +1 -1
  13. package/dist/core/index.d.cts +2 -2
  14. package/dist/core/index.d.ts +2 -2
  15. package/dist/core/index.js +1 -1
  16. package/dist/{create-verification-EnZ13uY4.d.cts → create-verification-Ce1y5kJR.d.ts} +26 -3
  17. package/dist/{create-verification-CjLnJ0w6.d.ts → create-verification-Dkb2tXuY.d.cts} +26 -3
  18. package/dist/{hosted-client-BpvBqu9f.d.cts → hosted-client-DWzCRYJK.d.cts} +14 -1
  19. package/dist/{hosted-client-BpvBqu9f.d.ts → hosted-client-DWzCRYJK.d.ts} +14 -1
  20. package/dist/index.cjs +74 -12
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +3 -3
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +2 -2
  25. package/dist/internal/index.cjs +74 -12
  26. package/dist/internal/index.cjs.map +1 -1
  27. package/dist/internal/index.d.cts +4 -4
  28. package/dist/internal/index.d.ts +4 -4
  29. package/dist/internal/index.js +2 -2
  30. package/package.json +1 -1
  31. package/dist/chunk-BPYVZUJA.js.map +0 -1
  32. package/dist/chunk-LOTAJDRE.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @sekyro/web
2
2
 
3
+ ## 0.1.0-rc.4
4
+
5
+ ### Minor Changes
6
+
7
+ - 9991ea5: Face-capture timing on `captured` and on the liveness submission (session insights).
8
+
9
+ - `CapturedArtifacts` gains `engine` (`'v3'`) and three optional whole-millisecond durations measured by the capture engine with `performance.now()`: `cameraReadyMs` (camera request → preview live), `faceAcquireMs` (camera ready → first accepted face) and `captureMs` (first accepted face → gate progress 100 %, i.e. capture complete, before the clip window and any upload; a face lost and re-acquired keeps the first start). The core `CaptureArtifacts` carries the same marks as `timing`.
10
+ - `createVerification` forwards them, together with the already-known `width` / `height` / `clipMs` and the engine id, as `input.capture` on the `liveness` submission (`LivenessInput.capture`, `LivenessCaptureInput`) so the Sekyro dashboard can show how long the face capture, the upload and the processing of each check took. Nothing else changes on the wire; `face_match` is untouched.
11
+ - `@sekyro/react`: `<LivenessCapture onCaptured>` receives the new fields unchanged (no code change — the dependency bump only).
12
+
13
+ ## 0.1.0-rc.3
14
+
15
+ ### Patch Changes
16
+
17
+ - 99b157e: Liveness gate: a full progress ring now always fires; capture width/height no longer read as 0.
18
+
19
+ - **Gate readiness is consistent with the integrator.** `ready` required the trimmed 500 ms window *mean* q ≥ 0.75 while progress rose on q ≥ 0.6, so any steady quality in [0.6, 0.75) — e.g. a soft front camera / dim room pinning the sharpness factor at ~0.7 — saturated the ring at 100 %, passed every guidance threshold ("hold still") and never fired: the stage sat with a full green ring over the live feed and nothing was recorded or submitted (staging field report, iPhone, 2026-10-17). The window is now judged against `Q_GOOD` itself (every trimmed sample must be a good tick): one flaky tick is still forgiven, a second bad tick still blocks, and there is no dead band above the rise threshold. `READY_MEAN_Q` is removed from `V3GateThresholds` (internal engine option; not on the public API).
20
+ - **`captured.width` / `captured.height`** are latched while the stream is live. The engine stops the camera tracks before emitting artifacts and iOS Safari then reports `videoWidth`/`videoHeight` as 0, which was recorded as `0 × 0`.
21
+
3
22
  ## 0.1.0-rc.2
4
23
 
5
24
  ### Patch Changes
@@ -105,6 +105,23 @@ interface GateChecks {
105
105
  goodQuality: boolean;
106
106
  stable: boolean;
107
107
  }
108
+ /**
109
+ * Client-side capture timing (face-capture duration, 2026-10-17), measured
110
+ * by the engine with its monotonic clock (`performance.now()` by default) and
111
+ * rounded to whole milliseconds. All fields are optional: a phase that never
112
+ * happened (e.g. no face ever detected before a retry) is simply absent.
113
+ */
114
+ interface CaptureTiming {
115
+ /** `getUserMedia` call → stream attached and the preview playing. */
116
+ cameraReadyMs?: number;
117
+ /** Camera ready → the first tick on which the GATE accepted the face
118
+ * (present + size / centering / quality good enough to make progress —
119
+ * not merely detected). */
120
+ faceAcquireMs?: number;
121
+ /** FIRST accepted face → gate progress 100 % (capture complete, BEFORE the
122
+ * clip window / upload). A face lost and re-acquired keeps the first start. */
123
+ captureMs?: number;
124
+ }
108
125
  /** Capture output. Upload is NOT the engine's job — hosts submit these themselves. */
109
126
  interface CaptureArtifacts {
110
127
  /** Absent in photo mode. */
@@ -115,6 +132,8 @@ interface CaptureArtifacts {
115
132
  keyframe: Blob;
116
133
  /** JPEG data URL of the keyframe canvas (v1 preview, quality 0.92). */
117
134
  previewDataUrl: string;
135
+ /** Capture-phase timing of the attempt that produced these artifacts. */
136
+ timing?: CaptureTiming;
118
137
  }
119
138
  interface CaptureSessionEvents {
120
139
  statechange: {
@@ -167,12 +186,13 @@ interface V3GateThresholds {
167
186
  /** Velocities at/below this (IOD/s) count as stillness — absorbs detector
168
187
  * box jitter (largest on LOW's 128px input) before it reaches the EMA. */
169
188
  MOTION_DEADBAND_IOD_PER_S: number;
170
- /** ready = p ≥ 1 AND mean q over this window ≥ READY_MEAN_Q. The mean is
171
- * TRIMMED: with ≥3 samples in the window the single worst is dropped, so
172
- * one flaky tick (5 Hz LOW cadence = only 2–3 samples per window) cannot
173
- * veto an otherwise-ready user. */
189
+ /** ready = p ≥ 1 AND no bad tick (q < Q_GOOD) left in this window after
190
+ * TRIMMING: with ≥3 samples the single worst is dropped, so one flaky tick
191
+ * (5 Hz LOW cadence = only 2–3 samples per window) cannot veto an
192
+ * otherwise-ready user, while a SECOND bad tick (a real sustained wobble)
193
+ * still blocks. The readiness bar is Q_GOOD itself — the level the
194
+ * integrator rises on — so any input that fills the ring also fires it. */
174
195
  READY_WINDOW_MS: number;
175
- READY_MEAN_Q: number;
176
196
  /** Per-factor guidance trigger levels (lowest failing factor speaks). */
177
197
  GUIDE_SIZE: number;
178
198
  GUIDE_CENTERING: number;
@@ -215,8 +235,12 @@ interface V3GateTick {
215
235
  factors: Record<V3GateFactor, number>;
216
236
  /** fcv3.* message key for the lowest failing factor (host translates). */
217
237
  guidanceKey: string;
218
- /** Fire signal: p ≥ 1 AND the READY_WINDOW mean q ≥ READY_MEAN_Q. */
238
+ /** Fire signal: p ≥ 1 AND no second bad tick (q < Q_GOOD) in READY_WINDOW_MS. */
219
239
  ready: boolean;
240
+ /** The face on THIS tick is ACCEPTED by the gate — present and every factor
241
+ * (size, centering, pose, sharpness, exposure, motion) good enough that the
242
+ * integrator RISES (q ≥ Q_GOOD). The engine's "first accepted face" mark. */
243
+ accepted: boolean;
220
244
  }
221
245
  declare class FaceGateV3 {
222
246
  private readonly t;
@@ -418,6 +442,10 @@ declare class CaptureSessionV3 {
418
442
  private profiler;
419
443
  private lastGuidanceKey;
420
444
  private positioningStartedAt;
445
+ private cameraRequestedAt;
446
+ private cameraReadyAt;
447
+ private faceAcquiredAt;
448
+ private captureCompleteAt;
421
449
  private fired;
422
450
  private carriedDegrade;
423
451
  private carriedMeans;
@@ -440,6 +468,8 @@ declare class CaptureSessionV3 {
440
468
  * sharpest wins (reuses frame-quality.sharpness via the capture seam). */
441
469
  private collectKeyframes;
442
470
  private finishCapture;
471
+ /** The attempt's capture-phase timing (whole ms, ≥ 0); undefined when no mark landed. */
472
+ private captureTiming;
443
473
  private defaultKeyframe;
444
474
  /** Reused downscale canvas — falls back to the raw video where canvas 2D
445
475
  * is unavailable (jsdom) so tests and exotic hosts keep detecting. */
@@ -454,4 +484,4 @@ declare class CaptureSessionV3 {
454
484
  private now;
455
485
  }
456
486
 
457
- export { V3_GATE_DEFAULTS as A, VIDEO_BPS as B, type CameraErrorKind as C, DEFAULT_CLIP_MIME as D, type EscalationClipArtifact as E, FaceGateV3 as F, type GateChecks as G, buildLivenessV2FormData as H, cameraPreflight as I, classifyDevice as J, KEYFRAME_CANDIDATES as K, extForMime as L, MAX_CLIP_BYTES as M, isClipTooLarge as N, isMediaRecorderSupported as O, mapGetUserMediaError as P, pickMimeType as Q, probeDevice as R, STOP_GRACE_MS as S, TIMESLICE_MS as T, type V3ChecksPayload as V, CLIP_MS as a, type CaptureArtifacts as b, type CaptureErrorKind as c, type CaptureSessionEvents as d, CaptureSessionV3 as e, type CaptureSessionV3Events as f, type CaptureSessionV3Options as g, type CaptureState as h, ClipError as i, type ClipErrorKind as j, ClipRecorder as k, type ClipResult as l, DEVICE_PROFILES as m, type DeviceClass as n, type DeviceProbe as o, type DeviceProfile as p, DeviceProfiler as q, type EscalationHook as r, KEYFRAME_WINDOW_MS as s, MIME_CANDIDATES as t, type V3GateFactor as u, type V3GateQuality as v, type V3GateThresholds as w, type V3GateTick as x, type V3GateTickInput as y, type V3TelemetryEvent as z };
487
+ export { type V3TelemetryEvent as A, V3_GATE_DEFAULTS as B, type CameraErrorKind as C, DEFAULT_CLIP_MIME as D, type EscalationClipArtifact as E, FaceGateV3 as F, type GateChecks as G, VIDEO_BPS as H, buildLivenessV2FormData as I, cameraPreflight as J, KEYFRAME_CANDIDATES as K, classifyDevice as L, MAX_CLIP_BYTES as M, extForMime as N, isClipTooLarge as O, isMediaRecorderSupported as P, mapGetUserMediaError as Q, pickMimeType as R, STOP_GRACE_MS as S, TIMESLICE_MS as T, probeDevice as U, type V3ChecksPayload as V, CLIP_MS as a, type CaptureArtifacts as b, type CaptureErrorKind as c, type CaptureSessionEvents as d, CaptureSessionV3 as e, type CaptureSessionV3Events as f, type CaptureSessionV3Options as g, type CaptureState as h, type CaptureTiming as i, ClipError as j, type ClipErrorKind as k, ClipRecorder as l, type ClipResult as m, DEVICE_PROFILES as n, type DeviceClass as o, type DeviceProbe as p, type DeviceProfile as q, DeviceProfiler as r, type EscalationHook as s, KEYFRAME_WINDOW_MS as t, MIME_CANDIDATES as u, type V3GateFactor as v, type V3GateQuality as w, type V3GateThresholds as x, type V3GateTick as y, type V3GateTickInput as z };
@@ -105,6 +105,23 @@ interface GateChecks {
105
105
  goodQuality: boolean;
106
106
  stable: boolean;
107
107
  }
108
+ /**
109
+ * Client-side capture timing (face-capture duration, 2026-10-17), measured
110
+ * by the engine with its monotonic clock (`performance.now()` by default) and
111
+ * rounded to whole milliseconds. All fields are optional: a phase that never
112
+ * happened (e.g. no face ever detected before a retry) is simply absent.
113
+ */
114
+ interface CaptureTiming {
115
+ /** `getUserMedia` call → stream attached and the preview playing. */
116
+ cameraReadyMs?: number;
117
+ /** Camera ready → the first tick on which the GATE accepted the face
118
+ * (present + size / centering / quality good enough to make progress —
119
+ * not merely detected). */
120
+ faceAcquireMs?: number;
121
+ /** FIRST accepted face → gate progress 100 % (capture complete, BEFORE the
122
+ * clip window / upload). A face lost and re-acquired keeps the first start. */
123
+ captureMs?: number;
124
+ }
108
125
  /** Capture output. Upload is NOT the engine's job — hosts submit these themselves. */
109
126
  interface CaptureArtifacts {
110
127
  /** Absent in photo mode. */
@@ -115,6 +132,8 @@ interface CaptureArtifacts {
115
132
  keyframe: Blob;
116
133
  /** JPEG data URL of the keyframe canvas (v1 preview, quality 0.92). */
117
134
  previewDataUrl: string;
135
+ /** Capture-phase timing of the attempt that produced these artifacts. */
136
+ timing?: CaptureTiming;
118
137
  }
119
138
  interface CaptureSessionEvents {
120
139
  statechange: {
@@ -167,12 +186,13 @@ interface V3GateThresholds {
167
186
  /** Velocities at/below this (IOD/s) count as stillness — absorbs detector
168
187
  * box jitter (largest on LOW's 128px input) before it reaches the EMA. */
169
188
  MOTION_DEADBAND_IOD_PER_S: number;
170
- /** ready = p ≥ 1 AND mean q over this window ≥ READY_MEAN_Q. The mean is
171
- * TRIMMED: with ≥3 samples in the window the single worst is dropped, so
172
- * one flaky tick (5 Hz LOW cadence = only 2–3 samples per window) cannot
173
- * veto an otherwise-ready user. */
189
+ /** ready = p ≥ 1 AND no bad tick (q < Q_GOOD) left in this window after
190
+ * TRIMMING: with ≥3 samples the single worst is dropped, so one flaky tick
191
+ * (5 Hz LOW cadence = only 2–3 samples per window) cannot veto an
192
+ * otherwise-ready user, while a SECOND bad tick (a real sustained wobble)
193
+ * still blocks. The readiness bar is Q_GOOD itself — the level the
194
+ * integrator rises on — so any input that fills the ring also fires it. */
174
195
  READY_WINDOW_MS: number;
175
- READY_MEAN_Q: number;
176
196
  /** Per-factor guidance trigger levels (lowest failing factor speaks). */
177
197
  GUIDE_SIZE: number;
178
198
  GUIDE_CENTERING: number;
@@ -215,8 +235,12 @@ interface V3GateTick {
215
235
  factors: Record<V3GateFactor, number>;
216
236
  /** fcv3.* message key for the lowest failing factor (host translates). */
217
237
  guidanceKey: string;
218
- /** Fire signal: p ≥ 1 AND the READY_WINDOW mean q ≥ READY_MEAN_Q. */
238
+ /** Fire signal: p ≥ 1 AND no second bad tick (q < Q_GOOD) in READY_WINDOW_MS. */
219
239
  ready: boolean;
240
+ /** The face on THIS tick is ACCEPTED by the gate — present and every factor
241
+ * (size, centering, pose, sharpness, exposure, motion) good enough that the
242
+ * integrator RISES (q ≥ Q_GOOD). The engine's "first accepted face" mark. */
243
+ accepted: boolean;
220
244
  }
221
245
  declare class FaceGateV3 {
222
246
  private readonly t;
@@ -418,6 +442,10 @@ declare class CaptureSessionV3 {
418
442
  private profiler;
419
443
  private lastGuidanceKey;
420
444
  private positioningStartedAt;
445
+ private cameraRequestedAt;
446
+ private cameraReadyAt;
447
+ private faceAcquiredAt;
448
+ private captureCompleteAt;
421
449
  private fired;
422
450
  private carriedDegrade;
423
451
  private carriedMeans;
@@ -440,6 +468,8 @@ declare class CaptureSessionV3 {
440
468
  * sharpest wins (reuses frame-quality.sharpness via the capture seam). */
441
469
  private collectKeyframes;
442
470
  private finishCapture;
471
+ /** The attempt's capture-phase timing (whole ms, ≥ 0); undefined when no mark landed. */
472
+ private captureTiming;
443
473
  private defaultKeyframe;
444
474
  /** Reused downscale canvas — falls back to the raw video where canvas 2D
445
475
  * is unavailable (jsdom) so tests and exotic hosts keep detecting. */
@@ -454,4 +484,4 @@ declare class CaptureSessionV3 {
454
484
  private now;
455
485
  }
456
486
 
457
- export { V3_GATE_DEFAULTS as A, VIDEO_BPS as B, type CameraErrorKind as C, DEFAULT_CLIP_MIME as D, type EscalationClipArtifact as E, FaceGateV3 as F, type GateChecks as G, buildLivenessV2FormData as H, cameraPreflight as I, classifyDevice as J, KEYFRAME_CANDIDATES as K, extForMime as L, MAX_CLIP_BYTES as M, isClipTooLarge as N, isMediaRecorderSupported as O, mapGetUserMediaError as P, pickMimeType as Q, probeDevice as R, STOP_GRACE_MS as S, TIMESLICE_MS as T, type V3ChecksPayload as V, CLIP_MS as a, type CaptureArtifacts as b, type CaptureErrorKind as c, type CaptureSessionEvents as d, CaptureSessionV3 as e, type CaptureSessionV3Events as f, type CaptureSessionV3Options as g, type CaptureState as h, ClipError as i, type ClipErrorKind as j, ClipRecorder as k, type ClipResult as l, DEVICE_PROFILES as m, type DeviceClass as n, type DeviceProbe as o, type DeviceProfile as p, DeviceProfiler as q, type EscalationHook as r, KEYFRAME_WINDOW_MS as s, MIME_CANDIDATES as t, type V3GateFactor as u, type V3GateQuality as v, type V3GateThresholds as w, type V3GateTick as x, type V3GateTickInput as y, type V3TelemetryEvent as z };
487
+ export { type V3TelemetryEvent as A, V3_GATE_DEFAULTS as B, type CameraErrorKind as C, DEFAULT_CLIP_MIME as D, type EscalationClipArtifact as E, FaceGateV3 as F, type GateChecks as G, VIDEO_BPS as H, buildLivenessV2FormData as I, cameraPreflight as J, KEYFRAME_CANDIDATES as K, classifyDevice as L, MAX_CLIP_BYTES as M, extForMime as N, isClipTooLarge as O, isMediaRecorderSupported as P, mapGetUserMediaError as Q, pickMimeType as R, STOP_GRACE_MS as S, TIMESLICE_MS as T, probeDevice as U, type V3ChecksPayload as V, CLIP_MS as a, type CaptureArtifacts as b, type CaptureErrorKind as c, type CaptureSessionEvents as d, CaptureSessionV3 as e, type CaptureSessionV3Events as f, type CaptureSessionV3Options as g, type CaptureState as h, type CaptureTiming as i, ClipError as j, type ClipErrorKind as k, ClipRecorder as l, type ClipResult as m, DEVICE_PROFILES as n, type DeviceClass as o, type DeviceProbe as p, type DeviceProfile as q, DeviceProfiler as r, type EscalationHook as s, KEYFRAME_WINDOW_MS as t, MIME_CANDIDATES as u, type V3GateFactor as v, type V3GateQuality as w, type V3GateThresholds as x, type V3GateTick as y, type V3GateTickInput as z };
@@ -464,10 +464,12 @@ var V3_GATE_DEFAULTS = Object.freeze({
464
464
  MOTION_TAU_S: 0.4,
465
465
  MOTION_DEADBAND_IOD_PER_S: 0.1,
466
466
  READY_WINDOW_MS: 500,
467
- // 0.7→0.75 with the trimmed window: trimming forgives exactly ONE flaky
468
- // tick; the raised mean keeps a SECOND bad tick (a real sustained wobble)
469
- // from diluting through at 10 Hz cadences.
470
- READY_MEAN_Q: 0.75,
467
+ // (READY_MEAN_Q, the former 0.75 window MEAN, is gone — 2026-10-17 field
468
+ // bug: a steady q anywhere in [Q_GOOD, 0.75) saturated p at 1, passed every
469
+ // guide ("hold still") and could never reach the mean, so the ring sat full
470
+ // and green with nothing recording. Readiness now checks the window against
471
+ // Q_GOOD, which keeps the one-flaky-tick forgiveness and the second-bad-tick
472
+ // block without a dead band above the rise threshold.)
471
473
  GUIDE_SIZE: 0.6,
472
474
  GUIDE_CENTERING: 0.6,
473
475
  GUIDE_POSE: 0.6,
@@ -545,15 +547,15 @@ var FaceGateV3 = class {
545
547
  const worstIndex = windowSamples.indexOf(worst);
546
548
  windowSamples = windowSamples.filter((_, index) => index !== worstIndex);
547
549
  }
548
- const windowMean = windowSamples.reduce((sum, value) => sum + value, 0) / Math.max(1, windowSamples.length);
549
- const ready = this.p >= 1 && windowMean >= this.t.READY_MEAN_Q;
550
+ const ready = this.p >= 1 && windowSamples.every((sample) => sample >= this.t.Q_GOOD);
550
551
  return {
551
552
  at: timestampMs,
552
553
  q,
553
554
  p: this.p,
554
555
  factors,
555
556
  guidanceKey: this.guidanceOf(factors, quality, q),
556
- ready
557
+ ready,
558
+ accepted: q >= this.t.Q_GOOD
557
559
  };
558
560
  }
559
561
  geometryOf(observation, input) {
@@ -671,6 +673,11 @@ var CaptureSessionV3 = class {
671
673
  this.profiler = null;
672
674
  this.lastGuidanceKey = null;
673
675
  this.positioningStartedAt = 0;
676
+ // Face-capture timing marks of the CURRENT attempt (engine clock; reset per run).
677
+ this.cameraRequestedAt = null;
678
+ this.cameraReadyAt = null;
679
+ this.faceAcquiredAt = null;
680
+ this.captureCompleteAt = null;
674
681
  this.fired = false;
675
682
  this.carriedDegrade = 0;
676
683
  this.carriedMeans = [];
@@ -725,6 +732,10 @@ var CaptureSessionV3 = class {
725
732
  this.lastGuidanceKey = null;
726
733
  this.carriedDegrade = 0;
727
734
  this.carriedMeans = [];
735
+ this.cameraRequestedAt = null;
736
+ this.cameraReadyAt = null;
737
+ this.faceAcquiredAt = null;
738
+ this.captureCompleteAt = null;
728
739
  this.setState("preflight");
729
740
  const preflight = cameraPreflight();
730
741
  if (preflight !== "OK") {
@@ -734,6 +745,7 @@ var CaptureSessionV3 = class {
734
745
  this.setState("acquiring");
735
746
  const getUserMedia = options.getUserMedia ?? ((constraints) => navigator.mediaDevices.getUserMedia(constraints));
736
747
  let stream;
748
+ this.cameraRequestedAt = this.now();
737
749
  try {
738
750
  stream = await getUserMedia(buildCaptureConstraints(detectIsMobile(), this.facing));
739
751
  } catch (err) {
@@ -749,6 +761,7 @@ var CaptureSessionV3 = class {
749
761
  options.video.srcObject = stream;
750
762
  await options.video.play().catch(() => void 0);
751
763
  if (attempt !== this.attempt) return;
764
+ this.cameraReadyAt = this.now();
752
765
  if (this.detector === null) {
753
766
  const ladderOptions = {
754
767
  ...options.v3AssetBaseUrl !== void 0 ? { v3AssetBaseUrl: options.v3AssetBaseUrl } : {}
@@ -848,6 +861,7 @@ var CaptureSessionV3 = class {
848
861
  frameWidth: video.videoWidth,
849
862
  frameHeight: video.videoHeight
850
863
  });
864
+ if (tick.accepted && this.faceAcquiredAt === null) this.faceAcquiredAt = tick.at;
851
865
  this.emit("checks", {
852
866
  engine: "v3",
853
867
  progress: tick.p,
@@ -861,6 +875,7 @@ var CaptureSessionV3 = class {
861
875
  }
862
876
  if (tick.ready && this.stateValue === "positioning") {
863
877
  this.fired = true;
878
+ this.captureCompleteAt = tick.at;
864
879
  void this.fire(attempt);
865
880
  }
866
881
  }
@@ -955,7 +970,21 @@ var CaptureSessionV3 = class {
955
970
  finishCapture(artifacts) {
956
971
  this.stopTracks();
957
972
  this.setState("captured");
958
- this.emit("artifacts", artifacts);
973
+ const timing = this.captureTiming();
974
+ this.emit("artifacts", timing === void 0 ? artifacts : { ...artifacts, timing });
975
+ }
976
+ /** The attempt's capture-phase timing (whole ms, ≥ 0); undefined when no mark landed. */
977
+ captureTiming() {
978
+ const span = (from, to) => from !== null && to !== null ? Math.max(0, Math.round(to - from)) : void 0;
979
+ const cameraReadyMs = span(this.cameraRequestedAt, this.cameraReadyAt);
980
+ const faceAcquireMs = span(this.cameraReadyAt, this.faceAcquiredAt);
981
+ const captureMs = span(this.faceAcquiredAt, this.captureCompleteAt);
982
+ const timing = {
983
+ ...cameraReadyMs !== void 0 ? { cameraReadyMs } : {},
984
+ ...faceAcquireMs !== void 0 ? { faceAcquireMs } : {},
985
+ ...captureMs !== void 0 ? { captureMs } : {}
986
+ };
987
+ return Object.keys(timing).length > 0 ? timing : void 0;
959
988
  }
960
989
  // ── defaults for the canvas-bound seams ────────────────────────────────────
961
990
  async defaultKeyframe(video) {
@@ -1091,4 +1120,4 @@ export {
1091
1120
  KEYFRAME_WINDOW_MS,
1092
1121
  CaptureSessionV3
1093
1122
  };
1094
- //# sourceMappingURL=chunk-LOTAJDRE.js.map
1123
+ //# sourceMappingURL=chunk-EWRNPKTR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/capture-constraints.ts","../src/core/media.ts","../src/core/clip-recorder.ts","../src/core/keyframe.ts","../src/core/v3/device-profile.ts","../src/core/v3/detect-scheduler.ts","../src/core/v3/face-gate-v3.ts","../src/core/v3/capture-session-v3.ts"],"sourcesContent":["// Shared capture helpers — used by BOTH the (now sole) v3 engine and the\n// hosted gate-debug overlay. Extracted from the former v1 capture-session.ts\n// when the v1 engine was removed: the camera constraint tier + the mobile\n// heuristic are engine-agnostic, and the gate-geometry factors feed the\n// hosted debug overlay's tolerance rings. Zero React, calibration-pinned.\n\n/** getUserMedia constraint tier — 720p-class on every device since 2026-07-06.\n * The v2 FAS-CLIP liveness head loses its texture cues below ~720p: the SAME\n * genuine clip scores heads 0.0001 at 720x1280 vs 0.355 at the old 480x360\n * mobile tier, which pushed real users into review (first observed live on\n * staging 2026-07-06; falsified on corpus clips the same day). Dimensions are\n * sensor-relative (landscape); mobile browsers deliver portrait via rotation\n * metadata, which the liveness service's decoder applies (cv2\n * ORIENTATION_AUTO). `isMobile` is kept for call-site compat and future\n * divergence — both tiers are 720p today. Bitrate pairs with VIDEO_BPS in\n * clip-recorder.ts (2.5 Mbps). */\nexport function buildCaptureConstraints(\n isMobile: boolean,\n facingMode: \"user\" | \"environment\",\n): MediaStreamConstraints {\n void isMobile;\n return {\n video: {\n width: { ideal: 1280, max: 1280 },\n height: { ideal: 720, max: 720 },\n facingMode: facingMode,\n frameRate: { ideal: 24, max: 30 },\n },\n audio: false,\n };\n}\n\n/** v1 mobile heuristic (FaceCaptureV2.tsx L216-224). */\nexport function detectIsMobile(): boolean {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") return false;\n return (\n window.innerWidth <= 768 ||\n /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(\n String(navigator.userAgent ?? \"\"),\n )\n );\n}\n\n// ── gate geometry factors ─────────────────────────────────────────────────────\n// The calibration-pinned gate math the hosted `?debug=gate` overlay derives its\n// tolerance rings from (lib/gate-geometry-constants.ts). Kept exported here after\n// the v1 FaceGate was removed so the overlay never restates them as literals.\n\n/** Gate radius as a fraction of the guidance circle (v1 L501: circleSize × 0.45). */\nexport const CIRCLE_RADIUS_FACTOR = 0.45;\n/** `inCircle` tolerance as a fraction of the gate radius (v1 L508: r × 0.9). */\nexport const IN_CIRCLE_FACTOR = 0.9;\n/** `centered` tolerance as a fraction of the gate radius (v1 L509: r × 0.4). */\nexport const CENTERED_FACTOR = 0.4;\n","// PORTED byte-identical from kyc-ibtikar/lib/faceCaptureV2/media.ts (calibration-pinned\n// capture contract — spec §6 / risk 1). Do NOT edit without a calibration re-validation.\n\n// Pure, framework-free helpers for FaceCaptureV2. No React, minimal DOM.\n\n/** MP4-first ladder: one container (H.264 MP4) covers iOS/macOS Safari + Chrome 130+.\n * WebM/VP9→VP8 only as the Firefox/older-Chromium fallback. */\nexport const MIME_CANDIDATES = [\n \"video/mp4;codecs=avc1.42E01E,mp4a.40.2\",\n \"video/mp4;codecs=avc1.42E01E\",\n \"video/mp4\",\n \"video/webm;codecs=vp9,opus\",\n \"video/webm;codecs=vp9\",\n \"video/webm;codecs=vp8,opus\",\n \"video/webm;codecs=vp8\",\n \"video/webm\",\n] as const;\n\nexport type CameraErrorKind =\n | \"permission\"\n | \"notfound\"\n | \"inuse\"\n | \"overconstrained\"\n | \"insecure\"\n | \"unsupported\"\n | \"generic\";\n\n/** First supported candidate, or \"\" to let the browser pick its own default. */\nexport function pickMimeType(): string {\n const MR = (globalThis as unknown as { MediaRecorder?: { isTypeSupported?: (t: string) => boolean } })\n .MediaRecorder;\n if (!MR || typeof MR.isTypeSupported !== \"function\") return \"\";\n return MIME_CANDIDATES.find((t) => MR.isTypeSupported!(t)) ?? \"\";\n}\n\n/** Default clip container when pickMimeType() === \"\" — NEVER .bin/octet-stream (REVIEW FIX 5).\n * Prefer the first webm in the ladder the browser most likely defaults to. */\nexport const DEFAULT_CLIP_MIME = \"video/webm\";\n\n/** Branch on err.name (never the message). TypeError ⇒ navigator.mediaDevices was undefined\n * (insecure/unsupported context) — surfaced as \"insecure\". */\nexport function mapGetUserMediaError(err: unknown): CameraErrorKind {\n if (err instanceof TypeError) return \"insecure\";\n const name = (err as { name?: string })?.name ?? \"\";\n switch (name) {\n case \"NotAllowedError\":\n case \"PermissionDeniedError\":\n return \"permission\";\n case \"NotFoundError\":\n case \"DevicesNotFoundError\":\n return \"notfound\";\n case \"NotReadableError\":\n case \"TrackStartError\":\n return \"inuse\";\n case \"OverconstrainedError\":\n case \"ConstraintNotSatisfiedError\":\n return \"overconstrained\";\n default:\n return \"generic\";\n }\n}\n\nexport function cameraPreflight(): \"INSECURE_CONTEXT\" | \"UNSUPPORTED\" | \"OK\" {\n const w = globalThis as unknown as { window?: { isSecureContext?: boolean }; navigator?: Navigator };\n if (typeof w.window !== \"undefined\" && w.window.isSecureContext === false)\n return \"INSECURE_CONTEXT\";\n const md = (w.navigator as Navigator | undefined)?.mediaDevices as\n | { getUserMedia?: unknown }\n | undefined;\n if (!md || typeof md.getUserMedia !== \"function\") return \"UNSUPPORTED\";\n return \"OK\";\n}\n\n/** iOS Safari (and any browser) may lack MediaRecorder entirely — detect BEFORE prompting\n * for the camera so we can fall back to the v1 single-photo path (REVIEW FIX 1). */\nexport function isMediaRecorderSupported(): boolean {\n const MR = (globalThis as unknown as { MediaRecorder?: { isTypeSupported?: unknown } }).MediaRecorder;\n return typeof MR !== \"undefined\" && typeof (MR as { isTypeSupported?: unknown }).isTypeSupported === \"function\";\n}\n\nexport const MAX_CLIP_BYTES = 8 * 1024 * 1024; // 8 MB hard cap (shared with the /api/liveness/v2 contract)\nexport function isClipTooLarge(blob: Blob): boolean {\n return blob.size > MAX_CLIP_BYTES;\n}\n\nexport function extForMime(mime: string): string {\n const m = (mime || \"\").toLowerCase();\n if (m.startsWith(\"image/jpeg\")) return \"jpg\";\n if (m.startsWith(\"video/mp4\")) return \"mp4\";\n if (m.startsWith(\"video/webm\")) return \"webm\";\n // Never .bin — default to the clip container we actually record (REVIEW FIX 5).\n return \"webm\";\n}\n\n/** Build the exact multipart body /api/liveness/v2 expects: fields `token`, files `clip` + `keyframe`.\n * Caller must NOT set Content-Type — the browser adds the boundary. */\nexport function buildLivenessV2FormData(args: {\n token: string;\n clip: Blob;\n keyframe: Blob;\n ts?: number;\n}): FormData {\n const { token, clip, keyframe } = args;\n const ts = args.ts ?? Date.now();\n const clipType = clip.type || DEFAULT_CLIP_MIME;\n const kfType = keyframe.type || \"image/jpeg\";\n const fd = new FormData();\n fd.append(\"token\", token);\n fd.append(\"clip\", new File([clip], `liveness-${ts}.${extForMime(clipType)}`, { type: clipType }));\n fd.append(\"keyframe\", new File([keyframe], `keyframe-${ts}.${extForMime(kfType)}`, { type: kfType }));\n return fd;\n}\n","// De-hooked port of kyc-ibtikar/lib/faceCaptureV2/useClipRecorder.ts (calibration-pinned\n// capture contract — spec §6 / risk 1). Framework-free class: useRef → private fields,\n// useCallback(start) → record(), stopAndCleanup + the unmount useEffect → stop().\n// Behavior is the v1 hook's, VERBATIM (iOS workarounds included). Source line references\n// (kyc-ibtikar/lib/faceCaptureV2/useClipRecorder.ts):\n// - constants CLIP_MS / TIMESLICE_MS / STOP_GRACE_MS / VIDEO_BPS ............ L10-13\n// - ClipError(kind, msg?) .................................................. L15-21\n// - guarded double MediaRecorder construction (REVIEW FIX 1) ............... L47-63\n// - single-settle guard + timer clearing ................................... L66-71\n// - finalize: Blob(type = recorder.mimeType || picked || DEFAULT_CLIP_MIME)\n// + post-hoc size-cap check .............................................. L73-82\n// - incremental 8MB cap enforcement on dataavailable (REVIEW FIX 7) ........ L84-100\n// - onstop → finalize, onerror → generic, guarded start(TIMESLICE_MS) ...... L101-109\n// - hard stop at CLIP_MS + iOS stop-grace finalize (REVIEW FIX 3) .......... L111-124\n// - teardown: clear timers, stop recorder, stop ALL tracks (REVIEW FIX 8) .. L128-145\n// V2 additions on top of the verbatim behavior (both zero-impact on clip bytes, both\n// required because a class instance outlives one recording where v1's hook state was\n// remounted fresh): a re-entrancy guard in record(), and clearing stale timers when a\n// new recording starts. See the record() JSDoc.\n// Do NOT edit without a calibration re-validation.\nimport {\n pickMimeType,\n isClipTooLarge,\n MAX_CLIP_BYTES,\n DEFAULT_CLIP_MIME,\n type CameraErrorKind,\n} from \"./media\";\n\nexport const CLIP_MS = 2500; // 2.5s, within the 2.0–3.0s target window\nexport const TIMESLICE_MS = 1000; // periodic dataavailable — iOS reliability workaround\nexport const STOP_GRACE_MS = 1500; // iOS: if onstop never fires, finalize from chunks\n// 2.5 Mbps pairs with the 720p capture tier (capture-session.ts): 1.5 Mbps at\n// 720p over-compresses exactly the texture the FAS-CLIP head needs. ~0.8MB per\n// 2.5s clip — well under the 8MB client cap / 15MB server caps.\nexport const VIDEO_BPS = 2_500_000;\n\nexport type ClipErrorKind = CameraErrorKind | \"cliptoolarge\";\n\nexport class ClipError extends Error {\n kind: ClipErrorKind;\n constructor(kind: ClipErrorKind, msg?: string) {\n super(msg ?? kind);\n this.kind = kind;\n }\n}\n\nexport interface ClipResult {\n blob: Blob;\n /** Container mime the clip was finalized with (recorder.mimeType || picked || DEFAULT_CLIP_MIME). */\n mimeType: string;\n}\n\nexport class ClipRecorder {\n private recorder: MediaRecorder | null = null;\n private stream: MediaStream | null = null; // own field so teardown can stop tracks (REVIEW FIX 8)\n private chunks: Blob[] = [];\n private bytes = 0;\n private hardStopTimer: ReturnType<typeof setTimeout> | null = null;\n private graceTimer: ReturnType<typeof setTimeout> | null = null;\n private settled = false;\n\n private clearTimers(): void {\n if (this.hardStopTimer) clearTimeout(this.hardStopTimer);\n if (this.graceTimer) clearTimeout(this.graceTimer);\n this.hardStopTimer = null;\n this.graceTimer = null;\n }\n\n /** Records CLIP_MS of the stream. Resolves with the clip blob + its container mime;\n * rejects with ClipError. Behavior contract is the v1 hook's `start`, verbatim\n * (iOS workarounds included).\n *\n * ONE RECORDING AT A TIME: chunk/byte/timer state is shared per instance, so a\n * second record() while one is in flight rejects ClipError(\"generic\") instead of\n * corrupting the in-flight clip. (v2 guard — v1's hook was gated by its owning\n * component; a class API invites reuse. Zero impact on clip bytes.)\n *\n * `onStopped` (optional) fires ONCE the moment the CLIP_MS recording window\n * elapses (the hard-stop), BEFORE finalize — which on iOS can lag up to\n * STOP_GRACE_MS while onstop flushes the last chunk. It lets the host freeze\n * the display on the already-captured keyframe the instant recording ends,\n * instead of waiting for the blob. PURE NOTIFICATION — it does not touch\n * timers, chunks, or the finalize path, so it is calibration-neutral (no\n * effect on clip bytes). Never fires on a rejected capture (error /\n * cliptoolarge settle before the hard-stop and clear the timer). */\n record(stream: MediaStream, onStopped?: () => void): Promise<ClipResult> {\n if (this.recorder && !this.settled) {\n return Promise.reject(\n new ClipError(\n \"generic\",\n \"record() called while a recording is in flight — ClipRecorder handles one recording at a time\",\n ),\n );\n }\n this.recorder = null; // so a failed construction below never blocks a retry\n // De-hooking correction (zero impact on clip bytes): v1 got fresh refs per hook\n // mount, so a previous recording's post-settle grace timer (the hard-stop callback\n // arms it AFTER settle cleared timers — v1-verbatim) could never leak into a new\n // one. On a reused instance it would fire mid-flight, settle the fresh state, and\n // clear the new hard-stop timer — so drop stale timers before starting.\n this.clearTimers();\n this.chunks = [];\n this.bytes = 0;\n this.settled = false;\n this.stream = stream;\n const picked = pickMimeType();\n\n return new Promise<ClipResult>((resolve, reject) => {\n // Guarded construction — iOS Safari may lack MediaRecorder entirely (REVIEW FIX 1).\n let recorder: MediaRecorder;\n try {\n recorder = new MediaRecorder(\n stream,\n picked\n ? { mimeType: picked, videoBitsPerSecond: VIDEO_BPS }\n : { videoBitsPerSecond: VIDEO_BPS },\n );\n } catch {\n try {\n recorder = new MediaRecorder(stream); // last resort: browser defaults\n } catch {\n reject(new ClipError(\"unsupported\"));\n return;\n }\n }\n this.recorder = recorder;\n\n const settle = (fn: () => void) => {\n if (this.settled) return;\n this.settled = true;\n this.clearTimers();\n fn();\n };\n\n const finalize = () =>\n settle(() => {\n const type = recorder.mimeType || picked || DEFAULT_CLIP_MIME;\n const blob = new Blob(this.chunks, { type });\n if (isClipTooLarge(blob)) {\n reject(new ClipError(\"cliptoolarge\"));\n return;\n }\n resolve({ blob, mimeType: type });\n });\n\n recorder.ondataavailable = (e: BlobEvent) => {\n if (e.data && e.data.size) {\n this.chunks.push(e.data);\n this.bytes += e.data.size;\n // Enforce the 8MB cap INCREMENTALLY — stop + reject as soon as exceeded (REVIEW FIX 7).\n if (this.bytes > MAX_CLIP_BYTES) {\n settle(() => {\n try {\n if (recorder.state !== \"inactive\") recorder.stop();\n } catch {\n /* noop */\n }\n reject(new ClipError(\"cliptoolarge\"));\n });\n }\n }\n };\n recorder.onstop = finalize;\n recorder.onerror = () => settle(() => reject(new ClipError(\"generic\")));\n\n try {\n recorder.start(TIMESLICE_MS);\n } catch {\n settle(() => reject(new ClipError(\"generic\")));\n return;\n }\n\n // Hard stop at CLIP_MS; gate on state, not on frame count.\n this.hardStopTimer = setTimeout(() => {\n // Recording window elapsed — the clip is DONE. Notify BEFORE finalize\n // (which may lag on iOS) so the host can freeze on the keyframe now.\n // Guarded by `settled`: an error/cliptoolarge before the hard-stop has\n // already cleared this timer, so this only runs for a real stop.\n if (!this.settled) onStopped?.();\n if (recorder.state !== \"inactive\") {\n try {\n recorder.stop();\n } catch {\n finalize();\n }\n // iOS safety-net: onstop may never fire — finalize from chunks (REVIEW FIX 3).\n this.graceTimer = setTimeout(finalize, STOP_GRACE_MS);\n } else {\n finalize();\n }\n }, CLIP_MS);\n });\n }\n\n /** Idempotent teardown: stop recorder + ALL tracks (v1 `stopAndCleanup` + the unmount\n * effect). Optional `stream` override mirrors v1's `stopAndCleanup(stream?)`.\n *\n * MID-RECORDING SEMANTICS (verbatim v1): calling stop() while a record() is in\n * flight may LATER resolve the pending promise with a TRUNCATED clip (recorder.stop()\n * fires onstop → finalize from whatever chunks arrived so far), or — because this\n * method clears the hard-stop/grace timers — never settle it at all if onstop never\n * fires (the iOS grace path has been torn down). Callers MUST NOT treat a resolution\n * arriving after stop() as a full CLIP_MS clip, and must not await a record()\n * promise past stop() without their own timeout. (Task 4's CaptureSession owns\n * sequencing on top of this contract.) */\n stop(stream?: MediaStream | null): void {\n this.clearTimers();\n const rec = this.recorder;\n if (rec && rec.state !== \"inactive\") {\n try {\n rec.stop();\n } catch {\n /* noop */\n }\n }\n this.recorder = null;\n const s = stream ?? this.stream;\n s?.getTracks().forEach((t) => t.stop());\n this.stream = null;\n }\n}\n","// PORTED byte-identical from kyc-ibtikar/lib/faceCaptureV2/keyframe.ts (calibration-pinned\n// capture contract — spec §6 / risk 1). Do NOT edit without a calibration re-validation.\n\n// Grab ONE mirrored frontal keyframe from the live <video> into a JPEG Blob (a file part,\n// not a dataURL). Mirror matches the selfie preview (front camera), same as FaceCapture v1.\nexport function captureMirroredKeyframe(\n video: HTMLVideoElement,\n canvas: HTMLCanvasElement,\n quality = 0.92,\n): Promise<Blob> {\n return new Promise((resolve, reject) => {\n const w = video.videoWidth;\n const h = video.videoHeight;\n if (!w || !h) {\n reject(new Error(\"video-not-ready\"));\n return;\n }\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n reject(new Error(\"no-2d-context\"));\n return;\n }\n canvas.width = w;\n canvas.height = h;\n ctx.scale(-1, 1);\n ctx.drawImage(video, -w, 0);\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(\"empty-keyframe\"))),\n \"image/jpeg\",\n quality,\n );\n });\n}\n","// Device profiles (Task 9) — LOW/MID/HIGH from a boot probe, refined by the\n// first measured detect times. Profiles adapt DETECTION cost only (cadence,\n// downscale input width, animation richness): clip constraints are NOT here —\n// the 720p/2.5Mbps floor is global (locked decision 1) and lives in the v1\n// constraint builder both engines share.\n\nexport type DeviceClass = \"LOW\" | \"MID\" | \"HIGH\";\n\nexport interface DeviceProfile {\n /** Detection cadence — the scheduler's pace. */\n cadenceHz: number;\n /** Downscaled snapshot width handed to the detector. */\n inputWidth: number;\n /** Chrome hint: 'reduced' tones down ticks/breathe on LOW even without\n * prefers-reduced-motion. */\n animations: \"full\" | \"reduced\";\n}\n\nexport const DEVICE_PROFILES: Readonly<Record<DeviceClass, Readonly<DeviceProfile>>> =\n Object.freeze({\n LOW: Object.freeze({ cadenceHz: 5, inputWidth: 128, animations: \"reduced\" as const }),\n MID: Object.freeze({ cadenceHz: 8, inputWidth: 160, animations: \"full\" as const }),\n HIGH: Object.freeze({ cadenceHz: 10, inputWidth: 192, animations: \"full\" as const }),\n });\n\nexport interface DeviceProbe {\n cores: number;\n /** null where the UA hides it — iOS Safari NEVER exposes deviceMemory\n * (field lesson: a 2GB default classified every iPhone LOW). Unknown is\n * treated as NEUTRAL (4GB): never demotes to LOW on its own, never\n * promotes to HIGH; the dynamic profiler corrects truly slow devices. */\n memGB: number | null;\n /** WebAssembly SIMD available (informs wasm-tier expectations/telemetry). */\n simd: boolean;\n}\n\n/** Canonical WASM SIMD feature-test module (validates only where SIMD is\n * supported) — the standard wasm-feature-detect bytes. */\nconst SIMD_TEST_BYTES = new Uint8Array([\n 0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253,\n 15, 253, 98, 11,\n]);\n\nexport function probeDevice(): DeviceProbe {\n const nav = typeof navigator === \"undefined\" ? undefined : navigator;\n let simd = false;\n try {\n simd = typeof WebAssembly !== \"undefined\" && WebAssembly.validate(SIMD_TEST_BYTES);\n } catch {\n simd = false;\n }\n return {\n cores: nav?.hardwareConcurrency ?? 2,\n memGB: (nav as { deviceMemory?: number } | undefined)?.deviceMemory ?? null,\n simd,\n };\n}\n\n/** Static class: LOW (<4 cores or <3GB), HIGH (≥8 cores AND ≥6GB), else MID.\n * Unknown memory counts as a neutral 4GB (see DeviceProbe.memGB). */\nexport function classifyDevice(probe: DeviceProbe): DeviceClass {\n const memGB = probe.memGB ?? 4;\n if (probe.cores < 4 || memGB < 3) return \"LOW\";\n if (probe.cores >= 8 && memGB >= 6) return \"HIGH\";\n return \"MID\";\n}\n\nconst ORDER: DeviceClass[] = [\"LOW\", \"MID\", \"HIGH\"];\n\n/** Dynamic refinement: after the FIRST 10 measured detect ticks, a mean over\n * 70ms demotes straight to LOW; under 25ms promotes ONE class — at most one\n * adjustment ever (no oscillation). */\nexport class DeviceProfiler {\n private current: DeviceClass;\n private readonly samples: number[] = [];\n private adjusted = false;\n\n constructor(initial: DeviceClass) {\n this.current = initial;\n }\n\n get deviceClass(): DeviceClass {\n return this.current;\n }\n\n get profile(): DeviceProfile {\n return DEVICE_PROFILES[this.current];\n }\n\n record(detectMs: number): DeviceClass {\n if (this.adjusted || this.samples.length >= 10) return this.current;\n this.samples.push(detectMs);\n if (this.samples.length < 10) return this.current;\n this.adjusted = true;\n const mean = this.samples.reduce((sum, value) => sum + value, 0) / this.samples.length;\n if (mean > 70) {\n this.current = \"LOW\";\n } else if (mean < 25) {\n const index = ORDER.indexOf(this.current);\n this.current = ORDER[Math.min(ORDER.length - 1, index + 1)] ?? this.current;\n }\n return this.current;\n }\n}\n","// Detect scheduler (Task 9) — paces the detection pass at the profile's\n// cadence, preferring requestVideoFrameCallback (runs only when a REAL camera\n// frame arrived) with a plain interval fallback. A tick whose predecessor is\n// still in flight is SKIPPED — never queued (no backlog on a slow device) —\n// and counted as a degrade for telemetry.\n\nexport interface DetectSchedulerOptions {\n /** Ticks per second (profile cadenceHz). */\n cadenceHz: number;\n /** One detection pass — the scheduler awaits it and measures wall time. */\n run: () => Promise<void>;\n /** When provided AND it exposes requestVideoFrameCallback, ticks ride real\n * camera frames (throttled to the cadence) instead of a timer. */\n video?: HTMLVideoElement;\n /** Injectable clock (tests) — defaults to performance.now. */\n now?: () => number;\n}\n\ninterface VideoFrameCallbackTarget {\n requestVideoFrameCallback?: (callback: () => void) => number;\n cancelVideoFrameCallback?: (id: number) => void;\n}\n\n/** Rolling window for meanDetectMs. */\nconst STATS_WINDOW = 20;\n\nexport class DetectScheduler {\n private readonly options: DetectSchedulerOptions;\n private readonly periodMs: number;\n private timer: ReturnType<typeof setInterval> | null = null;\n private frameCallbackId: number | null = null;\n private running = false;\n private busy = false;\n private lastRunAt = Number.NEGATIVE_INFINITY;\n private degradeCount = 0;\n private readonly durations: number[] = [];\n\n constructor(options: DetectSchedulerOptions) {\n this.options = options;\n this.periodMs = 1000 / options.cadenceHz;\n }\n\n start(): void {\n if (this.running) return;\n this.running = true;\n const video = this.options.video as VideoFrameCallbackTarget | undefined;\n if (typeof video?.requestVideoFrameCallback === \"function\") {\n const loop = () => {\n if (!this.running) return;\n this.frameCallbackId = video.requestVideoFrameCallback?.(() => {\n this.frameCallbackId = null;\n // Throttle: camera frames arrive faster than the cadence.\n if (this.now() - this.lastRunAt >= this.periodMs) this.tick();\n loop();\n }) as number;\n };\n loop();\n return;\n }\n this.timer = setInterval(() => this.tick(), this.periodMs);\n }\n\n stop(): void {\n this.running = false;\n if (this.timer !== null) {\n clearInterval(this.timer);\n this.timer = null;\n }\n if (this.frameCallbackId !== null) {\n (this.options.video as VideoFrameCallbackTarget | undefined)?.cancelVideoFrameCallback?.(\n this.frameCallbackId,\n );\n this.frameCallbackId = null;\n }\n }\n\n stats(): { degradeCount: number; meanDetectMs: number } {\n const meanDetectMs =\n this.durations.length === 0\n ? 0\n : this.durations.reduce((sum, value) => sum + value, 0) / this.durations.length;\n return { degradeCount: this.degradeCount, meanDetectMs };\n }\n\n private now(): number {\n return this.options.now?.() ?? performance.now();\n }\n\n private tick(): void {\n if (!this.running) return;\n if (this.busy) {\n // Skip-when-busy: no queue growth on slow devices; telemetry counts it.\n this.degradeCount += 1;\n return;\n }\n this.busy = true;\n this.lastRunAt = this.now();\n const startedAt = this.now();\n this.options\n .run()\n .catch(() => undefined) // a failed pass must never kill the loop\n .finally(() => {\n this.busy = false;\n this.durations.push(this.now() - startedAt);\n if (this.durations.length > STATS_WINDOW) this.durations.shift();\n });\n }\n}\n","// FaceGateV3 (Task 8) — the v3 gate: scale-invariant, landmark-aware,\n// continuous-quality, gentle-drain. PARALLEL to the calibration-pinned v1\n// FaceGate (face-gate.ts) — nothing here feeds or alters v1's math.\n//\n// Design deltas vs v1 (the two field complaints this kills):\n// - the gate point is the EYE MIDPOINT when the tier provides eyes (humans\n// center their EYES, detectors center their BBOX — v1's bbox-center gate\n// read a centered face as \"high\"); box-only tiers get a documented\n// 0.12×height upward shift instead;\n// - progress is a CONTINUOUS integrator (rise 0.9/s on good quality, fall\n// 0.25/s on bad) instead of v1's ±counters — a wobble dips the progress\n// slightly instead of resetting the attempt.\n//\n// q AGGREGATION = MIN over factors (operator ruling 2026-07-12, replacing\n// the original 7-factor PRODUCT). Field finding: with a product, seven\n// factors each at a decent 0.85 gave q≈0.32 — progress DRAINED while every\n// factor sat above its guidance threshold, so the chrome said \"hold still\"\n// and the only discoverable escape was leaning unnaturally close (size,\n// centering falloff (∝IOD) and crop sharpness all improve together with\n// proximity). min() makes q the same quantity guidance reasons about — the\n// worst factor — so \"no complaint\" ⇔ \"progress rises\" by construction, one\n// genuinely bad factor still blocks (non-compensatory), and many mediocre\n// factors (the low-end-device reality: noisy sensor, dim room, wide FOV)\n// are no longer multiplied into failure.\n//\n// 100% deterministic: the clock is a tick INPUT (timestampMs); nothing here\n// reads Date.now or performance.now.\nimport type { FaceObservation } from \"./detector\";\nimport { EXPOSURE_BAND } from \"./frame-quality\";\n\nexport interface V3GateThresholds {\n /** q at/above which progress rises; below it progress falls. */\n Q_GOOD: number;\n /** Integrator slopes, per second. */\n RISE_PER_S: number;\n FALL_PER_S: number;\n /** Centering falloff radius = this × IOD (scale-invariant). The plan's\n * formula is 0.45 × IOD × 3 — the 0.45 echoes v1's CIRCLE_RADIUS_FACTOR\n * heritage — folded here to 1.35 × IOD. */\n CENTER_FALLOFF_IOD_MULTIPLE: number;\n /** Box-only tiers: gate point = bbox center shifted UP by this × height\n * (the documented bbox-vs-eyes offset heuristic). */\n BOX_EYE_OFFSET_FACTOR: number;\n /** Box-only IOD proxy = this × box width. */\n IOD_BOX_PROXY_FACTOR: number;\n /** IOD / frame short side: ideal band + hard limits. */\n SIZE_IDEAL_MIN: number;\n SIZE_IDEAL_MAX: number;\n SIZE_HARD_MIN: number;\n SIZE_HARD_MAX: number;\n /** Eye-line roll (deg) that zeroes the pose factor. */\n ROLL_MAX_DEG: number;\n /** Center velocity that zeroes the motion factor, in IOD-per-second. */\n MOTION_VELOCITY_IOD_PER_S: number;\n /** Velocity-EMA time constant (seconds) — the EMA weight is derived per\n * tick as 1−exp(−dt/τ), so the smoothing HORIZON is cadence-invariant:\n * a LOW device detecting at 5 Hz gets the same effective smoothing as a\n * HIGH device at 10 Hz (a fixed per-sample alpha under-smoothed exactly\n * the jittery low-cadence ticks that needed it most). */\n MOTION_TAU_S: number;\n /** Velocities at/below this (IOD/s) count as stillness — absorbs detector\n * box jitter (largest on LOW's 128px input) before it reaches the EMA. */\n MOTION_DEADBAND_IOD_PER_S: number;\n /** ready = p ≥ 1 AND no bad tick (q < Q_GOOD) left in this window after\n * TRIMMING: with ≥3 samples the single worst is dropped, so one flaky tick\n * (5 Hz LOW cadence = only 2–3 samples per window) cannot veto an\n * otherwise-ready user, while a SECOND bad tick (a real sustained wobble)\n * still blocks. The readiness bar is Q_GOOD itself — the level the\n * integrator rises on — so any input that fills the ring also fires it. */\n READY_WINDOW_MS: number;\n /** Per-factor guidance trigger levels (lowest failing factor speaks). */\n GUIDE_SIZE: number;\n GUIDE_CENTERING: number;\n GUIDE_POSE: number;\n GUIDE_EXPOSURE: number;\n GUIDE_SHARPNESS: number;\n GUIDE_MOTION: number;\n}\n\n/** Defaults — retuned 2026-07-12 (operator ruling superseding the 2026-07\n * freeze; the capture Task-15 refit set must be split at this boundary):\n * Q_GOOD 0.55→0.6 so it aligns with the 0.6 guidance thresholds under\n * min-q (\"no complaint\" ⇔ \"progress rises\"); SIZE_IDEAL_MIN 0.16→0.12 —\n * wide-FOV front cameras (common low-end) put an arm's-length face below\n * 0.16, silently forcing lean-in; the 720p clip floor keeps a 0.12-IOD\n * face fully resolvable server-side. */\nexport const V3_GATE_DEFAULTS: Readonly<V3GateThresholds> = Object.freeze({\n Q_GOOD: 0.6,\n RISE_PER_S: 0.9,\n FALL_PER_S: 0.25,\n CENTER_FALLOFF_IOD_MULTIPLE: 1.35,\n BOX_EYE_OFFSET_FACTOR: 0.12,\n IOD_BOX_PROXY_FACTOR: 0.42,\n SIZE_IDEAL_MIN: 0.12,\n SIZE_IDEAL_MAX: 0.3,\n SIZE_HARD_MIN: 0.08,\n SIZE_HARD_MAX: 0.45,\n ROLL_MAX_DEG: 25,\n MOTION_VELOCITY_IOD_PER_S: 0.5,\n MOTION_TAU_S: 0.4,\n MOTION_DEADBAND_IOD_PER_S: 0.1,\n READY_WINDOW_MS: 500,\n // (READY_MEAN_Q, the former 0.75 window MEAN, is gone — 2026-10-17 field\n // bug: a steady q anywhere in [Q_GOOD, 0.75) saturated p at 1, passed every\n // guide (\"hold still\") and could never reach the mean, so the ring sat full\n // and green with nothing recording. Readiness now checks the window against\n // Q_GOOD, which keeps the one-flaky-tick forgiveness and the second-bad-tick\n // block without a dead band above the rise threshold.)\n GUIDE_SIZE: 0.6,\n GUIDE_CENTERING: 0.6,\n GUIDE_POSE: 0.6,\n GUIDE_EXPOSURE: 0.6,\n GUIDE_SHARPNESS: 0.35,\n GUIDE_MOTION: 0.5,\n});\n\nexport interface V3GateQuality {\n /** From frame-quality.sharpness on the face crop. */\n sharpness: number;\n /** From frame-quality.exposure on the face crop. */\n exposure: number;\n /** Mean crop luma — picks more_light vs too_bright guidance. */\n meanLuma: number;\n}\n\nexport interface V3GateTickInput {\n timestampMs: number;\n observation: FaceObservation | null;\n quality: V3GateQuality;\n frameWidth: number;\n frameHeight: number;\n}\n\nexport type V3GateFactor =\n | \"presence\"\n | \"centering\"\n | \"size\"\n | \"pose\"\n | \"sharpness\"\n | \"exposure\"\n | \"motion\";\n\n/** Per-tick output — the telemetry + chrome contract. */\nexport interface V3GateTick {\n at: number;\n /** Instantaneous quality 0..1 (MIN over the factors — the worst factor). */\n q: number;\n /** Integrated progress 0..1. */\n p: number;\n factors: Record<V3GateFactor, number>;\n /** fcv3.* message key for the lowest failing factor (host translates). */\n guidanceKey: string;\n /** Fire signal: p ≥ 1 AND no second bad tick (q < Q_GOOD) in READY_WINDOW_MS. */\n ready: boolean;\n /** The face on THIS tick is ACCEPTED by the gate — present and every factor\n * (size, centering, pose, sharpness, exposure, motion) good enough that the\n * integrator RISES (q ≥ Q_GOOD). The engine's \"first accepted face\" mark. */\n accepted: boolean;\n}\n\nconst clamp01 = (value: number) => Math.min(1, Math.max(0, value));\n\nexport class FaceGateV3 {\n private readonly t: V3GateThresholds;\n private p = 0;\n private lastTickAt: number | null = null;\n private lastCenter: { x: number; y: number; at: number } | null = null;\n private velocityEma = 0;\n private qWindow: Array<{ at: number; q: number }> = [];\n\n constructor(overrides: Partial<V3GateThresholds> = {}) {\n this.t = { ...V3_GATE_DEFAULTS, ...overrides };\n }\n\n reset(): void {\n this.p = 0;\n this.lastTickAt = null;\n this.lastCenter = null;\n this.velocityEma = 0;\n this.qWindow = [];\n this.lastSizeDirection = \"ideal\";\n }\n\n tick(input: V3GateTickInput): V3GateTick {\n const { observation, quality, timestampMs } = input;\n const dtSeconds =\n this.lastTickAt === null\n ? 0\n : Math.max(0, (timestampMs - this.lastTickAt) / 1000);\n this.lastTickAt = timestampMs;\n\n const factors: Record<V3GateFactor, number> = {\n presence: 0,\n centering: 0,\n size: 0,\n pose: 0,\n sharpness: clamp01(quality.sharpness),\n exposure: clamp01(quality.exposure),\n motion: 1,\n };\n\n if (observation !== null) {\n factors.presence = 1;\n const geometry = this.geometryOf(observation, input);\n factors.centering = geometry.centering;\n factors.size = geometry.size;\n factors.pose = geometry.pose;\n factors.motion = this.motionOf(\n geometry.center,\n geometry.iod,\n timestampMs,\n );\n } else {\n // No face: the motion history must not survive to the next sighting\n // (a re-acquired face \"jumping\" from its old position is not motion,\n // and a stale pre-loss velocity EMA must not penalize re-acquisition).\n this.lastCenter = null;\n this.velocityEma = 0;\n }\n\n // Worst factor IS the quality (see header) — presence 0 zeroes it exactly\n // as the old product did; the compensatory many-mediocre penalty is gone.\n const q = Math.min(\n factors.presence,\n factors.centering,\n factors.size,\n factors.pose,\n factors.sharpness,\n factors.exposure,\n factors.motion,\n );\n\n // Integrator: gentle drain — bad ticks pull p down at FALL×(1−q), never\n // reset it (the v1 counter-reset feel this replaces).\n this.p = clamp01(\n this.p +\n (q >= this.t.Q_GOOD\n ? this.t.RISE_PER_S * q\n : -this.t.FALL_PER_S * (1 - q)) *\n dtSeconds,\n );\n\n // Sustained-quality window (prevents luck-firing through a wobble).\n // TRIMMED: with ≥3 samples the single worst is dropped — at LOW's 5 Hz\n // cadence a window holds 2–3 ticks, and one jittery detection must not\n // veto a user who is otherwise steadily ready. Every remaining sample must\n // be a GOOD tick (q ≥ Q_GOOD, the integrator's own rise level): a second\n // bad tick blocks, and — by construction — any steady input that fills\n // the ring is ready the moment the window is clean (no dead band where\n // p = 1, guidance says hold_still and nothing fires; 2026-10-17).\n this.qWindow.push({ at: timestampMs, q });\n this.qWindow = this.qWindow.filter(\n (entry) => entry.at >= timestampMs - this.t.READY_WINDOW_MS,\n );\n let windowSamples = this.qWindow.map((entry) => entry.q);\n if (windowSamples.length >= 3) {\n const worst = Math.min(...windowSamples);\n const worstIndex = windowSamples.indexOf(worst);\n windowSamples = windowSamples.filter((_, index) => index !== worstIndex);\n }\n const ready =\n this.p >= 1 && windowSamples.every((sample) => sample >= this.t.Q_GOOD);\n\n return {\n at: timestampMs,\n q,\n p: this.p,\n factors,\n guidanceKey: this.guidanceOf(factors, quality, q),\n ready,\n accepted: q >= this.t.Q_GOOD,\n };\n }\n\n private geometryOf(\n observation: FaceObservation,\n input: V3GateTickInput,\n ): {\n center: { x: number; y: number };\n iod: number;\n centering: number;\n size: number;\n pose: number;\n } {\n const { box, eyes } = observation;\n const frameCenter = { x: input.frameWidth / 2, y: input.frameHeight / 2 };\n const shortSide = Math.min(input.frameWidth, input.frameHeight);\n\n // Gate point: eye midpoint, or the bbox center shifted UP so box-only\n // tiers behave like eye tiers (BOX_EYE_OFFSET heuristic).\n const center =\n eyes !== undefined\n ? {\n x: (eyes.left.x + eyes.right.x) / 2,\n y: (eyes.left.y + eyes.right.y) / 2,\n }\n : {\n x: box.x + box.width / 2,\n y:\n box.y +\n box.height / 2 -\n this.t.BOX_EYE_OFFSET_FACTOR * box.height,\n };\n const iod =\n eyes !== undefined\n ? Math.hypot(eyes.right.x - eyes.left.x, eyes.right.y - eyes.left.y)\n : this.t.IOD_BOX_PROXY_FACTOR * box.width;\n\n // Smooth, SCALE-INVARIANT centering falloff: distance measured in IODs.\n const distance = Math.hypot(\n center.x - frameCenter.x,\n center.y - frameCenter.y,\n );\n const centering =\n iod <= 0\n ? 0\n : 1 - clamp01(distance / (this.t.CENTER_FALLOFF_IOD_MULTIPLE * iod));\n\n // Size factor from IOD relative to the frame short side.\n const ratio = shortSide > 0 ? iod / shortSide : 0;\n this.lastSizeDirection =\n ratio < this.t.SIZE_IDEAL_MIN\n ? \"far\"\n : ratio > this.t.SIZE_IDEAL_MAX\n ? \"close\"\n : \"ideal\";\n let size: number;\n if (ratio < this.t.SIZE_HARD_MIN || ratio > this.t.SIZE_HARD_MAX) {\n size = 0;\n } else if (ratio < this.t.SIZE_IDEAL_MIN) {\n size =\n (ratio - this.t.SIZE_HARD_MIN) /\n (this.t.SIZE_IDEAL_MIN - this.t.SIZE_HARD_MIN);\n } else if (ratio > this.t.SIZE_IDEAL_MAX) {\n size =\n (this.t.SIZE_HARD_MAX - ratio) /\n (this.t.SIZE_HARD_MAX - this.t.SIZE_IDEAL_MAX);\n } else {\n size = 1;\n }\n\n // Pose from the eye-line roll; box-only tiers stay neutral (no signal).\n let pose = 1;\n if (eyes !== undefined) {\n const rollDeg =\n (Math.abs(\n Math.atan2(eyes.right.y - eyes.left.y, eyes.right.x - eyes.left.x),\n ) *\n 180) /\n Math.PI;\n pose = 1 - clamp01(rollDeg / this.t.ROLL_MAX_DEG);\n }\n\n return { center, iod, centering, size, pose };\n }\n\n private motionOf(\n center: { x: number; y: number },\n iod: number,\n at: number,\n ): number {\n const previous = this.lastCenter;\n this.lastCenter = { ...center, at };\n if (previous === null || iod <= 0) return 1;\n const dtSeconds = (at - previous.at) / 1000;\n if (dtSeconds <= 0) return 1;\n let velocityIodPerS =\n Math.hypot(center.x - previous.x, center.y - previous.y) /\n iod /\n dtSeconds;\n // Deadband: sub-jitter velocities are stillness. Detector boxes quantize\n // hardest on LOW's 128px input, where a ±1px wobble at 5 Hz reads as\n // ~0.25 IOD/s — half the zeroing velocity — despite a rock-steady user.\n if (velocityIodPerS <= this.t.MOTION_DEADBAND_IOD_PER_S)\n velocityIodPerS = 0;\n // Time-constant EMA: alpha derived from dt so the smoothing horizon is\n // the same at 5 Hz and 10 Hz (see MOTION_TAU_S).\n const alpha = 1 - Math.exp(-dtSeconds / this.t.MOTION_TAU_S);\n this.velocityEma = alpha * velocityIodPerS + (1 - alpha) * this.velocityEma;\n return 1 - clamp01(this.velocityEma / this.t.MOTION_VELOCITY_IOD_PER_S);\n }\n\n /** Lowest failing factor speaks — fixed priority (diagnostic, incl. light):\n * presence → size → centering → pose → exposure → sharpness → hold.\n *\n * HONESTY FALLBACK (operator ruling 2026-07-12): when every factor clears\n * its guide threshold but q (= min factor) is still below Q_GOOD — i.e.\n * progress is DRAINING — \"hold still\" would be a lie (\"Perfect\" while\n * nothing advances; the field symptom was users discovering by accident\n * that only leaning in fires the gate). In that state the WORST factor\n * speaks even though it passed its guide level, so the user always hears\n * the change that actually unblocks recording. */\n private guidanceOf(\n factors: Record<V3GateFactor, number>,\n quality: V3GateQuality,\n q: number,\n ): string {\n if (factors.presence <= 0) return \"fcv3.no_face\";\n if (factors.size < this.t.GUIDE_SIZE) return this.sizeGuidance();\n if (factors.centering < this.t.GUIDE_CENTERING) return \"fcv3.center_face\";\n if (factors.pose < this.t.GUIDE_POSE) return \"fcv3.straighten\";\n if (factors.exposure < this.t.GUIDE_EXPOSURE)\n return this.exposureGuidance(quality);\n if (factors.sharpness < this.t.GUIDE_SHARPNESS) return \"fcv3.steady_camera\";\n if (factors.motion < this.t.GUIDE_MOTION) return \"fcv3.hold_still\";\n if (q < this.t.Q_GOOD) {\n // All guides pass but progress drains — coach the worst factor anyway.\n const coachable: Array<[V3GateFactor, string]> = [\n [\"size\", this.sizeGuidance()],\n [\"centering\", \"fcv3.center_face\"],\n [\"pose\", \"fcv3.straighten\"],\n [\"exposure\", this.exposureGuidance(quality)],\n [\"sharpness\", \"fcv3.steady_camera\"],\n [\"motion\", \"fcv3.hold_still\"],\n ];\n let worst = coachable[0] as [V3GateFactor, string];\n for (const entry of coachable) {\n if (factors[entry[0]] < factors[worst[0]]) worst = entry;\n }\n return worst[1];\n }\n return \"fcv3.hold_still\";\n }\n\n /** Direction from the size band set THIS tick (never inverted at the hard\n * limits: past SIZE_HARD_MAX the direction is 'close' → move_back). */\n private sizeGuidance(): string {\n return this.lastSizeDirection === \"far\"\n ? \"fcv3.come_closer\"\n : \"fcv3.move_back\";\n }\n\n /** Band midpoint — direction only (tracks EXPOSURE_BAND automatically). */\n private exposureGuidance(quality: V3GateQuality): string {\n const bandMid = (EXPOSURE_BAND[0] + EXPOSURE_BAND[1]) / 2;\n return quality.meanLuma < bandMid ? \"fcv3.more_light\" : \"fcv3.too_bright\";\n }\n\n /** Which side of the ideal band the last size measurement fell on. */\n private lastSizeDirection: \"far\" | \"close\" | \"ideal\" = \"ideal\";\n}\n","// CaptureSessionV3 (Task 10) — the v3 engine behind the v1 event surface\n// (`statechange | guidance | checks | artifacts | error`), plus ONE v3-only\n// chrome event (`frozen`, fired at recording-stop so the host can freeze the\n// display on the keyframe before the clip finalizes), so hosts bind it with\n// minimal changes. PARALLEL module: it REUSES v1's building blocks AS\n// IMPORTS — ClipRecorder (the 720p/2.5Mbps clip floor lives in ONE place),\n// captureMirroredKeyframe, buildCaptureConstraints, the camera error taxonomy\n// — and touches no v1 file.\n//\n// v3 deltas: detector LADDER (native → blaze; the face-api floor was dropped\n// 2026-07-09) on an adaptive scheduler with device profiles; FaceGateV3\n// (eye-midpoint, continuous\n// integrator); keyframe BEST-OF-N (sharpest of up to 4 candidates in the\n// first 600ms of the capture window); `checks` carries a v3 payload\n// discriminated on `engine: 'v3'` (no v1 counters).\n//\n// Supersession: retry()/switchCamera()/destroy() bump the attempt token —\n// in-flight async continuations from an old attempt observe the bump and\n// stop (the v1 pattern, ported; see capture-session.ts header).\nimport { buildCaptureConstraints, detectIsMobile } from \"../capture-constraints\";\nimport { ClipRecorder, type ClipResult } from \"../clip-recorder\";\nimport { captureMirroredKeyframe } from \"../keyframe\";\nimport { cameraPreflight, isClipTooLarge, mapGetUserMediaError } from \"../media\";\nimport type { CaptureArtifacts, CaptureErrorKind, CaptureState, CaptureTiming } from \"../types\";\nimport {\n createDetector as createLadderDetector,\n type Detector,\n DetectorLadderError,\n intrinsicSize,\n} from \"./detector\";\nimport { WorkerDetectorProxy, type WorkerLike, workerOffloadSupported } from \"./detect-worker\";\nimport {\n classifyDevice,\n type DeviceClass,\n DeviceProfiler,\n probeDevice,\n} from \"./device-profile\";\nimport { DetectScheduler } from \"./detect-scheduler\";\nimport {\n FaceGateV3,\n type V3GateFactor,\n type V3GateQuality,\n type V3GateThresholds,\n} from \"./face-gate-v3\";\nimport { exposure, meanLuma, sharpness } from \"./frame-quality\";\n\n/** Best-of-N keyframe window: up to 4 candidates over the first 600ms. */\nexport const KEYFRAME_CANDIDATES = 4;\nexport const KEYFRAME_WINDOW_MS = 600;\n\nexport interface V3ChecksPayload {\n engine: \"v3\";\n /** Gate integrator 0..1 — the ring drives off this directly. */\n progress: number;\n /** Instantaneous quality 0..1. */\n q: number;\n factors: Record<V3GateFactor, number>;\n /** Attempt ordinal (bumps on retry/switchCamera). */\n attempt: number;\n}\n\nexport interface CaptureSessionV3Events {\n statechange: { state: CaptureState };\n checks: V3ChecksPayload;\n /** fcv3.* message KEY — hosts translate; never prose. Deduped. */\n guidance: { key: string };\n artifacts: CaptureArtifacts;\n error: { kind: CaptureErrorKind; retryable: boolean };\n /** v3-only, CHROME-only (no data flows here): fires the instant the clip\n * recording window ends — BEFORE the blob finalizes (iOS onstop can lag up\n * to STOP_GRACE_MS). Carries the already-captured keyframe's JPEG data URL so\n * the host can freeze the display on the still immediately instead of leaving\n * the live feed running while the clip flushes. Clip mode only (photo mode\n * goes straight to `artifacts`). A host that ignores it is unaffected. */\n frozen: { previewDataUrl: string };\n}\n\nexport type V3TelemetryEvent =\n | { type: \"detector\"; tier: Detector[\"tier\"]; fallbackReason?: string }\n | { type: \"profile\"; staticClass: DeviceClass; activeClass: DeviceClass; simd: boolean }\n | { type: \"fired\"; timeToGateMs: number; degradeCount: number; meanDetectMs: number }\n | { type: \"error\"; kind: CaptureErrorKind; fallbackReason?: string };\n\ninterface KeyframeCandidate {\n blob: Blob;\n previewDataUrl: string;\n sharpness: number;\n}\n\n/** Challenge-clip artifact for the Phase 3 escalation path. */\nexport interface EscalationClipArtifact {\n clip: Blob;\n clipMimeType: string;\n}\n\n/** INERT Phase 3 seam (docs/superpowers/specs/2026-07-07-active-liveness-\n * escalation-spec.md): when the server settles a liveness verdict in the\n * REVIEW band it may mint ONE gesture challenge; the host then asks the\n * engine for a challenge clip through this hook. Typed and documented NOW so\n * capture-v3+ starts from a design — NEVER invoked anywhere in the current\n * flow (pinned by a test). */\nexport interface EscalationHook {\n onBorderline(challenge: \"blink\" | \"turn\"): Promise<EscalationClipArtifact>;\n}\n\nexport interface CaptureSessionV3Options {\n /** Host-owned <video playsInline muted> — same iOS contract as v1. */\n video: HTMLVideoElement;\n /** v3 assets base (tasks-vision wasm + tflite); default /models-v3. */\n v3AssetBaseUrl?: string;\n mode?: \"clip\" | \"photo\";\n /** FaceGateV3 threshold overrides (Phase 2 tuning). */\n thresholds?: Partial<V3GateThresholds>;\n telemetry?: (event: V3TelemetryEvent) => void;\n facingMode?: \"user\" | \"environment\";\n /** Static class override (tests / host pinning); default = boot probe. */\n deviceClass?: DeviceClass;\n // ── seams (production defaults in parens) ─────────────────────────────────\n /** (detector ladder) */\n createDetector?: () => Promise<Detector>;\n /** (fresh ClipRecorder per attempt) */\n createRecorder?: () => Pick<ClipRecorder, \"record\" | \"stop\">;\n /** (navigator.mediaDevices.getUserMedia) */\n getUserMedia?: (constraints: MediaStreamConstraints) => Promise<MediaStream>;\n /** (mirrored canvas keyframe + downscaled Laplacian sharpness) */\n captureKeyframe?: (video: HTMLVideoElement) => Promise<KeyframeCandidate>;\n /** (downscaled face-crop sharpness/exposure via canvas) */\n measureQuality?: (\n video: HTMLVideoElement,\n box: { x: number; y: number; width: number; height: number },\n ) => V3GateQuality;\n /** (reused canvas at profile.inputWidth; falls back to the raw video where\n * canvas is unavailable) */\n makeSnapshot?: (video: HTMLVideoElement, targetWidth: number) => CanvasImageSource;\n /** Worker offload opt-in (Task 9): when provided AND the platform supports\n * it (OffscreenCanvas + createImageBitmap + Worker), detection runs\n * worker-side through WorkerDetectorProxy. Hosts own Worker construction\n * (bundler-specific `new Worker(new URL(...))`). */\n createWorker?: () => WorkerLike;\n /** INERT (Phase 3 escalation seam — see EscalationHook). Accepted and\n * retained, never called in Phases 1–2. */\n escalation?: EscalationHook;\n /** (performance.now) */\n now?: () => number;\n}\n\ntype Handler<K extends keyof CaptureSessionV3Events> = (\n payload: CaptureSessionV3Events[K],\n) => void;\n\nconst NEUTRAL_QUALITY: V3GateQuality = { sharpness: 1, exposure: 1, meanLuma: 128 };\n\nexport class CaptureSessionV3 {\n private readonly options: CaptureSessionV3Options;\n private readonly handlers = new Map<string, Set<(payload: never) => void>>();\n private stateValue: CaptureState = \"idle\";\n private attempt = 0;\n private facing: \"user\" | \"environment\";\n private stream: MediaStream | null = null;\n private detector: Detector | null = null;\n private scheduler: DetectScheduler | null = null;\n private gate: FaceGateV3 | null = null;\n private profiler: DeviceProfiler | null = null;\n private lastGuidanceKey: string | null = null;\n private positioningStartedAt = 0;\n // Face-capture timing marks of the CURRENT attempt (engine clock; reset per run).\n private cameraRequestedAt: number | null = null;\n private cameraReadyAt: number | null = null;\n private faceAcquiredAt: number | null = null;\n private captureCompleteAt: number | null = null;\n private fired = false;\n private carriedDegrade = 0;\n private carriedMeans: number[] = [];\n\n constructor(options: CaptureSessionV3Options) {\n this.options = options;\n this.facing = options.facingMode ?? \"user\";\n }\n\n get state(): CaptureState {\n return this.stateValue;\n }\n\n on<K extends keyof CaptureSessionV3Events>(event: K, handler: Handler<K>): () => void {\n let set = this.handlers.get(event);\n if (!set) {\n set = new Set();\n this.handlers.set(event, set);\n }\n set.add(handler as (payload: never) => void);\n return () => set?.delete(handler as (payload: never) => void);\n }\n\n private emit<K extends keyof CaptureSessionV3Events>(\n event: K,\n payload: CaptureSessionV3Events[K],\n ): void {\n for (const handler of [...(this.handlers.get(event) ?? [])]) {\n (handler as Handler<K>)(payload);\n }\n }\n\n async start(): Promise<void> {\n this.attempt += 1;\n await this.run(this.attempt);\n }\n\n /** Full reset + camera restart — the documented recovery path (v1 parity). */\n retry(): void {\n this.teardownAttempt();\n void this.start();\n }\n\n switchCamera(): void {\n this.facing = this.facing === \"user\" ? \"environment\" : \"user\";\n this.teardownAttempt();\n void this.start();\n }\n\n destroy(): void {\n this.attempt += 1; // supersede in-flight continuations\n this.teardownAttempt();\n this.detector?.destroy();\n this.detector = null;\n this.options.video.srcObject = null;\n }\n\n // ── attempt lifecycle ──────────────────────────────────────────────────────\n\n private async run(attempt: number): Promise<void> {\n const { options } = this;\n this.fired = false;\n this.lastGuidanceKey = null;\n this.carriedDegrade = 0;\n this.carriedMeans = [];\n this.cameraRequestedAt = null;\n this.cameraReadyAt = null;\n this.faceAcquiredAt = null;\n this.captureCompleteAt = null;\n this.setState(\"preflight\");\n const preflight = cameraPreflight();\n if (preflight !== \"OK\") {\n this.fail(preflight === \"INSECURE_CONTEXT\" ? \"insecure\" : \"unsupported\");\n return;\n }\n\n this.setState(\"acquiring\");\n const getUserMedia =\n options.getUserMedia ??\n ((constraints: MediaStreamConstraints) => navigator.mediaDevices.getUserMedia(constraints));\n let stream: MediaStream;\n this.cameraRequestedAt = this.now();\n try {\n // The 720p/2.5Mbps floor: v1's constraint builder, imported — never forked.\n stream = await getUserMedia(buildCaptureConstraints(detectIsMobile(), this.facing));\n } catch (err) {\n if (attempt !== this.attempt) return;\n this.fail(mapGetUserMediaError(err));\n return;\n }\n if (attempt !== this.attempt) {\n for (const track of stream.getTracks()) track.stop();\n return;\n }\n this.stream = stream;\n options.video.srcObject = stream;\n await options.video.play().catch(() => undefined);\n // play() awaited — a retry/destroy may have superseded this attempt while\n // it was suspended; continuing would overwrite the LIVE attempt's\n // profiler/gate/scheduler with stale ones (review S2-2).\n if (attempt !== this.attempt) return;\n // Camera ready: stream attached + preview playing (the detector may still\n // be loading — that wait is part of the capture phase, not the camera's).\n this.cameraReadyAt = this.now();\n\n if (this.detector === null) {\n const ladderOptions = {\n ...(options.v3AssetBaseUrl !== undefined\n ? { v3AssetBaseUrl: options.v3AssetBaseUrl }\n : {}),\n };\n const create =\n options.createDetector ??\n (options.createWorker !== undefined && workerOffloadSupported()\n ? async () => {\n // Worker offload (Task 9): the ladder runs WORKER-side; a\n // failed/hung init falls back to the in-thread ladder.\n try {\n return await WorkerDetectorProxy.create(\n (options.createWorker as () => WorkerLike)(),\n ladderOptions,\n );\n } catch {\n return createLadderDetector(ladderOptions);\n }\n }\n : () => createLadderDetector(ladderOptions));\n let created: Detector;\n try {\n created = await create();\n } catch (err) {\n // With the face-api floor gone, the ladder REJECTS once exhausted\n // (DetectorLadderError) — and any throwing seam is likewise a models\n // failure. Surface a retryable error{kind:'models'} (never a hang),\n // carrying the per-tier reasons for telemetry so a blaze init failure\n // stays observable in the field.\n if (attempt === this.attempt) {\n this.fail(\n \"models\",\n err instanceof DetectorLadderError ? err.fallbackReason : describeError(err),\n );\n }\n return;\n }\n // Resolve into a LOCAL first: assigning a detector into a superseded\n // (or destroyed) session would leak it — destroy() already nulled the\n // field, and a racing attempt may have created its own (review S2-3).\n if (attempt !== this.attempt) {\n created.destroy();\n return;\n }\n this.detector = created;\n this.options.telemetry?.({\n type: \"detector\",\n tier: created.tier,\n ...(created.fallbackReason !== undefined\n ? { fallbackReason: created.fallbackReason }\n : {}),\n });\n }\n\n const probe = probeDevice();\n const staticClass = options.deviceClass ?? classifyDevice(probe);\n this.profiler = new DeviceProfiler(staticClass);\n this.options.telemetry?.({\n type: \"profile\",\n staticClass,\n activeClass: this.profiler.deviceClass,\n simd: probe.simd,\n });\n\n this.gate = new FaceGateV3(options.thresholds ?? {});\n this.setState(\"positioning\");\n this.positioningStartedAt = this.now();\n this.startScheduler(attempt);\n }\n\n /** Chrome hint (Task 11): 'reduced' on LOW even without the media query. */\n get animations(): \"full\" | \"reduced\" {\n return this.profiler?.profile.animations ?? \"full\";\n }\n\n private startScheduler(attempt: number): void {\n const profiler = this.profiler;\n if (profiler === null) return;\n if (this.scheduler !== null) {\n // A dynamic profile change replaces the scheduler — carry its stats so\n // the `fired` telemetry keeps the PRE-adjustment struggle visible.\n const stats = this.scheduler.stats();\n this.carriedDegrade += stats.degradeCount;\n if (stats.meanDetectMs > 0) this.carriedMeans.push(stats.meanDetectMs);\n this.scheduler.stop();\n }\n const scheduler = new DetectScheduler({\n cadenceHz: profiler.profile.cadenceHz,\n video: this.options.video,\n ...(this.options.now !== undefined ? { now: this.options.now } : {}),\n run: () => this.detectTick(attempt),\n });\n this.scheduler = scheduler;\n scheduler.start();\n }\n\n private async detectTick(attempt: number): Promise<void> {\n const { detector, gate, profiler } = this;\n const video = this.options.video;\n if (attempt !== this.attempt || detector === null || gate === null || profiler === null)\n return;\n if (this.fired || video.videoWidth === 0) return;\n\n const before = profiler.deviceClass;\n const t0 = this.now();\n // Profile-adaptive DOWNSCALE: the detector sees an inputWidth-wide\n // snapshot, never the full 720p frame (the profile system's perf win);\n // stream-px normalization scales the observation back up.\n const source =\n this.options.makeSnapshot?.(video, profiler.profile.inputWidth) ??\n this.defaultSnapshot(video, profiler.profile.inputWidth);\n let observation: Awaited<ReturnType<Detector[\"detect\"]>> = null;\n try {\n observation = await detector.detect(source, video.videoWidth, video.videoHeight);\n } catch {\n observation = null; // detectors never reject by contract; belt-and-braces\n }\n if (attempt !== this.attempt || this.fired) return;\n profiler.record(this.now() - t0);\n if (profiler.deviceClass !== before) this.startScheduler(attempt); // cadence follows the class\n\n const quality =\n observation === null\n ? NEUTRAL_QUALITY\n : (this.options.measureQuality?.(video, observation.box) ??\n this.defaultQuality(video, observation.box));\n\n const tick = gate.tick({\n timestampMs: this.now(),\n observation,\n quality,\n frameWidth: video.videoWidth,\n frameHeight: video.videoHeight,\n });\n // First face ACCEPTED by the gate (size / centering / quality all good —\n // the integrator rises), not merely detected. Set once; a later loss and\n // re-acquisition never moves it.\n if (tick.accepted && this.faceAcquiredAt === null) this.faceAcquiredAt = tick.at;\n this.emit(\"checks\", {\n engine: \"v3\",\n progress: tick.p,\n q: tick.q,\n factors: tick.factors,\n attempt,\n });\n if (tick.guidanceKey !== this.lastGuidanceKey) {\n this.lastGuidanceKey = tick.guidanceKey;\n this.emit(\"guidance\", { key: tick.guidanceKey });\n }\n if (tick.ready && this.stateValue === \"positioning\") {\n this.fired = true;\n this.captureCompleteAt = tick.at; // gate progress 100 % — capture complete\n void this.fire(attempt);\n }\n }\n\n private async fire(attempt: number): Promise<void> {\n const { options } = this;\n const scheduler = this.scheduler;\n scheduler?.stop();\n const stats = scheduler?.stats() ?? { degradeCount: 0, meanDetectMs: 0 };\n const means = [...this.carriedMeans, ...(stats.meanDetectMs > 0 ? [stats.meanDetectMs] : [])];\n options.telemetry?.({\n type: \"fired\",\n timeToGateMs: this.now() - this.positioningStartedAt,\n degradeCount: this.carriedDegrade + stats.degradeCount,\n meanDetectMs:\n means.length === 0 ? 0 : means.reduce((sum, value) => sum + value, 0) / means.length,\n });\n\n const mode = options.mode ?? \"clip\";\n const bestOf = this.collectKeyframes(attempt);\n if (mode === \"photo\") {\n const best = await bestOf;\n if (attempt !== this.attempt) return;\n if (best === null) {\n this.fail(\"generic\");\n return;\n }\n this.finishCapture({ keyframe: best.blob, previewDataUrl: best.previewDataUrl });\n return;\n }\n\n this.setState(\"recording\");\n const recorder = options.createRecorder?.() ?? new ClipRecorder();\n const stream = this.stream;\n if (stream === null) {\n this.fail(\"generic\");\n return;\n }\n // The keyframe (the face_match selfie) resolves within the first 600ms —\n // well before the 2.5s clip ends. Latch it so the recording-STOP callback\n // below can emit `frozen` immediately, letting the host freeze the display\n // on the still without waiting for the clip blob to finalize (iOS lag).\n let frozenBest: KeyframeCandidate | null = null;\n void bestOf.then(\n (candidate) => {\n frozenBest = candidate;\n },\n () => {},\n );\n const onRecordingStopped = (): void => {\n if (attempt !== this.attempt) return; // superseded (retry/switch/destroy)\n if (frozenBest !== null) {\n this.emit(\"frozen\", { previewDataUrl: frozenBest.previewDataUrl });\n }\n };\n let clip: ClipResult;\n let best: KeyframeCandidate | null;\n try {\n [clip, best] = await Promise.all([recorder.record(stream, onRecordingStopped), bestOf]);\n } catch (err) {\n if (attempt !== this.attempt) return;\n const kind = (err as { kind?: CaptureErrorKind }).kind;\n this.fail(kind === \"cliptoolarge\" ? \"cliptoolarge\" : \"recorder\");\n return;\n }\n if (attempt !== this.attempt) return;\n if (isClipTooLarge(clip.blob)) {\n this.fail(\"cliptoolarge\");\n return;\n }\n if (best === null) {\n this.fail(\"generic\");\n return;\n }\n this.finishCapture({\n clip: clip.blob,\n clipMimeType: clip.mimeType,\n keyframe: best.blob,\n previewDataUrl: best.previewDataUrl,\n });\n }\n\n /** Up to KEYFRAME_CANDIDATES mirrored frames over KEYFRAME_WINDOW_MS —\n * sharpest wins (reuses frame-quality.sharpness via the capture seam). */\n private async collectKeyframes(attempt: number): Promise<KeyframeCandidate | null> {\n const capture = this.options.captureKeyframe ?? ((video) => this.defaultKeyframe(video));\n const stepMs = KEYFRAME_WINDOW_MS / (KEYFRAME_CANDIDATES - 1);\n let best: KeyframeCandidate | null = null;\n for (let i = 0; i < KEYFRAME_CANDIDATES; i += 1) {\n if (attempt !== this.attempt) return best;\n try {\n const candidate = await capture(this.options.video);\n if (best === null || candidate.sharpness > best.sharpness) best = candidate;\n } catch {\n // a failed candidate never sinks the capture — keep the best so far\n }\n if (i < KEYFRAME_CANDIDATES - 1) await delay(stepMs);\n }\n return best;\n }\n\n private finishCapture(artifacts: CaptureArtifacts): void {\n this.stopTracks(); // privacy posture: camera off the moment we have artifacts\n this.setState(\"captured\");\n const timing = this.captureTiming();\n this.emit(\"artifacts\", timing === undefined ? artifacts : { ...artifacts, timing });\n }\n\n /** The attempt's capture-phase timing (whole ms, ≥ 0); undefined when no mark landed. */\n private captureTiming(): CaptureTiming | undefined {\n const span = (from: number | null, to: number | null): number | undefined =>\n from !== null && to !== null ? Math.max(0, Math.round(to - from)) : undefined;\n const cameraReadyMs = span(this.cameraRequestedAt, this.cameraReadyAt);\n const faceAcquireMs = span(this.cameraReadyAt, this.faceAcquiredAt);\n // Product definition: from the FIRST accepted face (a later loss /\n // re-acquisition never moves the start) until the gate reached 100 %.\n const captureMs = span(this.faceAcquiredAt, this.captureCompleteAt);\n const timing: CaptureTiming = {\n ...(cameraReadyMs !== undefined ? { cameraReadyMs } : {}),\n ...(faceAcquireMs !== undefined ? { faceAcquireMs } : {}),\n ...(captureMs !== undefined ? { captureMs } : {}),\n };\n return Object.keys(timing).length > 0 ? timing : undefined;\n }\n\n // ── defaults for the canvas-bound seams ────────────────────────────────────\n\n private async defaultKeyframe(video: HTMLVideoElement): Promise<KeyframeCandidate> {\n const canvas = document.createElement(\"canvas\");\n const blob = await captureMirroredKeyframe(video, canvas);\n const previewDataUrl = canvas.toDataURL(\"image/jpeg\", 0.92);\n return { blob, previewDataUrl, sharpness: this.centerSharpness(video) };\n }\n\n /** Reused downscale canvas — falls back to the raw video where canvas 2D\n * is unavailable (jsdom) so tests and exotic hosts keep detecting. */\n private snapshotCanvas: HTMLCanvasElement | null = null;\n private defaultSnapshot(video: HTMLVideoElement, targetWidth: number): CanvasImageSource {\n try {\n const canvas = this.snapshotCanvas ?? document.createElement(\"canvas\");\n const scale = targetWidth / Math.max(1, video.videoWidth);\n const width = Math.max(1, Math.round(video.videoWidth * scale));\n const height = Math.max(1, Math.round(video.videoHeight * scale));\n const ctx = canvas.getContext(\"2d\");\n if (ctx === null) return video;\n if (canvas.width !== width) canvas.width = width;\n if (canvas.height !== height) canvas.height = height;\n ctx.drawImage(video, 0, 0, width, height);\n this.snapshotCanvas = canvas;\n return canvas;\n } catch {\n return video;\n }\n }\n\n private defaultQuality(\n video: HTMLVideoElement,\n box: { x: number; y: number; width: number; height: number },\n ): V3GateQuality {\n const crop = cropImageData(video, box, 96);\n if (crop === null) return NEUTRAL_QUALITY;\n return { sharpness: sharpness(crop), exposure: exposure(crop), meanLuma: meanLuma(crop) };\n }\n\n private centerSharpness(video: HTMLVideoElement): number {\n const size = intrinsicSize(video);\n const crop = cropImageData(\n video,\n { x: size.width * 0.25, y: size.height * 0.25, width: size.width * 0.5, height: size.height * 0.5 },\n 96,\n );\n return crop === null ? 0 : sharpness(crop);\n }\n\n // ── teardown ──────────────────────────────────────────────────────────────\n\n private teardownAttempt(): void {\n // No token bump here: retry()/switchCamera() call start() synchronously\n // right after, and START's own bump supersedes; destroy() bumps itself.\n this.scheduler?.stop();\n this.scheduler = null;\n this.gate = null;\n this.stopTracks();\n }\n\n private stopTracks(): void {\n if (this.stream === null) return;\n for (const track of this.stream.getTracks()) track.stop();\n this.stream = null;\n }\n\n private fail(kind: CaptureErrorKind, fallbackReason?: string): void {\n this.scheduler?.stop();\n this.stopTracks();\n this.setState(\"error\");\n this.options.telemetry?.({\n type: \"error\",\n kind,\n ...(fallbackReason !== undefined ? { fallbackReason } : {}),\n });\n // v1 canRetry semantics (capture-session.ts L424).\n this.emit(\"error\", { kind, retryable: kind !== \"insecure\" && kind !== \"unsupported\" });\n }\n\n private setState(state: CaptureState): void {\n this.stateValue = state;\n this.emit(\"statechange\", { state });\n }\n\n private now(): number {\n return this.options.now?.() ?? performance.now();\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction describeError(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Downscaled face-crop ImageData for the quality measures; null where canvas\n * is unavailable (jsdom) — callers fall back to neutral quality. */\nfunction cropImageData(\n video: HTMLVideoElement,\n box: { x: number; y: number; width: number; height: number },\n targetWidth: number,\n): ImageData | null {\n try {\n const canvas = document.createElement(\"canvas\");\n const scale = targetWidth / Math.max(1, box.width);\n canvas.width = Math.max(1, Math.round(box.width * scale));\n canvas.height = Math.max(1, Math.round(box.height * scale));\n const ctx = canvas.getContext(\"2d\");\n if (ctx === null) return null;\n ctx.drawImage(\n video,\n box.x,\n box.y,\n box.width,\n box.height,\n 0,\n 0,\n canvas.width,\n canvas.height,\n );\n return ctx.getImageData(0, 0, canvas.width, canvas.height);\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":["best"]}