Voice Assistant

Pro

A voice turn with a level meter driven by measured amplitude, interim-vs-final transcript rendering, and barge-in as a real transition. 3 variants: button, transcript and call.

</>sv-voice-assistant aivoicespeechtranscript
import { SvVoiceAssistantComponent } from '@styloviz-pro/voice-assistant';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Transcript & call

Pro

When to use

  • A voice input surface where the user needs to know whether the microphone is actually being heard.
  • A live transcript where the recogniser emits interim guesses that must not read as settled text.
  • A call-style turn where the user has to be able to interrupt the assistant mid-sentence.

Async states

  • loading
  • error
  • retry

Inputs

NameTypeDefaultDescription
loadingbooleanfalseTrue while the session is being set up.
errorSvDataError | string | nullnullSession failure. Use `toSvDataError()` from `@styloviz/core`.
variantVoiceVariant'button'Presentation: a push-to-talk button, a transcript, or a call.
turnVoiceTurn (two-way)'idle'Where the turn is. Two-way, so the host can drive it from its own recogniser and the component can move it on a user action.
levelsreadonly number[][]Measured input amplitude, 0–1, oldest first. THIS IS THE ONE INPUT THAT MAKES THE METER HONEST. Feed it from an `AnalyserNode` — `getByteFrequencyData`, averaged and scaled — and the bars mean something. Leave it empty and the meter says the microphone is silent, which is the truth about a microphone nobody is measuring.
transcriptreadonly VoiceSegment[][]The transcript so far, interim segments included.
copyablebooleantrueShow the settled-transcript copy affordance.
silenceFramesnumber8Quiet frames before the component suggests the turn has ended. Not one. The pause between two words is a quiet frame, and so is the breath before the most important clause.
ambientLevelsreadonly number[][]Ambient amplitude measured while nobody was talking, for calibration. `SV_SILENCE_LEVEL` IS A GUESS ABOUT A QUIET OFFICE. In a car, a café or a warehouse the ambient floor sits above it, so every frame reads as speech: the auto-stop never fires, `hearing` is permanently true, and the meter claims to hear a user who has not spoken. Feed the first second of capture in here and the threshold is measured instead. Left empty, nothing changes: the default threshold stands, and a floor calibrated from two frames is a floor calibrated from a cough.
consentRequiredbooleanfalseWhether capture needs the user's agreement first. THE MICROPHONE PERMISSION IS NOT CONSENT. The browser asks whether the page may use the microphone; it does not ask whether the user agrees to their voice being transcribed, stored, or sent to a third-party model, and in several jurisdictions that is the question that matters.
consentSvVoiceConsent (two-way)'unknown'The user's answer. Two-way, so a host can persist it. `declined` is a person saying no, which is a different fact from a browser saying no (`turn: 'denied'`) — the difference between "this is not for me" and "fix your settings".

Outputs

EventPayload typeDescription
retryvoidEmitted when the user presses the error panel's "Try again" button (retryable errors only); the host should re-run the failed session setup.
actionSvVoiceActionThe user asked to start, stop or interrupt.
silenceDetectedvoidA run of silence has lasted `silenceFrames` while listening, so the turn may be over. Fires once per run of silence — not on every quiet frame of the same pause — and again only after speech has broken the run.
turnChangeVoiceTurnEmits the new `turn` whenever it changes — the output half of `[(turn)]`.
consentChangeSvVoiceConsentEmits the new `consent` whenever it changes — the output half of `[(consent)]`.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Pro

Bound to the names this component actually exposes.

Host class

Pro

