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.
59 lines
2.2 KiB
Python
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)
|