Skip to content

Scene-aware presence/quality axis + ASR transcription overhaul (CrisperWhisper 2.0, Qwen) - #536

Merged
satra merged 36 commits into
alphafrom
20260722-175022-scene-quality-utterance
Jul 27, 2026
Merged

Scene-aware presence/quality axis + ASR transcription overhaul (CrisperWhisper 2.0, Qwen)#536
satra merged 36 commits into
alphafrom
20260722-175022-scene-quality-utterance

Conversation

@satra

@satra satra commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Reworks the audio_analysis uncertainty workflow's presence axis into a scene-aware dimension and overhauls the ASR / transcription stack, driven by SPEECH_PRESENCE_CERTAINTY_ANALYSIS.md, a spec-kit design (specs/20260722-175022-scene-quality-utterance/), and validation against a Label Studio ground-truth annotation of the 4-speaker, noisy audio_48khz_mono_16bits clip.

All new presence signals are additive columns — the presence axis name, parquet paths, LS tracks, and aggregated_uncertainty are unchanged, so downstream consumers keep working.

Scene-aware presence axis

  • Audio quality (per bucket, [0,1] degradation): quality_snr (Brouhaha frame SNR ✕ existing DSP SNR), quality_clip (existing proportion_clipped_metric), quality_reverb (Brouhaha C50), quality_bandwidth (torch.stft spectral rolloff vs Nyquist), plus quality_uncertainty (estimator spread).
  • Background sound sources: src_{speech,people,machine,environment} + src_dominant, from a checked-in AudioSet→category map over full AST/YAMNet distributions (--scene-top-k).
  • Fine temporal resolution: continuous frame posteriors from pyannote/segmentation-3.0 (+ Brouhaha VAD), per-axis grids (presence 0.1 s/0.02 s), and a presence_confidence / presence_uncertainty split; coarse voters demoted on fine grids. Long recordings handled via chunked sliding-window stitching.
  • pyannote/brouhaha runs in an isolated subprocess venv (it needs the GitHub-only brouhaha-vad + old pyannote); loads via ensure_hf_model.
  • Timeline PNG gains scene-quality and sound-source rows.

ASR / transcription overhaul

  • CrisperWhisper 2.0 turbo (new default) — verbatim + native word timestamps, via a platform-aware subprocess venv (ct2 on Linux x86_64, transformers elsewhere). Granite and whisper-large-v3-turbo dropped from defaults (kept opt-in).
  • Qwen3-ASR-1.7B retained (native word timestamps).
  • Qwen3-ForcedAligner-0.6B is the new default aligner for text-only ASR (e.g. canary); MMS still available via --aligner mms.
  • Native confidence flows via ScriptLine.score when a backend exposes it (verified: none of the current three do; plumbing is ready).
  • Quality-vs-noise: speech-appropriate SNR anchors (25→5 dB) so noisy recordings read more degraded than clean.

Infra fixes (surfaced during validation)

  • numba>=0.60 in all NeMo/Qwen subprocess venvs (qwen-asr, canary-qwen, nemo, nemo-diarization) — fixes the librosa→numba→llvmlite 0.36 py3.12 backtrack.
  • huggingface_hub<0.24 in the Brouhaha venv; peft in the canary venv; ctranslate2 on the CrisperWhisper transformers path.
  • librosa reverted from an explicit dep (bandwidth reimplemented on torch.stft).

Validation against ground truth (audio_48khz_mono_16bits)

  • Transcription: CrisperWhisper 2.0 + Qwen3-ASR both WER 0.05 with native word timestamps; canary works (Qwen-aligned).
  • Presence: frame acc 0.91, recall 1.0; the brief 0.38 s "Kenny" turn resolved; the ~0.4 s gap correctly high-uncertainty (both VADs detect activity).
  • Quality: noisy clip reads SNR-degradation 0.65 (clean ≈ 0); bandwidth 0.85→0.95 rolloff fix (no longer flags full-band speech).
  • Identity: diar models collapse on the 5 s clip (model-side); finer 0.25 s embedding clustering carries the signal.

Status / scope

  • Spec specs/20260722-175022-scene-quality-utterance/ (spec/plan/research/data-model/contracts/tasks) committed. US1–US3 implemented; US4 (utterance token-entropy) is partial (native-confidence plumbing only, as no backend exposes it); US5 (synthetic calibration harness) deferred.
  • Speaker-detection measures deferred per design discussion (transcription-cleanup prioritized).

