core/tests/test_audio_eval_gates.py
Shay 53573263cb
feat(adr-0181-p4): audio compiler eval gate lane (sensorium/audio) (#470)
PR-4 of ADR-0181. The acceptance-gate lane that decides whether audio_core_v1's
gate may open. Deterministic synthesis-spec fixtures (no .wav blobs) with
predicted parses, so the gates grade parser semantics as well as determinism.

evals/audio_sensorium/:
- synth.py            deterministic fixture synthesis (PCG64 + float32-at-boundary)
- fixtures.json       5 specs: silence, rising-pitch question, falling statement,
                      noise burst, speech-then-pause
- generate_expected.py reproducible pin generator (uv run -m ...)
- expected_ir.jsonl   frozen canonical_sha256 + ir_sha256 + event_type_counts
- expected_projection.json frozen projection_sha256 + reference versor

tests/test_audio_eval_gates.py (12): the gate table per fixture —
shape/dtype, versor_condition<1e-6, within-run replay, canonical-checksum
stability (hard int/cast-stable pin), IR-replay + frozen ir_sha256, semantic
event_type_counts (parser-accuracy gate), and cross-platform versor stability
within atol=1e-6 of the reference (float-safe per eval plan); plus trace
hygiene and gate-closure refusal.

Verified semantics: rise→prosody.rise, fall→prosody.fall, silence→pause.long+
turn.boundary, noise→nonspeech.noise, speech_then_pause→all three.

Cross-platform note: int/quantized-derived hashes are pinned hard; the float
versor is compared within tolerance rather than hash-pinned, since cos/sin/
geometric_product can differ by a ULP across arches. This is the eval-plan's
"equal within declared numeric tolerance" reading — keeps CI stable.

All audio 44 + arch-invariants 40 + smoke 67 green. No core mutation.
2026-05-29 11:20:31 -07:00

149 lines
5.4 KiB
Python

"""
ADR-0181 PR-4 — audio compiler eval gate table.
Implements the acceptance gates that lift `audio_core_v1` from gate-closed to
gate-engaged (eval plan §2). Each fixture is a deterministic synthesis spec
with a *predicted* parse, so the gates grade parser semantics as well as
determinism.
Gate → assertion:
projection shape -> exactly (32,)
projection dtype -> exactly float32
compiler replay -> bit-identical within a run
versor_condition -> < 1e-6
canonical checksum stability-> canonical_sha256 == frozen pin (int/cast-stable)
IR replay -> compile_ir versor equal + ir_sha256 == frozen
cross-platform stability -> versor within atol=1e-6 of reference (float-safe)
semantic structure -> event_type_counts == frozen pin (parser accuracy)
trace hygiene -> no PCM in the evidence trace
gate closure -> closed pack refuses projection
The int-derived pins (ir_sha256, event_type_counts, canonical_sha256) are
asserted hard cross-platform; the float versor is compared within tolerance
(eval plan: "cross-platform stability — equal within declared numeric
tolerance"). Regenerate pins with
``uv run python -m evals.audio_sensorium.generate_expected``.
"""
from __future__ import annotations
import json
from collections import Counter
from pathlib import Path
import numpy as np
import pytest
from evals.audio_sensorium.synth import synthesize
from sensorium.audio.canonical import canonicalize
from sensorium.audio.compiler import AudioCompiler
from sensorium.audio.trace import audio_evidence_trace
from sensorium.audio.types import AudioIR
from sensorium.adapters.audio import make_audio_pack
_EVAL_DIR = Path("evals/audio_sensorium")
SR = 24_000
TOL = 1e-6
def _load_fixtures() -> list[dict]:
return json.loads((_EVAL_DIR / "fixtures.json").read_text())["fixtures"]
def _load_expected_ir() -> dict[str, dict]:
out: dict[str, dict] = {}
for line in (_EVAL_DIR / "expected_ir.jsonl").read_text().splitlines():
if line.strip():
row = json.loads(line)
out[row["id"]] = row
return out
def _load_expected_projection() -> dict[str, dict]:
return json.loads((_EVAL_DIR / "expected_projection.json").read_text())
FIXTURES = _load_fixtures()
EXPECTED_IR = _load_expected_ir()
EXPECTED_PROJ = _load_expected_projection()
IDS = [fx["id"] for fx in FIXTURES]
def _event_type_counts(ir: AudioIR) -> dict[str, int]:
events = (
*ir.speech_spans, *ir.pause_spans, *ir.prosody_arcs,
*ir.turn_events, *ir.non_speech_events, *ir.content_anchors,
)
return dict(sorted(Counter(e.event_type for e in events).items()))
@pytest.fixture(scope="module")
def compiler() -> AudioCompiler:
return AudioCompiler()
@pytest.mark.parametrize("fx", FIXTURES, ids=IDS)
def test_gate_table(fx, compiler):
signal = canonicalize(synthesize(fx), SR)
unit = compiler.compile_signal(signal)
fid = fx["id"]
# shape / dtype
assert unit.versor.shape == (32,)
assert unit.versor.dtype == np.float32
# versor condition (A-5)
assert unit.versor_condition < TOL
# compiler replay — bit-identical within a run (A-1)
again = compiler.compile_signal(canonicalize(synthesize(fx), SR))
assert np.array_equal(unit.versor, again.versor)
assert unit.merge_key == again.merge_key
# canonical checksum stability (int/cast-stable pin)
assert unit.canonical_sha256 == EXPECTED_IR[fid]["canonical_sha256"]
# IR replay + frozen ir_sha256
replay = compiler.compile_ir(unit.audio_ir)
assert np.array_equal(unit.versor, replay.versor)
assert unit.ir_sha256 == replay.ir_sha256 == EXPECTED_IR[fid]["ir_sha256"]
# semantic structure — the parser-accuracy gate
assert _event_type_counts(unit.audio_ir) == EXPECTED_IR[fid]["event_type_counts"]
# cross-platform projection stability — within tolerance of the reference
reference = np.asarray(EXPECTED_PROJ[fid]["reference_versor"], dtype=np.float32)
assert np.allclose(unit.versor, reference, atol=TOL)
@pytest.mark.parametrize("fx", FIXTURES, ids=IDS)
def test_trace_hygiene_no_pcm(fx, compiler):
unit = compiler.compile_signal(canonicalize(synthesize(fx), SR))
trace = audio_evidence_trace(unit)
for value in trace.values():
assert not isinstance(value, (np.ndarray, bytes, bytearray))
assert "samples" not in trace
def test_gate_closure_refuses_projection():
"""A gate-closed pack refuses to project (eval plan: gate closure)."""
from sensorium.registry import ModalityRegistry
sig = canonicalize(synthesize(FIXTURES[1]), SR)
reg = ModalityRegistry()
reg.mount(make_audio_pack("audio_core_v1"), sample=sig)
with pytest.raises(RuntimeError, match="gate is not engaged"):
reg.project("audio_core_v1", sig)
def test_semantic_expectations_match_designed_fixtures():
"""Guard the fixture design intent: the frozen event_type_counts must match
what each fixture's 'expect' note describes (rise/fall/pause/noise)."""
must_contain = {
"silence_500ms": {"pause.long", "turn.boundary"},
"rise_question": {"speech.voiced", "prosody.rise"},
"fall_statement": {"speech.voiced", "prosody.fall"},
"noise_burst": {"nonspeech.noise"},
"speech_then_pause": {"speech.voiced", "pause.long", "turn.boundary"},
}
for fid, required in must_contain.items():
assert required <= set(EXPECTED_IR[fid]["event_type_counts"]), fid