Skip to content

PayPal sandbox only. No real money moves.

BuildersBuildersPackages

@gavel/voice

Audio framing, speech-to-text providers, the bid parser and the decision step that turns speech into a confirm prompt or a bid.

Partialmeasured; some of the bar is met, with limits stated on the page

Page last changed 8 Nov 2026 (from the git history)

packages/voice turns speech into a structured bid, or into a question for the bidder. It never decides whether a bid is valid: the does that. Its job ends at a decision (auto, confirm or drop) and, for an accepted one, a PlaceBid command the server submits to the room.

G5 PARTIAL Speech accuracy was measured on synthetic audio only (PARTIAL: the first run missed the threshold, the fixes were chosen on the same clips), so spoken bids always go through the . See Voice and the confirm gate and Gate G5.

Responsibilities

Capture and frame audio

src/audio frames PCM (20 ms frames, 100 ms batches), resamples, finds the end of an utterance and queues it. src/browser holds the browser capture state machine, the mute controller (push-to-talk and open mic, with a keepalive every 5 s while muted) and the degraded-mode handling.

Talk to a speech provider

src/stt has one interface (SttProvider) and five implementations: Deepgram (ReconnectingDeepgramStt), Groq (GroqStt), Web Speech (browser only), FakeStt and ReplayStt (cached transcripts). FailoverStt chains providers and selectProvider picks them from the environment.

Parse and decide

src/parser reads the amount and the or speaker from a transcript with rules. src/decide scores the reading, merges it with an optional model reading, applies the policy thresholds and writes the confirm prompt. src/pipeline/pipeline.ts ties segmentation, parsing and deciding together; bid-command.ts maps a decision to an engine PlaceBid.

Limit the stream

src/session.ts enforces the limits of the /audio socket (byte and message rates, stream caps, idle sweep). It holds no socket and no timer, so it is a pure state holder that the server drives.

Where it sits

The dependency rule in .dependency-cruiser.cjs lets voice import only shared. The server imports voice, and the web client is allowed to. The engine and payments are not imported as values; the package ESLint config allows type-only imports of them.

Public interface

Everything is exported from src/index.ts (main and types both point at it). The groups:

GroupExports
ParsingparseUtterance, parseUtteranceOutcome, ParseResult, PARSE_FLAGS
Decidingdecide, CONFIRM_TTL_MS, Decision, ConfirmPrompt, VOICE_REASONS, buildAudit
PipelineVoicePipeline, STALE_SEGMENT_MS, RETRACT_ADVISORY_MS, toPlaceBid, resolveConfirm, commandIdFromKey
ProvidersselectProvider, STT_NAMES, FailoverStt, ReplayStt, SttProvider, SttEvent
WireencodeBatch, decodeBatch, VoiceFrameError
SessioncreateVoiceSession, AUDIO_ERROR_CODES, AUDIO_IDLE_MS
AudittoAuditRecord, auditRecordSchema, voiceEvidenceSchema
BudgetBUDGET_LIMITS_MS, budgetCheck, latencyOf, percentile

The exported surface is recorded in etc/voice.api.md; a change to it needs the interface label (see Contributing).

Key files

provider.ts
select.ts
failover.ts
deepgram-reconnect.ts
groq.ts
replay.ts
config.ts
policy.ts
decide.ts
validate.ts
pipeline.ts
bid-command.ts
faults.ts
latency.ts
session.ts
privacy.ts
index.ts
FileWhat it holds
src/stt/select.tsselectProvider: unset VOICE_STT tries Deepgram then Groq; a named provider is used or startup fails with unknown_provider, missing_key or unavailable. Errors never contain a key
src/stt/failover.tsFailoverStt: switches provider on a closed connection, an error or a silent stream. Ambient room noise does not arm the silence timer: only a mean-square level above about -35 dBFS counts as speech
src/stt/deepgram-reconnect.tsReconnecting Deepgram client; the audio of the last seconds is replayed after a switch
src/decide/config.tsdefaultConfig: auto 0.85, confirm 0.5
src/decide/decide.tsThe decision step; CONFIRM_TTL_MS is 8000
src/pipeline/pipeline.tsVoicePipeline; a segment older than STALE_SEGMENT_MS (5000) is treated as stale, RETRACT_ADVISORY_MS is 8000
src/pipeline/faults.tsSpeech health: status degraded when no final transcript arrives for 8 s while the level meter shows speech, down after 15 s; transcript sanity checks (2000 characters, character runs)
src/pipeline/latency.tsLatency budget: components sum to the total; limits p50 1200 ms and p95 2000 ms with a model, p50 700 ms and p95 2000 ms rules-only
src/privacy.tsPRIVACY_NOTICE and PERSISTED_VOICE_KEYS, the only voice fields that are stored

Thresholds and limits

Prop

Type

Only the auctioneer and console roles may open an audio stream (AUDIO_ROLES); screens and bidders never can.

Invariants

  • A spoken bid never skips the engine. toPlaceBid sets commandId with commandIdFromKey (a hash of the auction id and the utterance idempotency key), so a replayed utterance maps to the same id and books once, even if two providers heard it. The provider name is not part of the key.
  • Below the auto threshold, the bidder is asked. With no model reading wired into the running pipeline, every bid scores below 0.85 and reaches a confirm prompt (stated in the package README).
  • Only audit fields in PERSISTED_VOICE_KEYS are persisted. The test test/privacy.test.ts guards the list against the audit schema.
  • No key in an error. SttConfigError messages name the provider, never the key.
  • The held-out text set is frozen. fixtures/text/HELDOUT.sha256 is checked in CI against the base branch.

Tests that guard it

AreaTest files
Provider selection and failovertest/stt/select.test.ts, select-deepgram.test.ts, failover.test.ts, deepgram-reconnect.test.ts, groq-retry.test.ts, groq-timeout.test.ts
Decision policy and promptstest/decide/policy.test.ts, score.test.ts, validate.test.ts, prompts.test.ts, wording.test.ts, audit.test.ts
Pipelinetest/pipeline/pipeline.test.ts, faults.test.ts, latency.test.ts, prompt-expiry.test.ts, prompt-identity.test.ts, crosstalk.test.ts
Bid command mappingtest/pipeline-bid-command.test.ts, test/pipeline-bounds.test.ts
Stream limitstest/session.test.ts, test/session-idle.test.ts
Parser corpustest/corpus.test.ts, test/labels.test.ts
Privacy and held-out guardtest/privacy.test.ts, test/heldout-guard.test.ts
Evaluation harnesstest/eval/gate.test.ts, metrics.test.ts, report.test.ts, cli.test.ts
Browser capture and mutetest/browser/capture.test.ts, mute.test.ts, degraded.test.ts, capture-state.test.ts

Scripts

ScriptWhat it does
npm run test -w @gavel/voicePackage unit tests
npm run eval -w @gavel/voice -- --mode live --provider deepgramScores recordings through a real provider; needs a key
npm run scan:secrets -w @gavel/voiceScans the package for vendor key patterns (runs in the ci.yml voice job)
npm run check:heldout -w @gavel/voiceGuards the held-out text set
npm run size -w @gavel/voiceBundle size limit
Edit on GitHub

Last updated on