Caveats for reviewers

  • CrisperWhisper ct2 path is validated only via the transformers backend on macOS arm64; the ct2 (Linux x86_64) path is wired but unrun here.
  • Native transcription confidence is currently absent (the chosen ASRs don't expose logprobs); re-add a whisper if that signal is needed.
  • Subprocess-venv model paths (Brouhaha, CrisperWhisper, Qwen, canary) are skipped by default CI (venv-absent skipif) and validated locally with HF_TOKEN.

🤖 Generated with Claude Code

Version

Published prerelease version: 1.3.1-alpha.35

Changelog

🐛 Bug Fix

  • Scene-aware presence/quality axis + ASR transcription overhaul (CrisperWhisper 2.0, Qwen) #536 (@satra @claude)
  • feat(canary): split long audio at pauses to avoid the ~40s truncation #534 (@wilke0818)
  • ScriptLine: accept empty text as "model produced no transcript" signal #520 (@wilke0818)
  • Feat/pii subprocess venv #519 (@wilke0818)
  • subprocess_venv: route torch/torchaudio via CUDA-aware PyTorch index #516 (@satra @wilke0818)
  • subprocess_venv: two-stage install to fix torch/torchaudio CUDA-tag s… #518 (@wilke0818)
  • Fix/ser wav2vec2 regression head #511 (@satra @wilke0818 @claude)
  • comparison & uncertainty stage for analyze_audio.py #512 (@satra)
  • audio analysis script + multilingual MMS aligner + 4 new ASR backends #510 (@satra)

Authors: 3

Rework the presence axis into a scene-aware dimension that also quantifies
audio quality (SNR, clipping, reverb, bandwidth) and background sound sources
(people/machine/environment), add per-axis temporal grids + frame-level
posteriors, and improve the utterance uncertainty estimator.

Presence axis name and aggregated_uncertainty output are preserved; all new
signals are additive columns. Reuses existing quality_control metrics + SQUIM
and adds pyannote/brouhaha (gated, no new dep, no subprocess venv) for
frame-level VAD + SNR + C50. Calibration bootstrapped from synthetic
clean+noise+RIR mixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 22, 2026 21:54 — with GitHub Actions Inactive
Phase 0 (research.md, D1-D10) + Phase 1 (plan.md, data-model.md,
contracts/, quickstart.md) for the spec-kit workflow.

Key decisions grounded in codebase research:
- Frame posteriors via pyannote segmentation-3.0 + brouhaha (Model+Inference,
  ensure_hf_model/local_files_only); no new dep, no subprocess venv.
- Quality: reuse quality_control DSP (clip/SNR) + brouhaha (SNR/C50) +
  librosa rolloff (bandwidth); computed on a 0.5s analysis window broadcast
  to the finer presence grid.
- Sources: raise scene top_k to full distributions + hand-authored AudioSet
  display-name -> {speech,people,machine,environment} map (no ontology in repo).
- Presence already uses mean-p_voice (not Shannon entropy); surface
  confidence/uncertainty split as additive columns.
- Utterance token entropy requires plumbing Whisper output_scores and
  extending ScriptLine (also revives the dead avg_logprob signal) - highest
  risk, phase 4, splittable.
- Calibration from synthetic noise + decay-RIR (numpy, no new dep).

Constitution: PASS with two justified deviations (backward-compat additive
columns; ScriptLine optional fields) recorded in Complexity Tracking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 02:13 — with GitHub Actions Inactive
46 tasks across 8 phases (setup, foundational, US1-US5, polish), organized
by user story for independent delivery. Tests-first per story. Records the
US4 (ScriptLine/Whisper token) risk containment and the compute.py shared
integration-point serialization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 03:15 — with GitHub Actions Inactive
satra and others added 2 commits July 22, 2026 23:51
…1-T005)

Phase 1-2 (setup + foundational) of the scene-quality/utterance rework:
- librosa promoted to an explicit dependency (spectral rolloff for bandwidth)
- UncertaintyRow gains 14 defaulted scene/quality/source/utterance fields
- write_axis_parquet emits matching nullable columns (uniform schema across
  the three axis parquets)
- scene_quality package scaffold

Existing workflow readers unaffected (22 regression tests pass;
aggregated_uncertainty unchanged). Foundation for US1-US5.

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

Adds four [0,1] audio-quality degradation scores + quality_uncertainty per
presence bucket:
- quality_snr    ← Brouhaha frame SNR (primary) x-checked vs existing DSP SNR
- quality_clip   ← existing proportion_clipped_metric
- quality_reverb ← Brouhaha C50
- quality_bandwidth ← librosa spectral rolloff vs Nyquist
- quality_uncertainty ← spread among independent SNR estimators

New pyannote/brouhaha loader (Model+Inference, ensure_hf_model/local_files_only,
gated HF token; no new dep, no subprocess venv). Quality computed on a fixed
0.5s/0.25s analysis window and broadcast onto presence buckets (analysis
resolution != reporting grid). Null-safe when the model/audio is unavailable
(FR-023); silent buckets report null signal-dependent quality.

compute_uncertainty_axes gains scene_quality=True; provenance records the
analysis window + brouhaha id/revision. Existing hermetic tests stub the loader
offline. 8 model-free quality tests + 2 end-to-end wiring tests. mypy/ruff/
codespell clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 04:02 — with GitHub Actions Inactive
…012-T017)

