ltuncay commited on
Commit
86dc2b6
·
verified ·
1 Parent(s): a03eeb5

Add Transformers loading for the existing AECC 2026 encoder

Browse files
CODE_LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 audio-embeddings contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 audio-embeddings contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,35 +1,149 @@
1
- # BEST-RQ-2 (xares-llm encoder)
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- This folder contains the BEST-RQ-2 audio encoder integration for `xares-llm`.
 
 
4
 
5
- Benchmark repository: [xiaomi-research/xares-llm](https://github.com/xiaomi-research/xares-llm)
 
 
6
 
7
- ## Setup
8
 
9
- 1. Make sure the environment to run `xares-llm` is set up properly (virtual environment initialized and the `xares-llm` package downloaded/installed).
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- 2. Add the `BEST-RQ-2` folder to the `xares-llm` (current) directory so it is available at `./BEST-RQ-2`.
12
 
13
- 3. Before running a `xares-llm` evaluation, you must install the required packages for BEST-RQ-2:
14
 
15
  ```bash
16
- uv pip install -r BEST-RQ-2/audio-embeddings/pyproject.toml
17
  ```
18
 
19
- ## Run an evaluation
 
20
 
21
- Single task (e.g. to test that everything works):
 
 
22
 
23
- ```bash
24
- uv run -m xares_llm.run BEST-RQ-2.BEST-RQ-2_encoder.BestRQ2Encoder <task> <task> # e.g. replace <task> with esc-50, score should be around 0.48
 
 
 
 
 
 
 
 
25
  ```
26
 
27
- All tasks:
 
 
 
 
 
28
 
29
- ```bash
30
- uv run -m xares_llm.run BEST-RQ-2.BEST-RQ-2_encoder.BestRQ2Encoder all
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  ```
32
 
33
- ## Help
34
 
35
- If you encounter any problems, contact: [ludovic.tuncay@irit.fr](mailto:ludovic.tuncay@irit.fr)
 
 
1
+ ---
2
+ library_name: transformers
3
+ license: mit
4
+ tags:
5
+ - audio
6
+ - feature-extraction
7
+ - custom_code
8
+ - self-supervised-learning
9
+ - audio-embeddings
10
+ - best-rq-2
11
+ - audioset
12
+ ---
13
+ # BEST-RQ-2
14
 
15
+ BEST-RQ-2 is a self-supervised audio encoder trained on AudioSet for
16
+ a configured budget of **200,000 optimizer steps**. It produces **768-dimensional** clip and frame
17
+ embeddings from mono **16 kHz** waveforms, and supports downstream fine-tuning.
18
 
19
+ This repository contains the trained encoder, its preprocessing configuration,
20
+ and the custom Transformers implementation. No installation of the research
21
+ repository is needed.
22
 
23
+ ## Model and training
24
 
25
+ | Property | Value |
26
+ | --- | --- |
27
+ | Architecture | 12-layer Transformer, 768 dimensions, 12 attention heads |
28
+ | Input frontend | 128-bin mel spectrogram with a linear patch projection |
29
+ | Patch shape | 16 mel bins by 16 time frames |
30
+ | Transformer | Sinusoidal positional embeddings, LayerNorm, GELU MLP |
31
+ | Training data | AudioSet |
32
+ | Training objective | Masked prediction of frozen codebook targets |
33
+ | Masking ratio | 40–60% |
34
+ | Checkpoint step metadata | Not recorded in this older safetensors file |
35
+ | Exported weight dtype | float32 |
36
+ | Extraction policy | `overlap50_two_phase` |
37
 
38
+ ## Load the model
39
 
40
+ Install the runtime dependencies in your Python environment:
41
 
42
  ```bash
43
+ pip install "torch>=2.9.1" "torchaudio>=2.9.1" "timm>=0.9" "einops>=0.7" "transformers>=4.57,<6"
44
  ```
45
 
46
+ Use matching PyTorch and torchaudio versions. GPU installations may require the
47
+ appropriate PyTorch build for your CUDA version.
48
 
49
+ ```python
50
+ import torch
51
+ from transformers import AutoFeatureExtractor, AutoModel
52
 
53
+ model_name = "ltuncay/BEST-RQ-2"
54
+ processor = AutoFeatureExtractor.from_pretrained(model_name, trust_remote_code=True)
55
+ model = AutoModel.from_pretrained(model_name, trust_remote_code=True).eval()
56
+ audio = torch.zeros(processor.sampling_rate) # Replace with real mono audio.
57
+ inputs = processor(audio, sampling_rate=processor.sampling_rate, return_tensors="pt")
58
+ with torch.inference_mode():
59
+ outputs = model(**inputs)
60
+ clip_embeddings = outputs.pooler_output
61
+ frame_embeddings = outputs.last_hidden_state
62
+ output_dim = model.config.encoder_kwargs["embed_dim"]
63
  ```
64
 
65
+ Resample to `processor.sampling_rate` and downmix stereo before preprocessing.
66
+ The extractor performs padding only; spectrogram/convolution features are computed
67
+ inside the model. Pass the returned sample attention mask for variable durations.
68
+ Outputs include a frame attention mask and timestamps in milliseconds (-1 for padding).
69
+ `pooler_output` uses the saved HEAR extraction preset, including phase-balanced pooling.
70
+ Frame features average frequency patches; they are not the raw frequency-time ViT grid.
71
 
72
+ ## Fine-tuning
73
+
74
+ For fine-tuning call `model.train()`, attach a task head and optimize its parameters
75
+ alongside the model. Save with `model.save_pretrained(path)` and
76
+ `processor.save_pretrained(path)`. This encoder export excludes pretraining predictors,
77
+ quantizers, teachers and optimizer state. Continue self-supervised research training
78
+ with the original Lightning code and checkpoints.
79
+
80
+ ## Reproducibility and provenance
81
+
82
+ `revision` is optional; pin both loaders to the same full commit hash for reproducibility.
83
+ Custom Python code is included in this repository and requires `trust_remote_code=True`.
84
+ The weights use safetensors. See `export_manifest.json` for source and validation details.
85
+ Source: the existing **AECC 2026 submission** in this repository, pinned at
86
+ [`a03eeb5c4433f4bf7a7e6e8b4724af862789959c`](https://huggingface.co/ltuncay/BEST-RQ-2/tree/a03eeb5c4433f4bf7a7e6e8b4724af862789959c).
87
+ The export uses its `BEST-RQ-2.safetensors` and matching `config.yaml`.
88
+ The saved configuration specifies a 200,000-step training budget, but this older
89
+ safetensors file has no saved-step metadata. The actual checkpoint step therefore
90
+ remains unspecified in the export manifest. Source file hashes are recorded there.
91
+
92
+ ## AECC 2026 compatibility
93
+
94
+ The [original submission](https://huggingface.co/ltuncay/BEST-RQ-2/tree/aecc-2026-submission)
95
+ is preserved at revision `aecc-2026-submission`, including its model card, checkpoint,
96
+ technical report and xares-llm integration. The legacy files also remain available
97
+ on `main` for existing consumers.
98
+
99
+ This Transformers export uses the **same pretrained encoder weights**. Its public
100
+ outputs follow the shared BEST-RQ-2 family extraction policy: frequency-averaged
101
+ frame embeddings and phase-balanced clip embeddings with overlapping windows.
102
+ The original AECC wrapper returned spectrogram patch tokens with sequential audio
103
+ chunking, so the two public interfaces are not interchangeable for reproducing
104
+ benchmark results. Use the archived revision for the original evaluation setup.
105
+
106
+ A comparison against the archived frontend and encoder on a 10-second waveform
107
+ found exact spectrogram and patch-projection agreement, with maximum encoder
108
+ absolute difference `1.67e-6` (within floating-point tolerance). The validation
109
+ manifest records this comparison.
110
+
111
+ The exported model matched the pre-export encoder exactly on the exporter's
112
+ variable-duration test batch. These are integration checks, not a downstream
113
+ benchmark evaluation of this release.
114
+
115
+ ## Intended uses and limitations
116
+
117
+ Use these embeddings as features for audio research, classification, retrieval,
118
+ or downstream fine-tuning. This is an encoder: it does not generate transcripts,
119
+ audio, or class labels without a downstream model.
120
+
121
+ AudioSet training does not establish performance on every language, acoustic
122
+ domain, demographic group, or downstream task. Evaluate the model on your target
123
+ data. No new downstream benchmark scores are claimed for this export.
124
+
125
+ The feature extractor pads waveforms and checks the sample rate; it does not
126
+ resample or downmix. Unequal-length clips are processed individually after padding
127
+ is removed. The output embeddings follow the saved windowing policy rather than
128
+ exposing the raw spectrogram token grid.
129
+
130
+ Browse the [BEST-RQ-2 family collection](https://huggingface.co/collections/ltuncay/best-rq-2-family-6aad761396dfe31da48ff91a).
131
+
132
+ ## Source and citation
133
+
134
+ Research code: [audio-embeddings](https://github.com/LudovicTuncay/audio-embeddings).
135
+ BEST-RQ-2 belongs to the BEST-RQ-2 family. Cite the family paper when using it:
136
+
137
+ ```bibtex
138
+ @inproceedings{tuncay2026best,
139
+ title={BEST-RQ-2: Contextualize-Then-Predict, a Two-Step Approach for Self-Supervised Audio Representations},
140
+ author={Tuncay, Ludovic K and Labb{\'e}, Etienne and Pellegrini, Thomas},
141
+ booktitle={Interspeech 2026},
142
+ year={2026}
143
+ }
144
  ```
145
 
146
+ ## License
147
 
148
+ The model weights and bundled implementation are released under the MIT license.
149
+ See `LICENSE` and `CODE_LICENSE`.
adapters.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import asdict, dataclass
26
+ from typing import Any, Mapping
27
+
28
+ import torch
29
+ import torch.nn as nn
30
+ import torch.nn.functional as F
31
+
32
+ from .extraction import fuse_context_windows
33
+ from .extraction import get_preset
34
+ from .extraction import merge_phases
35
+ from .patch_embed import PatchEmbed
36
+ from .spectrogram import Spectrogram
37
+ from .vit import ViT
38
+ from .vit import vit_config_with_patch_geometry
39
+ from .waveform_feature_encoder import WaveformFeatureEncoder
40
+
41
+ SPECTROGRAM_TARGETS = {
42
+ "src.models.audio_jepa_module.AudioJEPAModule": "student",
43
+ "src.models.rqa_jepa_module.RQAJEPAModule": "student",
44
+ "src.models.best_rq_module.BestRQModule": "encoder",
45
+ "src.models.best_rq2_module.BestRQ2Module": "encoder",
46
+ "src.models.best_rq22_module.BestRQ22Module": "encoder",
47
+ "src.models.best_rq23_module.BestRQ23Module": "encoder",
48
+ }
49
+ WAVEFORM_TARGETS = {
50
+ "src.models.best_rq3_module.BestRQ3Module": "encoder",
51
+ }
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class AdapterSpec:
56
+ adapter_key: str
57
+ model_target: str
58
+ encoder_prefix: str
59
+ sample_rate: int
60
+ embedding_dim: int
61
+ max_context_tokens: int
62
+ temporal_grid_tokens: int
63
+ token_hop_samples: int
64
+ receptive_field_samples: int
65
+ supported_phase_offsets_samples: tuple[int, ...]
66
+ channel_policy: str = "mono_mean"
67
+
68
+ def to_dict(self) -> dict[str, Any]:
69
+ return asdict(self)
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class EmbeddingOutput:
74
+ timestamp_embeddings: torch.Tensor
75
+ timestamps_ms: torch.Tensor
76
+ scene_embedding: torch.Tensor
77
+
78
+
79
+ def _mapping(value: Any, path: str) -> Mapping[str, Any]:
80
+ if not isinstance(value, Mapping):
81
+ raise ValueError(f"Expected mapping at {path}, got {type(value).__name__}")
82
+ return value
83
+
84
+
85
+ def _positive_int(value: Any, path: str) -> int:
86
+ try:
87
+ normalized = int(value)
88
+ except (TypeError, ValueError) as error:
89
+ raise ValueError(f"Expected integer at {path}, got {value!r}") from error
90
+ if normalized <= 0:
91
+ raise ValueError(f"Expected positive integer at {path}, got {normalized}")
92
+ return normalized
93
+
94
+
95
+ def resolve_adapter_spec(config: Mapping[str, Any]) -> AdapterSpec:
96
+ model = _mapping(config.get("model"), "model")
97
+ target = str(model.get("_target_", ""))
98
+ net = _mapping(model.get("net"), "model.net")
99
+ encoder = _mapping(net.get("encoder"), "model.net.encoder")
100
+
101
+ if target in SPECTROGRAM_TARGETS:
102
+ adapter_key = "spectrogram_patch"
103
+ encoder_prefix = SPECTROGRAM_TARGETS[target]
104
+ frontend = _mapping(
105
+ net.get("spectrogram"),
106
+ "model.net.spectrogram",
107
+ )
108
+ sample_rate_path = "model.net.spectrogram.sample_rate"
109
+ patch = _mapping(net.get("patch_embed"), "model.net.patch_embed")
110
+ patch_size = tuple(patch.get("patch_size", ()))
111
+ image_size = tuple(patch.get("img_size", ()))
112
+ if len(patch_size) != 2 or len(image_size) != 2:
113
+ raise ValueError("Spectrogram HEAR adapters require 2-D patch/image sizes")
114
+ patch_height, patch_width = map(int, patch_size)
115
+ frequency_tokens = int(image_size[0]) // patch_height
116
+ n_fft = _positive_int(
117
+ frontend.get("n_fft", 4096), "model.net.spectrogram.n_fft"
118
+ )
119
+ if frontend.get("win_length") is not None:
120
+ win_length = _positive_int(
121
+ frontend.get("win_length"), "model.net.spectrogram.win_length"
122
+ )
123
+ elif frontend.get("win_length_ms") is not None:
124
+ win_length = int(
125
+ int(frontend["sample_rate"]) * float(frontend["win_length_ms"]) / 1000
126
+ )
127
+ else:
128
+ win_length = n_fft
129
+ if frontend.get("hop_length") is not None:
130
+ frontend_hop = _positive_int(
131
+ frontend.get("hop_length"), "model.net.spectrogram.hop_length"
132
+ )
133
+ elif frontend.get("hop_length_ms") is not None:
134
+ frontend_hop = int(
135
+ int(frontend["sample_rate"]) * float(frontend["hop_length_ms"]) / 1000
136
+ )
137
+ else:
138
+ frontend_hop = win_length // 2
139
+ token_hop = frontend_hop * patch_width
140
+ receptive_field = n_fft + (patch_width - 1) * frontend_hop
141
+ elif target in WAVEFORM_TARGETS:
142
+ adapter_key = "waveform_conv"
143
+ encoder_prefix = WAVEFORM_TARGETS[target]
144
+ frontend = _mapping(net.get("sampling"), "model.net.sampling")
145
+ sample_rate_path = "model.net.sampling.sample_rate"
146
+ feature_config = dict(
147
+ _mapping(net.get("feature_encoder"), "model.net.feature_encoder")
148
+ )
149
+ feature_encoder = WaveformFeatureEncoder(**feature_config)
150
+ receptive_field = 1
151
+ token_hop = 1
152
+ for _, kernel, stride in feature_encoder.conv_layers_spec:
153
+ receptive_field += (kernel - 1) * token_hop
154
+ token_hop *= stride
155
+ frequency_tokens = 1
156
+ else:
157
+ supported = sorted((*SPECTROGRAM_TARGETS, *WAVEFORM_TARGETS))
158
+ raise ValueError(
159
+ f"No HEAR adapter is registered for model target {target!r}. "
160
+ f"Register one for the new model. Current targets: {supported}"
161
+ )
162
+
163
+ sample_rate = _positive_int(frontend.get("sample_rate"), sample_rate_path)
164
+ data = config.get("data")
165
+ if isinstance(data, Mapping) and data.get("target_sample_rate") is not None:
166
+ data_sample_rate = _positive_int(
167
+ data.get("target_sample_rate"),
168
+ "data.target_sample_rate",
169
+ )
170
+ if data_sample_rate != sample_rate:
171
+ raise ValueError(
172
+ "Model/data sampling-rate mismatch: "
173
+ f"{sample_rate_path}={sample_rate}, "
174
+ f"data.target_sample_rate={data_sample_rate}"
175
+ )
176
+
177
+ max_context_tokens = _positive_int(
178
+ encoder.get("num_patches"),
179
+ "model.net.encoder.num_patches",
180
+ )
181
+ if frequency_tokens <= 0 or max_context_tokens < frequency_tokens:
182
+ raise ValueError(
183
+ "Encoder context cannot hold one complete frequency-token column"
184
+ )
185
+ return AdapterSpec(
186
+ adapter_key=adapter_key,
187
+ model_target=target,
188
+ encoder_prefix=encoder_prefix,
189
+ sample_rate=sample_rate,
190
+ embedding_dim=_positive_int(
191
+ encoder.get("embed_dim"),
192
+ "model.net.encoder.embed_dim",
193
+ ),
194
+ max_context_tokens=max_context_tokens,
195
+ temporal_grid_tokens=max_context_tokens // frequency_tokens,
196
+ token_hop_samples=token_hop,
197
+ receptive_field_samples=receptive_field,
198
+ supported_phase_offsets_samples=(
199
+ (0, token_hop // 2) if token_hop % 2 == 0 else (0,)
200
+ ),
201
+ )
202
+
203
+
204
+ class HearEncoderAdapter(nn.Module):
205
+ spec: AdapterSpec
206
+
207
+ @property
208
+ def sample_rate(self) -> int:
209
+ return self.spec.sample_rate
210
+
211
+ @property
212
+ def embedding_dim(self) -> int:
213
+ return self.spec.embedding_dim
214
+
215
+ def extract(self, waveform: torch.Tensor, *, preset_name: str) -> EmbeddingOutput:
216
+ raise NotImplementedError
217
+
218
+
219
+ def _single_waveform(waveform: torch.Tensor) -> torch.Tensor:
220
+ if waveform.ndim == 1:
221
+ waveform = waveform.unsqueeze(0)
222
+ if waveform.ndim != 2 or waveform.shape[0] != 1:
223
+ raise ValueError(
224
+ "Adapter extraction expects one mono waveform [samples] or [1, samples], "
225
+ f"got {tuple(waveform.shape)}"
226
+ )
227
+ if waveform.shape[-1] == 0:
228
+ raise ValueError("Cannot embed an empty waveform")
229
+ return waveform.unsqueeze(0)
230
+
231
+
232
+ class SpectrogramPatchAdapter(HearEncoderAdapter):
233
+ def __init__(self, config: Mapping[str, Any], spec: AdapterSpec) -> None:
234
+ super().__init__()
235
+ self.spec = spec
236
+ model = _mapping(config.get("model"), "model")
237
+ net = _mapping(model.get("net"), "model.net")
238
+ spectrogram_config = dict(
239
+ _mapping(net.get("spectrogram"), "model.net.spectrogram")
240
+ )
241
+ patch_config = dict(_mapping(net.get("patch_embed"), "model.net.patch_embed"))
242
+ encoder_config = dict(_mapping(net.get("encoder"), "model.net.encoder"))
243
+ self.spectrogram = Spectrogram(**spectrogram_config)
244
+ self.patch_embed = PatchEmbed(**patch_config)
245
+ self.encoder = ViT(
246
+ **vit_config_with_patch_geometry(
247
+ encoder_config,
248
+ img_size=self.patch_embed.img_size,
249
+ patch_size=self.patch_embed.patch_size,
250
+ )
251
+ )
252
+ self.adjustment_mode = str(model.get("spectrogram_adjustment_mode", "pad"))
253
+ if self.adjustment_mode not in {"pad", "truncate"}:
254
+ raise ValueError(
255
+ f"Unknown spectrogram_adjustment_mode {self.adjustment_mode!r}"
256
+ )
257
+
258
+ def _phase_tokens(
259
+ self,
260
+ spectrogram: torch.Tensor,
261
+ *,
262
+ frame_offset: int,
263
+ ) -> tuple[torch.Tensor, int]:
264
+ patch_height, patch_width = self.patch_embed.patch_size
265
+ phase = spectrogram[..., frame_offset:]
266
+ original_frames = phase.shape[-1]
267
+ if original_frames < patch_width:
268
+ phase = F.pad(phase, (0, patch_width - original_frames))
269
+ else:
270
+ remainder = original_frames % patch_width
271
+ if remainder:
272
+ if self.adjustment_mode == "pad":
273
+ phase = F.pad(phase, (0, patch_width - remainder))
274
+ else:
275
+ phase = phase[..., : original_frames - remainder]
276
+ tokens = self.patch_embed(phase)
277
+ frequency = phase.shape[-2] // patch_height
278
+ time = phase.shape[-1] // patch_width
279
+ return tokens.reshape(frequency, time, -1), original_frames
280
+
281
+ def extract(self, waveform: torch.Tensor, *, preset_name: str) -> EmbeddingOutput:
282
+ preset = get_preset(preset_name)
283
+ waveform = _single_waveform(waveform)
284
+ duration_samples = waveform.shape[-1]
285
+ spectrogram = self.spectrogram(waveform)
286
+ patch_width = self.patch_embed.patch_size[1]
287
+ if preset.num_phases == 2 and patch_width % 2:
288
+ raise ValueError(
289
+ "Two-phase extraction requires an exact half-hop temporal patch offset"
290
+ )
291
+ frame_offsets = [0]
292
+ if preset.num_phases == 2:
293
+ frame_offsets.append(patch_width // 2)
294
+
295
+ hop_samples = int(self.spectrogram.mel_spec.hop_length)
296
+ phases: list[tuple[torch.Tensor, torch.Tensor]] = []
297
+ for frame_offset in frame_offsets:
298
+ token_grid, _ = self._phase_tokens(
299
+ spectrogram,
300
+ frame_offset=frame_offset,
301
+ )
302
+ frequency = token_grid.shape[0]
303
+
304
+ def encode_window(
305
+ window: torch.Tensor,
306
+ position_ids: torch.Tensor,
307
+ ) -> torch.Tensor:
308
+ width = window.shape[1] // frequency
309
+ return self.encoder(
310
+ window,
311
+ pos_ids=position_ids,
312
+ grid_size=(frequency, width),
313
+ )
314
+
315
+ embeddings = fuse_context_windows(
316
+ token_grid,
317
+ max_context_tokens=self.spec.max_context_tokens,
318
+ overlap=preset.overlap,
319
+ encode_window=encode_window,
320
+ )
321
+ centers_in_frames = (
322
+ torch.arange(embeddings.shape[0], device=embeddings.device)
323
+ * patch_width
324
+ + frame_offset
325
+ + (patch_width - 1) / 2.0
326
+ )
327
+ centers_in_samples = centers_in_frames * hop_samples
328
+ centers_in_samples = torch.clamp(
329
+ centers_in_samples,
330
+ max=max(0, duration_samples - 1),
331
+ )
332
+ timestamps_ms = centers_in_samples * (1000.0 / self.sample_rate)
333
+ phases.append((embeddings, timestamps_ms))
334
+
335
+ merged, timestamps, scene = merge_phases(phases)
336
+ return EmbeddingOutput(merged, timestamps, scene)
337
+
338
+
339
+ class WaveformConvAdapter(HearEncoderAdapter):
340
+ def __init__(self, config: Mapping[str, Any], spec: AdapterSpec) -> None:
341
+ super().__init__()
342
+ self.spec = spec
343
+ model = _mapping(config.get("model"), "model")
344
+ net = _mapping(model.get("net"), "model.net")
345
+ feature_config = dict(
346
+ _mapping(net.get("feature_encoder"), "model.net.feature_encoder")
347
+ )
348
+ encoder_config = dict(_mapping(net.get("encoder"), "model.net.encoder"))
349
+ self.feature_encoder = WaveformFeatureEncoder(**feature_config)
350
+ feature_dim = self.feature_encoder.embedding_dim
351
+ self.encoder_input_proj: nn.Module
352
+ if feature_dim == spec.embedding_dim:
353
+ self.encoder_input_proj = nn.Identity()
354
+ else:
355
+ self.encoder_input_proj = nn.Linear(feature_dim, spec.embedding_dim)
356
+ self.encoder = ViT(**encoder_config)
357
+ receptive_field = 1
358
+ stride = 1
359
+ for _, kernel, layer_stride in self.feature_encoder.conv_layers_spec:
360
+ receptive_field += (kernel - 1) * stride
361
+ stride *= layer_stride
362
+ self.receptive_field_samples = receptive_field
363
+ self.token_hop_samples = stride
364
+ if self.receptive_field_samples != spec.receptive_field_samples:
365
+ raise ValueError("Waveform adapter receptive-field metadata mismatch")
366
+ if self.token_hop_samples != spec.token_hop_samples:
367
+ raise ValueError("Waveform adapter hop metadata mismatch")
368
+
369
+ def extract(self, waveform: torch.Tensor, *, preset_name: str) -> EmbeddingOutput:
370
+ preset = get_preset(preset_name)
371
+ waveform = _single_waveform(waveform)
372
+ duration_samples = waveform.shape[-1]
373
+ offsets = [0]
374
+ if preset.num_phases == 2:
375
+ if self.token_hop_samples % 2:
376
+ raise ValueError(
377
+ "Two-phase extraction requires an exact half-hop sample offset"
378
+ )
379
+ offsets.append(self.token_hop_samples // 2)
380
+
381
+ phases: list[tuple[torch.Tensor, torch.Tensor]] = []
382
+ for offset in offsets:
383
+ phase = waveform[..., offset:]
384
+ if phase.shape[-1] < self.receptive_field_samples:
385
+ phase = F.pad(
386
+ phase,
387
+ (0, self.receptive_field_samples - phase.shape[-1]),
388
+ )
389
+ local_features = self.feature_encoder(phase)
390
+ tokens = self.encoder_input_proj(local_features).squeeze(0).unsqueeze(0)
391
+
392
+ def encode_window(
393
+ window: torch.Tensor,
394
+ position_ids: torch.Tensor,
395
+ ) -> torch.Tensor:
396
+ return self.encoder(
397
+ window,
398
+ pos_ids=position_ids,
399
+ grid_size=(1, window.shape[1]),
400
+ )
401
+
402
+ embeddings = fuse_context_windows(
403
+ tokens,
404
+ max_context_tokens=self.spec.max_context_tokens,
405
+ overlap=preset.overlap,
406
+ encode_window=encode_window,
407
+ )
408
+ centers = (
409
+ torch.arange(embeddings.shape[0], device=embeddings.device)
410
+ * self.token_hop_samples
411
+ + offset
412
+ + (self.receptive_field_samples - 1) / 2.0
413
+ )
414
+ centers = torch.clamp(centers, max=max(0, duration_samples - 1))
415
+ phases.append((embeddings, centers * (1000.0 / self.sample_rate)))
416
+
417
+ merged, timestamps, scene = merge_phases(phases)
418
+ return EmbeddingOutput(merged, timestamps, scene)
419
+
420
+
421
+ def _normalized_source_state(
422
+ state_dict: Mapping[str, torch.Tensor],
423
+ ) -> dict[str, torch.Tensor]:
424
+ normalized: dict[str, torch.Tensor] = {}
425
+ for key, value in state_dict.items():
426
+ name = str(key)
427
+ for prefix in ("module.", "model."):
428
+ if name.startswith(prefix):
429
+ name = name.removeprefix(prefix)
430
+ normalized[name] = value
431
+ return normalized
432
+
433
+
434
+ def _load_inference_weights(
435
+ adapter: HearEncoderAdapter,
436
+ source_state: Mapping[str, torch.Tensor],
437
+ ) -> None:
438
+ source = _normalized_source_state(source_state)
439
+ canonical: dict[str, torch.Tensor] = {}
440
+ missing: list[str] = []
441
+ for expected_key in adapter.state_dict():
442
+ source_key = expected_key
443
+ if expected_key not in source and expected_key.startswith("encoder."):
444
+ source_key = (
445
+ f"{adapter.spec.encoder_prefix}.{expected_key.removeprefix('encoder.')}"
446
+ )
447
+ if source_key not in source:
448
+ missing.append(source_key)
449
+ else:
450
+ canonical[expected_key] = source[source_key]
451
+ if missing:
452
+ raise ValueError(
453
+ "Checkpoint is missing inference weights: " + ", ".join(missing[:12])
454
+ )
455
+ adapter.load_state_dict(canonical, strict=True)
456
+
457
+
458
+ def build_encoder_adapter(
459
+ config: Mapping[str, Any],
460
+ state_dict: Mapping[str, torch.Tensor],
461
+ ) -> HearEncoderAdapter:
462
+ spec = resolve_adapter_spec(config)
463
+ if spec.adapter_key == "spectrogram_patch":
464
+ adapter: HearEncoderAdapter = SpectrogramPatchAdapter(config, spec)
465
+ elif spec.adapter_key == "waveform_conv":
466
+ adapter = WaveformConvAdapter(config, spec)
467
+ else:
468
+ raise AssertionError(f"Unsupported registered adapter {spec.adapter_key}")
469
+ _load_inference_weights(adapter, state_dict)
470
+ adapter.eval()
471
+ for parameter in adapter.parameters():
472
+ parameter.requires_grad = False
473
+ return adapter
config.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "AudioEmbeddingModel"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_audio.AudioEmbeddingConfig",
7
+ "AutoModel": "modeling_audio.AudioEmbeddingModel"
8
+ },
9
+ "dtype": "float32",
10
+ "encoder_kwargs": {
11
+ "attn_drop_rate": 0.0,
12
+ "depth": 12,
13
+ "drop_path_rate": 0.1,
14
+ "drop_rate": 0.0,
15
+ "embed_dim": 768,
16
+ "mlp_ratio": 4.0,
17
+ "num_heads": 12,
18
+ "num_patches": 128,
19
+ "pos_embed_type": "sincos",
20
+ "qkv_bias": true
21
+ },
22
+ "extraction_preset": "overlap50_two_phase",
23
+ "feature_encoder_kwargs": {},
24
+ "hidden_size": 768,
25
+ "model_target": "src.models.best_rq2_module.BestRQ2Module",
26
+ "model_type": "audio_embeddings",
27
+ "patch_embed_kwargs": {
28
+ "embed_dim": 768,
29
+ "img_size": [
30
+ 128,
31
+ 256
32
+ ],
33
+ "in_chans": 1,
34
+ "patch_size": [
35
+ 16,
36
+ 16
37
+ ]
38
+ },
39
+ "sampling_rate": 16000,
40
+ "spectrogram_adjustment_mode": "truncate",
41
+ "spectrogram_kwargs": {
42
+ "f_max": 8000,
43
+ "f_min": 0,
44
+ "hop_length_ms": 39.0625,
45
+ "n_fft": 2048,
46
+ "n_mels": 128,
47
+ "power": 2.0,
48
+ "sample_rate": 16000,
49
+ "win_length_ms": 128
50
+ },
51
+ "transformers_version": "5.17.0"
52
+ }
configuration_audio.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """Serializable architecture settings; no Hydra execution at inference time."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from copy import deepcopy
28
+ from typing import Any
29
+
30
+ from transformers import PretrainedConfig
31
+
32
+ from .adapters import SPECTROGRAM_TARGETS, WAVEFORM_TARGETS
33
+ from .extraction import get_preset
34
+
35
+
36
+ class AudioEmbeddingConfig(PretrainedConfig):
37
+ model_type = "audio_embeddings"
38
+
39
+ def __init__(
40
+ self,
41
+ model_target: str = "src.models.best_rq2_module.BestRQ2Module",
42
+ encoder_kwargs: dict[str, Any] | None = None,
43
+ spectrogram_kwargs: dict[str, Any] | None = None,
44
+ patch_embed_kwargs: dict[str, Any] | None = None,
45
+ feature_encoder_kwargs: dict[str, Any] | None = None,
46
+ sampling_rate: int = 16000,
47
+ spectrogram_adjustment_mode: str = "pad",
48
+ extraction_preset: str = "overlap50_two_phase",
49
+ **kwargs: Any,
50
+ ) -> None:
51
+ super().__init__(**kwargs)
52
+ if model_target not in {*SPECTROGRAM_TARGETS, *WAVEFORM_TARGETS}:
53
+ raise ValueError(f"Unsupported model_target: {model_target!r}")
54
+ if not isinstance(sampling_rate, int) or sampling_rate <= 0:
55
+ raise ValueError("sampling_rate must be a positive integer")
56
+ if spectrogram_adjustment_mode not in {"pad", "truncate"}:
57
+ raise ValueError("spectrogram_adjustment_mode must be 'pad' or 'truncate'")
58
+ get_preset(extraction_preset)
59
+ self.model_target = model_target
60
+ self.encoder_kwargs = deepcopy(
61
+ encoder_kwargs
62
+ or {
63
+ "embed_dim": 768,
64
+ "num_patches": 128,
65
+ }
66
+ )
67
+ self.spectrogram_kwargs = deepcopy(spectrogram_kwargs or {})
68
+ self.patch_embed_kwargs = deepcopy(patch_embed_kwargs or {})
69
+ self.feature_encoder_kwargs = deepcopy(feature_encoder_kwargs or {})
70
+ # Remote configuration must contain data, never Python expressions.
71
+ if isinstance(self.feature_encoder_kwargs.get("conv_layers_spec"), str):
72
+ raise ValueError(
73
+ "conv_layers_spec must be a JSON list, not a Python expression"
74
+ )
75
+ self.sampling_rate = sampling_rate
76
+ self.spectrogram_adjustment_mode = spectrogram_adjustment_mode
77
+ self.extraction_preset = extraction_preset
78
+ self.hidden_size = int(self.encoder_kwargs["embed_dim"])
79
+ if self.hidden_size <= 0:
80
+ raise ValueError("encoder_kwargs.embed_dim must be positive")
81
+
82
+ def to_adapter_config(self) -> dict[str, Any]:
83
+ net: dict[str, Any] = {"encoder": deepcopy(self.encoder_kwargs)}
84
+ if self.model_target in SPECTROGRAM_TARGETS:
85
+ net.update(
86
+ spectrogram=deepcopy(self.spectrogram_kwargs),
87
+ patch_embed=deepcopy(self.patch_embed_kwargs),
88
+ )
89
+ else:
90
+ net.update(
91
+ feature_encoder=deepcopy(self.feature_encoder_kwargs),
92
+ sampling={"sample_rate": self.sampling_rate},
93
+ )
94
+ return {
95
+ "data": {"target_sample_rate": self.sampling_rate},
96
+ "model": {
97
+ "_target_": self.model_target,
98
+ "spectrogram_adjustment_mode": self.spectrogram_adjustment_mode,
99
+ "net": net,
100
+ },
101
+ }
102
+
103
+
104
+ AudioEmbeddingConfig.register_for_auto_class()
105
+
106
+ from .adapters import __name__ as _bundled_adapters # noqa: F401
107
+ from .extraction import __name__ as _bundled_extraction # noqa: F401
108
+ from .patch_embed import __name__ as _bundled_patch_embed # noqa: F401
109
+ from .spectrogram import __name__ as _bundled_spectrogram # noqa: F401
110
+ from .vit import __name__ as _bundled_vit # noqa: F401
111
+ from .rope import __name__ as _bundled_rope # noqa: F401
112
+ from .transformer import __name__ as _bundled_transformer # noqa: F401
113
+ from .normalization import __name__ as _bundled_normalization # noqa: F401
114
+ from .waveform_feature_encoder import __name__ as _bundled_waveform_feature_encoder # noqa: F401
export_manifest.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": 1,
3
+ "model_name": "BEST-RQ-2",
4
+ "mock": false,
5
+ "model_target": "src.models.best_rq2_module.BestRQ2Module",
6
+ "source": {
7
+ "huggingface_repository": "ltuncay/BEST-RQ-2",
8
+ "huggingface_revision": "a03eeb5c4433f4bf7a7e6e8b4724af862789959c",
9
+ "checkpoint_name": "BEST-RQ-2.safetensors",
10
+ "training_config_sha256": "9673abdbd7fc0df52803d3ca7703687d6ef271208f11dd2e4f547d31af6296db",
11
+ "configured_max_steps": 200000,
12
+ "original_distribution": "AECC 2026 submission",
13
+ "checkpoint_sha256": "7111465e6c868e3d0b55c5fe9a23dc5069ac80beba8444c5ee9db4691d796899",
14
+ "global_step": null
15
+ },
16
+ "runtime_versions": {
17
+ "torch": "2.9.1",
18
+ "torchaudio": "2.9.1",
19
+ "transformers": "5.17.0",
20
+ "timm": "1.0.22",
21
+ "einops": "0.8.1"
22
+ },
23
+ "validation": {
24
+ "max_absolute_error": 0.0,
25
+ "batch_size": 2,
26
+ "aecc_component_parity": {
27
+ "source_revision": "a03eeb5c4433f4bf7a7e6e8b4724af862789959c",
28
+ "input_samples": 160000,
29
+ "encoder_output_shape": [
30
+ 1,
31
+ 128,
32
+ 768
33
+ ],
34
+ "spectrogram_max_absolute_error": 0.0,
35
+ "patch_max_absolute_error": 0.0,
36
+ "encoder_max_absolute_error": 1.6689300537109375e-06
37
+ }
38
+ },
39
+ "files": {
40
+ "CODE_LICENSE": "8fe9e8b749cd4abedabcb3100df445db899e72192394d14cc2dbf24a40811af6",
41
+ "README.md": "7537e8c7103f6c735f84dc0bfc781fac2eb8b2f9f831b35eb3f00f7cec15b83f",
42
+ "adapters.py": "ca0f26826763c0e48b7508da732242bb83adfeb4814e389ed3fde93ad648c728",
43
+ "config.json": "d37b85c37f17aa954f715b3b3d956c9ed18215ec6ea9565bcd4a403e9b51a134",
44
+ "configuration_audio.py": "59f3a0b8db0df5af85e677ac33a4431595e9b2eff6d34c1e6a1778dd75a4568d",
45
+ "extraction.py": "8949389abc52278e24634518f72eae1270bb1a649ad016b71e0d6e8113acaa7c",
46
+ "feature_extraction_audio.py": "5ed9004727be8b61bd6c0efbeb703b634c20c1b790d9fd59d6a9910438e89b39",
47
+ "model.safetensors": "f79b5c53ae7749420b8533ec459dcfd5ee0e82858d1df56896a5f74faa845236",
48
+ "modeling_audio.py": "ba394cd7f80929274090bc1b8986298247f61494db5ed99c0c5469f4e30bb874",
49
+ "normalization.py": "918b0eb9d547e77ba1adc154deb33d29861532e2a0cd7663497bfc9df6155cef",
50
+ "patch_embed.py": "825cf2ca0a9d8d10e12f40dc69d2293aabf04b627b926d42314a8b5193e231f7",
51
+ "preprocessor_config.json": "e37df5cada8190b709335e4c7d45e714617cfcdd532d0e270562225b173b26af",
52
+ "requirements.txt": "08c9ad278bc06a31d7c7ed3a2255dca48bd63faf29ae7c27635c284d082bb818",
53
+ "rope.py": "d74bdc7a7dd4ba98414f93c4f1937b95614b57394984d265af5999327efc2d82",
54
+ "spectrogram.py": "b1e6de8e8ec8220ad9369a765f80a1504b13a6a54aae2ab5bc18cd8dd1e0a894",
55
+ "transformer.py": "03ea1cb7509669f91003f8cd94edd3660b0c6788e14335ca995c60834a2017d4",
56
+ "vit.py": "cb6b05b5f617def68e05c40e6bf4d8a24a7b43e5acc4481649e3541bd172e727",
57
+ "waveform_feature_encoder.py": "69c811624650441e7f46d4fc4e8177c9bac2f65970aef5c6fbd826522f109e22",
58
+ "LICENSE": "8fe9e8b749cd4abedabcb3100df445db899e72192394d14cc2dbf24a40811af6"
59
+ }
60
+ }
extraction.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass
26
+ from typing import Callable
27
+
28
+ import torch
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class ExtractionPreset:
33
+ name: str
34
+ overlap: float
35
+ num_phases: int
36
+
37
+
38
+ PRESETS = {
39
+ preset.name: preset
40
+ for preset in (
41
+ ExtractionPreset("native_one_phase", 0.0, 1),
42
+ ExtractionPreset("overlap25_one_phase", 0.25, 1),
43
+ ExtractionPreset("overlap50_one_phase", 0.5, 1),
44
+ ExtractionPreset("native_two_phase", 0.0, 2),
45
+ ExtractionPreset("overlap25_two_phase", 0.25, 2),
46
+ ExtractionPreset("overlap50_two_phase", 0.5, 2),
47
+ )
48
+ }
49
+
50
+
51
+ def get_preset(name: str) -> ExtractionPreset:
52
+ try:
53
+ return PRESETS[name]
54
+ except KeyError as error:
55
+ raise ValueError(
56
+ f"Unknown extraction preset {name!r}. Expected one of {sorted(PRESETS)}"
57
+ ) from error
58
+
59
+
60
+ def _window_starts(length: int, window: int, overlap: float) -> list[int]:
61
+ if length <= window:
62
+ return [0]
63
+ if not 0.0 <= overlap < 1.0:
64
+ raise ValueError(f"overlap must be in [0, 1), got {overlap}")
65
+ stride = max(1, round(window * (1.0 - overlap)))
66
+ if overlap == 0.0:
67
+ return list(range(0, length, stride))
68
+ starts = list(range(0, length - window + 1, stride))
69
+ final_start = length - window
70
+ if starts[-1] != final_start:
71
+ starts.append(final_start)
72
+ return starts
73
+
74
+
75
+ def _positive_triangular_weights(
76
+ length: int,
77
+ *,
78
+ device: torch.device,
79
+ dtype: torch.dtype,
80
+ ) -> torch.Tensor:
81
+ positions = torch.arange(length, device=device, dtype=dtype)
82
+ return 1.0 - torch.abs((2.0 * positions) - (length - 1)) / (length + 1)
83
+
84
+
85
+ def fuse_context_windows(
86
+ tokens: torch.Tensor,
87
+ *,
88
+ max_context_tokens: int,
89
+ overlap: float,
90
+ encode_window: Callable[[torch.Tensor, torch.Tensor], torch.Tensor],
91
+ ) -> torch.Tensor:
92
+ """Contextualize a [frequency, time, dim] grid and fuse repeated tokens."""
93
+
94
+ if tokens.ndim != 3:
95
+ raise ValueError(
96
+ f"Expected token grid [frequency, time, dim], got {tuple(tokens.shape)}"
97
+ )
98
+ frequency, time, dimension = tokens.shape
99
+ max_time = max_context_tokens // frequency
100
+ if max_time <= 0:
101
+ raise ValueError(
102
+ f"Encoder context {max_context_tokens} cannot hold {frequency} frequency tokens"
103
+ )
104
+ if time == 0:
105
+ raise ValueError("Token grid has no time steps")
106
+
107
+ accumulator = torch.zeros_like(tokens)
108
+ denominator = torch.zeros(time, device=tokens.device, dtype=tokens.dtype)
109
+ for start in _window_starts(time, max_time, overlap):
110
+ end = min(time, start + max_time)
111
+ width = end - start
112
+ window = tokens[:, start:end, :]
113
+ flattened = window.reshape(1, frequency * width, dimension)
114
+ position_ids = torch.arange(
115
+ frequency * width,
116
+ device=tokens.device,
117
+ )
118
+ encoded = encode_window(flattened, position_ids)
119
+ if encoded.shape != flattened.shape:
120
+ raise ValueError(
121
+ "Encoder changed the token-grid shape: "
122
+ f"expected {tuple(flattened.shape)}, got {tuple(encoded.shape)}"
123
+ )
124
+ encoded_grid = encoded.reshape(frequency, width, dimension)
125
+ weights = _positive_triangular_weights(
126
+ width,
127
+ device=tokens.device,
128
+ dtype=tokens.dtype,
129
+ )
130
+ accumulator[:, start:end, :] += encoded_grid * weights.view(1, -1, 1)
131
+ denominator[start:end] += weights
132
+
133
+ if torch.any(denominator <= 0):
134
+ raise RuntimeError("At least one token received zero overlap weight")
135
+ fused = accumulator / denominator.view(1, -1, 1)
136
+ return fused.mean(dim=0)
137
+
138
+
139
+ def merge_phases(
140
+ phases: list[tuple[torch.Tensor, torch.Tensor]],
141
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
142
+ if not phases:
143
+ raise ValueError("At least one embedding phase is required")
144
+ embedding_dim = phases[0][0].shape[-1]
145
+ for embeddings, timestamps in phases:
146
+ if embeddings.ndim != 2 or embeddings.shape[-1] != embedding_dim:
147
+ raise ValueError("All phase embeddings must have shape [time, dimension]")
148
+ if timestamps.ndim != 1 or timestamps.shape[0] != embeddings.shape[0]:
149
+ raise ValueError("Each phase needs one timestamp per embedding")
150
+
151
+ merged_embeddings = torch.cat([phase[0] for phase in phases], dim=0)
152
+ merged_timestamps = torch.cat([phase[1] for phase in phases], dim=0)
153
+ order = torch.argsort(merged_timestamps, stable=True)
154
+ scene = torch.stack([embeddings.mean(dim=0) for embeddings, _ in phases]).mean(
155
+ dim=0
156
+ )
157
+ return merged_embeddings[order], merged_timestamps[order], scene
feature_extraction_audio.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """Prepare mono waveforms without changing the learned frontend's numerics."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any
28
+
29
+ import numpy as np
30
+ import torch
31
+ from transformers.feature_extraction_sequence_utils import SequenceFeatureExtractor
32
+ from transformers.feature_extraction_utils import BatchFeature
33
+
34
+
35
+ class AudioEmbeddingFeatureExtractor(SequenceFeatureExtractor):
36
+ model_input_names = ["input_values", "attention_mask"]
37
+
38
+ def __init__(
39
+ self,
40
+ sampling_rate: int = 16000,
41
+ padding_value: float = 0.0,
42
+ return_attention_mask: bool = True,
43
+ **kwargs: Any,
44
+ ) -> None:
45
+ if not isinstance(sampling_rate, int) or sampling_rate <= 0:
46
+ raise ValueError("sampling_rate must be a positive integer")
47
+ kwargs.pop("feature_size", None)
48
+ super().__init__(
49
+ feature_size=1,
50
+ sampling_rate=sampling_rate,
51
+ padding_value=padding_value,
52
+ return_attention_mask=return_attention_mask,
53
+ **kwargs,
54
+ )
55
+
56
+ def __call__(
57
+ self,
58
+ raw_speech: Any,
59
+ *,
60
+ sampling_rate: int | None = None,
61
+ padding: bool | str = True,
62
+ max_length: int | None = None,
63
+ truncation: bool = False,
64
+ pad_to_multiple_of: int | None = None,
65
+ return_attention_mask: bool | None = None,
66
+ return_tensors: str | None = "pt",
67
+ ) -> BatchFeature:
68
+ if sampling_rate != self.sampling_rate:
69
+ raise ValueError(
70
+ f"Pass sampling_rate={self.sampling_rate}; got {sampling_rate}. "
71
+ "Resample audio to the model rate before calling the feature extractor."
72
+ )
73
+ if isinstance(raw_speech, torch.Tensor):
74
+ raw_speech = raw_speech.detach().cpu().float().numpy()
75
+ if isinstance(raw_speech, np.ndarray):
76
+ if raw_speech.ndim not in {1, 2}:
77
+ raise ValueError(
78
+ "Expected mono audio [samples] or a batch [batch, samples]"
79
+ )
80
+ batch = [raw_speech] if raw_speech.ndim == 1 else list(raw_speech)
81
+ elif isinstance(raw_speech, (list, tuple)) and len(raw_speech):
82
+ batch = [raw_speech] if np.isscalar(raw_speech[0]) else list(raw_speech)
83
+ else:
84
+ raise ValueError("Provide a nonempty waveform or batch of mono waveforms")
85
+ waveforms = []
86
+ for waveform in batch:
87
+ if isinstance(waveform, torch.Tensor):
88
+ waveform = waveform.detach().cpu().float().numpy()
89
+ array = np.asarray(waveform, dtype=np.float32)
90
+ if array.ndim != 1 or array.size == 0 or not np.isfinite(array).all():
91
+ raise ValueError(
92
+ "Each waveform must be a nonempty, finite, mono 1-D array"
93
+ )
94
+ waveforms.append(array)
95
+ if not waveforms:
96
+ raise ValueError("The audio batch cannot be empty")
97
+ if max_length is not None and max_length <= 0:
98
+ raise ValueError("max_length must be positive")
99
+ return self.pad(
100
+ BatchFeature({"input_values": waveforms}),
101
+ padding=padding,
102
+ max_length=max_length,
103
+ truncation=truncation,
104
+ pad_to_multiple_of=pad_to_multiple_of,
105
+ return_attention_mask=(
106
+ self.return_attention_mask
107
+ if return_attention_mask is None
108
+ else return_attention_mask
109
+ ),
110
+ return_tensors=return_tensors,
111
+ )
112
+
113
+
114
+ AudioEmbeddingFeatureExtractor.register_for_auto_class("AutoFeatureExtractor")
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f79b5c53ae7749420b8533ec459dcfd5ee0e82858d1df56896a5f74faa845236
3
+ size 341956216
modeling_audio.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """Trainable audio embeddings with the same extraction policy as HEAR."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ import torch
32
+ from torch.nn.utils.rnn import pad_sequence
33
+ from transformers import PreTrainedModel
34
+ from transformers.utils import ModelOutput
35
+
36
+ from .adapters import SpectrogramPatchAdapter, WaveformConvAdapter
37
+ from .adapters import resolve_adapter_spec
38
+ from .configuration_audio import AudioEmbeddingConfig
39
+
40
+
41
+ @dataclass
42
+ class AudioEmbeddingOutput(ModelOutput):
43
+ last_hidden_state: torch.Tensor | None = None
44
+ pooler_output: torch.Tensor | None = None
45
+ attention_mask: torch.Tensor | None = None
46
+ timestamps_ms: torch.Tensor | None = None
47
+
48
+
49
+ class AudioEmbeddingModel(PreTrainedModel):
50
+ config_class = AudioEmbeddingConfig
51
+ base_model_prefix = "adapter"
52
+ main_input_name = "input_values"
53
+ # RoPE modules are shared by every attention block. Save all buffer keys,
54
+ # so loading does not need special tied-buffer handling.
55
+ _supports_assign_param_buffer = False
56
+
57
+ def __init__(self, config: AudioEmbeddingConfig) -> None:
58
+ super().__init__(config)
59
+ adapter_config = config.to_adapter_config()
60
+ # Transformers 5 loads under a default meta device, but torchaudio's
61
+ # filter-bank constructors need real values. Build on CPU; HF subsequently
62
+ # loads the checkpoint tensors onto the requested device/dtype.
63
+ with torch.device("cpu"):
64
+ spec = resolve_adapter_spec(adapter_config)
65
+ adapter_type = (
66
+ SpectrogramPatchAdapter
67
+ if spec.adapter_key == "spectrogram_patch"
68
+ else WaveformConvAdapter
69
+ )
70
+ self.adapter = adapter_type(adapter_config, spec)
71
+ self.post_init()
72
+
73
+ def _init_weights(self, module: torch.nn.Module) -> None:
74
+ """Preserve initialization performed by the research components themselves."""
75
+
76
+ def save_pretrained(
77
+ self, save_directory: str | Path, *args: Any, **kwargs: Any
78
+ ) -> None:
79
+ if kwargs.get("state_dict") is None:
80
+ kwargs["state_dict"] = {
81
+ key: value.detach().clone().contiguous()
82
+ for key, value in self.state_dict().items()
83
+ }
84
+ return super().save_pretrained(save_directory, *args, **kwargs)
85
+
86
+ def forward(
87
+ self,
88
+ input_values: torch.Tensor,
89
+ attention_mask: torch.Tensor | None = None,
90
+ return_dict: bool | None = None,
91
+ ) -> AudioEmbeddingOutput | tuple[torch.Tensor, ...]:
92
+ if input_values.ndim != 2 or min(input_values.shape) <= 0:
93
+ raise ValueError(
94
+ "input_values must have shape [batch, samples] with nonempty axes"
95
+ )
96
+ if (
97
+ not input_values.is_floating_point()
98
+ or not torch.isfinite(input_values).all()
99
+ ):
100
+ raise ValueError(
101
+ "input_values must contain finite floating-point waveforms"
102
+ )
103
+ if attention_mask is None:
104
+ lengths = [input_values.shape[1]] * input_values.shape[0]
105
+ else:
106
+ if attention_mask.shape != input_values.shape:
107
+ raise ValueError(
108
+ "attention_mask must have the same shape as input_values"
109
+ )
110
+ if not torch.all((attention_mask == 0) | (attention_mask == 1)):
111
+ raise ValueError("attention_mask must contain only zeros and ones")
112
+ lengths_tensor = attention_mask.long().sum(dim=1)
113
+ expected = (
114
+ torch.arange(input_values.shape[1], device=attention_mask.device)[None]
115
+ < lengths_tensor[:, None]
116
+ )
117
+ if not torch.equal(attention_mask.bool(), expected) or torch.any(
118
+ lengths_tensor == 0
119
+ ):
120
+ raise ValueError(
121
+ "attention_mask must describe nonempty, right-padded waveforms"
122
+ )
123
+ lengths = lengths_tensor.tolist()
124
+ # Clear non-buffer RoPE caches between calls: inference-mode caches cannot
125
+ # be reused for autograd, and .to(device/dtype) does not move these caches.
126
+ rope = self.adapter.encoder.rope
127
+ if rope is not None:
128
+ for name in ("cached_cos_sin", "cached_cos_sin_h", "cached_cos_sin_w"):
129
+ if hasattr(rope, name):
130
+ setattr(rope, name, None)
131
+ outputs = []
132
+ for waveform, length in zip(input_values, lengths):
133
+ if isinstance(self.adapter, SpectrogramPatchAdapter):
134
+ minimum = self.adapter.spectrogram.mel_spec.n_fft // 2 + 1
135
+ if length < minimum:
136
+ raise ValueError(
137
+ f"Audio requires at least {minimum} samples for this spectrogram; got {length}"
138
+ )
139
+ outputs.append(
140
+ self.adapter.extract(
141
+ waveform[:length], preset_name=self.config.extraction_preset
142
+ )
143
+ )
144
+ hidden = pad_sequence(
145
+ [item.timestamp_embeddings for item in outputs], batch_first=True
146
+ )
147
+ frame_lengths = torch.tensor(
148
+ [item.timestamp_embeddings.shape[0] for item in outputs],
149
+ device=hidden.device,
150
+ )
151
+ frame_mask = (
152
+ torch.arange(hidden.shape[1], device=hidden.device)[None]
153
+ < frame_lengths[:, None]
154
+ )
155
+ result = AudioEmbeddingOutput(
156
+ last_hidden_state=hidden,
157
+ pooler_output=torch.stack([item.scene_embedding for item in outputs]),
158
+ attention_mask=frame_mask.long(),
159
+ timestamps_ms=pad_sequence(
160
+ [item.timestamps_ms for item in outputs],
161
+ batch_first=True,
162
+ padding_value=-1.0,
163
+ ),
164
+ )
165
+ return (
166
+ result
167
+ if (self.config.return_dict if return_dict is None else return_dict)
168
+ else result.to_tuple()
169
+ )
170
+
171
+
172
+ AudioEmbeddingModel.register_for_auto_class("AutoModel")
173
+
174
+ from .adapters import __name__ as _bundled_adapters # noqa: F401
175
+ from .extraction import __name__ as _bundled_extraction # noqa: F401
176
+ from .patch_embed import __name__ as _bundled_patch_embed # noqa: F401
177
+ from .spectrogram import __name__ as _bundled_spectrogram # noqa: F401
178
+ from .vit import __name__ as _bundled_vit # noqa: F401
179
+ from .rope import __name__ as _bundled_rope # noqa: F401
180
+ from .transformer import __name__ as _bundled_transformer # noqa: F401
181
+ from .normalization import __name__ as _bundled_normalization # noqa: F401
182
+ from .waveform_feature_encoder import __name__ as _bundled_waveform_feature_encoder # noqa: F401
normalization.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from __future__ import annotations
24
+
25
+ import torch
26
+ import torch.nn as nn
27
+ import torch.nn.functional as F
28
+
29
+
30
+ class MixedPrecisionRMSNorm(nn.RMSNorm):
31
+ """RMSNorm that keeps its master weight while matching activation dtype.
32
+
33
+ PyTorch's fused RMSNorm requires the input and weight to have the same dtype.
34
+ Mixed-precision training keeps parameters in FP32, so use a differentiable
35
+ low-precision view of the weight for the operation itself.
36
+ """
37
+
38
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
39
+ weight = self.weight
40
+ if weight is not None and weight.dtype != input.dtype:
41
+ weight = weight.to(dtype=input.dtype)
42
+ return F.rms_norm(input, self.normalized_shape, weight, self.eps)
patch_embed.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from collections.abc import Sequence
24
+ from math import prod
25
+
26
+ import torch
27
+ import torch.nn as nn
28
+ from timm.layers import PatchEmbed as TimmPatchEmbed
29
+
30
+
31
+ def _is_power_of_two(value: int) -> bool:
32
+ return value > 0 and value & (value - 1) == 0
33
+
34
+
35
+ def _build_hmlp_kernel_schedule(
36
+ patch_size: tuple[int, int],
37
+ ) -> tuple[tuple[int, int], ...]:
38
+ """Build non-overlapping aggregation steps for one target patch.
39
+
40
+ The paper starts from 4x4 subpatches and doubles both axes until reaching
41
+ 16x16. For rectangular patches, an axis stops growing once it reaches its
42
+ target while the other axis keeps doubling.
43
+ """
44
+ patch_height, patch_width = patch_size
45
+ if not _is_power_of_two(patch_height) or not _is_power_of_two(patch_width):
46
+ raise ValueError(
47
+ "hMLP patch dimensions must be powers of two so each axis can be "
48
+ f"doubled exactly, got {patch_size}"
49
+ )
50
+
51
+ first_kernel = (min(4, patch_height), min(4, patch_width))
52
+ schedule = [first_kernel]
53
+ current_height, current_width = first_kernel
54
+
55
+ while (current_height, current_width) != patch_size:
56
+ kernel_height = 2 if current_height < patch_height else 1
57
+ kernel_width = 2 if current_width < patch_width else 1
58
+ schedule.append((kernel_height, kernel_width))
59
+ current_height *= kernel_height
60
+ current_width *= kernel_width
61
+
62
+ return tuple(schedule)
63
+
64
+
65
+ def _resolve_hmlp_kernel_schedule(
66
+ patch_size: tuple[int, int],
67
+ kernel_schedule: Sequence[Sequence[int]] | None,
68
+ ) -> tuple[tuple[int, int], ...]:
69
+ if kernel_schedule is None:
70
+ return _build_hmlp_kernel_schedule(patch_size)
71
+
72
+ schedule: list[tuple[int, int]] = []
73
+ for stage_index, stage in enumerate(kernel_schedule):
74
+ try:
75
+ stage_values = tuple(stage)
76
+ except TypeError as error:
77
+ raise ValueError(
78
+ "Each hMLP stage must contain [kernel_height, kernel_width], "
79
+ f"stage {stage_index} has {stage}"
80
+ ) from error
81
+ if len(stage_values) != 2:
82
+ raise ValueError(
83
+ "Each hMLP stage must contain [kernel_height, kernel_width], "
84
+ f"stage {stage_index} has {stage}"
85
+ )
86
+ try:
87
+ kernel_size = tuple(int(value) for value in stage_values)
88
+ except (TypeError, ValueError) as error:
89
+ raise ValueError(
90
+ f"hMLP stage kernels must be integers, got {stage_values}"
91
+ ) from error
92
+ if any(
93
+ isinstance(original, bool) or normalized != original
94
+ for normalized, original in zip(kernel_size, stage_values)
95
+ ):
96
+ raise ValueError(f"hMLP stage kernels must be integers, got {stage_values}")
97
+ if any(value <= 0 for value in kernel_size):
98
+ raise ValueError(f"hMLP stage kernels must be positive, got {kernel_size}")
99
+ schedule.append(kernel_size)
100
+
101
+ if not schedule:
102
+ raise ValueError("hMLP kernel_schedule must contain at least one stage")
103
+
104
+ aggregated_patch_size = tuple(
105
+ prod(kernel_size[axis] for kernel_size in schedule) for axis in range(2)
106
+ )
107
+ if aggregated_patch_size != patch_size:
108
+ raise ValueError(
109
+ "hMLP kernel_schedule stages must multiply to patch_size; "
110
+ f"got {aggregated_patch_size} from {tuple(schedule)}, expected {patch_size}"
111
+ )
112
+
113
+ return tuple(schedule)
114
+
115
+
116
+ class HierarchicalMLPPatchEmbed(nn.Module):
117
+ """hMLP patch stem with independent, hierarchical patch aggregation."""
118
+
119
+ def __init__(
120
+ self,
121
+ img_size: tuple[int, int] = (128, 256),
122
+ patch_size: tuple[int, int] = (16, 16),
123
+ in_chans: int = 1,
124
+ embed_dim: int = 768,
125
+ bias: bool = True,
126
+ kernel_schedule: Sequence[Sequence[int]] | None = None,
127
+ ) -> None:
128
+ super().__init__()
129
+ self.img_size = tuple(img_size)
130
+ self.patch_size = tuple(patch_size)
131
+ self.in_chans = in_chans
132
+ self.embed_dim = embed_dim
133
+ self.bias = bias
134
+ self.kernel_schedule = _resolve_hmlp_kernel_schedule(
135
+ self.patch_size,
136
+ kernel_schedule,
137
+ )
138
+ self.num_patches = (self.img_size[0] // self.patch_size[0]) * (
139
+ self.img_size[1] // self.patch_size[1]
140
+ )
141
+
142
+ hidden_dim = max(1, embed_dim // 4)
143
+ layers: list[nn.Module] = []
144
+ input_dim = in_chans
145
+ for stage_index, kernel_size in enumerate(self.kernel_schedule):
146
+ is_last = stage_index == len(self.kernel_schedule) - 1
147
+ output_dim = embed_dim if is_last else hidden_dim
148
+ layers.extend(
149
+ [
150
+ nn.Conv2d(
151
+ input_dim,
152
+ output_dim,
153
+ kernel_size=kernel_size,
154
+ stride=kernel_size,
155
+ bias=bias,
156
+ ),
157
+ nn.SyncBatchNorm(output_dim),
158
+ ]
159
+ )
160
+ if not is_last:
161
+ layers.append(nn.GELU())
162
+ input_dim = output_dim
163
+
164
+ self.proj = nn.Sequential(*layers)
165
+
166
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
167
+ if x.ndim != 4:
168
+ raise ValueError(f"Expected input with shape [B, C, H, W], got {x.shape}")
169
+ if x.shape[1] != self.in_chans:
170
+ raise ValueError(
171
+ f"Expected {self.in_chans} input channels, got {x.shape[1]}"
172
+ )
173
+ if x.shape[2] % self.patch_size[0] != 0 or x.shape[3] % self.patch_size[1] != 0:
174
+ raise ValueError(
175
+ "Input spatial dimensions must be divisible by the hMLP patch "
176
+ f"size {self.patch_size}, got {tuple(x.shape[2:])}"
177
+ )
178
+
179
+ return self.proj(x).flatten(2).transpose(1, 2)
180
+
181
+
182
+ class PatchEmbed(nn.Module):
183
+ """
184
+ 2D Image to Patch Embedding.
185
+
186
+ Args:
187
+ img_size (tuple[int, int]): Input image size (H, W).
188
+ patch_size (tuple[int, int]): Patch size (H, W).
189
+ in_chans (int): Number of input channels.
190
+ embed_dim (int): Embedding dimension.
191
+ """
192
+
193
+ def __init__(
194
+ self,
195
+ img_size: tuple[int, int] = (128, 256),
196
+ patch_size: tuple[int, int] = (16, 16),
197
+ in_chans: int = 1,
198
+ embed_dim: int = 768,
199
+ bias: bool = True,
200
+ stem_type: str = "linear",
201
+ hmlp_kernel_schedule: Sequence[Sequence[int]] | None = None,
202
+ ):
203
+ super().__init__()
204
+ self.img_size = tuple(img_size)
205
+ self.patch_size = tuple(patch_size)
206
+ self.in_chans = in_chans
207
+ self.embed_dim = embed_dim
208
+ self.bias = bias
209
+ self.stem_type = stem_type.strip().lower().replace("-", "_")
210
+
211
+ if self.stem_type == "linear":
212
+ self.patch_embed = TimmPatchEmbed(
213
+ img_size=img_size,
214
+ patch_size=patch_size,
215
+ in_chans=in_chans,
216
+ embed_dim=embed_dim,
217
+ flatten=True,
218
+ bias=bias,
219
+ strict_img_size=False,
220
+ )
221
+ elif self.stem_type == "hmlp":
222
+ self.patch_embed = HierarchicalMLPPatchEmbed(
223
+ img_size=img_size,
224
+ patch_size=patch_size,
225
+ in_chans=in_chans,
226
+ embed_dim=embed_dim,
227
+ bias=bias,
228
+ kernel_schedule=hmlp_kernel_schedule,
229
+ )
230
+ else:
231
+ raise ValueError(
232
+ f"Unknown stem_type={stem_type!r}; expected 'linear' or 'hmlp'"
233
+ )
234
+ self.num_patches = self.patch_embed.num_patches
235
+
236
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
237
+ """
238
+ Forward pass.
239
+
240
+ Args:
241
+ x (torch.Tensor): Input tensor [B, C, H, W].
242
+
243
+ Returns:
244
+ torch.Tensor: Patch embeddings [B, N, D].
245
+ """
246
+ return self.patch_embed(x)
preprocessor_config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoFeatureExtractor": "feature_extraction_audio.AudioEmbeddingFeatureExtractor"
4
+ },
5
+ "feature_extractor_type": "AudioEmbeddingFeatureExtractor",
6
+ "feature_size": 1,
7
+ "padding_side": "right",
8
+ "padding_value": 0.0,
9
+ "return_attention_mask": true,
10
+ "sampling_rate": 16000
11
+ }
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch>=2.9.1
2
+ torchaudio>=2.9.1
3
+ timm>=0.9
4
+ einops>=0.7
5
+ transformers>=4.57,<6
rope.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from typing import Optional, Tuple
24
+
25
+ import torch
26
+ import torch.nn as nn
27
+ import torch.nn.functional as F
28
+
29
+ from .normalization import MixedPrecisionRMSNorm
30
+
31
+
32
+ def _build_qk_norm(norm_type: str, head_dim: int) -> nn.Module:
33
+ normalized = norm_type.strip().lower().replace("_", "")
34
+ if normalized == "layernorm":
35
+ return nn.LayerNorm(head_dim)
36
+ if normalized == "rmsnorm":
37
+ return MixedPrecisionRMSNorm(head_dim)
38
+ raise ValueError(
39
+ f"Unknown qk_norm_type={norm_type!r}; expected 'layernorm' or 'rmsnorm'"
40
+ )
41
+
42
+
43
+ class RotaryEmbedding2D(nn.Module):
44
+ def __init__(
45
+ self,
46
+ dim: int,
47
+ max_res: Tuple[int, int] = (128, 256),
48
+ temperature: float = 10000.0,
49
+ ):
50
+ super().__init__()
51
+ self.dim = dim
52
+ self.max_h, self.max_w = max_res
53
+ self.temperature = temperature
54
+
55
+ # Check if dim is divisible by 4 (since we split into 2 for H/W, and each needs 2 for complex)
56
+ assert dim % 4 == 0, "Embedding dimension must be divisible by 4 for 2D RoPE"
57
+
58
+ dim_h = dim // 2
59
+ dim_w = dim // 2
60
+
61
+ # Generate frequencies for H and W
62
+ # inv_freq_h: [dim_h // 2]
63
+ inv_freq_h = 1.0 / (temperature ** (torch.arange(0, dim_h, 2).float() / dim_h))
64
+ inv_freq_w = 1.0 / (temperature ** (torch.arange(0, dim_w, 2).float() / dim_w))
65
+
66
+ self.register_buffer("inv_freq_h", inv_freq_h)
67
+ self.register_buffer("inv_freq_w", inv_freq_w)
68
+
69
+ # Cache
70
+ self.cached_cos_sin_h = None
71
+ self.cached_cos_sin_w = None
72
+
73
+ def _update_cache(self, h: int, w: int, device: torch.device, dtype: torch.dtype):
74
+ # Generate grid
75
+ # We need to support arbitrary positions, but usually we just precompute for max_res
76
+ # or compute on the fly for the given indices.
77
+ # Let's compute for max_res and index into it.
78
+
79
+ if self.cached_cos_sin_h is None or self.cached_cos_sin_h[0].shape[0] < h:
80
+ t_h = torch.arange(h, device=device, dtype=dtype)
81
+ freqs_h = torch.einsum("i,j->ij", t_h, self.inv_freq_h) # [H, dim_h/2]
82
+ emb_h = torch.cat((freqs_h, freqs_h), dim=-1) # [H, dim_h]
83
+ self.cached_cos_sin_h = (emb_h.cos(), emb_h.sin())
84
+
85
+ if self.cached_cos_sin_w is None or self.cached_cos_sin_w[0].shape[0] < w:
86
+ t_w = torch.arange(w, device=device, dtype=dtype)
87
+ freqs_w = torch.einsum("i,j->ij", t_w, self.inv_freq_w) # [W, dim_w/2]
88
+ emb_w = torch.cat((freqs_w, freqs_w), dim=-1) # [W, dim_w]
89
+ self.cached_cos_sin_w = (emb_w.cos(), emb_w.sin())
90
+
91
+ def forward(
92
+ self,
93
+ q: torch.Tensor,
94
+ k: torch.Tensor,
95
+ pos_ids: torch.Tensor,
96
+ grid_size: Tuple[int, int],
97
+ ):
98
+ # q, k: [B, num_heads, N, head_dim]
99
+ # pos_ids: [B, N] or [N] (indices of patches)
100
+ # grid_size: (H, W) - original grid size to decode pos_ids
101
+
102
+ B, num_heads, N, D = q.shape
103
+ H_grid, W_grid = grid_size
104
+
105
+ # Decode pos_ids to (h, w)
106
+ # pos_ids are indices in flattened grid [0, H*W-1]
107
+ # h = pos_ids // W_grid
108
+ # w = pos_ids % W_grid
109
+
110
+ h_idx = pos_ids.div(W_grid, rounding_mode="floor") # [B, N]
111
+ w_idx = pos_ids % W_grid # [B, N]
112
+
113
+ # Ensure cache is large enough
114
+ self._update_cache(H_grid, W_grid, q.device, q.dtype)
115
+
116
+ # Fetch cos/sin for H and W
117
+ # cos_h: [B, N, dim_h]
118
+ # We need to gather from cached [max_h, dim_h] using h_idx
119
+
120
+ # Handle shared pos_ids (if [N])
121
+ if h_idx.ndim == 1:
122
+ h_idx = h_idx.unsqueeze(0).expand(B, -1)
123
+ w_idx = w_idx.unsqueeze(0).expand(B, -1)
124
+
125
+ cos_h = F.embedding(h_idx, self.cached_cos_sin_h[0]) # [B, N, dim_h]
126
+ sin_h = F.embedding(h_idx, self.cached_cos_sin_h[1])
127
+ cos_w = F.embedding(w_idx, self.cached_cos_sin_w[0]) # [B, N, dim_w]
128
+ sin_w = F.embedding(w_idx, self.cached_cos_sin_w[1])
129
+
130
+ # Split q, k into halves
131
+ # q: [B, num_heads, N, D] -> [B, N, num_heads, D] for easier manipulation?
132
+ # Usually RoPE is applied on [B, num_heads, N, D] or [N, B, num_heads, D]
133
+ # Let's keep [B, num_heads, N, D]
134
+
135
+ dim_half = D // 2
136
+ q_h, q_w = q.split(dim_half, dim=-1)
137
+ k_h, k_w = k.split(dim_half, dim=-1)
138
+
139
+ # Apply RoPE
140
+ # We need to reshape cos/sin to broadcast over num_heads
141
+ # cos_h: [B, N, dim_h] -> [B, 1, N, dim_h]
142
+ cos_h = cos_h.unsqueeze(1)
143
+ sin_h = sin_h.unsqueeze(1)
144
+ cos_w = cos_w.unsqueeze(1)
145
+ sin_w = sin_w.unsqueeze(1)
146
+
147
+ q_h_rot = self._apply_rotary(q_h, cos_h, sin_h)
148
+ k_h_rot = self._apply_rotary(k_h, cos_h, sin_h)
149
+
150
+ q_w_rot = self._apply_rotary(q_w, cos_w, sin_w)
151
+ k_w_rot = self._apply_rotary(k_w, cos_w, sin_w)
152
+
153
+ q_rot = torch.cat((q_h_rot, q_w_rot), dim=-1)
154
+ k_rot = torch.cat((k_h_rot, k_w_rot), dim=-1)
155
+
156
+ return q_rot, k_rot
157
+
158
+ def _apply_rotary(
159
+ self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
160
+ ) -> torch.Tensor:
161
+ # x: [B, num_heads, N, dim_half]
162
+ # cos, sin: [B, 1, N, dim_half]
163
+ # Standard RoPE rotation:
164
+ # x = [x1, x2]
165
+ # out = [x1*cos - x2*sin, x1*sin + x2*cos]
166
+ # This assumes pairs are adjacent.
167
+ # My inv_freq generation: cat(freqs, freqs).
168
+ # This corresponds to x = [x_first_half, x_second_half] pairing?
169
+ # Usually RoPE pairs even/odd or first/second half.
170
+ # "The standard implementation ... pairs feature i with i + d/2"
171
+ # My emb generation: cat(freqs, freqs) -> [f0, f1, ..., f0, f1, ...] ? No.
172
+ # freqs is [0, 2, ...]
173
+ # cat(freqs, freqs) -> [f0, f2, ..., f0, f2, ...]
174
+ # So it expects x to be split into two halves and rotated.
175
+ # rotate_half(x) = [-x2, x1]
176
+
177
+ return (x * cos) + (self._rotate_half(x) * sin)
178
+
179
+ def _rotate_half(self, x: torch.Tensor) -> torch.Tensor:
180
+ x1, x2 = x.chunk(2, dim=-1)
181
+ return torch.cat((-x2, x1), dim=-1)
182
+
183
+
184
+ class RotaryEmbedding1D(nn.Module):
185
+ """Apply RoPE over the temporal axis using the full attention head."""
186
+
187
+ def __init__(
188
+ self,
189
+ dim: int,
190
+ max_seq_len: int = 256,
191
+ temperature: float = 10000.0,
192
+ ) -> None:
193
+ super().__init__()
194
+ assert dim % 2 == 0, "Embedding dimension must be divisible by 2 for 1D RoPE"
195
+
196
+ self.dim = dim
197
+ self.max_seq_len = max_seq_len
198
+ self.temperature = temperature
199
+ inv_freq = 1.0 / (temperature ** (torch.arange(0, dim, 2).float() / dim))
200
+ self.register_buffer("inv_freq", inv_freq)
201
+ self.cached_cos_sin: tuple[torch.Tensor, torch.Tensor] | None = None
202
+
203
+ def _update_cache(
204
+ self,
205
+ seq_len: int,
206
+ device: torch.device,
207
+ dtype: torch.dtype,
208
+ ) -> None:
209
+ if self.cached_cos_sin is None or self.cached_cos_sin[0].shape[0] < seq_len:
210
+ positions = torch.arange(seq_len, device=device, dtype=dtype)
211
+ freqs = torch.einsum("i,j->ij", positions, self.inv_freq)
212
+ embedding = torch.cat((freqs, freqs), dim=-1)
213
+ self.cached_cos_sin = (embedding.cos(), embedding.sin())
214
+
215
+ def forward(
216
+ self,
217
+ q: torch.Tensor,
218
+ k: torch.Tensor,
219
+ pos_ids: torch.Tensor,
220
+ grid_size: Tuple[int, int],
221
+ ) -> tuple[torch.Tensor, torch.Tensor]:
222
+ """Rotate using each patch's time coordinate from the flattened grid."""
223
+ batch_size = q.shape[0]
224
+ _, width = grid_size
225
+ time_indices = pos_ids % width
226
+
227
+ if time_indices.ndim == 1:
228
+ time_indices = time_indices.unsqueeze(0).expand(batch_size, -1)
229
+
230
+ self._update_cache(width, q.device, q.dtype)
231
+ cos = F.embedding(time_indices, self.cached_cos_sin[0]).unsqueeze(1)
232
+ sin = F.embedding(time_indices, self.cached_cos_sin[1]).unsqueeze(1)
233
+
234
+ return self._apply_rotary(q, cos, sin), self._apply_rotary(k, cos, sin)
235
+
236
+ def _apply_rotary(
237
+ self,
238
+ x: torch.Tensor,
239
+ cos: torch.Tensor,
240
+ sin: torch.Tensor,
241
+ ) -> torch.Tensor:
242
+ return (x * cos) + (self._rotate_half(x) * sin)
243
+
244
+ def _rotate_half(self, x: torch.Tensor) -> torch.Tensor:
245
+ x1, x2 = x.chunk(2, dim=-1)
246
+ return torch.cat((-x2, x1), dim=-1)
247
+
248
+
249
+ class RoPEAttention(nn.Module):
250
+ def __init__(
251
+ self,
252
+ dim: int,
253
+ num_heads: int = 8,
254
+ qkv_bias: bool = False,
255
+ proj_bias: bool = True,
256
+ attn_drop: float = 0.0,
257
+ proj_drop: float = 0.0,
258
+ rope: Optional[RotaryEmbedding1D | RotaryEmbedding2D] = None,
259
+ qk_norm: bool = False,
260
+ qk_norm_type: str = "layernorm",
261
+ ):
262
+ super().__init__()
263
+ self.num_heads = num_heads
264
+ head_dim = dim // num_heads
265
+ self.scale = head_dim**-0.5
266
+ self.rope = rope
267
+ self.qk_norm = qk_norm
268
+
269
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
270
+ self.q_norm = _build_qk_norm(qk_norm_type, head_dim) if qk_norm else None
271
+ self.k_norm = _build_qk_norm(qk_norm_type, head_dim) if qk_norm else None
272
+ self.attn_drop = nn.Dropout(attn_drop)
273
+ self.proj = nn.Linear(dim, dim, bias=proj_bias)
274
+ self.proj_drop = nn.Dropout(proj_drop)
275
+
276
+ def forward(
277
+ self,
278
+ x: torch.Tensor,
279
+ pos_ids: torch.Tensor = None,
280
+ grid_size: Tuple[int, int] = None,
281
+ ) -> torch.Tensor:
282
+ B, N, C = x.shape
283
+ qkv = (
284
+ self.qkv(x)
285
+ .reshape(B, N, 3, self.num_heads, C // self.num_heads)
286
+ .permute(2, 0, 3, 1, 4)
287
+ )
288
+ q, k, v = qkv[0], qkv[1], qkv[2] # [B, num_heads, N, head_dim]
289
+
290
+ if self.rope is not None and pos_ids is not None and grid_size is not None:
291
+ q, k = self.rope(q, k, pos_ids, grid_size)
292
+
293
+ if self.q_norm is not None and self.k_norm is not None:
294
+ q = self.q_norm(q)
295
+ k = self.k_norm(k)
296
+
297
+ x = F.scaled_dot_product_attention(
298
+ q,
299
+ k,
300
+ v,
301
+ dropout_p=self.attn_drop.p if self.training else 0.0,
302
+ scale=self.scale,
303
+ )
304
+ x = x.transpose(1, 2).reshape(B, N, C)
305
+ x = self.proj(x)
306
+ x = self.proj_drop(x)
307
+ return x
spectrogram.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from typing import Optional
24
+
25
+ import torch
26
+ import torch.nn as nn
27
+ import torchaudio
28
+
29
+
30
+ class Spectrogram(nn.Module):
31
+ """
32
+ Mel-frequency audio representation with optional temporal derivatives.
33
+
34
+ Args:
35
+ sample_rate (int): Sample rate of the audio.
36
+ n_fft (int): Size of FFT.
37
+ win_length (Optional[int]): Window length. Defaults to n_fft.
38
+ win_length_ms (Optional[float]): Window length in milliseconds. Overrides win_length if provided.
39
+ hop_length (Optional[int]): Hop length. Defaults to win_length // 2.
40
+ hop_length_ms (Optional[float]): Hop length in milliseconds. Overrides hop_length if provided.
41
+ n_mels (int): Number of mel filterbanks.
42
+ f_min (float): Minimum frequency.
43
+ f_max (Optional[float]): Maximum frequency.
44
+ power (float): Power of the magnitude.
45
+ representation (str): ``log_mel`` or phase-aware ``complex_mel``.
46
+ complex_log_dynamic_range_db (float): Retained complex-mel log range.
47
+ add_delta (bool): Append one temporal derivative per base channel.
48
+ add_delta_delta (bool): Append a second derivative per base channel.
49
+ delta_win_length (int): Odd regression window used for derivatives.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ sample_rate: int = 32000,
55
+ n_fft: int = 4096,
56
+ win_length: Optional[int] = None,
57
+ win_length_ms: Optional[float] = None,
58
+ hop_length: Optional[int] = None,
59
+ hop_length_ms: Optional[float] = None,
60
+ n_mels: int = 128,
61
+ f_min: float = 0.0,
62
+ f_max: Optional[float] = None,
63
+ power: float = 2.0,
64
+ representation: str = "log_mel",
65
+ complex_log_dynamic_range_db: float = 80.0,
66
+ add_delta: bool = False,
67
+ add_delta_delta: bool = False,
68
+ delta_win_length: int = 5,
69
+ ):
70
+ super().__init__()
71
+
72
+ if win_length is None:
73
+ if win_length_ms is None:
74
+ win_length = n_fft
75
+ else:
76
+ win_length = int(sample_rate * win_length_ms / 1000)
77
+
78
+ if hop_length is None:
79
+ if hop_length_ms is None:
80
+ hop_length = win_length // 2
81
+ else:
82
+ hop_length = int(sample_rate * hop_length_ms / 1000)
83
+
84
+ representation = representation.strip().lower().replace("-", "_")
85
+ if representation not in {"log_mel", "complex_mel"}:
86
+ raise ValueError(
87
+ "representation must be 'log_mel' or 'complex_mel', "
88
+ f"got {representation!r}"
89
+ )
90
+ if complex_log_dynamic_range_db <= 0.0:
91
+ raise ValueError(
92
+ "complex_log_dynamic_range_db must be positive, "
93
+ f"got {complex_log_dynamic_range_db}"
94
+ )
95
+ if delta_win_length < 3 or delta_win_length % 2 == 0:
96
+ raise ValueError(
97
+ f"delta_win_length must be an odd integer >= 3, got {delta_win_length}"
98
+ )
99
+
100
+ self.representation = representation
101
+ self.complex_log_dynamic_range_db = float(complex_log_dynamic_range_db)
102
+ self.add_delta = bool(add_delta)
103
+ self.add_delta_delta = bool(add_delta_delta)
104
+ self.delta_win_length = int(delta_win_length)
105
+ self.base_output_channels = 1 if representation == "log_mel" else 2
106
+ derivative_orders = int(self.add_delta) + int(self.add_delta_delta)
107
+ self.output_channels = self.base_output_channels * (1 + derivative_orders)
108
+
109
+ if representation == "log_mel":
110
+ self.mel_spec = torchaudio.transforms.MelSpectrogram(
111
+ sample_rate=sample_rate,
112
+ n_fft=n_fft,
113
+ win_length=win_length,
114
+ hop_length=hop_length,
115
+ n_mels=n_mels,
116
+ f_min=f_min,
117
+ f_max=f_max,
118
+ power=power,
119
+ normalized=True,
120
+ )
121
+ self.amplitude_to_db = torchaudio.transforms.AmplitudeToDB()
122
+ else:
123
+ # Keep the historical ``mel_spec`` attribute for sample-rate and
124
+ # hop-length discovery in callbacks and HEAR adapters.
125
+ self.mel_spec = torchaudio.transforms.Spectrogram(
126
+ n_fft=n_fft,
127
+ win_length=win_length,
128
+ hop_length=hop_length,
129
+ power=None,
130
+ normalized=True,
131
+ )
132
+ self.mel_spec.sample_rate = sample_rate
133
+ self.amplitude_to_db = None
134
+ mel_fb = torchaudio.functional.melscale_fbanks(
135
+ n_freqs=n_fft // 2 + 1,
136
+ f_min=f_min,
137
+ f_max=float(sample_rate / 2 if f_max is None else f_max),
138
+ n_mels=n_mels,
139
+ sample_rate=sample_rate,
140
+ norm=None,
141
+ mel_scale="htk",
142
+ )
143
+ self.register_buffer("mel_fb", mel_fb)
144
+
145
+ def _complex_mel(self, x: torch.Tensor) -> torch.Tensor:
146
+ if x.shape[1] != 1:
147
+ raise ValueError(
148
+ "complex_mel expects mono waveform input [B, 1, T], "
149
+ f"got {tuple(x.shape)}"
150
+ )
151
+
152
+ complex_spec = self.mel_spec(x[:, 0])
153
+ mel_fb = self.mel_fb.to(dtype=complex_spec.dtype)
154
+ complex_mel = torch.matmul(
155
+ complex_spec.transpose(-1, -2),
156
+ mel_fb,
157
+ ).transpose(-1, -2)
158
+
159
+ magnitude = complex_mel.abs()
160
+ eps = torch.finfo(magnitude.dtype).eps
161
+ reference = magnitude.amax(dim=(-2, -1), keepdim=True)
162
+ relative_magnitude = magnitude / reference.clamp_min(eps)
163
+ floor_ratio = 10.0 ** (-self.complex_log_dynamic_range_db / 20.0)
164
+ log_magnitude_db = 20.0 * torch.log10(relative_magnitude.clamp_min(floor_ratio))
165
+ log_radius = (
166
+ log_magnitude_db + self.complex_log_dynamic_range_db
167
+ ) / self.complex_log_dynamic_range_db
168
+ log_radius = log_radius.clamp_(0.0, 1.0)
169
+ log_radius = torch.where(
170
+ reference > eps, log_radius, torch.zeros_like(log_radius)
171
+ )
172
+
173
+ unit_phase = complex_mel / magnitude.clamp_min(eps)
174
+ compressed = unit_phase * log_radius
175
+ return torch.stack([compressed.real, compressed.imag], dim=1)
176
+
177
+ def _append_deltas(self, spec: torch.Tensor) -> torch.Tensor:
178
+ if not self.add_delta and not self.add_delta_delta:
179
+ return spec
180
+
181
+ delta = torchaudio.functional.compute_deltas(
182
+ spec,
183
+ win_length=self.delta_win_length,
184
+ mode="replicate",
185
+ )
186
+ channels = [spec]
187
+ if self.add_delta:
188
+ channels.append(delta)
189
+ if self.add_delta_delta:
190
+ channels.append(
191
+ torchaudio.functional.compute_deltas(
192
+ delta,
193
+ win_length=self.delta_win_length,
194
+ mode="replicate",
195
+ )
196
+ )
197
+ return torch.cat(channels, dim=1)
198
+
199
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
200
+ """
201
+ Forward pass.
202
+
203
+ Args:
204
+ x (torch.Tensor): Input waveform [B, C, T] or [B, T].
205
+
206
+ Returns:
207
+ torch.Tensor: Mel-frequency features [B, C, F, T].
208
+ """
209
+ if x.ndim == 2:
210
+ x = x.unsqueeze(1)
211
+ if x.ndim != 3:
212
+ raise ValueError(f"Expected waveform [B, C, T], got {tuple(x.shape)}")
213
+
214
+ if self.representation == "complex_mel":
215
+ spec = self._complex_mel(x)
216
+ else:
217
+ spec = self.mel_spec(x)
218
+ spec = self.amplitude_to_db(spec)
219
+
220
+ return self._append_deltas(spec)
transformer.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from __future__ import annotations
24
+
25
+ from collections.abc import Sequence
26
+ from typing import Callable
27
+
28
+ import torch
29
+ import torch.nn as nn
30
+ from timm.layers import DropPath
31
+ from timm.layers import Mlp
32
+
33
+ from .normalization import MixedPrecisionRMSNorm
34
+ from .rope import RoPEAttention
35
+ from .rope import RotaryEmbedding1D
36
+ from .rope import RotaryEmbedding2D
37
+
38
+
39
+ class FullAttentionResidual(nn.Module):
40
+ """Softmax attention over all preceding outputs along model depth."""
41
+
42
+ def __init__(self, dim: int) -> None:
43
+ super().__init__()
44
+ self.dim = dim
45
+ self.norm = MixedPrecisionRMSNorm(dim)
46
+ self.query = nn.Parameter(torch.zeros(dim))
47
+
48
+ def forward(self, values: Sequence[torch.Tensor]) -> torch.Tensor:
49
+ if not values:
50
+ raise ValueError("FullAttentionResidual requires at least one value")
51
+
52
+ expected_shape = values[0].shape
53
+ if len(expected_shape) != 3 or expected_shape[-1] != self.dim:
54
+ raise ValueError(
55
+ "FullAttentionResidual values must have shape [B, N, D] with "
56
+ f"D={self.dim}, got {expected_shape}"
57
+ )
58
+ for index, value in enumerate(values[1:], start=1):
59
+ if value.shape != expected_shape:
60
+ raise ValueError(
61
+ "FullAttentionResidual values must have identical shapes; "
62
+ f"value 0 has {expected_shape}, value {index} has {value.shape}"
63
+ )
64
+
65
+ stacked_values = torch.stack(tuple(values), dim=0)
66
+ keys = self.norm(stacked_values)
67
+ logits = torch.einsum("d,l b n d->l b n", self.query, keys)
68
+ weights = logits.softmax(dim=0)
69
+ return torch.einsum("l b n,l b n d->b n d", weights, stacked_values)
70
+
71
+
72
+ def build_norm_layer(
73
+ *,
74
+ dim: int,
75
+ norm_type: str = "layernorm",
76
+ norm_layer: Callable[[int], nn.Module] | None = None,
77
+ norm_eps: float | None = None,
78
+ ) -> nn.Module:
79
+ """Build a token-channel normalization layer.
80
+
81
+ `norm_layer` is kept for backward compatibility with direct Python
82
+ construction. Configs should prefer `norm_type` so choices are explicit in
83
+ Hydra overrides and experiment files.
84
+ """
85
+ if norm_layer is not None:
86
+ if norm_eps is not None:
87
+ raise ValueError("Set norm_eps or a custom norm_layer, not both")
88
+ return norm_layer(dim)
89
+
90
+ if norm_eps is not None and norm_eps <= 0:
91
+ raise ValueError(f"norm_eps must be positive, got {norm_eps}")
92
+ kwargs = {} if norm_eps is None else {"eps": norm_eps}
93
+ normalized = norm_type.strip().lower().replace("_", "")
94
+ if normalized == "layernorm":
95
+ return nn.LayerNorm(dim, **kwargs)
96
+ if normalized == "rmsnorm":
97
+ return MixedPrecisionRMSNorm(dim, **kwargs)
98
+
99
+ raise ValueError(
100
+ f"Unknown norm_type={norm_type!r}; expected 'layernorm' or 'rmsnorm'"
101
+ )
102
+
103
+
104
+ def build_gelu_mlp(
105
+ *,
106
+ dim: int,
107
+ mlp_ratio: float,
108
+ act_layer: type[nn.Module],
109
+ drop: float,
110
+ bias: bool = True,
111
+ ) -> nn.Module:
112
+ """Build the baseline ViT feed-forward layer used before ablations."""
113
+ return Mlp(
114
+ in_features=dim,
115
+ hidden_features=int(dim * mlp_ratio),
116
+ act_layer=act_layer,
117
+ bias=bias,
118
+ drop=drop,
119
+ )
120
+
121
+
122
+ class SwiGLUMlp(nn.Module):
123
+ """SwiGLU feed-forward layer for transformer ablations.
124
+
125
+ The hidden dimension is controlled by `mlp_ratio` in the same place as the
126
+ baseline MLP. GLU variants normally use a smaller ratio such as 8/3 because
127
+ they have two input projections before the output projection.
128
+ """
129
+
130
+ def __init__(
131
+ self,
132
+ *,
133
+ dim: int,
134
+ hidden_features: int,
135
+ drop: float,
136
+ bias: bool = True,
137
+ ) -> None:
138
+ super().__init__()
139
+ self.gate = nn.Linear(dim, hidden_features, bias=bias)
140
+ self.value = nn.Linear(dim, hidden_features, bias=bias)
141
+ self.proj = nn.Linear(hidden_features, dim, bias=bias)
142
+ self.drop = nn.Dropout(drop)
143
+
144
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
145
+ x = nn.functional.silu(self.gate(x)) * self.value(x)
146
+ x = self.drop(x)
147
+ x = self.proj(x)
148
+ return self.drop(x)
149
+
150
+
151
+ def build_mlp(
152
+ *,
153
+ dim: int,
154
+ mlp_ratio: float,
155
+ mlp_type: str = "gelu_mlp",
156
+ act_layer: type[nn.Module],
157
+ drop: float,
158
+ bias: bool = True,
159
+ ) -> nn.Module:
160
+ """Build the transformer feed-forward layer selected by config."""
161
+ hidden_features = int(dim * mlp_ratio)
162
+ normalized = mlp_type.strip().lower().replace("-", "_")
163
+
164
+ if normalized == "gelu_mlp":
165
+ return build_gelu_mlp(
166
+ dim=dim,
167
+ mlp_ratio=mlp_ratio,
168
+ act_layer=act_layer,
169
+ drop=drop,
170
+ bias=bias,
171
+ )
172
+ if normalized == "swiglu":
173
+ return SwiGLUMlp(
174
+ dim=dim,
175
+ hidden_features=hidden_features,
176
+ drop=drop,
177
+ bias=bias,
178
+ )
179
+
180
+ raise ValueError(f"Unknown mlp_type={mlp_type!r}; expected 'gelu_mlp' or 'swiglu'")
181
+
182
+
183
+ class RoPEBlock(nn.Module):
184
+ """Pre-norm transformer block with RoPE-aware attention."""
185
+
186
+ def __init__(
187
+ self,
188
+ dim: int,
189
+ num_heads: int,
190
+ mlp_ratio: float = 4.0,
191
+ qkv_bias: bool = False,
192
+ proj_bias: bool = True,
193
+ mlp_bias: bool = True,
194
+ qk_norm: bool = False,
195
+ qk_norm_type: str = "layernorm",
196
+ mlp_type: str = "gelu_mlp",
197
+ proj_drop: float = 0.0,
198
+ attn_drop: float = 0.0,
199
+ drop_path: float = 0.0,
200
+ act_layer: type[nn.Module] = nn.GELU,
201
+ norm_type: str = "layernorm",
202
+ norm_layer: Callable[[int], nn.Module] | None = None,
203
+ rope: RotaryEmbedding1D | RotaryEmbedding2D | None = None,
204
+ residual_type: str = "standard",
205
+ norm_eps: float | None = None,
206
+ ) -> None:
207
+ super().__init__()
208
+ self.residual_type = residual_type.strip().lower().replace("-", "_")
209
+ if self.residual_type not in {"standard", "full_attnres"}:
210
+ raise ValueError(
211
+ f"Unknown residual_type={residual_type!r}; expected 'standard' "
212
+ "or 'full_attnres'"
213
+ )
214
+ self.norm1 = build_norm_layer(
215
+ dim=dim,
216
+ norm_type=norm_type,
217
+ norm_layer=norm_layer,
218
+ norm_eps=norm_eps,
219
+ )
220
+ self.attn = RoPEAttention(
221
+ dim,
222
+ num_heads=num_heads,
223
+ qkv_bias=qkv_bias,
224
+ proj_bias=proj_bias,
225
+ attn_drop=attn_drop,
226
+ proj_drop=proj_drop,
227
+ rope=rope,
228
+ qk_norm=qk_norm,
229
+ qk_norm_type=qk_norm_type,
230
+ )
231
+ self.norm2 = build_norm_layer(
232
+ dim=dim,
233
+ norm_type=norm_type,
234
+ norm_layer=norm_layer,
235
+ norm_eps=norm_eps,
236
+ )
237
+ self.mlp = build_mlp(
238
+ dim=dim,
239
+ mlp_ratio=mlp_ratio,
240
+ mlp_type=mlp_type,
241
+ act_layer=act_layer,
242
+ drop=proj_drop,
243
+ bias=mlp_bias,
244
+ )
245
+ self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
246
+ if self.residual_type == "full_attnres":
247
+ self.attention_residual = FullAttentionResidual(dim)
248
+ self.mlp_residual = FullAttentionResidual(dim)
249
+ else:
250
+ self.attention_residual = None
251
+ self.mlp_residual = None
252
+
253
+ def attention_output(
254
+ self,
255
+ x: torch.Tensor,
256
+ pos_ids: torch.Tensor | None = None,
257
+ grid_size: tuple[int, int] | None = None,
258
+ ) -> torch.Tensor:
259
+ return self.drop_path(
260
+ self.attn(self.norm1(x), pos_ids=pos_ids, grid_size=grid_size)
261
+ )
262
+
263
+ def mlp_output(self, x: torch.Tensor) -> torch.Tensor:
264
+ return self.drop_path(self.mlp(self.norm2(x)))
265
+
266
+ def forward(
267
+ self,
268
+ x: torch.Tensor,
269
+ pos_ids: torch.Tensor | None = None,
270
+ grid_size: tuple[int, int] | None = None,
271
+ ) -> torch.Tensor:
272
+ x = x + self.attention_output(x, pos_ids=pos_ids, grid_size=grid_size)
273
+ x = x + self.mlp_output(x)
274
+ return x
vit.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from collections.abc import Mapping
24
+ from typing import Any, Optional, Tuple
25
+
26
+ import torch
27
+ import torch.nn as nn
28
+ from timm.layers import build_sincos2d_pos_embed
29
+
30
+ from .rope import RotaryEmbedding1D
31
+ from .rope import RotaryEmbedding2D
32
+ from .transformer import build_norm_layer
33
+ from .transformer import FullAttentionResidual
34
+ from .transformer import RoPEBlock
35
+
36
+
37
+ def vit_config_with_patch_geometry(
38
+ config: Mapping[str, Any],
39
+ *,
40
+ img_size: tuple[int, int],
41
+ patch_size: tuple[int, int],
42
+ ) -> dict[str, Any]:
43
+ """Return a ViT config aligned with the patch embedding's actual geometry."""
44
+ resolved = dict(config)
45
+ resolved["img_size"] = tuple(img_size)
46
+ resolved["patch_size"] = tuple(patch_size)
47
+ resolved.setdefault("rope_mode", "auto")
48
+ return resolved
49
+
50
+
51
+ class ViT(nn.Module):
52
+ """
53
+ Vision Transformer with support for RoPE and 2D positional embeddings.
54
+
55
+ Args:
56
+ embed_dim (int): Embedding dimension.
57
+ depth (int): Number of transformer blocks.
58
+ num_heads (int): Number of attention heads.
59
+ mlp_ratio (float): Ratio of MLP hidden dim to embedding dim.
60
+ qkv_bias (bool): Enable bias for QKV projections.
61
+ drop_rate (float): Dropout rate.
62
+ attn_drop_rate (float): Attention dropout rate.
63
+ drop_path_rate (float): Stochastic depth rate.
64
+ norm_layer (nn.Module): Normalization layer.
65
+ norm_eps (float | None): Explicit normalization epsilon; None preserves
66
+ the normalization implementation's default.
67
+ act_layer (nn.Module): Activation layer.
68
+ num_patches (int): Total number of patches (used for learnable/sincos pos embed).
69
+ img_size (tuple[int, int]): Input image size (H, W).
70
+ patch_size (tuple[int, int]): Patch size (H, W).
71
+ pos_embed_type (str): Type of positional embedding ("rope", "sincos", "learnable").
72
+ rope_mode (str | None): RoPE axes ("2d" or temporal "1d"). ``None``
73
+ or ``"auto"`` selects 1D when the patch height equals the image height,
74
+ otherwise 2D.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ embed_dim: int = 768,
80
+ depth: int = 12,
81
+ num_heads: int = 12,
82
+ mlp_ratio: float = 4.0,
83
+ mlp_type: str = "gelu_mlp",
84
+ qkv_bias: bool = True,
85
+ proj_bias: bool = True,
86
+ mlp_bias: bool = True,
87
+ qk_norm: bool = False,
88
+ qk_norm_type: str = "layernorm",
89
+ drop_rate: float = 0.0,
90
+ attn_drop_rate: float = 0.0,
91
+ drop_path_rate: float = 0.0,
92
+ norm_layer: nn.Module | None = None,
93
+ norm_type: str = "layernorm",
94
+ act_layer: nn.Module = nn.GELU,
95
+ num_patches: int = 128,
96
+ img_size: tuple[int, int] = (128, 256),
97
+ patch_size: tuple[int, int] = (16, 16),
98
+ pos_embed_type: str = "rope",
99
+ rope_mode: str | None = "2d",
100
+ residual_type: str = "standard",
101
+ norm_eps: float | None = None,
102
+ ):
103
+ super().__init__()
104
+ self.embed_dim = embed_dim
105
+ self.num_patches = num_patches
106
+ self.grid_size = (img_size[0] // patch_size[0], img_size[1] // patch_size[1])
107
+ self.pos_embed_type = pos_embed_type
108
+ requested_rope_mode = (
109
+ "auto" if rope_mode is None else rope_mode.strip().lower().replace("-", "_")
110
+ )
111
+ if requested_rope_mode == "auto":
112
+ self.rope_mode = "1d" if patch_size[0] == img_size[0] else "2d"
113
+ else:
114
+ self.rope_mode = requested_rope_mode
115
+ self.norm_type = norm_type
116
+ self.mlp_type = mlp_type
117
+ self.residual_type = residual_type.strip().lower().replace("-", "_")
118
+ if self.residual_type not in {"standard", "full_attnres"}:
119
+ raise ValueError(
120
+ f"Unknown residual_type={residual_type!r}; expected 'standard' "
121
+ "or 'full_attnres'"
122
+ )
123
+
124
+ # Positional Embeddings
125
+ if pos_embed_type == "rope":
126
+ head_dim = embed_dim // num_heads
127
+ if self.rope_mode == "2d":
128
+ self.rope = RotaryEmbedding2D(dim=head_dim, max_res=self.grid_size)
129
+ elif self.rope_mode == "1d":
130
+ self.rope = RotaryEmbedding1D(
131
+ dim=head_dim,
132
+ max_seq_len=self.grid_size[1],
133
+ )
134
+ else:
135
+ raise ValueError(
136
+ f"Unknown rope_mode: {rope_mode!r}; expected 'auto', '1d', or '2d'"
137
+ )
138
+ self.pos_embed = None
139
+ elif pos_embed_type == "sincos":
140
+ self.rope = None
141
+ # build_sincos2d_pos_embed(feat_shape, dim, ...)
142
+ # We assume grid_size matches num_patches
143
+ pos_embed = build_sincos2d_pos_embed(self.grid_size, embed_dim)
144
+ self.register_buffer("pos_embed", pos_embed.unsqueeze(0)) # [1, N, D]
145
+ elif pos_embed_type == "learnable":
146
+ self.rope = None
147
+ self.pos_embed = nn.Parameter(torch.zeros(1, num_patches, embed_dim))
148
+ nn.init.trunc_normal_(self.pos_embed, std=0.02)
149
+ else:
150
+ raise ValueError(f"Unknown pos_embed_type: {pos_embed_type}")
151
+
152
+ # Stochastic Depth
153
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
154
+
155
+ self.blocks = nn.ModuleList(
156
+ [
157
+ RoPEBlock(
158
+ dim=embed_dim,
159
+ num_heads=num_heads,
160
+ mlp_ratio=mlp_ratio,
161
+ mlp_type=mlp_type,
162
+ qkv_bias=qkv_bias,
163
+ proj_bias=proj_bias,
164
+ mlp_bias=mlp_bias,
165
+ qk_norm=qk_norm,
166
+ qk_norm_type=qk_norm_type,
167
+ proj_drop=drop_rate,
168
+ attn_drop=attn_drop_rate,
169
+ drop_path=dpr[i],
170
+ norm_type=norm_type,
171
+ norm_layer=norm_layer,
172
+ norm_eps=norm_eps,
173
+ act_layer=act_layer,
174
+ rope=self.rope,
175
+ residual_type=self.residual_type,
176
+ )
177
+ for i in range(depth)
178
+ ]
179
+ )
180
+
181
+ self.norm = build_norm_layer(
182
+ dim=embed_dim,
183
+ norm_type=norm_type,
184
+ norm_layer=norm_layer,
185
+ norm_eps=norm_eps,
186
+ )
187
+ self.output_residual = (
188
+ FullAttentionResidual(embed_dim)
189
+ if self.residual_type == "full_attnres"
190
+ else None
191
+ )
192
+
193
+ self.apply(self._init_weights)
194
+
195
+ def _init_weights(self, m: nn.Module) -> None:
196
+ if isinstance(m, nn.Linear):
197
+ nn.init.trunc_normal_(m.weight, std=0.02)
198
+ if m.bias is not None:
199
+ nn.init.constant_(m.bias, 0)
200
+ elif isinstance(m, nn.LayerNorm):
201
+ nn.init.constant_(m.bias, 0)
202
+ nn.init.constant_(m.weight, 1.0)
203
+ elif isinstance(m, nn.RMSNorm):
204
+ nn.init.constant_(m.weight, 1.0)
205
+
206
+ def forward(
207
+ self,
208
+ x: torch.Tensor,
209
+ pos_ids: Optional[torch.Tensor] = None,
210
+ add_pos_embed: bool = True,
211
+ grid_size: Optional[Tuple[int, int]] = None,
212
+ ) -> torch.Tensor:
213
+ """
214
+ Forward pass.
215
+
216
+ Args:
217
+ x (torch.Tensor): Input tensor [B, N, D].
218
+ pos_ids (Optional[torch.Tensor]): Positional indices [B, N] or [N].
219
+ add_pos_embed (bool): Whether to add positional embeddings (for non-RoPE).
220
+ grid_size (Optional[Tuple[int, int]]): Grid size for RoPE/PosEmbed.
221
+
222
+ Returns:
223
+ torch.Tensor: Output tensor [B, N, D].
224
+ """
225
+ # Determine grid size
226
+ if grid_size is None:
227
+ if pos_ids is None:
228
+ # Infer from x assuming full sequence
229
+ B, N, D = x.shape
230
+ H_grid = self.grid_size[0]
231
+ W_grid = N // H_grid
232
+ current_grid_size = (H_grid, W_grid)
233
+ else:
234
+ # Cannot infer, use default (might be wrong if variable length)
235
+ current_grid_size = self.grid_size
236
+ else:
237
+ current_grid_size = grid_size
238
+
239
+ if self.pos_embed_type != "rope" and add_pos_embed:
240
+ if pos_ids is not None:
241
+ # Select positional embeddings
242
+ if pos_ids.ndim == 1:
243
+ # Shared pos_ids across batch
244
+ pos_embed = self.pos_embed[:, pos_ids, :] # [1, N_subset, D]
245
+ else:
246
+ # Different pos_ids per sample
247
+ pos_embed = self.pos_embed.expand(x.shape[0], -1, -1)
248
+ pos_embed = torch.gather(
249
+ pos_embed,
250
+ 1,
251
+ pos_ids.unsqueeze(-1).expand(-1, -1, self.embed_dim),
252
+ )
253
+ x = x + pos_embed
254
+ else:
255
+ # Assume full sequence
256
+ if x.shape[1] == self.num_patches:
257
+ x = x + self.pos_embed
258
+ elif (
259
+ self.pos_embed is not None and x.shape[1] <= self.pos_embed.shape[1]
260
+ ):
261
+ x = x + self.pos_embed[:, : x.shape[1], :]
262
+
263
+ # For RoPE, we need pos_ids. If not provided, generate them.
264
+ if self.pos_embed_type == "rope" and pos_ids is None:
265
+ device = x.device
266
+ # We need to generate pos_ids for the current grid
267
+ # If we inferred current_grid_size, we should use it.
268
+ # pos_ids should be 0..N-1
269
+ B, N, D = x.shape
270
+ pos_ids = torch.arange(N, device=device)
271
+
272
+ if self.residual_type == "full_attnres":
273
+ values = [x]
274
+ for block in self.blocks:
275
+ if block.attention_residual is None or block.mlp_residual is None:
276
+ raise RuntimeError(
277
+ "Full AttnRes block is missing depth aggregators"
278
+ )
279
+ attention_input = block.attention_residual(values)
280
+ values.append(
281
+ block.attention_output(
282
+ attention_input,
283
+ pos_ids=pos_ids,
284
+ grid_size=current_grid_size,
285
+ )
286
+ )
287
+ mlp_input = block.mlp_residual(values)
288
+ values.append(block.mlp_output(mlp_input))
289
+
290
+ if self.output_residual is None:
291
+ raise RuntimeError("Full AttnRes ViT is missing its output aggregator")
292
+ x = self.output_residual(values)
293
+ else:
294
+ for block in self.blocks:
295
+ x = block(x, pos_ids=pos_ids, grid_size=current_grid_size)
296
+
297
+ x = self.norm(x)
298
+ return x
waveform_feature_encoder.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 audio-embeddings contributors
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Sequence
26
+
27
+ import torch
28
+ from einops import rearrange
29
+ from einops.layers.torch import Rearrange
30
+ from torch import nn
31
+
32
+
33
+ def _parse_conv_layers_spec(
34
+ conv_layers_spec: str | Sequence[Sequence[int]] | Sequence[tuple[int, int, int]],
35
+ ) -> list[tuple[int, int, int]]:
36
+ if isinstance(conv_layers_spec, str):
37
+ # Config-driven expression style used by wavjepa, e.g.
38
+ # "[(512, 10, 5)] + [(512, 3, 2)] * 4 + [(512, 2, 2)]"
39
+ parsed = eval(conv_layers_spec, {"__builtins__": {}}, {}) # noqa: S307
40
+ else:
41
+ parsed = conv_layers_spec
42
+
43
+ out: list[tuple[int, int, int]] = []
44
+ for layer in parsed:
45
+ if len(layer) != 3:
46
+ raise ValueError(f"Invalid conv layer spec {layer}, expected (dim, k, s)")
47
+ dim, kernel, stride = layer
48
+ out.append((int(dim), int(kernel), int(stride)))
49
+ if len(out) == 0:
50
+ raise ValueError("conv_layers_spec must contain at least one layer")
51
+ return out
52
+
53
+
54
+ class WaveformFeatureEncoder(nn.Module):
55
+ """
56
+ Convolutional waveform feature encoder that outputs a token sequence.
57
+
58
+ Input shape: [B, C, T]
59
+ Output shape: [B, N, F]
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ conv_layers_spec: str
65
+ | Sequence[Sequence[int]]
66
+ | Sequence[
67
+ tuple[int, int, int]
68
+ ] = "[(512, 10, 5)] + [(512, 3, 2)] * 4 + [(512, 2, 2)]",
69
+ in_channels: int = 1,
70
+ dropout: float = 0.0,
71
+ mode: str = "default",
72
+ conv_bias: bool = False,
73
+ depthwise: bool = False,
74
+ ) -> None:
75
+ super().__init__()
76
+ if mode not in {"default", "layer_norm"}:
77
+ raise ValueError(
78
+ f"Unknown mode='{mode}', expected 'default' or 'layer_norm'"
79
+ )
80
+ self.conv_layers_spec = _parse_conv_layers_spec(conv_layers_spec)
81
+ self.in_channels = in_channels
82
+ self.depthwise = depthwise
83
+
84
+ layers: list[nn.Module] = []
85
+ in_dim = in_channels
86
+ for idx, (out_dim, kernel, stride) in enumerate(self.conv_layers_spec):
87
+ layers.append(
88
+ self._make_block(
89
+ in_dim=in_dim,
90
+ out_dim=out_dim,
91
+ kernel=kernel,
92
+ stride=stride,
93
+ dropout=dropout,
94
+ mode=mode,
95
+ conv_bias=conv_bias,
96
+ depthwise=depthwise,
97
+ is_first=idx == 0,
98
+ )
99
+ )
100
+ in_dim = out_dim
101
+
102
+ self.cnn = nn.Sequential(*layers)
103
+ self.embedding_dim = self.conv_layers_spec[-1][0]
104
+
105
+ @staticmethod
106
+ def _make_block(
107
+ in_dim: int,
108
+ out_dim: int,
109
+ kernel: int,
110
+ stride: int,
111
+ dropout: float,
112
+ mode: str,
113
+ conv_bias: bool,
114
+ depthwise: bool,
115
+ is_first: bool,
116
+ ) -> nn.Module:
117
+ if depthwise:
118
+ if out_dim % in_dim != 0:
119
+ raise ValueError(
120
+ "Depthwise mode requires out_dim to be a multiple of in_dim, "
121
+ f"got out_dim={out_dim}, in_dim={in_dim}"
122
+ )
123
+ conv = nn.Conv1d(
124
+ in_dim,
125
+ out_dim,
126
+ kernel_size=kernel,
127
+ stride=stride,
128
+ bias=conv_bias,
129
+ groups=in_dim,
130
+ )
131
+ else:
132
+ conv = nn.Conv1d(
133
+ in_dim,
134
+ out_dim,
135
+ kernel_size=kernel,
136
+ stride=stride,
137
+ bias=conv_bias,
138
+ )
139
+ nn.init.kaiming_normal_(conv.weight)
140
+
141
+ if mode == "layer_norm":
142
+ return nn.Sequential(
143
+ conv,
144
+ nn.Dropout(p=dropout),
145
+ Rearrange("... c t -> ... t c"),
146
+ nn.LayerNorm(out_dim, elementwise_affine=True),
147
+ Rearrange("... t c -> ... c t"),
148
+ nn.GELU(),
149
+ )
150
+
151
+ if mode == "default" and is_first:
152
+ return nn.Sequential(
153
+ conv,
154
+ nn.Dropout(p=dropout),
155
+ nn.GroupNorm(out_dim, out_dim, affine=True),
156
+ nn.GELU(),
157
+ )
158
+
159
+ return nn.Sequential(conv, nn.Dropout(p=dropout), nn.GELU())
160
+
161
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
162
+ x = self.cnn(x)
163
+ return rearrange(x, "b f n -> b n f")
164
+
165
+ def total_patches(self, time_samples: int) -> int:
166
+ n = int(time_samples)
167
+ for _, kernel, stride in self.conv_layers_spec:
168
+ if n < kernel:
169
+ return 0
170
+ n = (n - kernel) // stride + 1
171
+ return int(n)