Drive the three async inputs from your own fetch.

  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `levels`, `loading` and `error`.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • `[ambientLevels]` — the first second of capture, measured while nobody is talking — calibrates the silence threshold from the room. `SV_SILENCE_LEVEL` (0.04) is a guess about a quiet office: in a car, a café or a warehouse the ambient floor sits above it, so every frame reads as speech, the auto-stop never fires and the meter claims to hear a user who has not spoken. Derived from the 90th percentile of ambient rather than the mean (the mean sits under most of the room) and never from the maximum, so one cough cannot deafen the assistant for the session.
  • The calibrated threshold is **capped at 0.35** and floored at 0.02. The cap is the important one: a café calibration would otherwise climb above ordinary speech and the assistant would stop hearing anybody at all — a worse failure than the one calibration fixes, and one the user cannot diagnose. When the cap is doing the deciding the component says so and tells the user to move somewhere quieter.
  • `[consentRequired]` with two-way `[(consent)]` (`unknown` / `granted` / `declined`) gates capture behind an explicit 'this records your voice' prompt. The browser's microphone permission is not consent — it asks whether the page may use the microphone, not whether the user agrees to being transcribed, stored or sent to a third-party model. The gate is enforced in `dispatch`, the one entry point, so no control or host call routes around it, and it blocks `start` only: stop, interrupt and reset always work, because a gate that trapped a live microphone would be the opposite of what it is for.
  • A **Recording** indicator (`role='status'`) shows whenever the microphone is open, in every variant, with no host opt-in. An indicator that appears only when somebody remembered to ask for it is not an indicator.
  • Redaction, from the pack-wide policy (`X-2`): transcript segments AND the settled-text footer are masked — they are two renderings of the same words, so masking one would leave the secret six lines below where it was hidden. `copy: false` withdraws the footer entirely, because it exists to be selected and copied. People dictate what they cannot type one-handed: a card number read off the card, an address, a patient identifier. The mask covers every string that reaches the DOM, not only visible text: each segment's `aria-label` (what a screen reader reads instead of the text) and the settled-character count are built from the masked text too. The policy's `reveal` and `telemetry` flags are not read here, because there is nothing for them to govern: the component offers no unmask control and emits no analytics, and none of its outputs (`action`, `silenceDetected`, `retry`, `turnChange`, `consentChange`) carries transcript text. Telemetry of the transcript is therefore the host's own code, and the host should consult `svAiPrivacy().telemetry` there.
  • `levels` (measured amplitude, 0–1) and `segments` are inputs defaulting to empty; import `DEMO_LEVELS` / `DEMO_TRANSCRIPT` for a demo.
  • Three layouts via `variant`: `button` (the control and meter alone), `transcript` (plus the folded transcript) and `call` (plus interrupt and hang-up).
  • The turn is a state machine — `idle`, `listening`, `thinking`, `speaking`, `denied` — driven by `start` / `stop` / `interrupt` / `reset`. `denied` is terminal until `reset`.
  • `SV_SILENCE_LEVEL` (0.04, not zero) is the amplitude floor: a live microphone in a quiet room reads a non-zero noise floor, so a `> 0` test calls silence speech forever.
  • The meter draws a 6 % floor while capturing, so a live-but-silent microphone is visibly live rather than indistinguishable from a dead one.
  • `SV_LOW_CONFIDENCE` (0.6) flags settled text the recogniser was unsure about — never interim text, which is already drawn as provisional.

Best practices

  • Do: Feed `levels` from a real AnalyserNode. A meter animated on a timer moves identically whether the microphone is live, muted or refused — which makes the one question it exists to answer the one question it cannot answer.
  • Do: Mark interim segments with `final: false`. They render as provisional, and a final segment replaces its own running guess rather than following it.
  • Don't: Do not let a `start` action climb out of `denied`. A refused microphone does not become available because the button was pressed again.
  • Tip: Auto-stop on a run of quiet frames, never on the first one — the pause between two words is a quiet frame, and so is the breath before the important clause.

Accessibility

  • The meter is `role="img"` with an accessible name stating whether input is being heard, not only drawn.
  • A denied microphone gets a written explanation of where the browser hid the permission, not a dead-end "unavailable".
  • The turn label and hint are text; the control reports what pressing it will do.

Feedback on Voice Assistant

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.