Adds per-bucket AudioSet source-category masses to the presence axis:
src_speech / src_people / src_machine / src_environment + src_dominant.

- data/audioset_source_map.json: versioned map of all 527 AST classes to the
  4 categories, derived by walking each class to its top-level AudioSet
  ontology ancestor (Speech-subtree->speech, Human sounds->people, Sounds of
  things->machine, Music/Animal/Natural/Source-ambiguous/Channel-env->
  environment). YAMNet's 521 classes are a complete subset (vendored list +
  coverage test, SC-003).
- sound_sources.py harvester: sums per-window scores into category masses,
  averages AST+YAMNet, normalizes, picks dominant; nearest-window projection.
- compute.py: sound_sources=True wires src_* columns + provenance additively.
- scripts/analyze_audio.py: --scene-top-k (default 50) persists enough of the
  distribution for meaningful masses; top-1 consumers unaffected.
- .gitignore: negate the broad data/ ignore for this package's shipped data.

Null-safe when neither classifier ran (FR-023). 5 source tests + 1 wiring
test; 95 workflow tests green; ruff/mypy/codespell clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 04:31 — with GitHub Actions Inactive
…18-T025)

- frame_posteriors.py: continuous per-frame P(speech) from pyannote
  segmentation-3.0 raw scores (Model+Inference, ensure_hf_model/local_files_only,
  gated; powerset -> 1-P(silence)); FramePosterior.mean_std_in_window.
- Per-axis grids: compute_uncertainty_axes gains presence_grid (default None ->
  shared grid, so legacy callers unchanged); CLI --presence-grid-win/hop
  default 0.1/0.02. Quality + sources now computed on the presence grid.
- presence.py: two frame-posterior voters (segmentation-3.0 + Brouhaha VAD);
  coarse voters (AST/YAMNet/no_speech_prob/ASR-overlap/embedding) demoted to
  weight 0.25 on grids finer than 0.5s (FR-014) via a weighted p_voice mean.
- aggregate.py: weighted _weighted_p_voice shared by aggregate_presence and
  presence_p_voice; weights default 1.0 so existing values are unchanged.
- Confidence/uncertainty split columns: presence_confidence (= p_voice) and
  presence_uncertainty (decisiveness OR within-bucket frame instability).
  aggregated_uncertainty stays byte-for-byte the legacy value (SC-008).
- Intensity mask unified on overlap-averaging so it's correct when presence
  runs on a finer grid than identity/utterance.

frame_posteriors + grid tests; split-column + instability test. 82 workflow
tests pass (excl. unrelated PII); ruff/mypy/codespell clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 11:36 — with GitHub Actions Inactive
Surfaced by running the gated models with a real HF_TOKEN:

- Null-safety (FR-023): PyannoteAudioModel validates against HF *at
  construction* and raises pydantic ValidationError for an inaccessible gated
  repo — that was OUTSIDE the try/except, crashing the workflow. Moved model
  construction inside the guard in both loaders (+ regression tests).
- Frame resolution: default pyannote Inference returns a coarse ~1s per-chunk
  aggregate; segmentation-3.0 now uses window="whole" for the native ~17ms
  per-frame posterior (validated: 0.0169s hop). Frame hop derived from the
  model receptive_field (window="whole" returns a bare ndarray, not a
  SlidingWindowFeature). Long-file sliding+stitching noted as follow-up.
- quality_bandwidth reimplemented on torch.stft instead of librosa (prefer
  torch for new algorithms); reverted the librosa pyproject/uv.lock addition
  (librosa was already transitive + directly used by quality_control).
- Script: scene quality is a REQUIRED signal — analyze_audio.py now hard-fails
  (exit 2) if Brouhaha is unavailable rather than silently shipping null
  quality columns; added --no-scene-quality / --no-sound-sources opt-outs.

Known blocker (surfaced, not yet resolved): pyannote/brouhaha is not a plain
pyannote-audio model — its checkpoint needs the `brouhaha` module from the
GitHub-only brouhaha-vad package, which pins old pyannote incompatible with
our 4.0.4. Loading path (subprocess venv vs DSP fallback) pending decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 12:14 — with GitHub Actions Inactive
Replaces the single window="whole" pass (which pyannote warns risks memory /
degraded results on long, frame-based inputs) with chunked_frame_inference:
slides bounded 10s windows (2s overlap), runs a whole-window pass per chunk,
maps chunk-local frames to absolute frame indices, and overlap-averages into
one continuous per-frame timeline. Flat memory for any length; native ~17ms
resolution preserved. Short clips (<=10s) stay a single pass.

Shared by both the segmentation-3.0 speech-posterior extractor and the
Brouhaha loader (dedup of the earlier per-module output/frame-grid helpers).

