Record what a run was, not just what it printed - #28
Merged
Conversation
Evaluation persisted nothing: every WER figure and keyword table existed only as terminal output. Training kept metrics as a side effect of the HuggingFace trainer but recorded nothing about its own invocation, so an adapter directory could not tell you what produced it. Both gaps defeat the point of evaluating, which is comparing runs. listenr eval --output FILE writes the full result as JSON: version, model and dataset paths, aggregate WER per model, per-keyword recall as named fields, and a per-clip array pairing each model's hypothesis with the ground truth. The per-clip rows make the aggregate auditable and allow re-scoring under a different normalization without re-running inference. Works in split mode and single-audio mode. listenr finetune now always writes run.json beside the adapter: resolved args, base model, architecture, dataset path and split sizes, trainable parameter count, and the accelerator line. Written with status "started" before the first step so a crashed run still leaves evidence of what was attempted, rewritten with status "completed" at the end. Together with trainer_state.json this answers both questions an adapter directory raises: what produced this, and how did it go. Writes are atomic (tmp then replace, as the manifest writer does), so a crash mid-write cannot destroy a previous good record. WER is rounded to two decimals; a corpus WER's third decimal is noise. The new report module is pure and importable without torch, like the other helpers. Docs gain a "Keeping the results" section, including a pointer at the tensorboard logs the trainer already writes by default, which is real captured data most people do not realise they have. Verified on hardware: an eval with --output on a gfx1151 ROCm container wrote the report to a mounted volume, including the case that motivated this, where a previous ad-hoc attempt died on a read-only mount and left the day's most interesting result existing only in scrollback.
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.
The gap
listenr evalhas nine flags and not one writes to disk. Every WER figure and keyword table is stdout only.listenr finetunekeeps metrics via the HF trainer (trainer_state.json, tensorboard) but records nothing about its own invocation, so an adapter directory cannot tell you what produced it.This bit for real: a three-way keyword comparison (base 22% / base+prompt 62% / fine-tuned 13% recall) briefly existed only as terminal scrollback after an ad-hoc capture attempt died with
PermissionErroron a read-only container mount. And #26 made splits comparable; comparability is worthless if the comparison is never recorded.What this adds
listenr eval --output results.json— everything printed, plus what the printout compresses away:{ "listenr_version": "0.1.1.dev5+...", "created_utc": "2026-08-25T23:45:15+00:00", "mode": "split", "model": "/data/merged", "base_model": "openai/whisper-tiny", "dataset": "/data/dataset/hf_dataset", "split": "test", "n_requested": 8, "n_evaluated": 8, "wer_normalization": "whisper_english", "wer_pct": {"fine_tuned": 13.86, "base": 11.98}, "keyword_recall": {"fine_tuned": {"technology": {"hits": 3, "expected": 3}}}, "clips": [ {"uuid": "...", "audio_path": "...", "duration_s": 18.26, "reference": "...", "hypotheses": {"fine_tuned": "...", "base": "..."}, "keywords": {"fine_tuned": {"technology": true}}} ] }The per-clip array is the point: aggregates say whether something moved, clip rows say why, and they allow re-scoring under a different normalization without re-running inference. Works in split mode and
--audiosingle mode. Keyword tallies become named fields (hits/expected) — a results file should not require knowing that index 0 means hits.listenr finetunealways writesrun.jsonbeside the adapter — resolved args, base model, architecture, dataset path and split sizes, trainable-param count, accelerator line. Written withstatus: "started"before the first step so a crashed run still leaves evidence, rewritten"completed"at the end. Withtrainer_state.jsonit answers both questions an adapter dir raises: what produced this, and how did it go.Design notes
finetune/report.pyis pure (no torch), like the other tested helpers; 32 new tests, full suite 381 green.mergedmaps tofine_tunedin the file — keys in a results file outlive the code's terminology.finetune-amd.md, including the tensorboard logs the trainer already writes by default.Verified on hardware
Live eval on the gfx1151 ROCm container (
--n 8 --compare-base --keyword technology --keyword phone --output /data/out/results.json) wrote the report to a mounted volume, including the exact scenario that motivated this.🤖 Generated with Claude Code