[Evals] Synthetic-audio end-to-end harness for the per-speaker transcription pipeline #433
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
The per-speaker transcription change (#342) is the riskiest thing in v4.0.0 and has never run against a real Whisper server outside unit mocks. Neither has speaker resolution against real member rows, nor beats → validation → compose → anchors against a real LLM. The eval harness starts from a transcript, so nothing in it exercises transcription at all.
Every real recording has been destroyed and is unrecoverable, so this is built from invented audio: no real voices, no real content, committable, re-runnable against any provider indefinitely.
Design
Full plan and rationale: https://claude.ai/code/artifact/d8bd7647-9fc2-4c25-a3ad-375c977cdb2f
Settled:
CLAUDE.md's "do not commit audio files" rule.POST /api/bot/sessions/{id}/audiowithforce=true— the production intake path. Admin retry (admin.py:155) 409s unless the session is alreadyfailed, so it cannot be used for a fresh synthetic session.ic/ooc/meta/proposed.fullandic_only— so the cost of the table talk is measurable rather than assumed.What this measures that nothing currently does
Three prompts already instruct against table talk (
beat_service.py:456,speaker_service.py:381,audio_service.py:154) and nothing tests any of them.scoring.pyhas coverage, attribution, chronology and beat-validation — all recall or correctness. There is no precision metric in the harness at all, so nothing can penalise a summary for containing material that shouldn't be there. This adds the first one.Key design constraints
transcribe_tracksetsspeaker_labelandtrack_owner_idfrom the sameTrackobject, so asserting they agree tests nothing. Replaced with a per-speaker marker lexicon, asserting foreign-marker count is exactly zero — licensed by the asymmetry that ASR error can only miss a marker, never manufacture a foreign one. Markers are sequence-numbered so the assertion also catches timestamp corruption with attribution intact (the #320 shape).keptseconds, not speech seconds.compute_speech_spanspads each span 300 ms per side and folds gaps under 2 s, so fragmented speech inflateskeptwell above real speech time. Measure and assert it; do not infer it from the script.Acceptance criteria
CLAUDE.mdcarve-out for synthetic TTS clips, written before the first audio commit.track_sha256asserted.resolve_speakerslabel paths (GM, linked player with character, unlinked guest).Related
process_audiopath run in CI with no GPU.Spike complete — the premise holds, one design change needed
Ten utterances, three piper voices, eight invented proper nouns of varied difficulty. Rendered, put through the full Opus chain (
libopus -b:a {32,64,96}k -vbr on -application voip -frame_duration 20, decoded, then the bot's own resample command verbatim), assembled onto one 72-second clock, transcribed by the production Whisper server —large-v3-turboon CUDA.Results
The asymmetry the exact attribution assertion rests on — ASR error can only lose a marker, never manufacture a foreign one — is now measured rather than argued. Five of eight markers were mangled and not one appeared on another speaker's track.
Design change: markers must be screened, not chosen
Only 3/8 survived exactly. The failures follow no clean rule:
Quillfeather,Thistledown,BramblewickVarnholtAshgroveMarrowgateCopperkettleZhaltirmekSo: screen marker candidates empirically before committing them (render → transcribe → keep what survives, using the spike as the rig), and lean the assertion on a per-speaker aggregate floor rather than any individual marker. At ~2 markers/minute over 30 minutes there is ample redundancy for a ~40% loss rate, provided no single marker is load-bearing.
Match on word boundaries, not substrings. Substring matching passed
Varnholtinside varnholtz andAshgroveinside ashgrover — overstating survival by 2/8. Worse, it drifts upward as ASR gets worse, since mangled words keep the marker as a prefix.Five things for the build
--seed. Re-rendering is not reproducible. This independently settles the commit-the-clips decision: the clip must be the artifact, because it cannot be regenerated.--download-dirbelongs topiper.download_voices, not the synthesiser; passed to the synthesiser it was swallowed as input text and spoken aloud ("Download ire slash work slash voices"). Caught only by transcribing the clip. The assembler must verify clip content, not just existence — this is the repo's recurring failure mode appearing inside its own test harness.Sizing
Committed Opus totalled ~354 KB for ~48 s of speech, extrapolating to ~10 MB for the 30-minute scene. In line with the estimate in the plan.
Environment notes
POST /transcribewithaudio+speaker+ optionallanguage; also exposes/healthand/transcribe/session.Plan updated: https://claude.ai/code/artifact/d8bd7647-9fc2-4c25-a3ad-375c977cdb2f (§7 revised, §11 added)
Landed in PR #497 (merged 2026-09-05, CI green including the new harness job).
seed_db --i-know-this-writes --verifyseeds all three label paths and proves each resolves.Design change made on the way to green CI, worth knowing: the exact per-track PCM hash turned out to be a property of the CPU, not the audio — both the Opus float decode and the resample choose SIMD code paths at runtime, and a Ryzen with AVX-512 and a QEMU vCPU without AVX2 differ by at most ±1 LSB per sample. The contract is now: exact sample count, exact silence, exact all-zero track, exact golden transcripts, and a banded per-window RMS fingerprint (250 ms windows, 4 LSB / 0.5 % tolerance calibrated against the no-AVX box, where only 2 of 8 673 windows moved and by exactly 1 LSB); a determinism test proves the assembler is bit-exact within one environment. Recorded in the README with the numbers.
Known gaps (README): the full live run through
POST /api/bot/sessions/{id}/audiowith a real LLM and thefullvsic_onlysummary comparison run during dev validation of v4.2.0; #432 is exercised directly, not pinned end to end; per-carrier marker screening over-predicts survival by ~28 points in a full session.First live end-to-end run of the harness through a deployed stack (dev, v4.2.0, 2026-09-05). Both assemblies were seeded with
seed_db, submitted throughPOST /api/bot/sessions/{id}/audioand processed by the realprocess_audioagainst the bundled WhisperX v2 server (large-v3-turbo, cuda) and llama.cppqwen3.5.fullic_onlyleak_rules_numbers)This is the comparison the harness exists to make, and it came out the way the design predicts: removing the out-of-character register drops table-talk leakage to zero and lifts in-character coverage to 5/5. The one leak in
fullwas dice/difficulty talk rendered as narrative; the one lost required beat was the quiet player's second contribution (required_wren_objection), which fired and was correctly attributed inic_only.Also confirmed live: all three speaker-resolution paths (
label_mismatches: []), the silent track dropped with zero segments, both structural EXACT_ZERO guards (segments_before_late_join,segments_in_scripted_silence) empty, the provider queue holding oneqb:slot:asr:…lease during ASR and none after, and twosession_usagerows with the right provider and host.One fixture defect found and filed on #506 (item 5): seeded platform links have no
verified_at, so the attendance-autofill path resolves nobody and the uncaptured-member check is not actually exercised yet.