Validated on the real segmentation-3.0 model: a 15s input yields frame-level
output spanning the full duration at 0.0169s hop across multiple stitched
chunks. Unit tests cover the single-pass and stitched paths with a fake
inference. 86 workflow tests pass; ruff/mypy/codespell clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 12:21 — with GitHub Actions Inactive
pyannote/brouhaha isn't loadable in the main env — its model class lives in the
GitHub-only brouhaha-vad package, which pins pyannote.audio 3.1-3.3 /
speechbrain<1.0 / numpy<2.0 (incompatible with our pyannote 4.0.4). So Brouhaha
now runs in a dedicated venv (same pattern as the NeMo/Qwen ASR backends):

- ensure_venv("brouhaha", [brouhaha @ git, torch<2.3, numpy<2, soundfile]);
  torch routed via the CUDA-aware index by ensure_venv.
- Parent chunks each audio with senselab's Audio.window_generator (overlapping
  ~10s windows), saves chunk wavs; worker loads the gated model (HF_TOKEN from
  env), runs whole-window Inference per chunk, saves per-frame (VAD,SNR,C50)
  npy; parent stitches via the shared stitch_frames helper into BrouhahaFrames.
- Null-safe (FR-023): venv-build or worker failure → [None]; analyze_audio.py
  still hard-fails on all-null (scene quality is required there).

frame_posteriors: extracted the pure stitch_frames helper (shared by the
in-process segmentation extractor and the Brouhaha subprocess path).

Tests: venv-unavailable null-safety, mocked-worker frame assembly, stitch_frames
unit coverage. 87 workflow tests pass; ruff/mypy/codespell clean. Real venv
build validation pending (old-stack pins may need tuning on first build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 13:00 — with GitHub Actions Inactive
brouhaha-vad pins pyannote 3.x but not huggingface_hub; uv pulled a hub too new
(dropped the use_auth_token kwarg pyannote 3.x passes to hf_hub_download),
failing the worker load. Pin huggingface_hub>=0.19,<0.24.

Validated end-to-end against the real gated model (subprocess venv builds,
worker loads pyannote/brouhaha, runs inference): 288 frames @ 0.0169s on the
test clip; VAD in [0,1], SNR ~12dB, C50 ~27dB — confirming the channel order
and dB denormalization. Brouhaha SNR/C50 quality signals now functional.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 13:03 — with GitHub Actions Inactive
…FR-024)

Adds two rows to the aligned timeline PNG, sourced from the presence axis's
additive columns and rendered only when present:
- scene quality: the four [0,1] degradation lines (snr/clip/reverb/bandwidth),
  raw solid + enhanced dashed;
- sound sources: stacked category masses (speech/people/machine/environment)
  for the raw pass.

Refactors the row-index map to a running counter so inserting rows no longer
needs fragile hardcoded offsets. Existing rows/behavior unchanged when the new
columns are absent (scene_quality/sound_sources disabled). Test + synthetic
render confirm the new rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 14:55 — with GitHub Actions Inactive
qwen-asr depends on librosa without a version floor; in a fresh Python 3.12
venv uv backtracks librosa -> numba -> llvmlite to llvmlite 0.36.0, which
predates 3.12 support, failing the install ("Cannot install on Python version
3.12.0"). Pinning numba>=0.60 keeps the chain on llvmlite>=0.43 (numba 0.66 /
llvmlite 0.48 / librosa 0.11 — all 3.12-compatible). Verified by clean-venv
dry-run resolution. Unblocks the Qwen3-ASR backend in analyze_audio runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 18:50 — with GitHub Actions Inactive
Verified against a Label Studio ground-truth annotation of the 4-speaker
audio_48khz_mono_16bits clip (brief 0.38s turn, ~0.4s gap, background noise).
Generic changes (not clip-specific):

- quality_bandwidth: raise the rolloff percentile 0.85 -> 0.95 so it tracks the
  upper spectral edge (band-limiting / telephone) instead of the voiced-energy
  tilt. An 85% rolloff sat at ~1.8 kHz even for full-band speech, flagging every
  recording as ~0.77 band-limited; at 0.95 the same clip reads ~0.27-0.38 and a
  real 3.4 kHz telephone cut still reads ~0.57.
- Identity windows/hops: --cross-stream-win/hop 0.5 -> 0.25 and --embedding-hop-s
  0.5 -> 0.25 so sub-second speaker turns localize (kept the 1s ECAPA window).
  Embedding-clustered speakers went from 9 -> 17 windows with boundaries that
  track the GT turn structure. Fixed the stale --cross-stream help (presence has
  its own grid now).

Presence left as-is: recall 1.0 vs GT, resolves the brief turn, and the ~0.4s
GT "gap" is correctly high-uncertainty (both trained VADs detect voice-like
activity there — a real model/annotation disagreement, not a bug).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 19:28 — with GitHub Actions Inactive
Transcription-cleanup overhaul (part 1 of the ASR rework):
- New crisperwhisper.py subprocess-venv backend for CrisperWhisper 2.0
  (verbatim, word-level timestamps). Platform-aware backend: ct2 on Linux
  x86_64 (ctranslate2-crisperwhisper wheels are linux-x86-only), transformers
  backend elsewhere (macOS arm64 dev) loading the same safetensors weights.
  Standard ctranslate2 pinned on the transformers path to satisfy the library's
  unconditional top-level `import ctranslate2` (engine/hallucination modules).
  Native per-word/line confidence flows via ScriptLine.score when the library
  exposes it (2.0.1's WordTimestamp does not, so it degrades to None).
- api.py: route nyralabs/CrisperWhisper2.0* -> CrisperWhisperASR.
- analyze_audio.py default ASR set: CrisperWhisper2.0_turbo + canary-qwen-2.5b
  + Qwen3-ASR-1.7B (whisper-large-v3-turbo + granite dropped from defaults,
  kept as opt-in). Docstring updated.

Validated end-to-end on the GT clip (transformers backend): correct verbatim
text with word timestamps tracking the ground-truth turn boundaries, including
the brief 0.38s "Kenny" turn. Hermetic assembly + platform tests + a
venv-present integration smoke test. ruff/mypy/codespell clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 23, 2026 20:05 — with GitHub Actions Inactive
…ded)

Narrow the SNR->degradation window to 25 dB (clean) .. 5 dB (floor) so quality
discriminates noisy from clean recordings: clean conversational/studio speech
(>=25 dB) -> ~0, a noisy recording (~10-15 dB) -> 0.5-0.75, heavy noise
(<=5 dB) -> 1.0. The GT-annotated noisy clip's ~12 dB Brouhaha SNR now reads
0.65 (was 0.60 at the old 30..0 span), clearly elevated vs a clean recording's
~0. Generic, not clip-specific; overridable via the calibration profile.

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

