voicepipe
A pipeline for live, turn-taking voice calls with a language model, in Rust.
Audio streams in. voicepipe decides when the speaker's turn is over, transcribes it, runs the host's turn, and speaks the reply back a sentence at a time while it is still being written. It stops the reply the moment the speaker talks over it, and keeps only what was heard.
Speech-to-text, text-to-speech and the model are traits the host implements, so the same pipeline runs over local models or hosted APIs. voicepipe owns what is common to every voice agent: turn-taking, barge-in, ordering and timing.
What the host provides
| Trait | The host's part |
|---|---|
Stt |
One clip (a 16 kHz mono PCM16 WAV) to its words. transcribe_after takes one chunk of a turn still being spoken, plus the words before it, for a transcriber that accepts a prompt. |
Tts |
One piece of the reply to one audio clip, in any container. |
Llm |
The turn: a TurnRequest in, text deltas and tool rounds out through a sink, and an outcome (Completed, Interrupted or AwaitingApproval). It honours a CancellationToken, and truncate_reply cuts the recorded reply back to what was heard. |
Approvals |
Whether an approval card is waiting, and what to do with an utterance heard while one waits. |
Vad, TurnDetector |
Optional. EarshotVad (default feature, pure Rust) and EnergyVad ship, with Endpointer as the turn detector. |
The transport is two channels, Inbound (audio frames and JSON control
frames) and Outbound (events and audio clips). Bridging them to a WebSocket
takes a few lines of host code, and the host authenticates the connection
before it bridges.
What the pipeline does
- Turn-taking. Onset, a barge-in bar that a backchannel "mm" does not clear, pauses, end of turn, and pre-roll so the first syllable is not clipped.
- Streaming transcription. The turn is cut into chunks at the speaker's pauses and transcribed while they talk on, so the transcript is ready about when the turn ends. Whole-clip transcription is the fallback, and a mode of its own.
- Sentence-streamed speech. The reply is cut into sentences (the first at its first clause), synthesized ahead of playback and sent in order. A filler line covers a tool round that comes before any text.
- Barge-in. Voice over the reply cancels the turn, tells the client to drop its queued audio, and asks the host to record only what had started playing, as the client reports it or as estimated from clip lengths.
- Metrics. Each response reports where its time went, from the end of speech to the first audio.
The approval invariant
A turn may stop at an approval card: a change the host wants a person to
confirm. Nothing a person says resolves one. While Approvals::pending
reports a card, an utterance is answered with a reminder, handed to
Approvals::held_utterance, and never reaches the Llm.
TurnRequest::Resolve is built only from an action.resolve control frame,
which is a button press. No audio or transcript path can construct one, and
the crate's tests assert that a spoken "yes" leaves the card waiting.
Example
A complete host with stand-in providers. A real one calls its speech services and model where these return fixed values, and forwards a socket's frames instead of a synthetic tone.
use Arc;
use mpsc;
use CancellationToken;
use wav_from_pcm16;
use ;
;
;
;
;
async
The wire protocol
One duplex connection per call. The client streams its microphone as binary
frames of little-endian PCM16, mono, 16 kHz, and sends JSON control frames as
text: session.update, response.cancel, output_audio.started /
output_audio.idle (playback reports, which make "what was heard" exact),
action.resolve (the approval button) and ping. The server answers with
JSON events named, where the meaning is the same, after the OpenAI Realtime
API's: session.created (which carries the protocol version), call.state,
input_audio_buffer.speech_started, transcription captions,
response.created, response.text.delta, response.audio.start followed by
one binary frame holding that piece's clip, response.done with metrics,
output_audio.clear, conversation.item.truncated, and approval.pending /
approval.resolved. The types are in the protocol module.
Status
Pre-1.0. While the version is 0.x, a minor release may break the API or the wire protocol and a patch release only fixes. The minimum supported Rust version is 1.88.
voicepipe was extracted from the call path of
Vogt, which is its first
host; its design notes are in docs/DESIGN.md.
Licence
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.