SER silent-failure guards: Phases 1-7 (loud-fail guard + encoder registry + problem_type + Phase 2 warning + diagnostics CLI)#514
Merged
satra merged 7 commits intoMay 10, 2026
Conversation
…ation plan Phase 1 of specs/20260509-204500-ser-silent-failure-guards/plan.md. The custom Wav2Vec2 emotion-head path in `_classify_wav2vec2_speech_cls_ser` trusts the dispatcher peek to have correctly matched the checkpoint's head layout. When that assumption is wrong (new encoder family, unrecognized final-layer attribute name, or single-bin checkpoint not in the registry) transformers silently random-initializes the head and inference returns ~uniform softmax — the exact bug PR #511 is fixing, just one routing branch upstream. Convert that to a loud `RuntimeError`. After `from_pretrained` returns we inspect `loading_info` for any `classifier.*` entry in `missing_keys` (weights absent, would be randomly initialized) **or** `mismatched_keys` (weights present but wrong shape — equally a silent-random failure that the original missing-keys check would miss; flagged by architect review). We also assert the final layer's `out_features` equals `config.num_labels` / `len(id2label)`, catching the case where head and config disagree on the number of classes. Adds a parametrised unit test that mocks `EmotionModel.from_pretrained` to return each broken-load shape and asserts the diagnostic raises with a matching message. Also commits the full remediation plan (specs/.../plan.md) enumerating Phases 2-7 (standard-pipeline missing-key warning, encoder-family generalization for HuBERT/WavLM, head-registry richening, problem_type based discrete/continuous detection, resampling in the standard pipeline, diagnostics CLI). Plan was self-audited and reviewed by the Plan agent before this commit; its recommendations are folded in (mismatched_keys coverage promoted into Phase 1, dynamic HeadSpec cut from Phase 4 as premature, encoder-attribute-name registry promoted from a Phase 3 footnote to a load-bearing task). https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
Contributor
There was a problem hiding this comment.
Code Review
This pull request implements Phase 1 of a plan to eliminate silent-failure modes in speech emotion recognition by adding a plan document and implementing post-load assertions for classifier keys and output shapes. New unit tests verify these safeguards. Review feedback suggests optimizing a list comprehension with the walrus operator and updating the plan document to mark the unit testing task as completed.
Implements the remaining phases from
specs/20260509-204500-ser-silent-failure-guards/plan.md, layered on top
of Phase 1's loud-fail guard:
- Phase 4 (head registry): introduce ``_HeadEntry`` dataclass holding
``final_layer`` / ``activation`` / ``dropout_field``; promote
``_KNOWN_WAV2VEC2_EMOTION_HEADS`` (str→str) to ``_KNOWN_HEAD_LAYOUTS``
(str→_HeadEntry). Generalize ``_make_wav2vec2_regression_head_class``
to ``_make_regression_head_class(head: _HeadEntry)`` with a small
``_ACTIVATIONS`` table. Cache key now includes activation/dropout_field
so future entries don't collide.
- Phase 3 (encoder-family generalization): ``_BASE_REGISTRY`` maps
``config.model_type`` → ``(PreTrainedModel base name, encoder attr
name)`` for wav2vec2/hubert/wavlm/wav2vec2-bert. Resolved lazily so
imports only fire when the custom-head path runs. ``_emotion_head_kind``
now returns ``(model_type, _HeadEntry)`` tuples; ``_make_emotion_model_class``
threads the right encoder class and attribute name. The Phase-1 guard
catches mis-registered families: any wrong attr-name will surface as
"every classifier.* key missing" with a clear ``RuntimeError``.
- Phase 5 (problem_type detection): ``_resolve_apply_softmax`` prefers
``config.problem_type`` over the keyword-based label heuristic. A
regression head with non-AVD axes (e.g. ``["energy","pleasantness"]``)
no longer gets softmax misapplied. Falls back to ``_get_ser_type`` for
legacy checkpoints that omit ``problem_type``.
- Phase 2 (standard-pipeline missing-key warning):
``_get_hf_audio_classification_pipeline`` now loads the model
explicitly via ``AutoModelForAudioClassification.from_pretrained(...,
output_loading_info=True)`` and threads the loaded instance into
``pipeline(model=..., feature_extractor=...)``. ``_check_head_loaded_cleanly``
warns on suspect missing/mismatched head keys
(``classifier.``/``head.``/``score.``/``out_proj.``/``projector.``).
``SENSELAB_STRICT_HEAD_LOAD=1`` promotes the warning to a hard error.
- Phase 6 (resampling in standard pipeline): verified already implemented
at ``classify_audios_with_transformers`` lines 141-142; no code change
required. Plan updated to reflect the discovery.
- Phase 7 (docs + diagnostics CLI):
- ``doc.md`` enumerates the three dispatch paths (custom in-process
head / subprocess venv / standard pipeline) and points at the CLI.
- New ``python -m
senselab.audio.tasks.classification.speech_emotion_recognition
--probe <repo_id>`` reports model_type, architectures, dispatch
decision, head source, and emits advisories. Pure config / manifest
inspection — no weights loaded.
Backwards-compat shims kept in place:
- ``_make_wav2vec2_emotion_model_class(final_layer)`` (delegates to
``_make_emotion_model_class("wav2vec2", _HeadEntry(final_layer=...))``)
- ``_wav2vec2_emotion_head_kind`` (returns just the final-layer name from
the new ``_emotion_head_kind`` tuple).
- ``final_layer=`` kwarg on ``_classify_wav2vec2_speech_cls_ser``
short-circuits to ``_HeadEntry(final_layer=...)``.
Verified locally: all 8 classification tests pass (1 GPU-skip), audeering
happy path passes, ehcalabres CLI probe correctly identifies its head
from ``_KNOWN_HEAD_LAYOUTS`` registry, audeering CLI probe peeks the
manifest, superb correctly routes to the standard pipeline with an
advisory.
Test coverage gaps (deferred per plan):
- Phase 2 unit test for ``_check_head_loaded_cleanly`` (covered
end-to-end by audeering/ehcalabres tests for now).
- Phase 5 unit test for ``_resolve_apply_softmax`` (no fixture model
with problem_type=regression + confounding labels).
- Phase 4 numerical-regression test on existing checkpoints.
https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
Two small fixes from the second-pass architect review:
1. Make ``_BASE_REGISTRY`` a 3-tuple ``(base_pretrained_cls_name,
encoder_model_cls_name, encoder_attr_name)`` instead of deriving the
encoder class name via ``base_name.replace("PreTrainedModel", "Model")``.
The string-mutation worked for current Wav2Vec2/HuBERT/WavLM/W2V-BERT
but would silently break the day a family ships an encoder with a
different naming convention. Explicit > clever.
Updated both indexers (``_emotion_head_kind`` in api.py and the
probe CLI's status print) to read attr name from index ``[2]``.
2. Probe CLI: when ``AutoConfig.from_pretrained`` fails (e.g. audeering's
``vocab_size: null`` tripping the strict validator), make the
diagnostic note explicit that the dispatcher's raw-config-dict
fallback handles this — the routing decision and advisories that
follow still work correctly because ``getattr(None, ...)`` short-
circuits the ``config.model_type`` checks.
Verified: existing guard tests pass; ``uv run python -m
senselab.audio.tasks.classification.speech_emotion_recognition --probe
audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim`` still produces a
correct routing report end-to-end.
https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
CI pre-commit hook runs mypy with --extra-checks which flagged the inferred type for ``model = HFModel(...)`` in __main__.py. Add the explicit annotation. cpu-tests already pass; this is the only blocker. https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
…2 unit tests
The strict reviewer identified 5 critical defects against the user's
"zero tolerance for silent failures" criterion. All addressed in this
commit:
D1. ``wav2vec2-bert`` removed from ``_BASE_REGISTRY``.
The family consumes log-mel ``input_features`` (via
``SeamlessM4TFeatureExtractor``) rather than raw ``input_values``.
The loader hardcodes the latter and accesses ``inputs["input_values"]``
so any wav2vec2-bert checkpoint would have produced ``KeyError`` at
best, silent garbage at worst. Add a per-family preprocessing
adapter before re-introducing.
D2. Phase-1 guard now covers encoder-key misses.
Previous filter was ``classifier.*`` only, which meant a wrong
``_BASE_REGISTRY`` entry (wrong encoder attribute name) would leave
every encoder weight missing yet pass the guard — silent garbage
output. The guard now treats any missing key under either
``classifier.`` or ``{encoder_attr}.`` as suspect, with a small
whitelist of HF post_init buffers (``masked_spec_embed``).
D3. ``_resolve_apply_softmax`` peeks ``architectures`` on
``problem_type``-absent checkpoints. Any model declaring
``Wav2Vec2ForSpeechClassification`` (audeering signature) is treated
as regression. Reduces the silent-corruption window for regression
heads with English-emotion-word labels.
D4. ``_peek_head_final_layer`` raises when ``classifier.dense.weight``
is present but no recognized final-layer attribute matches.
Previously returned None and let the standard pipeline silently
random-initialize the head. The error message lists the actual
classifier keys so a maintainer can extend ``_FINAL_LAYER_NAMES``
or ``_KNOWN_HEAD_LAYOUTS``.
D5. ``SENSELAB_STRICT_HEAD_LOAD`` default inverted to ``=1`` (raise).
Warn-only meant downstream consumers like
``classify_audios`` windowed segmentation kept aggregating
corrupted scores. Set ``SENSELAB_STRICT_HEAD_LOAD=0`` to demote to
a warning if you intentionally want a partial-head load (e.g.
fine-tuning warm-start).
T203. Three unit tests for ``_check_head_loaded_cleanly``: parametrised
raise on suspect missing/mismatched-shape keys (default strict),
warn-only mode under ``SENSELAB_STRICT_HEAD_LOAD=0``, and silent
pass on clean loads (encoder-buffer misses don't trip).
Plan checkboxes T203 / T304 / T502 promoted from deferred-shortcut to
actually-shipped. Audeering happy-path test still passes; mypy clean
with ``--extra-checks``.
https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
Reviewer round 3 verified D1-D5 + T203 fixes are functionally correct, but flagged three pieces of user-facing text that still contradict the new behavior — themselves a silent-assumption violation per the user's "no silent failures and assumptions beneath those" criterion: * ``doc.md`` listed ``wav2vec2-bert`` as a supported encoder for the custom-head path (it isn't; D1 dropped it from ``_BASE_REGISTRY``) and described ``SENSELAB_STRICT_HEAD_LOAD=1`` as a way to "promote the warning to a RuntimeError" — but ``=1`` is now the default (D5). A user who reads the doc, doesn't set the env var, and expects warn-mode would be surprised by a hard fail. * ``__main__.py`` probe CLI advisory had the same inverted message about the env var. * ``api.py:503`` dispatcher comment + ``api.py:144`` encoder-attribute comment still mentioned ``wav2vec2-bert``. This commit: 1. Removes ``wav2vec2-bert`` from the doc's supported-encoder list, adds an explicit "intentionally not supported" sentence with the ``SeamlessM4TFeatureExtractor`` reason, so users know why and what it would take to add. 2. Inverts the env-var description in both the doc and the CLI to match the strict-by-default semantics. 3. Drops stale ``wav2vec2-bert`` mentions from the api.py comments. No code-path or behavior change. Verified clean: ruff, ruff format, mypy --extra-checks all pass. https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
When the strict reviewer's second pass forced T203 and T502 from deferred-shortcut to actually-shipped, I appended the new ``[x]`` text and kept the original ``[ ]`` lines tagged "legacy text, kept for traceability". On re-read those duplicates just made the plan look like there were unfinished items when there weren't. Drop the duplicates; the ``[x]`` entries are sufficient. Remaining ``[ ]`` items after cleanup: T305 — attention-mask shim for batched HuBERT/WavLM (don't optimize for unknowns) T403 — numerical regression test against fixed scores (Phase-1 guard + bounded-score covers) T602 — sampling-rate test (Phase 6 was a no-change phase, end-to-end coverage exists) All three are honest deferrals, not shortcuts. https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66
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
Implements all phases of
specs/20260509-204500-ser-silent-failure-guards/plan.md. Targets PR #511's branch (fix/ser-wav2vec2-regression-head) so it stacks on the in-flight work.Two reviewer passes: a quick "ship it" pass that landed Phases 1–7, then a strict adversarial pass against the user's "zero tolerance for silent failures" criterion that found 5 critical defects and forced changes to the routing/guard semantics. The current state addresses all 5.
Commits
1ce3453Phase 1 — loud-fail post-load assertion (missing_keys+mismatched_keys+ final-layerout_featuresshape sanity).11d5800Phases 2–7 — registry refactor, problem_type detection, standard-pipeline warning, doc + CLI.9b92b72First-pass reviewer cleanups — explicit encoder class names in_BASE_REGISTRY, CLI null-config note.0849f3dmypy--extra-checksannotation fix from CI.bb4de3bStrict reviewer second pass — 5 silent-failure surfaces closed:wav2vec2-bertfrom_BASE_REGISTRY. It uses log-melinput_featuresviaSeamlessM4TFeatureExtractor, not rawinput_values. Re-introducing requires a per-family preprocessing adapter.classifier.*and{encoder_attr}.*missing/mismatched keys as suspect (with a small whitelist for HF post_init buffers). A wrong_BASE_REGISTRYentry now fails loudly instead of silently scrambling encoder weights._resolve_apply_softmaxnow peeksarchitectureswhenproblem_typeis absent. Any model declaringWav2Vec2ForSpeechClassification(audeering signature) is treated as regression — closes the silent-corruption window where a regression checkpoint with English-emotion labels would have been softmaxed._peek_head_final_layernow raises whenclassifier.dense.weightis present but no recognized final-layer attribute matches, instead of returning None and letting the standard pipeline silently random-init the head. Error message lists the actual classifier keys to guide adding to_FINAL_LAYER_NAMES/_KNOWN_HEAD_LAYOUTS.SENSELAB_STRICT_HEAD_LOADdefault inverted to=1(raise). Warn-only let downstream consumers like windowed segmentation aggregate corrupted scores. SetSENSELAB_STRICT_HEAD_LOAD=0to demote to warning for intentional partial loads (fine-tuning warm-start)._check_head_loaded_cleanlycover strict-by-default raise, warn-only mode, and silent pass on clean loads.What changed (by phase)
Phase 1 — Custom-path loud-fail (
api.py)After
EmotionModel.from_pretrained(..., output_loading_info=True):classifier.or{encoder_attr}.(the Phase-1 guard updated in commitbb4de3bper D2).out_features≠config.num_labels.Phase 2 — Standard-pipeline guard (
huggingface.py)_get_hf_audio_classification_pipelineconstructsAutoModelForAudioClassification.from_pretrained(..., output_loading_info=True)and threads the loaded instance intopipeline(model=..., feature_extractor=...). No double download._check_head_loaded_cleanlyraises by default on suspect head keys (classifier.,head.,score.,out_proj.,projector.).SENSELAB_STRICT_HEAD_LOAD=0demotes to a warning.Phase 3 — Encoder-family generalization (
api.py)_BASE_REGISTRY: dict[str, tuple[str, str, str]]coverswav2vec2/hubert/wavlm(wav2vec2-bert dropped per D1). Class names explicit; resolved lazily._make_emotion_model_class(model_type, head)uses the right base class + encoder attribute name._emotion_head_kindreturns(model_type, _HeadEntry).Phase 4 — Head registry (
api.py)_HeadEntrydataclass:(final_layer, activation, dropout_field)._KNOWN_HEAD_LAYOUTS: dict[str, _HeadEntry]— currently the ehcalabres single-bin entry._make_regression_head_class(head)builds the matching head;_ACTIVATIONStable covers tanh/gelu/relu.Phase 5 —
problem_typedetection (api.py)_resolve_apply_softmax(model, ser_type)prefersconfig.problem_type; falls back to architecture peek (ForSpeechClassification→ regression) per D3; finally to keyword heuristic.Phase 6 — Resampling (no code change)
Verified
classify_audios_with_transformersalready resampled in its audio loop.Phase 7 — Docs + diagnostic CLI
doc.mdenumerates dispatch paths and points users at the CLI.python -m senselab.audio.tasks.classification.speech_emotion_recognition --probe <repo_id>reports model_type, architectures, dispatch decision, head source, and emits advisories. No weights loaded.Caveats this PR explicitly does NOT solve
wav2vec2-bertintentionally not registered (see D1).classifierwhoseproblem_typecovers only one branch is not handled.Wav2Vec2FeatureExtractor.from_pretraineddefaults;do_normalize/return_attention_mask/pre-emphasis overrides inpreprocessor_config.jsonare silently ignored.Backward compatibility
Legacy aliases retained as 2-line trampolines so external monkeypatching keeps working:
_make_wav2vec2_emotion_model_class(final_layer)_wav2vec2_emotion_head_kind(model)returning just the final-layer name_peek_wav2vec2_head_final_layer(model)_classify_wav2vec2_speech_cls_ser(..., final_layer="out_proj")Test plan
uv run pytest src/tests/audio/tasks/classification_test.py -q— all pass (1 GPU-skipped).test_speech_emotion_recognition_continuous(audeering happy path).test_wav2vec2_speech_cls_ser_raises_on_random_head(parametrised: missing + mismatched).test_wav2vec2_speech_cls_ser_raises_on_shape_mismatch(final-layer out_features sanity).test_check_head_loaded_cleanly_raises_strict_by_default(parametrised across head prefixes).test_check_head_loaded_cleanly_warns_when_lax(env-var demotion).test_check_head_loaded_cleanly_silent_on_clean_load(encoder-buffer whitelist).uv run ruff check+uv run ruff format --checkclean.uv run mypy --ignore-missing-imports --extra-checksclean.https://claude.ai/code/session_01DyXMQNsF7F5Ze8j3GGVw66