The smoke test called ensure_venv WITHOUT max_cuda_version, so it reproduced the
old cu128 routing regardless of the fix (which lives in extract_brouhaha_frames)
— failing on the GPU runner even though the fix is correct. Pass
_BROUHAHA_MAX_CUDA_VERSION (the production constant) so the test builds through
cu121 like production does, and assert the ceiling is declared. This also
tightens the guard: blank/remove the ceiling and the build routes cu128 and
fails. Only this test failed on the prior GPU run (785 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 24, 2026 20:28 — with GitHub Actions Inactive
@satra satra removed ec2-gpu-test Trigger EC2 GPU tests gpu-ondemand:true Use on-demand EC2 instead of spot pricing labels Jul 25, 2026
@satra

satra commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @wilke0818 — the diagnosis is exactly right, and confirmed against the code + reproduced. pick_torch_index (src/senselab/utils/cuda_probe.py) keyed the wheel index solely on the host CUDA version and had no knowledge of a venv's torch ceiling, so on a CUDA-12.4+ host it selected cu128, which publishes no torch<2.3 linux-x86_64 wheel → brouhaha's Stage-1 install was unsatisfiable and the venv never built.

Reproduced directly with uv pip install --dry-run (py3.11, x86_64-manylinux2014):

  • cu128"you require torch>=2.0,<2.3 … unsatisfiable" (only an aarch64 wheel for torch 2.0.1)
  • cu121 → resolves torch==2.2.2+cu121 + matching torchaudio

Fix (your first suggestion — cap to the newest compatible index):

  • pick_torch_index / ensure_venv gained an optional max_cuda_version ceiling; the picker now selects the newest index ≤ min(host, ceiling). SENSELAB_TORCH_INDEX_URL still wins outright, CPU hosts and modern-torch venvs (canary/qwen, torch>=2.8cu128) are unchanged.
  • brouhaha declares _BROUHAHA_MAX_CUDA_VERSION = (12, 1)cu121 (forward-compatible with 12.x/13.x drivers for inference).

Validation: unit coverage for the ceiling in cuda_probe_test.py, plus a GPU-gated regression test (src/tests/audio/tasks/scene_quality/brouhaha_venv_gpu_test.py) that builds the real brouhaha venv on a CUDA host — it caught the bug (red on cu128), then went green after the fix. Confirmed on an EC2 GPU runner: the brouhaha venv builds via cu121 and the full suite passes (786 passed).

Commits: fix 90d43842, regression test 8c2b84f7/82745973.

On the secondary blocker — you're right that pyannote/brouhaha is gated. That's orthogonal to the routing bug (the venv build fails before any model load), so the smoke test deliberately isolates the venv/torch build and doesn't require HF_TOKEN; the model-access requirement remains for actually running the estimator.

…ask APIs

Architecture-review follow-up (T049/T050): pull the transcript-fusion and
forced-alignment math out of the adaptive workflow into reusable task packages,
and consolidate the duplicated ScriptLine tree-walks.

- New `senselab.audio.tasks.speech_to_text_ensemble` (fuse_word_streams,
  iter_word_leaves, load_calibrator) — the ROVER-lite word-slot voting, now
  workflow-agnostic. adaptive/fusion.py re-exports these for loop callers and
  keeps only the workflow-specific parts (word-stream collection, policy→weights
  translation, belief-state lookups, final/ artifact writers).
- New `forced_alignment/mms_fa.py::align_words_mms_fa` — extracted MMS aligner.
- `ScriptLine.iter_leaves()` consolidates the recursive word-leaf walk that was
  re-implemented across forced_alignment, plotting, and the workflow.
- Threads through backends/audio_io/evaluate/interventions/loop, aggregate,
  labelstudio, frame_posteriors, and the evaluation utils accordingly.

Fixes the `w` weight/word-dict shadowing (float vs dict[str,Any]) reintroduced
in the extracted fuse_word_streams. Full pre-commit (ruff/mypy/codespell/json)
green repo-wide; 245 affected tests pass locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 26, 2026 15:19 — with GitHub Actions Inactive
…ng (US4)

Completes User Story 4 of the scene-quality/utterance spec (T026-T034):
the utterance axis now hears a single model's private doubt, not just
cross-model transcript disagreement.

- ScriptLine gains optional avg_logprob / no_speech_prob / token_entropy
  (T028), mapped in from_dict. None for every non-Whisper backend, which
  revives the previously-dead avg_logprob reads in the harvesters.
- New speech_to_text/token_confidence.py (T029) extracts per-token softmax
  entropy, avg_logprob and Whisper's no_speech_prob.
- harvesters.mean_token_entropy_in_window + a token_entropy vote (T030),
  folded into aggregate_utterance as a fourth sub-signal (T031) through the
  existing --uncertainty-aggregator.
- ASR confidences are temperature-scaled onto a common [0,1] scale (T032);
  T=1.0 default reproduces the historical numbers bit-for-bit.
- votes.scene_quality_coupling (T033) raises reported utterance uncertainty
  where SNR is poor or a machine/environment source competes, recorded on
  the scene_quality_coupling column with the pre-coupling value kept on
  raw_aggregated_uncertainty and in model_votes — visible, not invisible
  (FR-019). New --utterance-scene-coupling-weights flag (T034).

Two deviations from the contract, both forced by transformers 5.5.4 and
documented in tasks.md + the module docstring:

- Contract A's `pipe(generate_kwargs={"output_scores": True})` cannot work:
  AutomaticSpeechRecognitionPipeline._forward keeps only sequences and
  token_timestamps and drops the scores before postprocess. Implemented as
  a context manager that temporarily wraps pipe.model.generate to observe
  its output, leaving pipeline behavior untouched.
- Requests output_logits (raw) rather than output_scores (post-processor),
  since Whisper's logits processors suppress <|nospeech|> to -inf, which
  would pin no_speech_prob at 0 and distort the entropy.

Verified end-to-end on openai/whisper-tiny, not just in unit tests: the
first cut returned all-None because Whisper's long-form path nests each
generate result under segments[batch][window]["result"] with unbatched 1-D
tensors, and because whisper-tiny spells the token <|nocaptions|> while
<|nospeech|> maps to the unk id. Both are now handled and covered by tests.
A real run yields avg_logprob=-0.61, no_speech_prob=1.8e-08, and 175 token
entropies spanning 0.02-4.29 nats (mean 1.25) — the range that motivated
the documented 3.0-nat normalization reference over log(vocab).

Behavior change: utterance aggregated_uncertainty now carries the coupling
multiplier when scene columns are present. Per SC-008 values are unchanged
when the scene features are disabled (absent columns give coupling 1.0);
the existing votes_test assertion was updated to state this explicitly.

234 passed / 8 skipped across the audio_analysis, speech_to_text and
data_structures suites; ruff + mypy clean on all touched files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 26, 2026 16:53 — with GitHub Actions Inactive
Fixes the 3 cpu-tests failures from 581b18b. Both causes are in the
capture seam, not the uncertainty math.

1. `'ModelOutput' object has no attribute 'dtype'` (2 failures). The seam
   injected return_dict_in_generate=True unconditionally, which changes what
   generate *returns*. Whisper's word-timestamp path already requests dict
   output so it was unaffected, but plain greedy decoding expects a tensor and
   broke downstream. The wrapper now restores the caller's return type —
   handing back `sequences` unless the caller itself asked for a dict — so the
   claim that observing the decoder leaves pipeline behavior untouched is
   actually true. A backend whose generate rejects the kwargs is re-run clean.

2. `TypeError: 'Mock' object is not iterable` (1 failure). whisper_token_ids
   did `set(tokenizer.all_special_ids)`, which raises on a Mock tokenizer.
   Non-int and non-iterable values are now filtered, since failing a whole
   transcription over a purely additive signal is the wrong trade.

Root cause of missing these locally: I ran the `speech_to_text/` directory and
never the sibling `speech_to_text_test.py` file, which holds the mock-pipeline
and parametrized HF-model tests. Both are now in the verified set: 51 passed
across both paths, 29 passed in token_confidence_test, and a real
whisper-tiny run still yields avg_logprob=-0.61, no_speech_prob=1.8e-08 and
175 token entropies (mean 1.25 nats) — unchanged by the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 26, 2026 17:16 — with GitHub Actions Inactive
claude and others added 4 commits July 26, 2026 18:37
… compat)

