Skip to content

Add veeksha voice support - #206

Open
AgrawalAmey wants to merge 76 commits into
mainfrom
users/elton/veeksha-voice-v0
Open

Add veeksha voice support#206
AgrawalAmey wants to merge 76 commits into
mainfrom
users/elton/veeksha-voice-v0

Conversation

@AgrawalAmey

@AgrawalAmey AgrawalAmey commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds end-to-end voice benchmarking to Veeksha: streaming STT and TTS clients, audio latency/interactivity/WER metrics, trace preparation + HF hub tooling, managed Vajra server support, health checks, sample configs, and docs.

This is a large feature branch (multi-provider realtime audio) that unifies ASR and TTS under one measurement contract.

Architecture

  • One lifecycle per transport, many providers via protocol strategies:
    • client.type: stt → WebSocket PCM-in / text-out (veeksha/client/stt.py)
    • client.type: streaming_tts → WebSocket paced text-in / audio-out (streaming_tts.py)
    • client.type: tts → HTTP complete-text TTS (tts.py)
  • Shared helpers in client/utils.py (text pacing, WS URL/auth/error mapping).
  • Shared metric vocabulary in core/audio_contract.py (AudioMetricKey).
  • Unified audio evaluator for TTS interactivity + STT WER (evaluator/performance/audio.py + asr.py).

Metric contract (important)

Metric STT Streaming TTS
TTFC First audio byte on wire → first content-bearing transcript event Response trigger → first audio
e2e Request start (post-decode) → completion Connect start → completion
RTF e2e / input clip duration e2e / generated audio duration
Missing TTFC Emitted as null and excluded from percentile sketches (never invent 0 ms) same

Additional TTS keys: duplex/fluidity/interactivity timestamps; STT: WER (Open ASR Leaderboard normalizer), time_to_first_visible_text, partial/final latencies.

How to run

# Engine STT (Vajra / vLLM)
uvx -p 3.14t veeksha benchmark --config veeksha/sample_configs/stt_vajra.yml
uvx -p 3.14t veeksha benchmark --config veeksha/sample_configs/stt_vllm_realtime.yml

# Hosted STT
uvx -p 3.14t veeksha benchmark --config veeksha/sample_configs/stt_deepgram_flux.yml

# Streaming TTS
uvx -p 3.14t veeksha benchmark --config veeksha/sample_configs/tts_streaming_elevenlabs.yml

# Post-run health (optional standalone)
veeksha health --run_dir <output_dir>

Optional extras: audio-verification (faster-whisper), utmos (MOS quality).

Traces: scripts/prepare_audio_traces.py + scripts/audio_trace_hub.py (HF fetch/publish).

Other notable changes

  • Concurrent traffic rampup advances on time (not only completions); reset_reference_time() after warmup so ramp is not consumed during setup.
  • Load-aware default num_client_threads; GIL re-enable warning on free-threaded builds.
  • Managed engines refactor (start/stop, endpoint object); Vajra server support.
  • Standalone veeksha health + TTS zombie-session probe.
  • Higher-is-better SLO metrics for fluidity indices.

Test plan

  • Unit tests for STT/TTS/streaming clients, audio evaluator (including missing TTFC exclusion), concurrent rampup + config validation
  • Smoke: STT against local Vajra/vLLM with sample config
  • Smoke: streaming TTS against one hosted provider (API key required)
  • CI sanity_check green

Follow-ups (non-blocking)

  • Split provider protocols into submodules for maintainability
  • Optional: treat mid-stream abort as non-success for health success-rate clarity

AgrawalAmey and others added 30 commits March 4, 2026 19:58
_stream_vajra now sends audio and receives deltas concurrently instead of
sending the whole (paced) clip before reading any response. With the old
sequential order the first delta could not be observed until the upload
finished, so ttft tracked clip length (~16s) rather than server latency.

Adds two per-request metrics that flow through the evaluator into
summary_stats.json / request_level_metrics.jsonl:
  - first_partial: first audio frame -> first partial transcript (TTFB)
  - final_latency: end-of-audio sentinel -> final transcript (server tail,
    independent of clip length)

audio.py: new AudioRequestMetrics fields, CDFSketch sketches, extraction in
record_request_completed, and JSONL row output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflict resolutions (8 files):
- types: AUDIO=5, SEED_TTS_TEXT=6 (both trace flavors coexist)
- session.py/trace: took theirs (char-mode ShareGPT superset + SeedTTS
  flavor), re-added STT AudioTraceFlavorConfig + AUDIO registry entry
- audio evaluator metric naming (per review decision): keep BOTH ttft
  (STT first-token) and ttfa (TTS first-audio) as distinct fields; unify
  end-to-end as `e2e`. Re-added ttfa (dataclass/summaries/extraction/put/
  JSONL) that auto-merge had dropped.
- openai_chat/tts clients now emit ttfa+e2e for audio (was mislabeled ttft)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… as ttfc

Keep end_to_end_latency as the request-level key (e2e stays the SLO alias),
and collapse ttfa (TTS) / ttft (STT) into a single ttfc across clients,
evaluators, SLO config, and analysis scripts. Fixes 3 stale unit tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor STTClient into a base + per-provider subclasses (vajra, vllm_realtime)
selected by provider via a __new__ factory, then trim to the two paced-WebSocket
paths actually used: drop the vllm batch/SSE provider, vajra batch, httpx
plumbing, tpot, and the mime/language/streaming config knobs.

Fix the capacity gate: P95 RTF < 1.0 is unsatisfiable under 1x pacing
(e2e >= clip length, so RTF >= 1 by construction), so gate on P99 ttfc instead
and drop the dead TPOT SLO. Equate the two configs to engine-vs-engine (both
serve Voxtral) leaving only provider/api_base/output_dir different, and strip
verbose comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- stt.py: drop the __new__/__init_subclass__ provider registry for a plain
  factory + dict; share one concurrent send/recv stream loop across both
  providers (consistent live ttfc under 1x pacing); remove vajra-only
  first_partial/final_latency timings.
- audio.py: drop dead tpot field; compute WER via the vendored Open ASR
  Leaderboard EnglishTextNormalizer + jiwer instead of hand-rolled tables.
- asr_normalizer/: vendor normalizer.py + english_abbreviations.py from
  open_asr_leaderboard (attributed) so WER matches the leaderboard.
- Merge prepare_audio_buckets.py into a single prepare_audio_traces.py.
- Remove the {datetime} YAML template-token feature; update configs/sweep.
- pyproject: add regex dependency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-vajra/veeksha into users/mayank/stt_client

# Conflicts:
#	pyproject.toml
#	scripts/utils.py
#	veeksha/client/tts.py
#	veeksha/config/slo.py
#	veeksha/evaluator/performance/audio.py
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Added Artificial Analysis style datasets and metrics to exactly match the metrics they publish for ASR models
# Conflicts:
#	.gitignore
#	pyproject.toml
#	veeksha/config/core/flat_dataclass.py
#	veeksha/config/evaluator.py
#	veeksha/config/generator/session.py
#	veeksha/config/server.py
#	veeksha/config/utils.py
#	veeksha/generator/session/trace/__init__.py
#	veeksha/generator/session/trace/registry.py
#	veeksha/orchestration/__init__.py
#	veeksha/orchestration/registry.py
#	veeksha/types/__init__.py
* [stt] Add ASR interactivity traces

Major changes:

1. Add word-timestamp interactivity scoring and STT transcript snapshots.

2. Refactor ASR trace preparation into source adapters plus NeMo alignment helper.

3. Keep ASR WER aggregation request-scoped and remove Earnings22 chunk regrouping.

4. Add max-duration trace selection with explicit underfilled-count failure.

5. Add mamba environment setup docs and focused unit coverage.

* [stt] Consolidate ASR trace timestamping

Fold the ASR trace dataset adapters and NeMo Docker alignment path into prepare_audio_traces.py so one command generates timestamped VoxPopuli, Earnings22, and AMI traces.

Key changes:

- Make word timestamping the default trace-generation path with a --without-word-timestamping escape hatch

- Chunk timestamped long clips on word boundaries with a 30 second default max duration

- Remove the separate align_audio_trace and asr_trace_sources modules

- Update unit imports and ASR benchmarking notes for the consolidated flow

* [stt] Automate AMI trace preparation

Download AMI manual word annotations and Mix-Headset audio from the official AMI host when building ami_word_timed traces. This removes the local path setup requirement while keeping native AMI word timestamps separate from the NeMo Docker alignment path.

Key changes:

- Cache AMI annotations and meeting audio under benchmark_output/ami_cache

- Derive trace output roots from the selected dataset family

- Remove the public output-dir override from prepare_audio_traces

* [stt] Harden ASR interactivity scoring

Allow requests with missing or empty transcript snapshots to skip interactivity scoring instead of failing the completion worker. Expand normalized reference entries so timestamped words that split or disappear under normalization do not crash aligned-manifest runs.

Key changes:

- Return no interactivity metric when snapshots are absent or empty

- Expand normalized reference words before matching snapshots

- Add regression coverage for normalized references and empty snapshots

* [stt] Add metadata for interactivity frontend

* Added non negative clamp for interactivity

* [stt] Add ASR replay viewer

Add a standalone replay viewer for ASR request metrics so benchmark runs can visualize ground-truth word timing, streamed transcript arrival, and replay-derived latency over time.

Key changes:

- Export ASR request metrics into an asr_replay.json bundle

- Add a self-contained browser viewer under tools/asr_replay_viewer

- Load ECharts dynamically from a pinned CDN URL

- Document how to export and open the replay viewer
…y tests (#201)

* Added a --target-duration flag to prepare_audio_traces.py to loop the dataset audio clips and transcripts to make them close to the passed duration

* Made asr replay viewer more efficient for 10min workloads

* Changed _maybe_pace->_maybe_pace_until in stt.py so that we sleep until a specified timestamp instead of a fixed time, this prevents time drifts in sending

* Added stddev to prepare_audio_traces target-duration
…ming asr (#202)

Fix: Removed _audio_to_pcm16_bytes from ttfc calculation
AgrawalAmey and others added 30 commits July 17, 2026 21:43
…snapshots

Disconnected benchmark clients leave sessions decoding server-side
(the ws handler finalizes them, but the Talker keeps decoding to
eos/length_cap) -- ~122 zombies measured after a c=320 run. They
compete for decode capacity and silently skew subsequent runs.

New TTSZombieSessionProbe GETs {api_base}/debug/tts_worker_stats at
benchmark start and end (built only for Vajra endpoints that declare a
health_url) and the health check compares the server-side finished
delta (talker.finished.eos + talker.finished.length_cap) against the
requests this benchmark completed. A surplus fails the check with the
per-cause delta; a deficit is noted (sessions still decoding at the
end snapshot). A 404 / unreachable endpoint / missing Talker snapshot
skips the check with a note instead of failing the run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
…e-tts-longform)

New `veeksha score-tts-longform` command implementing the quality-ladder
Tier-2 drift metrics for a single long-form waveform (PCM int16 or WAV):

- WER(t): non-overlapping 28 s faster-whisper chunks (greedy, language
  pinned, VAD off -- seed-protocol-adjacent; backend CTranslate2 vs HF is
  the documented deviation), exact Seed-TTS-Eval normalization (punctuation
  minus apostrophe, single-pass double-space collapse, lowercase), then ONE
  global jiwer alignment of the concatenated transcript vs the reference.
  Per-chunk approximate WER by attributing global alignment ops to chunks
  via hypothesis-word spans; deletions split duration-proportionally with
  empty (silent) chunks at the same position so collapse chunks keep their
  omission mass. Per-chunk counts sum exactly to the global S/D/I.
- UTMOS(t): balacoon TorchScript on 10 s chunks at 16 kHz, per-minute
  mean + min (min catches local collapse).
- Repetition/omission: per-chunk ins/del split, duplicated-5-gram counts,
  zlib compression-ratio > 2.4 loop flag (Whisper convention).
- Energy: RMS dBFS + silence fraction per 30 s bin, model-free numpy.
- SIM(t): non-overlapping 3 s WavLM-SV cosine windows vs prompt (or
  audio-head) anchor; degrades to a skip-note when the UniSpeech
  wavlm_large_finetune.pth TorchScript export is not configured/present
  (download source documented in --help).

Outputs: summary.json (global + per-chunk + per-bucket arrays), curves.csv
(bucket, wer, utmos_mean, utmos_min, ins, del, dup5grams, rms,
silence_frac), report.txt (global block, first-2 vs last-2 bucket drift
deltas, per-bucket table). Every model track degrades to a note when its
model is unavailable; the energy track always runs.

Also: normalize_text now includes the seed protocol's double-space
collapse, and the balacoon UTMOS helpers in verification/audio.py are
parameterized by (hf_repo, jit_file, device) for reuse.

45 unit tests: seed-normalizer exactness, chunk boundaries, alignment
attribution (planted ins/del/sub, silent chunks), bucket weighting
conservation, loop detectors, energy on synthetic sine+silence PCM,
fake-injected full pipeline, no-model graceful degradation, CLI
registration and --help.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XaoSFTBvHyAP2JVrdeWpTo
Merge users/anirudha/tts_v2 with PR #203's duplex-fluidity work.

- retain Vajra native streaming, cap/zombie health checks, and long-form scoring
- add ElevenLabs/Deepgram clients plus duplex, playable-frame, and fluidity metrics
- reconcile provider/timing contracts, client enums, verification, and ASR correctness
- document feature decisions and validation in LOG.md

Validation:
- 135 changed-feature tests pass
- 428 full-suite tests pass; 2 unchanged port-8000 tests deselected
- Black/isort clean; pyright has no new diagnostics versus the common baseline
Unify provider-specific TTS and STT implementations behind
transport-level public clients. Keep provider wire details internal so
configuration, evaluation, and metrics remain provider agnostic.

Key changes:
- replace provider-specific TTS types with tts and streaming_tts
- move TTS and STT wire behavior into internal protocol strategies
- share authentication, WebSocket URL, and error handling utilities
- update registry, evaluator, tests, and documentation
Add deterministic client-side aborts to Vajra streaming TTS so canary
runs can exercise server cleanup and slot-reclamation paths. Treat
intentional hang-ups as successful adversarial outcomes and keep their
partial output from skewing service-quality aggregates.

Key changes:
- configure aborts by input progress, received audio, or wall time
- mark aborted audio results and bucket them separately in evaluator exports
- exclude aborted requests from quality and truncation aggregates
- add protocol, configuration, evaluator, and integration coverage
  Add a reproducible WebSocket benchmark matrix for hosted realtime TTS
  providers across SeedTTS and ShareGPT traces.

  Key changes:
  - Add 100-request configs for five models at 50 words per second
  - Report first-input TTFB, trigger-to-playable latency, TTFC, RTF,
    and user-side fluidity
  - Save generated WAV audio under third_party_numbers/{model}/{trace}
  - Use X-API-Key, per-request UUIDs, Skylar, and 3000 ms buffering
    for Cartesia
  - Add protocol, configuration, and benchmark-matrix test coverage
ConcurrentTrafficScheduler latched its rampup reference at construction, but
warmup and session pre-generation run between construction and the first
dispatch. A setup phase longer than the rampup window consumed the whole ramp
before any session was dispatched, so the full target concurrency was admitted
at once and a configured 60-second rampup silently never fired.

BaseTrafficScheduler already exposes reset_reference_time as the pre-dispatch
hook and benchmark.py already calls it; RateTrafficScheduler already
implemented it. Add the concurrent override so it re-anchors the monotonic
reference and clears the latched ramp-complete flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tions

The rampup target is a function of time, but it was only ever sampled from
schedule_session and notify_completion. A generator that schedules its whole
backlog in one burst therefore samples the ramp only during that burst, and
with sessions longer than the ramp window no completion occurs inside it, so
the target is never re-read. Admission froze after the first few sessions and
released the entire remainder at once when the pilot sessions finished — an
arrival shape sharper than no ramp at all.

Re-run admission on the dispatcher's wait path and bound that wait by the
next ramp step while sessions are pending, so admission tracks the ramp
whether or not any session has completed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Health checks previously ran only inline at the end of a benchmark and
never affected exit status, so pipelines had no way to gate on a run's
validity. Add a `veeksha health` subcommand that re-runs every health
check against a finished benchmark output directory by reconstructing
the BenchmarkConfig from the run's persisted config.yml.

Key changes:
- HealthCheckConfig (`veeksha health --run_dir <dir>`) with
  --output_file override and --strict (default) non-zero exit when any
  check fails, making it usable as a post-run pipeline stage
- Carry over non-re-runnable sections (TTS zombie-session check) from
  the in-run report instead of silently dropping them; their recorded
  outcome still counts toward the exit code
- load_benchmark_config_from_run_dir round-trips the persisted
  config.yml back into a BenchmarkConfig
Prepared ASR/TTS traces were rebuilt from scratch by every user, which
made runs hard to reproduce: upstream HF datasets are unpinned, skipped
rows shift clip selection, and NeMo forced alignment can jitter across
GPUs. Publishing frozen trace bundles to a Hugging Face dataset repo
(avartha/veeksha-voice-traces) fixes the references everyone measures
against.

Key changes:
- scripts/audio_trace_hub.py: fetch/publish subcommands with a repo
  layout organized by direction (stt/, tts/) mapped onto the local
  traces/asr layout; pinned revisions, filtered manifest variants
  sharing one audio pool, validation before upload/after download
- prepare_audio_traces.py now writes build_info.json provenance
  (git commit, argv, upstream dataset revision SHAs, NeMo model/image)
CI resolved dependencies fresh via `uv pip install -e .`, which ignores
uv.lock. Once numpy 2.5 shipped, that resolution took it (numpy is
unpinned), which excluded numba 0.66.0 (`numpy<2.5`). The resolver then
backtracked numba to 0.53.1 -- whose metadata declares no upper Python
bound -- and its llvmlite 0.36.0 hard-fails to build on 3.14:

    RuntimeError: Cannot install on Python version 3.14.3;
                  only versions >=3.6,<3.10 are supported.

The failure only surfaces at build time, so a resolution dry-run looks
clean while CI dies.

uv.lock already pinned the working set (numpy 2.4.6, numba 0.66.0,
llvmlite 0.48.0 -- all with cp314t wheels), so pointing both install
paths at `uv sync --locked` fixes the build and makes CI reproducible
against future upstream releases. `--locked` rather than `--frozen` so a
pyproject.toml edit without a relock fails loudly instead of silently
installing a stale set.

librosa, added by this branch, is what first pulled numba into the
dependency tree and exposed the cap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
librosa, added by this branch, pulls in numba, and numba 0.66.0 requires
`numpy<2.5`. With numpy unconstrained, a fresh resolve takes numpy 2.5,
which rules out numba 0.66.0; the resolver then backtracks numba to
0.53.1, whose metadata declares no upper Python bound, and that pins
llvmlite 0.36.0:

    RuntimeError: Cannot install on Python version 3.14.3;
                  only versions >=3.6,<3.10 are supported.

That guard only fires at build time, so resolution looks clean and the
install dies afterwards -- which is how CI failed while a dry-run passed.

Capping numpy holds numba at 0.66.0 / llvmlite 0.48.0, both of which
publish cp314t wheels and need no compilation at all.

This fixes the install for end users as well as CI: the documented entry
point is `uvx -p 3.14t veeksha`, which resolves fresh from PyPI and never
consults a lockfile, so pinning here is what keeps that working.

Lift the cap once numba supports numpy 2.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ates

TTFC was coerced to 0 ms whenever no first-content event was ever timed
(`round(ttfc or 0.0, 3)`), so a request that produced no audio or an empty
transcript reported an impossibly fast 0 ms and dragged every latency
percentile down. TTFC is now None in that case, and the evaluator keeps
None out of the sketch instead of inventing a sample.

Excluding only TTFC left the aggregates inconsistent, though.
StreamingTTSClient deliberately still attaches the AUDIO channel when a
request emitted text deltas before failing, so a 502 "stream completed
without audio" reached the evaluator and its end-to-end latency,
chunk_count and RTF all landed in the sketches while its TTFC did not --
different denominators per metric. The evaluator now gates on
`success`, the way it already gated on `aborted`, so a failed request
keeps its row for forensics but feeds no aggregate. Rows carry `success`
and `error_code`, and the summary reports `failed_requests_count`.

An empty CDFSketch reported a fabricated 0 for mean and every
percentile, which undid the fix one layer up: a run where everything
failed would summarize as flawless 0 ms. It now reports no keys at all,
matching what get_summary() already did for the ASR latency sketches.

Also hardens the STT client: every provider handshake now decodes its
first frame through _decode_stt_json_message rather than a bare
json.loads, so a proxy error page or non-object JSON fails as a named
protocol error instead of an opaque 520 carrying a JSONDecodeError. The
_STTProviderProtocol.finish_messages return type becomes Sequence
because list is invariant and every implementation returns list[str].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This repository is public, so a few things that were fine internally are
not fine shipped.

Removes THIRD_PARTY_BENCH.md. It is an internal working note, not
documentation: named-vendor result tables with explicit pass/fail SLO
verdicts, an internal branch name and cluster path, and a "Before
publishing" section listing four prerequisites that were never met (the
ASR summary is recorded there as out of date, and the TTS latency numbers
as unpaired with any intelligibility check). Its reproduction steps also
reference scripts and result directories that are not in the repo.

Replaces machine-local trace paths in the five ShareGPT streaming-TTS
configs with a /path/to placeholder, matching the convention already used
in managed_server.yml -- those configs could not have worked for anyone
outside the original machine anyway.

Renames output_dir from the internal experiment layout
(third_party_numbers/<model>/<corpus>) to benchmark_output/, matching
every other sample config, and updates the test that pinned the old
value.

Drops references to an internal product brief from the user guides and
two configs, describing the metrics on their own terms instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants