feat(voice): add duplex TTS benchmarking and validated metrics - #203
Draft
asmitks wants to merge 2 commits into
Draft
feat(voice): add duplex TTS benchmarking and validated metrics#203asmitks wants to merge 2 commits into
asmitks wants to merge 2 commits into
Conversation
Add provider-native streaming and HTTP adapters for ElevenLabs and Deepgram. Derive first-playable, overlap, stall, and Etalon-inspired fluidity metrics from client-side PCM timelines with conservative source attribution. Document reproducible Seed-TTS runs and cover protocols, SLOs, and config registration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Excluding unit tests, the PR changes 22 production/documentation files. The core change is that Veeksha can now run the same client-observed TTS benchmark against Vajra, ElevenLabs, and Deepgram.
1. New ElevenLabs and Deepgram clients
Streaming WebSocket clients
veeksha/client/native_streaming_tts.py
Adds native protocol adapters for:
trigger_to_first_playable_audio_msis the intended steady-state TTFA. It excludes WebSocket setup and input time before the semantic synthesis trigger.5. Fluidity and playback simulation
veeksha/evaluator/performance/audio_interactivity.py
The existing packet timeline is converted into fixed-duration playable PCM frames—20 ms by default. This removes dependence on how ElevenLabs, Deepgram, or Vajra happen to fragment WebSocket packets.
The Etalon-inspired algorithm:
A score of
1.0means the captured PCM supply could play continuously under the selected startup-buffer policy. It does not mean latency, quality, or naturalness was perfect.The evaluator also computes:
User fluidity versus service fluidity
The implementation explicitly addresses input starvation.
user_audio_fluidity_index: what the listener would experience, regardless of cause.tts_service_fluidity_index: emitted only when misses can reasonably be attributed to TTS.tts_service_fluidity_eligible: whether that attribution was valid.In conservative mode, the service score is emitted only when all source text was available before playback began.
For controlled traces that guarantee sufficient synthesis-ready text, the benchmark can use:
This prevents the benchmark from blaming TTS for silence caused by an upstream LLM that did not produce text.
6. Duplex-overlap measurements
The evaluator now reports:
audio_before_commit_ratio: fraction of audio received before the final text input.duplex_overlap_observed: whether a complete playable frame arrived before input completion.duplex_overlap_ms: how long playable audio delivery and text input overlapped.post_commit_audio_delivery_ms: audio delivery duration after the final input.These answer two separate questions:
7. Audio evaluator and output artifacts
veeksha/evaluator/performance/audio.py
The audio evaluator now:
request_level_metrics.jsonl.audio_raw_timing.jsonl.This means one captured request can be replayed locally under several playback-buffer policies without calling the provider again.
8. Configuration and registry plumbing
The following files make the new clients and metrics configurable from YAML:
veeksha/config/client.pyveeksha/client/registry.pyveeksha/types/__init__.pyveeksha/config/evaluator.pyThey add:
9. SLO support
Files:
veeksha/config/slo.pyveeksha/slo/metrics.pyveeksha/slo/slo.pyThe new metrics can be used as benchmark pass/fail conditions.
Example:
For latency and stalls, lower is better. For the following metrics, higher is better:
This also fixes percentile direction. For example, a fluidity SLO should normally evaluate the lower tail, such as P1 ≥ 0.99, rather than P99.
10. Correctness validation changes
ASR WER aggregation
veeksha/evaluator/performance/asr.py
Clarifies and fixes empty-reference corpus WER:
0%100%The evaluator continues reporting:
Corpus WER remains the primary ASR comparison.
TTS WER judge
Files:
veeksha/config/verification.pyveeksha/verification/audio.pyWhisper verification is now pinned with:
transcribeThis makes provider comparisons more reproducible and avoids automatic language detection changing the WER result.
With
fail_on_threshold: true, verification now fails not only for bad WER but also for:Previously, such failures could reduce the evaluated sample without necessarily failing the run.
11. Import/runtime cleanup
librosais loaded only when STT audio decoding is actually required.transformersis loaded only when a Hugging Face tokenizer is requested.12. Documentation
docs/user_guide/tts_benchmarking.rstdocuments:A small documentation cleanup also changes “Claude Code trace” to the generic “coding-assistant trace”; Veeksha does not bundle a Claude Code TTS trace.
Excluding unit tests, the PR changes 22 production/documentation files. The core change is that Veeksha can now run the same client-observed TTS benchmark against Vajra, ElevenLabs, and Deepgram.1. New ElevenLabs and Deepgram clients
Streaming WebSocket clients
[veeksha/client/native_streaming_tts.py](https://github.com/project-vajra/veeksha/pull/203/files#diff)
Adds native protocol adapters for:
elevenlabs_streaming_tts/stream-inputdeepgram_flux_streaming_tts/v2/speakSpeakmessages; binary PCM responsesdeepgram_aura_streaming_tts/v1/speakSpeak, followed byFlush; binary PCMAll three use the same shared measurement loop:
time.monotonic().The adapters retain the provider-specific lifecycle:
Connected,SpeechStarted, andSpeechMetadata.Flushed.API keys can come from the client configuration or:
ELEVENLABS_API_KEYDEEPGRAM_API_KEYNon-streaming HTTP clients
[veeksha/client/native_http_tts.py](https://github.com/project-vajra/veeksha/pull/203/files#diff)
Adds:
elevenlabs_http_ttsdeepgram_flux_http_ttsThese send the complete text in one HTTP request and receive the complete PCM response.
For these clients:
TTFC = end-to-end response latency, because audio is unavailable until the full response arrives.2. Genuine duplex scheduling for Vajra
[veeksha/client/realtime_tts.py](https://github.com/project-vajra/veeksha/pull/203/files#diff)
Previously, the OpenAI Realtime-compatible client always did:
It now supports two modes.
complete_textMaintains the previous behavior:
duplexTriggers synthesis after a configurable number of input tokens:
Configuration:
The client records exactly when
response.createwas sent. It continues sending later text while the response is active.Important: message ordering proves transport overlap, not semantic correctness. A server could speak only the prefix available at trigger time. Therefore, the documentation requires full-reference WER or a stronger coverage check before calling the result genuine duplex synthesis.
3. A common raw timing contract
[veeksha/core/audio_contract.py](https://github.com/project-vajra/veeksha/pull/203/files#diff)
The clients now expose the same provider-independent event timeline:
New raw fields include:
providerprovider_modelprovider_protocolresponse_trigger_offset_mstext_delta_timestampsaudio_chunk_timestampsinput_commit_offset_msaudio_done_offset_msresponse_done_offset_msThis lets the evaluator compute every provider’s metrics from client-side observations rather than trusting provider-reported latency.
4. First-playable-audio metrics
A provider packet is not automatically playable. Veeksha first converts the received bytes into audio duration.
For 24 kHz, mono, PCM16:
Therefore, if the provider sends:
the first complete 20 ms frame becomes playable at
110 ms, when cumulative received PCM reaches 960 bytes.New latency metrics:
trigger_to_first_playable_audio_msfirst_input_to_first_playable_audio_msrequest_start_to_first_playable_audio_msttfctrigger_to_first_playable_audio_msis the intended steady-state TTFA. It excludes WebSocket setup and input time before the semantic synthesis trigger.5. Fluidity and playback simulation
[veeksha/evaluator/performance/audio_interactivity.py](https://github.com/project-vajra/veeksha/pull/203/files#diff)
The existing packet timeline is converted into fixed-duration playable PCM frames—20 ms by default. This removes dependence on how ElevenLabs, Deepgram, or Vajra happen to fragment WebSocket packets.
The Etalon-inspired algorithm:
A score of
1.0means the captured PCM supply could play continuously under the selected startup-buffer policy. It does not mean latency, quality, or naturalness was perfect.The evaluator also computes:
User fluidity versus service fluidity
The implementation explicitly addresses input starvation.
user_audio_fluidity_index: what the listener would experience, regardless of cause.tts_service_fluidity_index: emitted only when misses can reasonably be attributed to TTS.tts_service_fluidity_eligible: whether that attribution was valid.In conservative mode, the service score is emitted only when all source text was available before playback began.
For controlled traces that guarantee sufficient synthesis-ready text, the benchmark can use:
This prevents the benchmark from blaming TTS for silence caused by an upstream LLM that did not produce text.
6. Duplex-overlap measurements
The evaluator now reports:
audio_before_commit_ratio: fraction of audio received before the final text input.duplex_overlap_observed: whether a complete playable frame arrived before input completion.duplex_overlap_ms: how long playable audio delivery and text input overlapped.post_commit_audio_delivery_ms: audio delivery duration after the final input.These answer two separate questions:
7. Audio evaluator and output artifacts
[veeksha/evaluator/performance/audio.py](https://github.com/project-vajra/veeksha/pull/203/files#diff)
The audio evaluator now:
request_level_metrics.jsonl.audio_raw_timing.jsonl.This means one captured request can be replayed locally under several playback-buffer policies without calling the provider again.
8. Configuration and registry plumbing
The following files make the new clients and metrics configurable from YAML:
veeksha/config/client.pyveeksha/client/registry.pyveeksha/types/__init__.pyveeksha/config/evaluator.pyThey add:
9. SLO support
Files:
veeksha/config/slo.pyveeksha/slo/metrics.pyveeksha/slo/slo.pyThe new metrics can be used as benchmark pass/fail conditions.
Example:
For latency and stalls, lower is better. For the following metrics, higher is better:
This also fixes percentile direction. For example, a fluidity SLO should normally evaluate the lower tail, such as P1 ≥ 0.99, rather than P99.
10. Correctness validation changes
ASR WER aggregation
[veeksha/evaluator/performance/asr.py](https://github.com/project-vajra/veeksha/pull/203/files#diff)
Clarifies and fixes empty-reference corpus WER:
0%100%The evaluator continues reporting:
Corpus WER remains the primary ASR comparison.
TTS WER judge
Files:
veeksha/config/verification.pyveeksha/verification/audio.pyWhisper verification is now pinned with:
transcribeThis makes provider comparisons more reproducible and avoids automatic language detection changing the WER result.
With
fail_on_threshold: true, verification now fails not only for bad WER but also for:Previously, such failures could reduce the evaluated sample without necessarily failing the run.
11. Import/runtime cleanup
librosais loaded only when STT audio decoding is actually required.transformersis loaded only when a Hugging Face tokenizer is requested.12. Documentation
docs/user_guide/tts_benchmarking.rstdocuments:A small documentation cleanup also changes “Claude Code trace” to the generic “coding-assistant trace”; Veeksha does not bundle a Claude Code TTS trace.