core/evals/audio_sensorium/synth.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

59 lines
2.2 KiB
Python

"""
evals/audio_sensorium/synth.py — deterministic fixture synthesis (ADR-0181 PR-4).
Fixtures are described as synthesis specs (``fixtures.json``) rather than
committed .wav blobs: the parameters are diffable and the signal is a pure,
reproducible function of them. The same synthesiser is used by the expected-
artifact generator and by the gate tests, so what is pinned is exactly what is
checked.
`numpy.random.Generator(PCG64)` is bit-reproducible across platforms, and
sine/`standard_normal` results are cast to float32 at the boundary — the cast
absorbs cross-platform ULP noise in the float64 transcendentals, so the
canonical-signal hash is stable across builds (the versor itself is checked
within numeric tolerance — see the eval plan).
"""
from __future__ import annotations
import numpy as np
SAMPLE_RATE = 24_000
def _tone(ms: int, hz: float, amp: float, sweep: float) -> np.ndarray:
n = int(SAMPLE_RATE * ms / 1000)
t = np.arange(n, dtype=np.float64) / SAMPLE_RATE
span = t[-1] if t.size > 1 else 1.0
freq = hz + sweep * (t / span) # linear F0 sweep over the span
phase = 2 * np.pi * np.cumsum(freq) / SAMPLE_RATE
return (amp * np.sin(phase)).astype(np.float32)
def _silence(ms: int) -> np.ndarray:
return np.zeros(int(SAMPLE_RATE * ms / 1000), dtype=np.float32)
def _noise(ms: int, amp: float, seed: int) -> np.ndarray:
rng = np.random.default_rng(seed)
n = int(SAMPLE_RATE * ms / 1000)
return (amp * rng.standard_normal(n)).astype(np.float32)
def _part(spec: dict) -> np.ndarray:
kind = spec["kind"]
if kind == "silence":
return _silence(int(spec["ms"]))
if kind == "tone":
return _tone(int(spec["ms"]), float(spec["hz"]),
float(spec.get("amp", 0.5)), float(spec.get("sweep", 0.0)))
if kind == "noise":
return _noise(int(spec["ms"]), float(spec.get("amp", 0.3)), int(spec.get("seed", 0)))
raise ValueError(f"unknown synth kind: {kind!r}")
def synthesize(spec: dict) -> np.ndarray:
"""Return the canonical-rate float32 mono signal for a fixture spec."""
if spec["kind"] == "concat":
return np.concatenate([_part(p) for p in spec["parts"]]).astype(np.float32)
return _part(spec)