Feature Extraction
Transformers
Safetensors
audio_embeddings
audio
custom_code
self-supervised-learning
audio-embeddings
best-rq-2
audioset
Instructions to use ltuncay/BEST-RQ-2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ltuncay/BEST-RQ-2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="ltuncay/BEST-RQ-2", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("ltuncay/BEST-RQ-2", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Add Transformers loading for the existing AECC 2026 encoder
Browse files- CODE_LICENSE +21 -0
- LICENSE +21 -0
- README.md +131 -17
- adapters.py +473 -0
- config.json +52 -0
- configuration_audio.py +114 -0
- export_manifest.json +60 -0
- extraction.py +157 -0
- feature_extraction_audio.py +114 -0
- model.safetensors +3 -0
- modeling_audio.py +182 -0
- normalization.py +42 -0
- patch_embed.py +246 -0
- preprocessor_config.json +11 -0
- requirements.txt +5 -0
- rope.py +307 -0
- spectrogram.py +220 -0
- transformer.py +274 -0
- vit.py +298 -0
- waveform_feature_encoder.py +171 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
##
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
```bash
|
| 16 |
-
|
| 17 |
```
|
| 18 |
|
| 19 |
-
|
|
|
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
```
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
```
|
| 32 |
|
| 33 |
-
##
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
| 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)
|