# Browser VAD → Opus → batch ASR, protocol 4

Implementation acceptance is tracked separately from production deployment. Base:
chat-agent `ea675fa3`, ASR `6d23793d`. No new service, table or NATS connection.

## Capture

One microphone/AudioContext/AudioWorklet produces mono PCM16LE at the actual Chrome
sample rate. Requested and actual NS/EC/AGC remain visible. WebCodecs encodes this capture as Opus mono 32 kbit/s VBR, 20 ms, without
DTX or FEC. The archive receives the exact accepted Opus packets, including pauses.
No RMS gate or VAD transformation precedes encoding. A per-capture Worker receives a copy for speech detection.

Every capture uses Silero Web with its own state. There is no VAD selector,
provider preference, alternate implementation or fallback.

The analytic resampler is `@alexanderolsen/libsamplerate-js@2.1.2`, continuous
`SRC_SINC_FASTEST`. The pinned model revision and SHA256 are in
`src_front/src/audio/vad/vendor/manifest.json`. ORT Web 1.31.1 uses WASM CPU,
one thread, no proxy/Blob worker. Execution assets and notices are packaged locally.

Phrase rules: onset 160 ms, release 450 ms, maximum 30 s, thresholds .65/.50,
pre-roll 200 ms. The server accepts only Silero metadata for new browser phrases.
The original 34-second source buffer remains unchanged. Coordinates are source
sample counts; callback arrival time never changes them. Forced adjacent ranges
are contiguous. Explicit mute flushes the captured tail once; device, transport
or VAD failure aborts only that capture's unfinished phrase.

Compare computes one segmentation for the current recording and reuses it across
models. New recording or page disposal releases it. Selected diagnostic PCM and
archived phrases are replayed directly, without running VAD again. Historical VAD
metadata remains readable and does not choose an implementation.

## Transport and ownership

Protocol 4 START requires `format: opus` and browser boundaries. Protocol 3 PCM
clients receive `audio_upgrade_required`; there is no silent client fallback. `phrase_start` and
`phrase_end` carry the existing phrase ID, source samples, last encoded packet, VAD
revision/parameters and `pause`/`mute`/`limit`. Ingress acknowledges only after the
required packets have arrived and decoded. Repeating the same boundary returns its original receipt.

One completed range becomes one batch job. The internal envelope is a 4-byte
big-endian JSON length, JSON metadata, then binary PCM16LE (at most 128 KiB).
Begin fixes size and SHA256; ordered chunks and commit verify the entire range.
Live admission remains three connections plus one diagnostic owner. At most eight
ready jobs wait; live jobs take priority without interrupting a running model.
Model telemetry cannot mark a job finished; terminal result owns completion.

Monitor replay reads the verified source range on the server and submits the same
batch path. Exact phrase, phrase with 2 s left/1 s right context, and full recording
are distinct inputs. Diagnostic jobs never create chat commands or source recordings.
Full recordings use saved boundaries and at-most-30-second uncovered ranges.

Scores are observations, never text-rejection thresholds. Native inference time
includes consuming the result generator and excludes queue, resampling and transport.

## Historical acceptance record (protocol 3)

- Local contract checks: real disposable NATS/PostgreSQL, source PCM integrity,
  delayed boundary packets, idempotent submit, active-stream terminal delivery,
  cancellation/native ownership, archive replay and three shared Monitor listeners.
- Browser capture soak: 1803 seconds, three captures, 30 restarts, 365 boundaries,
  zero capture failures. Workers/AudioContexts remain 3/3 while active and 0/0 after
  stop; DOM 7 nodes / 12 listeners. This is a local browser/transport-fixture test,
  not mixed production GPU acceptance.
- Silero ran on source/quiet/short/silence samples in Chrome. No silence phrase.
  This does not establish WER, CER or physical microphone quality.
- Pending: four real models and labelled corpus, production mixed-load timings,
  exact CI/image/runtime delivery and physical microphone acceptance.
- Source-only unit/packaging checks do not establish these outcomes.

## Opus coordinates and archive

The existing 8-byte big-endian sequence prefix is followed by: uint64 valid end
sample at 48 kHz, uint16 configuration length (0 or 23), optional uint32 decoder
sample rate + the encoder’s original 19-byte OpusHead, then one raw Opus packet.
The capture rate remains in START and VAD coordinates. Absolute integer/rational
conversion to 48 kHz avoids accumulating rounding. The pre-skip is read from
WebCodecs metadata at its reported decoder rate, then converted to 48 kHz.
A new configuration begins a capture segment at the previous accepted end.

Ingress and archive replay share `src_shared/opus.py`. Ingress decodes once, trims
the encoder delay and final padding, then passes decoded PCM to the existing
16 kHz batch preparation. Archive blocks keep compressed bytes, packet hashes,
sequences and end_sample; `metadata.samples` gives duration independently of bytes.
No permanent PCM copy, additional Worker, table or NATS connection is introduced.

Playback and server replay decode from the start, retaining only bounded blocks
and the requested range. Late ranges can take additional preparation; cancellation
is checked between blocks. Waveform peaks come from the same decoded audio.
Old PCM blocks retain metadata only; playback and ASR replay return `audio_legacy_pcm_unsupported`. The UI calls the result
“Запись для распознавания”; the local Compare WAV remains microphone PCM before Opus.
