Skip to content

feat(voice): GPU-less client-side speaker-embedding module (audit H3, flag-gated default OFF)#224

Merged
ahmetabdullahgultekin merged 1 commit into
mainfrom
feat/voice-client-embedding
Jun 12, 2026
Merged

feat(voice): GPU-less client-side speaker-embedding module (audit H3, flag-gated default OFF)#224
ahmetabdullahgultekin merged 1 commit into
mainfrom
feat/voice-client-embedding

Conversation

@ahmetabdullahgultekin

Copy link
Copy Markdown
Contributor

Summary

Audit item H3 (web-app side). VOICE twin of the face client-embedding path: when VITE_CLIENT_SIDE_VOICE_EMBEDDING is ON, the browser computes the 256-d Resemblyzer speaker embedding locally (onnxruntime-web, WASM EP) and uploads only the vector — the raw audio never leaves the device. Default OFF = byte-identical legacy audio upload ({ voiceData }).

What's added

  • src/features/biometrics/voice-embedding/clientVoiceEmbeddingFlag.ts (mirrors clientEmbeddingFlag.ts), voicePreprocess.ts (WAV decode → dBFS normalize → mel partials + partial slicing), speakerEmbedder.ts (SpeakerEmbedder ONNX class — lazy import('onnxruntime-web'), reuses embedding/modelCache.getModel; mean-of-partials + L2-norm == Resemblyzer embed_utterance), embedCapturedVoice.ts (orchestrator, number[256] | null, never throws), prefetchVoiceModel.ts (mirrors prefetchFacenetModel).
  • MfaStepRenderer.tsx VOICE case — flag ON → verifyStep(VOICE, { embedding }); on ANY failure FALL BACK to { voiceData } (unlike FACE which hard-fails — the voice preprocessing is scaffold + the server accepts either when its flag is ON, so a model/preproc gap never blocks login). VoiceStep mount schedules the model prefetch (flag-gated no-op).
  • Model delivery: fetched at RUNTIME only when ON. NOT added to public/models/manifest.json (a build-prefetch entry would FATAL fetch-models/deploy — the exact facenet failure). FP32 ships (INT8 rejected on ort-web WASM).
  • i18n: mfa.voice.preparingSecure added to en.json + tr.json.

⚠️ SCAFFOLD — completeness honesty

The ONNX export + inference are exact (torch↔ONNX cosine 1.0, verified in the biometric-processor PR), but the JS preprocessing is NOT parity-validated: it does not reproduce Resemblyzer's WebRTC-VAD silence trim (no bit-exact JS port) and uses a librosa-approximate mel. Skipping the VAD measured ≈0.11 cosine shift vs the server on clean audio — risky near the 0.65 accept threshold. Keep VITE_CLIENT_SIDE_VOICE_EMBEDDING OFF until the client mel+VAD are validated to parity. Full contract + the exact validation/canary steps: biometric-processor docs/design/VOICE_CLIENT_EMBEDDING_SPEC.md.

Tests

vitest: voicePreprocess (WAV round-trip, dBFS normalize, mel shape, partial slicing), speakerEmbedder (per-partial (1,160,40) feed, mean+L2-norm, session reuse — fake ort), clientVoiceEmbeddingFlag, MfaStepRendererVoiceEmbedding (OFF→voiceData, ON→embedding[256], ON+fail→fallback to voiceData). 37 tests across 9 files green (incl. the existing face renderer tests unbroken). tsc --noEmit + eslint clean.

Paired PRs: biometric-processor (endpoints + ONNX export, VERIFIED) + identity-core-api (voice policy + handler routing + compose passthrough).

… flag-gated default OFF)

VOICE twin of the face client-embedding path. When VITE_CLIENT_SIDE_VOICE_EMBEDDING
is ON, the browser computes the 256-d Resemblyzer speaker embedding locally
(onnxruntime-web, WASM EP) and uploads ONLY the vector — the raw audio never
leaves the device. Default OFF = byte-identical legacy audio upload.

- src/features/biometrics/voice-embedding/: clientVoiceEmbeddingFlag.ts (mirrors
  clientEmbeddingFlag), voicePreprocess.ts (WAV decode → dBFS normalize → mel
  partials + partial slicing), speakerEmbedder.ts (SpeakerEmbedder ONNX class,
  lazy dynamic import, reuses embedding/modelCache.getModel; mean-of-partials +
  L2-norm = embed_utterance), embedCapturedVoice.ts (orchestrator, number[256] |
  null, never throws), prefetchVoiceModel.ts (mirrors prefetchFacenetModel).
- MfaStepRenderer VOICE case: flag ON → verifyStep(VOICE, { embedding }); on ANY
  failure FALL BACK to { voiceData } (the preprocessing is scaffold + the server
  accepts either when ON, so a gap never blocks login — unlike FACE which
  hard-fails). VoiceStep mount schedules the model prefetch (flag-gated no-op).
- Model fetched at RUNTIME only when ON; NOT added to public/models/manifest.json
  (a build-prefetch entry would FATAL fetch-models, the facenet failure). FP32.
- i18n: mfa.voice.preparingSecure added to en.json + tr.json.

SCAFFOLD — NOT parity-validated: the ONNX export + inference are exact
(torch<->ONNX cosine 1.0), but the JS preprocessing does NOT reproduce
Resemblyzer's WebRTC-VAD silence trim (no bit-exact JS port) and uses a
librosa-approximate mel — skipping the VAD shifts ≈0.11 cosine vs server (risky
near the 0.65 threshold). KEEP THE FLAG OFF until the client mel+VAD are validated.
Contract + canary steps: biometric-processor docs/design/VOICE_CLIENT_EMBEDDING_SPEC.md.

Tests (vitest): voicePreprocess (WAV decode round-trip, dBFS normalize, mel shape,
partial slicing), speakerEmbedder (per-partial (1,160,40) feed, mean+L2-norm,
session reuse — fake ort), clientVoiceEmbeddingFlag, MfaStepRendererVoiceEmbedding
(OFF→voiceData, ON→embedding[256], ON+fail→fallback to voiceData). 37 tests across
9 files green (incl. the existing face renderer tests unbroken); tsc + eslint clean.
@ahmetabdullahgultekin ahmetabdullahgultekin merged commit 8a7d84d into main Jun 12, 2026
4 checks passed
@ahmetabdullahgultekin ahmetabdullahgultekin deleted the feat/voice-client-embedding branch June 12, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant