from dataclasses import dataclass, asdict from typing import NamedTuple import torch from torch import Tensor, nn import torch.nn.functional as F MOODS=() ROLES=() INSTRUMENTS=() FRAME_FEATURES=() CONTROL_FEATURES=() @dataclass(frozen=True) class A02bConfig: width: int = 96 layers: int = 4 heads: int = 4 dropout: float = 0.1 frames_per_beat: int = 16 instrument_embedding_dim: int = 8 use_convolution: bool = True use_relative_bias: bool = True use_stop_head: bool = False relative_transitions_beats: tuple[float, ...] = (2 / 16, 3 / 16, 4 / 16, 5 / 16, 6 / 16, 7 / 16, 8 / 16, 3 / 4, 1, 2, 4, 8, 16, 32) def __post_init__(self) -> None: if self.instrument_embedding_dim <= 0: raise ValueError('instrument_embedding_dim must be positive') if self.width <= 0 or self.heads <= 0 or self.width % self.heads: raise ValueError('width must be positive and divisible by heads') if self.layers <= 0 or not 0 <= self.dropout < 1: raise ValueError('layers must be positive; dropout must be in [0,1)') if self.frames_per_beat not in (16, 48): raise ValueError('A02b supports separately configured rates 16 and 48') edges = self.relative_transitions_beats if len(edges) != 14 or not all((x > 0 for x in edges)): raise ValueError('32 signed buckets require 14 positive transitions') if not all((a < b for a, b in zip(edges, edges[1:]))): raise ValueError('relative distance transitions must increase') if not all((abs(x * self.frames_per_beat - round(x * self.frames_per_beat)) < 1e-08 for x in edges)): raise ValueError('bucket boundaries must align with the frame grid') class A02bOutput(NamedTuple): normalized_cc1: Tensor stop_logits: Tensor | None def zero_padding(h: Tensor, mask: Tensor) -> Tensor: return h.masked_fill(~mask[..., None], 0.0) class SignedRelativeBias(nn.Module): def __init__(self, config: A02bConfig) -> None: super().__init__() self.register_buffer('transitions_frames', torch.tensor([round(x * config.frames_per_beat) for x in config.relative_transitions_beats], dtype=torch.long)) self.table = nn.Embedding(32, config.heads) nn.init.zeros_(self.table.weight) def bucket_ids(self, length: int) -> Tensor: positions = torch.arange(length, device=self.transitions_frames.device) offset = positions[None, :] - positions[:, None] magnitude_bucket = torch.ones_like(offset) for transition in self.transitions_frames.unbind(): magnitude_bucket = magnitude_bucket + (offset.abs() >= transition) signed_bucket = magnitude_bucket + (offset > 0).long() * 16 return torch.where(offset == 0, 0, signed_bucket) def forward(self, length: int) -> Tensor: return self.table(self.bucket_ids(length)).permute(2, 0, 1)[None] class FiLM(nn.Module): def __init__(self, width: int) -> None: super().__init__() self.modulation = nn.Linear(64, 2 * width) nn.init.zeros_(self.modulation.weight) nn.init.zeros_(self.modulation.bias) def forward(self, u: Tensor, c: Tensor) -> Tensor: delta_gamma, beta = self.modulation(c).chunk(2, dim=-1) return (1 + delta_gamma[:, None]) * u + beta[:, None] class BidirectionalAttention(nn.Module): def __init__(self, config: A02bConfig) -> None: super().__init__() self.heads = config.heads self.head_dim = config.width // config.heads self.dropout = config.dropout self.qkv = nn.Linear(config.width, 3 * config.width) self.output = nn.Linear(config.width, config.width) def forward(self, u: Tensor, attention_bias: Tensor) -> Tensor: batch, length, width = u.shape qkv = self.qkv(u).reshape(batch, length, 3, self.heads, self.head_dim) q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0) result = F.scaled_dot_product_attention(q, k, v, attn_mask=attention_bias, dropout_p=self.dropout if self.training else 0.0, is_causal=False) result = result.transpose(1, 2).reshape(batch, length, width) return self.output(result) class EncoderBlock(nn.Module): def __init__(self, config: A02bConfig) -> None: super().__init__() d = config.width self.norm_attention = nn.LayerNorm(d) self.norm_ff = nn.LayerNorm(d) self.film_attention = FiLM(d) self.film_ff = FiLM(d) self.attention = BidirectionalAttention(config) self.ff = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d)) self.dropout = nn.Dropout(config.dropout) def forward(self, h: Tensor, c: Tensor, mask: Tensor, bias: Tensor) -> Tensor: u = self.film_attention(self.norm_attention(h), c) h = zero_padding(h + self.dropout(self.attention(u, bias)), mask) u = self.film_ff(self.norm_ff(h), c) return zero_padding(h + self.dropout(self.ff(u)), mask) class AireA02b(nn.Module): def __init__(self, config: A02bConfig | None=None) -> None: super().__init__() self.config = config or A02bConfig() cfg = self.config d = cfg.width self.mood_embedding = nn.Embedding(len(MOODS), 16) self.role_embedding = nn.Embedding(len(ROLES), 8) self.instrument_embedding = nn.Embedding(len(INSTRUMENTS), cfg.instrument_embedding_dim) self.control_mlp = nn.Sequential(nn.Linear(30 + cfg.instrument_embedding_dim, 64), nn.GELU(), nn.Linear(64, 64)) self.lead_embedding = nn.Embedding(129, d) self.pitch_projection = nn.Linear(128, d, bias=False) self.onset_projection = nn.Linear(128, d, bias=False) self.offset_projection = nn.Linear(128, d, bias=False) self.numeric_projection = nn.Linear(25, d) self.control_projection = nn.Linear(64, d) self.convolutions = nn.ModuleList([nn.Conv1d(d, d, kernel_size=5, padding=2) for _ in range(2)] if cfg.use_convolution else []) self.relative_bias = SignedRelativeBias(cfg) if cfg.use_relative_bias else None self.blocks = nn.ModuleList([EncoderBlock(cfg) for _ in range(cfg.layers)]) self.final_norm = nn.LayerNorm(d) self.cc_head = nn.Linear(d, 1) self.stop_head = nn.Linear(d, 1) if cfg.use_stop_head else None def manifest(self) -> dict: return {'model': 'aire-A02b', 'input_schema': 'A02b-25-instrument-v1', 'config': asdict(self.config), 'moods': list(MOODS), 'roles': list(ROLES), 'frame_features': list(FRAME_FEATURES), 'control_features': list(CONTROL_FEATURES), 'instruments': list(INSTRUMENTS), 'velocity_output': 'preserve-input-only', 'rest_pitch_index': 128, 'relative_bucket_rule': 'signed-piecewise-beats-v1; zero=0; past=1:15; future=17:31'} def forward(self, *, lead_pitch: Tensor, pitch_roll: Tensor, onset_roll: Tensor, offset_roll: Tensor, frame_numeric: Tensor, mood_id: Tensor, role_id: Tensor, instrument_id: Tensor, ctrl_numeric: Tensor, mask: Tensor) -> A02bOutput: if mask.ndim != 2 or mask.dtype != torch.bool: raise ValueError('mask must be bool [B,T]') batch, length = mask.shape inputs = {'lead_pitch': (lead_pitch, (batch, length), torch.long), 'pitch_roll': (pitch_roll, (batch, length, 128), self.cc_head.weight.dtype), 'onset_roll': (onset_roll, (batch, length, 128), self.cc_head.weight.dtype), 'offset_roll': (offset_roll, (batch, length, 128), self.cc_head.weight.dtype), 'frame_numeric': (frame_numeric, (batch, length, 25), self.cc_head.weight.dtype), 'mood_id': (mood_id, (batch,), torch.long), 'role_id': (role_id, (batch,), torch.long), 'instrument_id': (instrument_id, (batch,), torch.long), 'ctrl_numeric': (ctrl_numeric, (batch, 6), self.cc_head.weight.dtype)} device = self.cc_head.weight.device if mask.device != device: raise ValueError('mask and model must be on the same device') for name, (tensor, shape, dtype) in inputs.items(): if tuple(tensor.shape) != shape or tensor.dtype != dtype or tensor.device != device: raise ValueError(f'{name} must have shape {shape}, dtype {dtype}, device {device}') if batch == 0 or length == 0: empty = self.cc_head.weight.new_zeros((batch, length)) return A02bOutput(empty, empty if self.stop_head is not None else None) for name, tensor, count in (('mood_id', mood_id, len(MOODS)), ('role_id', role_id, len(ROLES)), ('instrument_id', instrument_id, len(INSTRUMENTS))): if bool(((tensor < 0) | (tensor >= count)).any()): raise ValueError(f'{name} is outside the frozen category range') if not bool(mask.any(dim=1).all()): raise ValueError('all-padding samples must bypass the model') if bool((~mask[:, :-1] & mask[:, 1:]).any()): raise ValueError('mask must consist of a valid prefix and right padding') lead_pitch = lead_pitch.masked_fill(~mask, 128) pitch_roll = zero_padding(pitch_roll, mask) onset_roll = zero_padding(onset_roll, mask) offset_roll = zero_padding(offset_roll, mask) numeric = zero_padding(frame_numeric, mask) velocity_enabled = ctrl_numeric[:, 5:6] != 0 gate = velocity_enabled[:, None, :] numeric = torch.cat([numeric[..., :3], torch.where(gate, numeric[..., 3:4], 0.0), numeric[..., 4:24], torch.where(gate, numeric[..., 24:25], 0.0)], dim=-1) c = self.control_mlp(torch.cat([self.mood_embedding(mood_id), self.role_embedding(role_id), self.instrument_embedding(instrument_id), ctrl_numeric], dim=-1)) h = zero_padding(self.lead_embedding(lead_pitch) + self.pitch_projection(pitch_roll) + self.onset_projection(onset_roll) + self.offset_projection(offset_roll) + self.numeric_projection(numeric) + self.control_projection(c)[:, None], mask) if self.convolutions: residual = h for convolution in self.convolutions: h = F.gelu(convolution(h.transpose(1, 2))).transpose(1, 2) h = zero_padding(h, mask) h = zero_padding(residual + h, mask) bias = h.new_zeros((batch, 1, 1, length)).masked_fill(~mask[:, None, None, :], float('-inf')) if self.relative_bias is not None: bias = bias + self.relative_bias(length).to(h.dtype) for block in self.blocks: h = block(h, c, mask, bias) h = self.final_norm(h) y = torch.sigmoid(self.cc_head(h).squeeze(-1)).masked_fill(~mask, 0.0) stop = None if self.stop_head is not None: stop = self.stop_head(h).squeeze(-1).masked_fill(~mask, 0.0) return A02bOutput(y, stop)