Phase 0: ASR cassette record/replay layer for GPU-free pipeline tests - #1
Phase 0: ASR cassette record/replay layer for GPU-free pipeline tests#1pasorobo wants to merge 6 commits into
Conversation
Phase 0 of the JA/ZH ASR pipeline plan: introduce a cassette mechanism so the full pipeline (FFmpeg, VAD, online policy, DiffTracker, output formatting) can be exercised end-to-end on a Web sandbox or any GPU-less environment, with model inference served from a JSON file. - whisperlivekit/test_cassettes.py: Cassette / CassetteRecorder / CassetteASR. Captures the (audio, init_prompt) -> (tokens, segment_ends) triples emitted at the OnlineASRProcessor <-> ASR boundary, hash-keyed on the exact audio buffer. - whisperlivekit/core.py + config.py: new "cassette" backend that loads a CassetteASR into TranscriptionEngine. - whisperlivekit/test_harness.py: TestHarness.replay() and .record() classmethods; auto-saves cassette on clean __aexit__; bypasses the engine cache in cassette mode to avoid leaking state. - scripts/record_cassettes.py: CLI for recording cassettes against a real model on a GPU machine. - tests/test_cassettes.py: 10 isolation tests covering hashing, token round-trip, recorder, replayer (strict + lenient miss), schema validation, and the core engine integration. - CLAUDE.md: Tier 1 (replay) / Tier 2 (record) workflow documented. Streaming-only backends (Voxtral HF, SimulStreaming) use a different result flow and need a future cassette family; v1 covers the LocalAgreement-style three-tuple backends only.
Phase 4 of the JA/ZH ASR pipeline plan: Word Error Rate is not a meaningful metric for Japanese, Chinese, or Korean because word boundaries are not orthographically marked. Add Character Error Rate (CER) so committed cassettes and live runs can be scored against ja/zh references. - whisperlivekit/metrics.py: normalize_cjk_text() (NFC, lowercase, strip whitespace + CJK/ASCII punctuation) and compute_cer() with the same character-level Levenshtein DP shape as compute_wer. - whisperlivekit/test_harness.py: TestState.cer() and cer_detailed(). - tests/test_metrics_cjk.py: 19 tests covering Japanese, Chinese, NFC normalisation, mixed CJK/English, punctuation/whitespace insensitivity, and edge cases (empty inputs, CER > 1.0). - CLAUDE.md: documents the cer/cer_detailed methods and explains why word-level WER for ja/zh requires MeCab / jieba (deferred work). This unblocks JA/ZH evaluation for upcoming FireRedASR2 / SenseVoice integrations and for any cassette-based regression test.
Phase 3 of the JA/ZH ASR pipeline plan: pre-bake the recommended
backend / language / streaming policy combinations from the report's
"use-case cheat sheet" so users get optimal defaults via one flag.
- whisperlivekit/presets.py: PRESETS dict with ja-accuracy,
ja-realtime, ja-broadcast, zh-accuracy, zh-realtime, ja-zh-en,
hri-multilang, apple-silicon-{ja,zh}, en-fast.
- whisperlivekit/config.py: WhisperLiveKitConfig.from_preset(name,
**overrides). Caller overrides win on conflict.
- whisperlivekit/parse_args.py: --preset CLI flag using two-pass
argparse so explicit CLI flags naturally override preset defaults.
- whisperlivekit/test_data.py: Google FLEURS download for ja_jp and
cmn_hans_cn; failures are non-fatal so the test_data flow still
works in environments where only some languages are reachable.
- tests/test_presets.py: 32 tests covering preset registry hygiene
(typo guards via dataclass field validation), every-preset clean
loading, override precedence, and CLI integration through parse_args.
- CLAUDE.md: presets table; documents Web-sandbox network
restrictions (HF and most CDNs blocked, GitHub raw works) so
contributors know real-model cassette recording requires a GPU
machine with HF access.
The zh-* and hri-multilang presets currently target Qwen3 because
FireRedASR2 (Phase 1) and SenseVoice (Phase 2) are not yet wired in;
they will switch to those backends as those phases land, transparently
to anyone using the preset name.
Phases 1 + 2 of the JA/ZH ASR pipeline plan. Closes the two SOTA gaps the report flagged: Chinese (FireRedASR2 averages 2.89% CER on 4 public benchmarks, beating Qwen3-ASR-1.7B / Doubao-ASR / Fun-ASR / Whisper-v3) and a single multilingual backend for HRI use cases (SenseVoice covers zh/en/yue/ja/ko in one non-autoregressive model with emotion + audio- event side-channels). - whisperlivekit/firered_asr.py: FireRedASR2 wrapper around fireredasr2s.fireredasr2.FireRedAsr2. Upstream API takes batches of file paths, so the wrapper writes each numpy chunk to a temp WAV. Auto-resolves AED vs LLM variant from model_size / model_dir; AED produces word-level timestamps, LLM falls back to uniform spacing. - whisperlivekit/sensevoice_asr.py: SenseVoiceASR wrapper around funasr.AutoModel. Parses the <|lang|><|emotion|><|event|><|itn|> tag prefix into ASRToken.detected_language and a metadata dict. Disables the bundled VAD (Silero VAD already runs in the WhisperLiveKit pipeline). Falls back to per-character spacing for CJK without timestamps. - core.py / parse_args.py: register `firered` and `sensevoice` as CLI / config backend choices, both routed through the LocalAgreement policy (no streaming AlignAtt — both models are non-causal AED). - presets.py: zh-accuracy now defaults to FireRed (the actual Mandarin SOTA) instead of Qwen3; hri-multilang now defaults to SenseVoice for the single-model multilingual robot use case. - pyproject.toml: optional [firered] and [sensevoice] extras. The fireredasr2s package has no PyPI release yet so it's pinned to the GitHub repo URL. - tests: 11 isolation tests for FireRed (variant resolution, dict and tuple timestamps, CJK uniform fallback, punctuation segment ends) and 13 for SenseVoice (metadata parsing for ja/en/zh with all four tag classes, ASCII vs CJK token spacing, language propagation). All construct wrappers via __new__ so no model is loaded — Web-safe. End-to-end coverage requires a GPU machine with HF / ModelScope access. The cassette mechanism (Phase 0) covers replay; recording fixtures lands in a follow-up once a contributor runs scripts/record_cassettes.py against real audio.
Phase 5 of the JA/ZH ASR pipeline plan. Surfaces the new public API and makes the use-case cheat sheet from the original report discoverable. - whisperlivekit/__init__.py: re-export FireRedASR2, SenseVoiceASR, the cassette types (Cassette, CassetteASR, CassetteRecorder, CassetteMissError), and the preset helpers (PRESETS, get_preset, list_preset_names). Class import is safe without the optional fireredasr2s / funasr packages installed — model dependencies are loaded lazily inside load_model(). - README.md: new "Japanese / Chinese Backends" section with a use-case → preset → CLI cheat sheet, plus install snippets for the [firered] and [sensevoice] extras. Adds a "Configuration Presets" subsection showing programmatic from_preset() use. - README.md: optional-deps table now lists [firered] and [sensevoice]. - CLAUDE.md: "Adding a New ASR Backend" gains an explicit reference table pointing at firered_asr.py (file-path API), sensevoice_asr.py (metadata tag parsing), and qwen3_asr.py (numpy-in template), so contributors have concrete patterns to copy from for each common backend shape. No new tests — these are documentation and re-export changes only. The 85 existing tests across cassettes, CJK metrics, presets, FireRed, and SenseVoice continue to pass.
Two related fixes for honest real-time semantics: (1) tmpfs-aware temp file in firered_asr.py - _resolve_tmp_root() picks /dev/shm on Linux when available (tmpfs in RAM, ~1-2 ms write vs ~5-10 ms on SSD); falls back to system tmp on macOS, Windows, or any system where /dev/shm is missing or unwritable. - transcribe() now uses tempfile.mkstemp inside that root so each call produces a unique path without us maintaining a counter scheme, and there is no persistent per-session directory to leak. - 3 new tests cover the resolver: prefers /dev/shm on Linux, returns a writable directory on every platform, falls back when /dev/shm is monkeypatched away. (2) Documentation: split backends into "true streaming" vs "quasi-realtime" FireRedASR2 and SenseVoice are non-causal AED; LocalAgreement re-runs full inference on the growing buffer each cycle. That gives ~1-2 s lag on GPU — fine for live captioning and accuracy-first transcription, NOT appropriate for sub-second dialogue UI. The previous wording suggested they were drop-in replacements for Voxtral / SimulStreaming; this commit makes the trade-off explicit: - README.md: new "Streaming Characteristics" table that classifies each backend (✅ true streaming vs⚠️ quasi-realtime), with concrete latency numbers. Calls out FireRed's per-call temp WAV as a small but non-zero overhead and points users at zh-realtime (Qwen3-SimulKV) when sub-second latency matters more than CER. - CLAUDE.md: Architecture section reorganised into the same two tiers. - presets.py: every preset now carries an inline comment stating whether it targets a true-streaming or quasi-realtime backend, so a contributor reading presets can pick correctly without checking the README. - firered_asr.py / sensevoice_asr.py: top-of-file "Real-time characteristics" section spells out why each is quasi-realtime and when to choose something else. No backend behaviour changes; existing 85 tests + 3 new resolver tests all pass on the Web sandbox.
Real-time streaming: current pipeline state, limits, and improvement roadmap
This documents what works today, what is not truly real-time despite being Current state (after this PR)8 ASR backends across two tiers — same pipeline, very different latency
Documentation in Concrete limitations1. LocalAgreement re-runs full inference every cycle
This is fundamental to LocalAgreement; it cannot be fixed without moving 2. FireRedASR2 file-path API forces a temp WAV per callThe upstream Mitigations already in this PR:
Remaining cost: ~10-15 ms per call from 3. SenseVoice produces no per-word timestamps by default
4. Tier 2 (real-model) validation has not happened yetThe Web sandbox where this PR was authored cannot reach HuggingFace, so
Total 88 passing, but no real audio has touched FireRed or 5. SimulStreaming family does not use the cassette formatThe cassette v1 format records the Improvement opportunitiesOrdered roughly by impact / effort:
Reference
Generated by Claude Code |
Summary
Phase 0 of the JA/ZH ASR pipeline plan. Lays the testing foundation needed before adding FireRedASR2 / SenseVoice / language presets in later phases.
Adds a cassette mechanism that captures the interaction between
OnlineASRProcessorand an ASR backend at thetranscribe → ts_words → segments_end_tsboundary, hash-keyed on the exact audio buffer. The full pipeline (FFmpeg, VAD, online policy, DiffTracker, output formatting) runs for real; only the model inference is served from a JSON file. This unblocks development on Claude Code Web and any other GPU-less environment.What changed
whisperlivekit/test_cassettes.pyCassette,CassetteRecorder,CassetteASR,audio_hash, schema v1.whisperlivekit/core.py+config.pycassettebackend that loads aCassetteASRintoTranscriptionEngine. Routed throughOnlineASRProcessor(LocalAgreement).whisperlivekit/test_harness.pyTestHarness.replay(cassette_path, **overrides)andTestHarness.record(cassette_path, **kwargs)classmethods. Auto-saves cassettes on clean__aexit__. Bypasses engine cache in cassette mode to avoid state leaks across tests.scripts/record_cassettes.pyTestHarness.record()for batch recording on a GPU machine.tests/test_cassettes.pyCLAUDE.mdTier 1 / Tier 2 workflow
The cassette is hash-keyed on the exact audio buffer passed to
transcribe(), so any change to buffer-trimming or chunking will triggerCassetteMissErroron Tier 1 — that's intentional, re-record on the GPU machine when this fires.Out of scope for Phase 0
Test plan
pytest tests/test_cassettes.py -v— 10/10 pass on a GPU-less sandboxruff checkclean on all new/modified filespython -c "from whisperlivekit.test_harness import TestHarness; assert hasattr(TestHarness, 'replay')"succeedsscripts/record_cassettes.py --backend qwen3 --lan ja --audio <wav> --output tests/cassettes/qwen3_ja_smoke.jsonto validate end-to-end round-trip, then replay it on this branch and confirm pipeline output matches.TestHarness.replay()against a committed fixture (deferred to Phase 1).Generated by Claude Code