The scene->utterance coupling (581b18b) multiplies the reported utterance
aggregated_uncertainty by the scene multiplier, which is not a pure function
of the votes. The adaptive belief store now compares its re-aggregation
against raw_aggregated_uncertainty (pre-coupling; identical on pre-coupling
artifacts) and documents the scale in disagreements_resolved.json.

Verified: raw-anchored parity ALL CLEAN on 538/538 buckets of the reference
run; 23/23 votes+adaptive unit tests.
…eements (FR-024, T040/T042)

Makes the scene columns visible to the two human-facing consumers, additively.

- Label Studio gains `<pass>__presence__quality` and `<pass>__presence__sources`
  tracks, emitted only when the underlying columns exist — so a run with the
  scene features disabled produces a byte-identical bundle to before.
- disagreements entries now carry presence_uncertainty / quality_uncertainty /
  src_dominant, with an equal-aggregated tiebreak on presence_uncertainty. The
  primary ranking is untouched, keeping SC-008 intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tive schema completion (T035-T039, T042/T044/T045)

US5 calibration (T035-T039). data-model §5 specifies a nested versioned
profile while quality.py / aggregate.py consume flat keys, so calibration.py
is the bridge: load_calibration_profile (bundled → documented-defaults
fallback, fail-loud validation) plus profile_to_runtime.
scripts/calibrate_scene_quality.py synthesizes white/pink-noise SNR sweeps and
exponential-RIR RT60 sweeps, runs the workflow's own estimators,
least-squares-fits the dB anchors, and emits the FR-022 validation plot +
table behind a monotonicity gate (SC-007). SNR is always fitted; C50 only when
Brouhaha is available, since the DSP path has no reverb estimator — the
profile provenance records which. --calibration-profile threads the runtime
dict into both consumers via compute_uncertainty_axes(calibration=...) and
comparator_params.

Known gap, recorded in tasks.md: per-axis temperatures are CLI passthroughs.
Fitting them needs labeled correctness (the adaptive eval harness), so that
US5 sub-item is left open by design rather than faked with a placeholder.

Adaptive schema completion. from_harvests gains its integration test (T044);
final/diarization gains the RTTM sidecar, member_labels and per-segment
overlap flags, and the transcript a language field (T042);
contracts/interventions.md carries an implementation-alignment section (T045).

Local CLAUDE.md was updated for the new flags but is deliberately left
untracked, matching how this repo already treats it and AGENTS.md.

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

`uv run mypy .` reported 15 errors across 7 files; the repo now type-checks
clean (279 files, 0 errors). These were pre-existing — CI stayed green because
pre-commit's mypy hook only sees files changed in a commit.

One was a real runtime defect, not an annotation nit:

- `dependencies.py::_cached_error` constructed `RepositoryNotFoundError(msg)` /
  `RevisionNotFoundError(msg)`, but huggingface_hub's `HfHubHTTPError` takes
  `response` as a *required* keyword-only argument. Replaying a cached negative
  model lookup therefore raised `TypeError` instead of the repo/revision error
  callers catch. Now passes a synthetic 404 response — with its `request` set,
  since `HfHubHTTPError` reads `response.request` while building the message and
  httpx raises `RuntimeError` otherwise. Verified both types construct, raise
  and catch correctly.

- `sparc.py` declared `-> List[Dict[str, torch.Tensor]]` while returning a
  `ft_len` scalar among the tensors; the annotation was simply untrue, so it is
  now `List[Dict[str, Any]]` with the reason documented. No behavior change.

Remaining were genuine narrowing gaps: `language.py` dereferenced a possibly
`None` pycountry lookup (unreachable given the field validator — now asserted
explicitly, preserving the documented AttributeError for languages with no
ISO 639-1 code); `talk_bank_helpers.py` iterated `tokens` which is optional;
`metrics.py` rebound Tensor-typed names to numpy views (split into separate
names so the static type matches what librosa/scipy receive);
`embeddings_extraction` cast AutoTokenizer's backend union to the cached
interface; `frame_posteriors_test.py` marked its duck-typed Inference fake.

Also added "AER" (Alignment Error Rate) to `[tool.codespell]` ignore-words, so
the `uv run codespell` command documented in CLAUDE.md matches what
pre-commit already whitelists via its args.

Verified: mypy clean, ruff check + format clean, codespell clean, and
485 passed / 9 skipped across the quality_control, utils, text and
audio_analysis suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 26, 2026 23:30 — with GitHub Actions Inactive
…037 run)

Ran scripts/calibrate_scene_quality.py with Brouhaha available, so both the SNR
and C50 anchors are measured rather than assumed. SC-007 monotonicity gate
passed. The profile now ships at
workflows/audio_analysis/data/scene_quality_calibration.json and is the default
— a run without --calibration-profile picks it up.

The fit corrects a real systematic bias, which is the point of US5 rather than
just another artifact. Brouhaha's SNR estimate is strongly compressed (fitted
slope 0.41): a true 0-30 dB sweep spans only ≈4.7-16.6 dB of estimator output.
The uncalibrated anchors (clean 25 dB / floor 5 dB) assumed a true-dB scale, so:

    true SNR   Brouhaha est   default degradation   fitted
      30 dB       16.57            0.421             0.000
      25 dB       15.92            0.454             0.000
      20 dB       14.58            0.521             0.141
      10 dB        9.94            0.753             0.703
       0 dB        4.74            1.000             1.000

A pristine 30 dB clip was reported as 42% degraded, with the whole scale
squashed into [0.42, 1.0] — almost no dynamic range across the clean half. That
also fed the FR-019 coupling I added in 581b18b, inflating utterance
uncertainty on clean speech by ≈1.21x for no reason. Fitted anchors
(clean 15.75 / floor 7.50, in estimator space) map that clip to 0.0.

Test fix: test_default_profile_when_no_path_and_no_bundle asserted the
defaults-fallback but never isolated the bundle — it passed only because no
profile shipped. It now redirects BUNDLED_PROFILE_PATH via monkeypatch so it
tests the fallback it names, and two new tests cover what became the real
default path (bundled profile preferred; it validates and maps to the flat
runtime keys).

Also fixed a stale docstring: _bandwidth_degradation said "85% spectral
rolloff" while _DEFAULT_BANDWIDTH_ROLLOFF_PCT has been 0.95 since the
quality_bandwidth tuning commit. Note the profile's `bandwidth` block is
currently inert — quality.py reads the module constant, not the profile.

Scene spec tasks.md is now at zero unchecked items (T045/T046 executed here:
ruff + mypy clean at 279 files, 162 passed / 3 skipped in the workflow suite).
The one US5 sub-item still open by design is per-axis temperature fitting,
which needs labeled correctness from the adaptive eval harness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@satra
satra temporarily deployed to docs-preview July 27, 2026 00:32 — with GitHub Actions Inactive
CI pre-commit failed on 88e812f: the bundled profile was written with
indent=2 and no trailing newline, which end-of-file-fixer and
pretty-format-json (--indent=4 --no-sort-keys) both rewrite.

Fixed at the source rather than just reformatting the artifact — the fitter now
writes indent=4 plus a trailing newline for both the profile and the validation
table, so a future run that targets the package tree is committable without a
reformat round-trip. Reformatting is whitespace only: the bundled profile is
byte-for-byte content-identical to the fitter's output (verified by json
comparison), and the loaded anchors are unchanged at clean 15.747 / floor 7.495.

`pre-commit run --all-files` now passes with zero failing hooks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@satra
satra merged commit 5cda028 into alpha Jul 27, 2026
9 checks passed
github-actions Bot added a commit that referenced this pull request Jul 27, 2026
@satra
satra deleted the 20260722-175022-scene-quality-utterance branch July 27, 2026 23:54
wilke0818 added a commit that referenced this pull request Jul 28, 2026
wilke0818 added a commit that referenced this pull request Jul 28, 2026
#536 (scene-quality/utterance) + #538 (cached-inference refactor) merged to
alpha, so their model-load sites now live here too. Route them through the
helpers (cherry-picked from the hf-cache-on-536 integration branch), guard-driven:

In-process (SHA-pin / snapshot-path):
- voice_activity_detection/frame_posteriors.py (use the SHA ensure_hf_model returns)
- workflows/audio_analysis/adaptive/audio_io.py (SepFormer via snapshot path)
- workflows/audio_analysis/adaptive/backends.py (ASR pipeline via load_hf_resilient)

Subprocess (hf_subprocess_env):
- scene_quality/brouhaha.py (gated; token forwarded, online fallback preserved)
- speech_to_text/crisperwhisper.py (online fallback preserved)

Guard-test allowlist updated to cover all five; PR #1 now covers every HF load
site on alpha.

Co-Authored-By: Claude Opus 4.8 <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.

3 participants