Skip to navigation

Voice Activity (VAD) events

Acoustic speech_started / speech_ended events emitted on the Pulse STT WebSocket alongside transcription.

View as Markdown
Real-Time

When vad_events=true is set on the WebSocket connection, the Pulse STT server emits two additional JSON message types: speech_started and speech_ended. They are interleaved with the transcription stream on the same socket. Event boundaries are derived from the audio signal and are independent of transcript finalization.

VAD events are WebSocket only. The pre-recorded REST endpoint does not emit them.

Enabling

Set vad_events=true on the WebSocket URL. Default is false.

const url = new URL("wss://api.smallest.ai/waves/v1/stt/live?model=pulse");
url.searchParams.append("language", "en");
url.searchParams.append("encoding", "linear16");
url.searchParams.append("sample_rate", "16000");
url.searchParams.append("vad_events", "true");
const ws = new WebSocket(url.toString(), {
headers: { Authorization: `Bearer ${API_KEY}` },
});

vad=true is accepted as an alias. When both are set, vad_events takes precedence and vad is ignored.

Message sequence

Boundaries are acoustic, not lexical. They are computed from the audio signal independently of transcript finalization, so they do not coincide with is_final transcript turns.

Event payloads

The two message types are interleaved with transcription messages on the same socket. Discriminate on the type field.

speech_started

Emitted when speech onset is detected after silence.

{
"type": "speech_started",
"session_id": "a1b2c3d4",
"timestamp": 1.84
}

speech_ended

Emitted when a voiced region ends and silence is detected.

{
"type": "speech_ended",
"session_id": "a1b2c3d4",
"timestamp": 4.52
}
FieldTypeDescription
typestringMessage-type discriminator. One of speech_started, speech_ended.
session_idstringSession identifier. Matches the session_id returned on transcription messages from the same connection.
timestampnumberSeconds, measured from the first audio frame received on the connection.

Handling

Discriminate on the type field. transcription messages route through the default branch unchanged.

ws.onmessage = (e) => {
const m = JSON.parse(e.data);
switch (m.type) {
case "speech_started":
onSpeechStart(m.timestamp);
break;
case "speech_ended":
onSpeechEnd(m.timestamp);
break;
case "transcription":
default:
handleTranscript(m);
break;
}
};
import json
async for raw in ws:
m = json.loads(raw)
t = m.get("type")
if t == "speech_started":
on_speech_start(m["timestamp"])
elif t == "speech_ended":
on_speech_end(m["timestamp"])
else:
handle_transcript(m)

Notes

  • Opt-in. When vad_events is unset or false, the connection emits only transcription messages.
  • No per-connection tuning. The API does not expose VAD sensitivity or debounce controls.
  • Acoustic, not lexical. Boundaries are derived from the audio signal, independent of is_final. They do not coincide with word boundaries.
  • speech_ended requires trailing silence. The event fires after a silence gap following a voiced region. If the connection closes while the audio still contains voiced energy, no speech_ended is emitted for the final voiced region. To force emission at the end of a finite audio file, append a short pad (about 1 second) of zero-valued PCM before sending close_stream.
  • timestamp is measured in seconds from the first audio frame received on the connection.
  • Sample rate. The sample_rate query parameter must equal the rate of the PCM frames sent on the connection. Mismatch causes timing drift on both transcripts and VAD timestamps.

Try it

Press play on each sample to hear the audio and watch representative, redacted server messages stream in sync.

0.00s / 6.94s24000 Hz · mono PCM
speech_started @ 0.03sspeech_ended @ 5.36s
Representative server messages
0.03s▶{ type: "speech_started", session_id: "demo-session", timestamp: 0.032 }
0.13s·{ is_final:false, transcript: "Hey, so" }
0.18s·{ is_final:false, transcript: "Hey, so I was looking at the dash" }
2.24s·{ is_final:true, transcript: " Hey, so I was looking at the dashboard" }
2.29s·{ is_final:false, transcript: ", and yeah, the numbers look pretty solid" }
2.34s·{ is_final:false, transcript: ", and yeah, the numbers look pretty solid, actually" }
4.56s·{ is_final:true, transcript: ", and yeah, the numbers look pretty solid, actually" }
5.36s■{ type: "speech_ended", session_id: "demo-session", timestamp: 5.360 }

The third sample illustrates the trailing-silence behavior described above: speech_ended only fires after the model observes silence.