feat(face): SP-A client-side embedding — production-ready + GPU-less-enforced#222
Merged
Merged
Conversation
…istence) The 47 MB Facenet512 FP16 model is now hosted at the manifest's base_url_default (app.fivucsas.com/models). Adding it to manifest.json makes prebuild->fetch-models fetch + SHA256-verify + bundle it into dist/models so the deploy rsync can't wipe a manual upload, and it fans out to the verify build. Matches DEFAULT_FACENET_MODEL_URL / DEFAULT_FACENET_MODEL_SHA256 in facenetEmbedder.ts.
…N (GPU-less enforcement)
The CPU-only server now EXPECTS the embedding and 400s the legacy image
path while app.auth.client-side-embedding is ON. Previously a null
embedding (no model / ORT load fail / inference error) silently fell back
to verifyStep(FACE, { image }) — an upload the server now rejects, so the
login failed with an opaque error.
Now, when the flag is ON and embedCapturedFace returns null, surface a
clear retryable error (mfa.face.clientPrepFailed, EN+TR) via onError so it
renders in FaceCaptureStep's existing error Alert, and return WITHOUT
sending the image. The captured frame stays so the user can re-submit.
Flag OFF is unchanged: byte-identical legacy { image } upload path. Also
adds the mfa.face.preparingSecure key (EN+TR) consumed by the next commit.
…en 47 MB-on-submit) Adds prefetchFacenetModel() + scheduleFacenetPrefetch() in the embedding feature: flag-gated (no-op when VITE_CLIENT_SIDE_EMBEDDING is OFF), de-duped (concurrent/repeat calls share one in-flight download), and never-throws (a failed warm-up is harmless — embedCapturedFace retries on submit + GPU-less-enforcement surfaces a real failure there). Wired fire-and-forget via requestIdleCallback (fallback setTimeout) on LoginPage (dashboard) and HostedLoginApp (hosted) mount, with effect- cleanup cancelling a still-pending idle schedule. So with the flag ON the ~47 MB model downloads on idle while the user types their identifier, and a later FACE step is a cache hit instead of a frozen on-submit download. Flag OFF: no schedule, no download — surfaces byte-identical to before. Unit-tested: flag-gating, single canonical-URL+SHA fetch, de-dup, never-rejects+retry, requestIdleCallback-vs-setTimeout + cleanup.
The FACE submit awaits embedCapturedFace (model download on first use + ONNX init + inference) BEFORE the verify call, with no feedback — on mobile the multi-second embed looked frozen. Adds a distinct `preparing` state (separate from the verify `loading`): handleSubmit now awaits onSubmit (type widened to void|Promise<void>) and toggles `preparing` around it, disabling both buttons and rendering a spinner + the i18n mfa.face.preparingSecure status line (role=status, aria-live=polite) on the submit button. Also schedules the flag-gated Facenet prefetch on FACE-step mount so the model is warm by capture time. Flag OFF: onSubmit returns synchronously, `preparing` flips within one tick (no visible flicker), and the prefetch is a no-op — unchanged UX.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the client-side FACE embedding (SP-A) production-ready and GPU-less-enforced. Live prod state: identity flag `app.auth.client-side-embedding=true` (server now 400s the legacy image path) + web flag `VITE_CLIENT_SIDE_EMBEDDING=true`, and the real 47 MB Facenet512 FP16 model is hosted at `app.fivucsas.com/models/facenet512-1ad91552.fp16.onnx`. The CPU-only server must not do image ML, so the embedding must be computed client-side; the old silent image fallback now just fails.
Four cohesive changes, one per commit:
1. Remove the silent image fallback (GPU-less enforcement) — `3baf8cb`
In `MfaStepRenderer.tsx` FACE case, when the flag is ON and `embedCapturedFace` returns null we no longer fall back to `verifyStep(FACE, { image })` (an upload the server now rejects). Instead we surface a clear, retryable error (`mfa.face.clientPrepFailed`, EN+TR) via `onError` — which renders in FaceCaptureStep's existing error Alert — and return without sending the image. The captured frame stays so the user can re-submit (the model cache self-heals + the prefetch re-warms). The legacy `{ image }` path is kept only when the flag is OFF.
2. Prefetch the model (no frozen 47 MB-on-submit) — `038504f`
New `prefetchFacenetModel()` + `scheduleFacenetPrefetch()` in the embedding feature: flag-gated (no-op when OFF), de-duped (concurrent/repeat calls share one in-flight download), never-throws. Wired fire-and-forget via `requestIdleCallback` (fallback `setTimeout`) on the mount of LoginPage (dashboard) AND HostedLoginApp (hosted) AND FaceCaptureStep, with effect-cleanup cancelling a still-pending idle schedule. With the flag ON the ~47 MB model downloads on idle while the user types, so a later FACE step is a cache hit.
3. Progress indicator during embed — `bf96f7d`
`FaceCaptureStep` now `await`s `onSubmit` (type widened to `void | Promise`) and toggles a distinct `preparing` state around it — separate from the verify `loading` — rendering a spinner + the `mfa.face.preparingSecure` status line (role=status, aria-live=polite) on the submit button while the model download + ONNX init + inference run. On mobile the user sees it working, not frozen.
4. Add facenet to `public/models/manifest.json` — `fe65ba9`
Now safe (the model is hosted at the manifest's `base_url_default`), so `prebuild`→`fetch-models` fetches + SHA256-verifies + bundles it into `dist/models` → persistence (a manual upload alone is wiped by the deploy rsync) + fan-out to the verify build. Name/sha256/bytes match the `facenetEmbedder.ts` constants and the live file.
Flag-OFF is byte-identical
With `VITE_CLIENT_SIDE_EMBEDDING` OFF: `scheduleFacenetPrefetch()` returns a no-op immediately (no schedule, no download); the MfaStepRenderer FACE case goes straight to the unchanged `verifyStep(FACE, { image, ...advisory })` line; `preparing` flips within one tick (no flicker). The only flag-OFF-reachable FACE line is unchanged vs origin/main.
Verification
Merging auto-deploys app.fivucsas; verify.fivucsas to be rebuilt by the coordinator after.