Turn-based Mode Setup Guide
View SourceThis guide explains how to implement explicit push-to-talk turns with Synaptic.
Turn-based mode is best when you want:
- deterministic user-controlled turn boundaries
- simpler UX than duplex
- less VAD tuning and less interruption complexity
1. Architecture and ownership
In turn-based mode:
- client owns when recording starts/stops
- server owns STT/TTS orchestration and workflow state
- turn completion is explicit via
end_turn
Headless output note:
- built-in providers now default to one TTS generation per assistant turn for more consistent tone
- assistant text can stream before audio starts; audio may begin only after the assistant turn is fully generated
- segmented per-sentence TTS remains the fallback path for adapters without capability metadata
Session engine:
Synaptic.Voice.Sessions.Headless(mode: :turn_based)
2. Prerequisites
Server:
- provider API key(s):
OPENAI_API_KEY,GEMINI_API_KEY, and/orELEVENLABS_API_KEY - Synaptic voice provider config
Client:
- mic capture support (
getUserMedia) - recorder support (
MediaRecorderor equivalent)
3. Session bootstrap
{:ok, %{session_id: session_id, run_id: run_id}} =
Synaptic.Voice.start_session(MyWorkflow, %{},
provider: :gemini, # or :openai / :eleven_labs
mode: :turn_based,
keep_alive: true
)
:ok = Synaptic.Voice.subscribe_session(session_id)
:ok = Synaptic.subscribe(run_id)Track:
session_idrun_id- current
statusvia session events
4. Push-to-talk protocol
Recommended client flow:
- user presses/starts talking -> start recorder
- stream chunks via
push_audio - user releases/stops talking -> stop recorder
- wait for final chunk flush
- call
end_turn
Important:
- same as duplex, wait for final chunk flush before end-turn to avoid corrupted container uploads
- do not call
end_turnrepeatedly for the same turn
5. Optional text turn path
Turn-based mode supports text-only turns cleanly:
:ok = Synaptic.Voice.push_text(session_id, "How many stars does React have?")
:ok = Synaptic.Voice.end_turn(session_id)Use this for:
- fallback when mic permission fails
- quick integration testing
- accessibility modes
6. Required server events to handle
From voice session PubSub:
:input_partial_text:input_final_text:assistant_text_chunk:assistant_audio_chunk:assistant_audio_done:duplex_state_changed(still useful for status rendering consistency):session_error:session_stopped
Even in turn-based mode, build status-driven UI and do not infer state from local assumptions only.
In particular, do not assume the first :assistant_audio_chunk arrives immediately after the first :assistant_text_chunk.
7. Error handling defaults
Minimum recommended behavior:
- empty transcript -> show retry hint, keep session alive
- STT upstream failure -> keep session alive, reset recording UI to idle
- TTS failure -> show text response path so user still gets output
- resume/workflow error -> keep session alive, allow next turn
For all recoverable errors, ensure your UI can start a fresh turn without reconnect.
8. UX and safety recommendations
- show explicit recording state (
idle,recording,processing) - disable start/stop button while final chunks are flushing
- prevent double-submit of
end_turn - always stop tracks and recorder on disconnect/navigation
9. Verification checklist
Run these scenarios:
- start/stop one voice turn -> spoken answer
- two consecutive turns without reconnect
- text turn followed by voice turn in same session
- stop session and reconnect cleanly
- simulate STT failure and confirm next turn still works
- assistant text streams first and audio starts only after turn completion
10. Reference implementation
Internal sample:
tmp/voice_lab(Turn-based mode)
Relevant files:
tmp/voice_lab/lib/voice_lab_web/live/home_live.extmp/voice_lab/assets/js/app.jslib/synaptic/voice/sessions/headless.ex
Frontend-focused companion guide: