PR-2 of ADR-0181. Lands the deterministic substrate only — no pack artifacts
(PR-3), no evals (PR-4), no Delta-CRDT wiring (PR-5), no teachers (PR-6).
Pipeline (spec §1): canonical signal → frame grid → acoustic lexer →
typed AudioIR → canonical event ordering → elliptic rotor lowering →
versor composition → AudioCompilationUnit (the future CRDT delta).
Modules (sensorium/audio/):
- types.py frozen AudioSignal/Token/Event/IR + AudioCompilationUnit.merge_key
- checksum.py layered sha256 chain (source→canonical→token→ir→manifest→projection)
- resample.py pure-numpy polyphase FIR (scipy absent; FIR taps are a PR-3 artifact)
- canonical.py mono/24kHz canonicalisation + provenance hashes
- frames.py 20ms/10ms deterministic frame grid (zero-padded tail)
- lexer.py quantized per-frame descriptors (energy/voicing/zcr/centroid/pYIN-style F0)
- parser.py runs → typed spans/events (pause/speech/prosody/turn/non-speech)
- operators.py elliptic-ONLY rotor registry; planes {6,7,8,10,11,13} square to -1
- compiler.py compile_events serialization barrier (ADR-0181 §2.1) + checksum chain
- trace.py trace-safe audio evidence (no PCM)
Correctness: v1 restricts to the six elliptic grade-2 planes (e_i e_j, i,j∈{1..4})
so every rotor and every composition is a unit versor — versor_condition < 1e-6
holds without weakening the threshold (CLAUDE.md §Non-Negotiable Field Invariant).
Non-elliptic blades (those touching e5) are rejected at OperatorSpec construction.
Tests (tests/test_audio_compiler.py, 13 passed): A-1 determinism, A-4 serialization
barrier order-sensitivity, A-5 versor condition, A-6 trace hygiene, IR-replay,
shape/dtype, elliptic-plane lawfulness. Smoke 67 + arch-invariants 40 green.
No core mutation: ingest/field/generate/vault/vocab untouched (ADR-0013).
unitize_versor is the only normalization, algebra-owned (CLAUDE.md §Normalization).
132 lines
5.6 KiB
Python
132 lines
5.6 KiB
Python
"""
|
|
sensorium/audio/operators.py — operator registry + rotor lowering (spec §6).
|
|
|
|
Each auditory event lowers to a *declared rotor specification*, not an opaque
|
|
vector. v1 uses **elliptic bivector operators only** (square = -1), so every
|
|
rotor is the numerically well-behaved R = cos(θ/2) + B·sin(θ/2) and the
|
|
composition of any sequence is a unit versor (versor_condition < 1e-6 holds
|
|
without weakening the threshold — CLAUDE.md §Non-Negotiable Field Invariant).
|
|
|
|
Elliptic planes in Cl(4,1) signature (+,+,+,+,-): a grade-2 blade e_a e_b
|
|
squares to -1 iff both a,b ∈ {e1..e4}. With the algebra's blade ordering
|
|
(combinations(range(5),2)), the elliptic grade-2 indices are:
|
|
|
|
6=(e1e2) 7=(e1e3) 8=(e1e4) 10=(e2e3) 11=(e2e4) 13=(e3e4)
|
|
|
|
Indices 9,12,14,15 involve e5 and are hyperbolic — excluded from v1. The
|
|
alias→index assignment below is versioned pack data (frozen in PR-3's
|
|
manifest); here it is the in-code default the compiler ships with.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from dataclasses import dataclass, field
|
|
|
|
import numpy as np
|
|
|
|
from sensorium.audio.checksum import sha256_json
|
|
from sensorium.audio.types import AuditoryEvent
|
|
|
|
CL41_DIM = 32
|
|
|
|
# The six elliptic grade-2 planes (square = -1).
|
|
ELLIPTIC_PLANES: tuple[int, ...] = (6, 7, 8, 10, 11, 13)
|
|
|
|
# θ_q is an integer; the radian angle is θ_q * THETA_STEP. 1024 steps span
|
|
# [0, 2π), so a rotor angle is always representable and bounded.
|
|
THETA_STEP = math.pi / 512.0
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class OperatorSpec:
|
|
"""Declared elliptic rotor spec for one event type (spec §6.2)."""
|
|
operator_id: str
|
|
event_type: str
|
|
blade_alias: str
|
|
blade_index: int
|
|
base_theta_q: int
|
|
gain_rules: tuple[tuple[str, int], ...] # (attr_name, gain) pairs
|
|
theta_clip_q: int
|
|
version: str = "1"
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.blade_index not in ELLIPTIC_PLANES:
|
|
raise ValueError(
|
|
f"operator '{self.operator_id}' uses non-elliptic blade "
|
|
f"{self.blade_index}; v1 permits only {ELLIPTIC_PLANES}"
|
|
)
|
|
|
|
def theta_q_from_event(self, event: AuditoryEvent) -> int:
|
|
"""Deterministic θ_q from quantized event attrs. Inputs are ints
|
|
only (spec §7: quantized inputs only), so the result is an int."""
|
|
attrs = dict(event.attrs)
|
|
theta_q = self.base_theta_q
|
|
for attr_name, gain in self.gain_rules:
|
|
value = attrs.get(attr_name, 0)
|
|
if isinstance(value, int):
|
|
theta_q += gain * value
|
|
return max(0, min(self.theta_clip_q, theta_q))
|
|
|
|
|
|
def build_elliptic_rotor(blade_index: int, theta_q: int) -> np.ndarray:
|
|
"""R = cos(θ/2) + B·sin(θ/2) for an elliptic plane B. θ = θ_q·THETA_STEP.
|
|
|
|
Returns a float64 unit versor of shape (32,)."""
|
|
if blade_index not in ELLIPTIC_PLANES:
|
|
raise ValueError(f"non-elliptic blade {blade_index}")
|
|
out = np.zeros(CL41_DIM, dtype=np.float64)
|
|
half = (theta_q * THETA_STEP) / 2.0
|
|
out[0] = math.cos(half)
|
|
out[blade_index] = math.sin(half)
|
|
return out
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AudioOperatorRegistry:
|
|
"""Maps event_type → OperatorSpec. Frozen and content-addressable."""
|
|
specs: dict[str, OperatorSpec] = field(default_factory=dict)
|
|
|
|
def __getitem__(self, event_type: str) -> OperatorSpec:
|
|
return self.specs[event_type]
|
|
|
|
def __contains__(self, event_type: str) -> bool:
|
|
return event_type in self.specs
|
|
|
|
def manifest_sha256(self) -> str:
|
|
"""Content hash over the registry's canonical serialization — the
|
|
``pack_manifest_sha256`` link of the checksum chain (spec §6)."""
|
|
payload = [
|
|
{
|
|
"operator_id": s.operator_id,
|
|
"event_type": s.event_type,
|
|
"blade_alias": s.blade_alias,
|
|
"blade_index": s.blade_index,
|
|
"base_theta_q": s.base_theta_q,
|
|
"gain_rules": [list(g) for g in s.gain_rules],
|
|
"theta_clip_q": s.theta_clip_q,
|
|
"version": s.version,
|
|
}
|
|
for s in sorted(self.specs.values(), key=lambda x: x.operator_id)
|
|
]
|
|
return sha256_json({"basis_version": "audio-basis-v1", "operators": payload})
|
|
|
|
|
|
def _spec(op_id, etype, alias, blade, base, gains, clip=768) -> OperatorSpec:
|
|
return OperatorSpec(op_id, etype, alias, blade, base, tuple(gains), clip)
|
|
|
|
|
|
# In-code default registry (PR-3 externalises this to operators.jsonl). Each
|
|
# atom family maps to one elliptic plane; planes are reused across families
|
|
# (only six exist) with distinct base angles. Full orthogonality is a later
|
|
# concern — lawfulness (elliptic, unit) is the PR-2 invariant.
|
|
DEFAULT_OPERATOR_REGISTRY = AudioOperatorRegistry({
|
|
"pause.short": _spec("audio.pause.short.v1", "pause.short", "B_PAUSE_SHORT", 6, 48, [("dur_hops", 2)]),
|
|
"pause.long": _spec("audio.pause.long.v1", "pause.long", "B_PAUSE_LONG", 7, 96, [("dur_hops", 2)]),
|
|
"speech.voiced": _spec("audio.speech.voiced.v1", "speech.voiced", "B_SPEECH", 8, 64, [("dur_hops", 1)]),
|
|
"prosody.rise": _spec("audio.prosody.rise.v1", "prosody.rise", "B_PITCH_RISE", 10, 64, [("slope_q", 3)]),
|
|
"prosody.fall": _spec("audio.prosody.fall.v1", "prosody.fall", "B_PITCH_FALL", 11, 64, [("slope_q", 3)]),
|
|
"prosody.emphasis": _spec("audio.prosody.emph.v1", "prosody.emphasis", "B_EMPHASIS", 13, 32, [("delta_db_q", 4)]),
|
|
"turn.boundary": _spec("audio.turn.boundary.v1", "turn.boundary", "B_TURN", 6, 160, [("boundary_q", 2)]),
|
|
"nonspeech.noise": _spec("audio.nonspeech.noise.v1", "nonspeech.noise", "B_NOISE", 7, 200, [("noise_q", 2)]),
|
|
})
|