Repository navigation
ci: add ASR test workflow for Whisper transcription - #799
Merged
Merged
Conversation
Runs on real NPU hardware whenever ASR code changes: the Whisper engine
and its prebuilt NPU libraries, the audio front end and FFT, and the
server files that host /v1/audio/transcriptions. Like the model sweep,
it waits for the commit's Linux portable builds (default and HRX) and
tests those instead of rebuilding. Linux only for now.
asr_test.py goes beyond sweep_audio.py's non-empty check:
- json: word error rate against a reference transcript of the bundled
110 s clip, which spans four 30 s Whisper windows
- default response_format: same {model, text} as json
- verbose_json: same text as json, duration and language of the clip,
and segments that are ordered, non-empty, inside the clip and join
back into the text
Builds without verbose_json support report those checks as skipped
with a warning; --require-verbose-json makes them fail instead.
Co-Authored-By: Claude <[email protected]>
Waits for the commit's "Build Windows Packages" run as well and runs asr_test.py on the Windows default and HRX portables, on the [self-hosted, windows, x64, npu, sweep] runners. Adds src/lib/libfftw3* (Windows-only FFT DLLs) to the trigger paths. Co-Authored-By: Claude <[email protected]>
- Trigger on Whisper's NPU kernels (src/xclbins/Whisper-V3-Turbo-NPU2), the multipart upload parser, and the FFmpeg DLLs Whisper decodes audio with. - Run weekly against main's latest build, to catch what no commit changes: republished Whisper weights and runner driver or firmware updates. The event name joins the concurrency group so the weekly run and a push to main do not cancel each other. - Save every response body to the results artifact before parsing it, and fail with "response is JSON" instead of a parser error when the body is not JSON (the server answers with an empty body when ASR is not loaded). Co-Authored-By: Claude <[email protected]>
The test now measures what PRs #510, #794 and #798 need validated: - assets/asr_eval: 84 FLEURS test files (24.5 min, 6.8 MB) with human transcripts. English clips inside one window, ~83 s and ~308 s files that cross window boundaries, and 10 clips each of de, es, fr, ja, zh and ar. build_asr_eval.py rebuilds it; README.md carries the CC BY 4.0 attribution. Replaces the model-made test_audio reference. - asr_test.py: WER/CER per set and pooled (each file capped at its reference length), runaway transcripts, speed, format agreement, Gemma 4 chat transcription, and feature groups (verbose_json, timestamp_rules, window_tail, language, prompt, quiet_log) that fail the run only when required. --baseline compares with another build's results, --expected with fixed rates. - asr-test.yml: on a pull request, main's latest build runs first on the same runner as the baseline. Pushes and the weekly run compare with expected.json. A dispatch can test any PR's builds and set the required groups and drift limit. window_tail and prompt are required by default, since main passes them. Co-Authored-By: Claude <[email protected]>
…ault The first CI run measured main on Windows and Windows HRX: identical sources decode differently per platform, by up to 5 points on a set (de-fleurs 43.3% on Windows, 38.1% on Linux). One set of expected rates would fail Windows pushes for no regression, so expected.json is now keyed by --platform: Linux and Linux HRX from local runs of main's portables, Windows and Windows HRX from that CI run's baselines. main's Linux HRX build loops on the long-form file (120 repeated phrases), so window_tail is no longer required by default: every PR's Linux HRX job would have failed on main's behaviour. The baseline check "no more repeated phrases than the baseline" still catches a PR that adds repeats. Co-Authored-By: Claude <[email protected]>
- Chat transcription also runs gemma4-it:12b, which has its own audio front end (modeling_gemma4_12b_audio.cpp); E2B's is shared by E4B and the flash variants. Both jobs pull it. - asr_summary.py renders the results as one report: a verdict per model and platform, then a section per model (Whisper accuracy per set, behaviour, checks by group, failures and warnings; each chat model's WER) with Linux, Linux HRX, Windows and Windows HRX side by side. A new summary job writes it to the run summary; asr_test.py uses the same renderer for each job's own report, which stays in its artifact. - asr_test.py refuses to start when its port is already taken. flm exits when it cannot bind, but another server answers /v1/version, so the run would have carried on against it. - State the tolerances: 1 point pooled and 5 per set against main; 2 points pooled and the same 5 per set against expected.json. Co-Authored-By: Claude <[email protected]>
3 of 4 tasks
quine00
pushed a commit
to quine00/FastFlowLM
that referenced
this pull request
Oct 16, 2026
The ASR test failed PR ROCm#798 on Windows HRX with "no more runaways than the baseline: 4 vs 3". The new runaway, de-fleurs-02, loops on its first half in both builds; ROCm#798's build also transcribed the second half that main's dropped. Same 12 errors, more of the real speech, but the extra words pushed it past 1.5 times the reference length. - A file now counts against a build only if it became a runaway, or repeats more phrases, and also has more errors than in the baseline. - The per-set limit applies to sets of ten or more files. A single long-form file moves by many points whenever one decode flips, which HRX builds do between identical runs (main's own Linux HRX build scored 18.5% and 36.8% on it); those files are left to the pooled rate and the two checks above. Replayed against every CI result so far (ROCm#798 and ROCm#799, four platforms each) and local runs of ROCm#510, ROCm#794 and ROCm#798: all pass, including ROCm#798 on Windows HRX. A synthetic new loop with more errors still fails. Co-Authored-By: Claude <[email protected]>
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.
Summary
Adds an ASR Test workflow that measures speech recognition on real NPU hardware whenever ASR-related code changes. It also gives PRs #510, #794 and #798 a way to be validated on hardware. CI currently has no hardware coverage for ASR, because the model sweep's audio task is disabled.
What it measures (
.github/script/asr_test.py):json, noresponse_format, andverbose_jsonmust agree.gemma4-it:e2b(whose audio front end E4B and the flash variants share) andgemma4-it:12b(which has its own,modeling_gemma4_12b_audio.cpp).Feature groups. These only fail the run when required; otherwise they are reported as warnings:
verbose_jsondurationequals the clip lengthtimestamp_ruleswindow_taillanguagepromptquiet_logOnly
promptis required by default, because it is the only groupmainpasses on every platform.main's Linux HRX build loops on the long-form file, so it failswindow_tail; a PR that adds repeats is still caught by the baseline comparison. A PR that makesmainpass another group adds it toASR_REQUIREin the workflow.Comparison. On a pull request,
main's latest build runs first on the same runner and becomes the baseline. The run fails if:The report also shows how many files changed and the drift from
main's transcripts. Pushes tomainand the weekly run compare againstasr_eval/expected.jsoninstead, with 2 points of slack on the pooled rate (versus 1 point againstmain) and the same 5 points per set. Its rates are kept per platform, because identical sources decode differently on each:mainscores 18.55% pooled on Linux, 22.63% on Linux HRX, 18.61% on Windows and 19.44% on Windows HRX, and some sets differ by up to 5 points. A manual dispatch can test any PR's builds (pr) and setrequireandmax_drift.Evaluation set (
.github/script/assets/asr_eval/, 6.8 MB, CC BY 4.0, attributed in its README):en-shorten-longen-longformde,es,fr,ja,zh,arbuild_asr_eval.pyrebuilds the set deterministically from the FLEURS Parquet files.Triggers: pull requests and pushes to
mainthat touch any of the following, plus a weekly run and manual dispatch:fftw,pocketfft) andaudio_process_utils;rest_handler.cpp,server.cppandmultipart.*;Linux and Windows are both tested, default and HRX builds alike.
Report. A
summaryjob writes one report to the run's summary page. It starts with a verdict per model and platform. Then each model gets its own section with Linux, Linux HRX, Windows and Windows HRX side by side:whisper-v3:turbo: accuracy per set againstmain; runaways, changed files and speed; checks by group; failures and warnings per platform.main, plus its own failures.Each job's own report, results JSON and raw responses are in its artifact. The script also refuses to start if its port is already taken, so it can't end up testing another server by mistake.
Validation of #510, #794 and #798 on hardware
I ran each PR's CI-built Linux portable against
main(77bc6bdd) on a Strix NPU. Each run required the default groups plus the ones that PR adds.mainmainverbose_jsonprompt#510 (
verbose_json). Onmain's decoder, segments are invalid in 28 of 84 files, 5 of them English short clips:Most of this comes from
main's decoder, which #794 fixes. #510 could still clamp segment ends to the duration and drop empty segments.#794 (decode loop). It reproduces the claimed gains on every set, removes all runaway loops, rejects unknown language codes, and stops logging transcripts. However,
promptlooks broken. When a prompt word is spoken in the clip, the transcript drops everything up to it:prompt="China's"returns " economic output." instead of the whole sentence;When the prompt words aren't spoken before the end, it works as intended: "Fichte" is then spelled correctly. The timestamp rules can't be observed without
verbose_json(#510).#798 (pocketfft). No regression: pooled +0.27 points, no new runaways, and Gemma is unchanged. 18 files changed, nearly all by one word or character; one change is a fix (漏れ効いた → 漏れ聞いた). That's the expected effect of slightly different FFT rounding tipping a borderline greedy decode.
en-shortis byte-identical.Test plan
mainand the three PR builds run throughasr_test.pyon a Strix NPU (results above)gh: pull request with an expiredmainrun and a missing HRX baseline, dispatch with a PR number, and push without a baselineactionlintis clean apart from the custom runner labelsmainthen this PR's build, about 18 min per job; 42/42 checks pass, 0 files changed (this PR changes no engine code)main's Linux HRX build measured locally forexpected.json: it loops on the long-form file ("the state of the state of …"), 36.8% WER against 16.5% on Linuxpromptchecks)gemma4-it:12b: first tested by this PR's next CI run. It could not run locally, because the test NPU was busy with another CI job.🤖 Generated with Claude Code