Add config.json, README, and CPU inference example

#1
by mmaudet - opened
Files changed (3) hide show
  1. README.md +81 -3
  2. config.json +57 -0
  3. inference_example.py +230 -0
README.md CHANGED
@@ -1,3 +1,81 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - audio-jepa
5
+ - jepa
6
+ - audio-representation-learning
7
+ - self-supervised
8
+ paper:
9
+ title: "Audio-JEPA: Joint-Embedding Predictive Architecture for Audio Representation Learning"
10
+ authors:
11
+ - Ludovic Tuncay
12
+ - Etienne Labbé
13
+ - Emmanouil Benetos
14
+ - Thomas Pellegrini
15
+ venue: "ICME 2025"
16
+ arxiv: "2507.02915"
17
+ hal: "hal-05128180"
18
+ ---
19
+
20
+ # Audio-JEPA: pretrained encoder
21
+
22
+ Weights for the encoder trained in *Audio-JEPA: Joint-Embedding Predictive Architecture for Audio Representation Learning* (Tuncay, Labbé, Benetos, Pellegrini, ICME 2025). Source code: [`LudovicTuncay/Audio-JEPA`](https://github.com/LudovicTuncay/Audio-JEPA).
23
+
24
+ ## Files
25
+
26
+ | File | Purpose |
27
+ |------|---------|
28
+ | `JEPA.ckpt` | PyTorch Lightning checkpoint (state_dict + trainer metadata). |
29
+ | `config.json` | Standalone documentation of the architecture and audio pipeline. Machine-readable. |
30
+ | `README.md` | This document. |
31
+
32
+ ## Model summary
33
+
34
+ - Encoder: `VisionTransformer` (ViT-Base, 12 layers, 768-dim, 12 heads).
35
+ - Input: log-mel spectrogram of shape `(target_time_bins=256, n_mels=128)` from a 10 s mono waveform at 32 kHz.
36
+ - Patchification: `(16, 16)`, giving a grid `8 x 16 = 128` patches.
37
+ - Each patch spans ~625 ms of audio × 16 mel bins.
38
+ - Output: `(128, 768)` embeddings per 10 s clip.
39
+ - Effective temporal resolution: **1.6 positions/s** (the 12.8 tokens/s counts 8 temporal × 16 frequency patches per second).
40
+
41
+ ## Minimal inference (CPU, no `flash-attn` install required)
42
+
43
+ The training repo depends on `flash-attn`, which requires CUDA to build. For inference-only use, `flash_attn.modules.mha.MHA` can be substituted with a torch-native equivalent that matches the checkpoint's parameter names (`qkv`, `proj`). See `inference_example.py` in this repository for a ~150-line standalone script.
44
+
45
+ ```bash
46
+ # 1. Clone the source code (needed for the ViT class)
47
+ git clone --depth 1 https://github.com/LudovicTuncay/Audio-JEPA.git
48
+
49
+ # 2. Install a lean set of deps
50
+ pip install torch torchaudio numpy huggingface_hub
51
+
52
+ # 3. Run the example (downloads JEPA.ckpt on first run)
53
+ python inference_example.py --audio-jepa-src ./Audio-JEPA
54
+ ```
55
+
56
+ The script prints the loading diagnostics (should be `0 missing, 0 unexpected`) and the embedding shape.
57
+
58
+ ## Domain fit: where the model excels vs where it doesn't
59
+
60
+ Audio-JEPA is designed to learn a **generic audio representation**. The 16×16 patch shape is a compromise across speech, music, and environmental sounds. Consequences:
61
+
62
+ - **Strong on**: music, environmental sounds, audio captioning, general audio tagging.
63
+ - **Weaker on**: speech-only downstream tasks (see the paper's X-ARES tables). Speech-specific SSL models such as `wav2vec 2.0`, HuBERT and the Whisper encoder currently outperform Audio-JEPA on speech-centric benchmarks.
64
+ - **A follow-up from the same author**, [`BEST-RQ-2`](https://huggingface.co/ltuncay/BEST-RQ-2) (Tuncay, Labbé, Pellegrini, Interspeech 2026, [arXiv 2606.30700](https://arxiv.org/abs/2606.30700)), combines the encoder-predictor decomposition of Audio-JEPA with BEST-RQ discrete targets. It yields substantially better cross-domain results while sharing the exact same encoder inference speed.
65
+
66
+ ## Citation
67
+
68
+ ```bibtex
69
+ @inproceedings{tuncay2025audio,
70
+ title = {Audio-JEPA: Joint-Embedding Predictive Architecture for Audio Representation Learning},
71
+ author = {Tuncay, Ludovic and Labb{\'e}, Etienne and Benetos, Emmanouil and Pellegrini, Thomas},
72
+ booktitle = {ICME 2025},
73
+ address = {Nantes, France},
74
+ year = {2025},
75
+ url = {https://hal.science/hal-05128180}
76
+ }
77
+ ```
78
+
79
+ ## License
80
+
81
+ MIT.
config.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "audio-jepa",
3
+ "architecture": "VisionTransformer",
4
+ "citation": "Tuncay, Labbé, Benetos, Pellegrini. Audio-JEPA: Joint-Embedding Predictive Architecture for Audio Representation Learning. ICME 2025 (arXiv 2507.02915)",
5
+ "audio": {
6
+ "sample_rate": 32000,
7
+ "clip_length_s": 10,
8
+ "n_mels": 128,
9
+ "target_time_bins": 256,
10
+ "mel_spec_backend": "torchaudio.compliance.kaldi.fbank",
11
+ "f_min": 20,
12
+ "f_max": null,
13
+ "log_fbank": true,
14
+ "window": "hanning"
15
+ },
16
+ "patch_embed": {
17
+ "patch_size": [16, 16],
18
+ "in_chans": 1,
19
+ "num_patches": 128,
20
+ "grid_time": 8,
21
+ "grid_freq": 16
22
+ },
23
+ "encoder": {
24
+ "embed_dim": 768,
25
+ "depth": 12,
26
+ "num_heads": 12,
27
+ "mlp_ratio": 4.0,
28
+ "qkv_bias": true,
29
+ "use_flash_attn": true,
30
+ "norm_layer": "LayerNorm",
31
+ "cls_token": false,
32
+ "pos_embed": "2d_sincos"
33
+ },
34
+ "output": {
35
+ "shape_per_10s_clip": [128, 768],
36
+ "temporal_positions_per_second": 1.6,
37
+ "tokens_per_second": 12.8,
38
+ "note": "Each patch spans 16 mel bins x 16 time bins ≈ 625 ms. The 12.8 Hz rate counts 8 temporal x 16 frequency patches per second; effective temporal resolution is 1.6 positions/s."
39
+ },
40
+ "training": {
41
+ "framework": "PyTorch Lightning + Hydra",
42
+ "pretraining_corpus": "AudioSet unlabeled (~5.3k h after silence filtering)",
43
+ "target_module": "target_encoder (EMA of encoder)",
44
+ "predictor": {
45
+ "type": "VisionTransformerPredictor",
46
+ "depth": 6,
47
+ "num_heads": 12,
48
+ "mlp_ratio": 4.0
49
+ },
50
+ "loss": "norm_mse (with norm_pix_loss=True)",
51
+ "note": "Only the encoder is used downstream. Predictor + target encoder are discarded at inference."
52
+ },
53
+ "known_speech_gap": {
54
+ "description": "This model is designed for generic audio representation. The 16x16 patch shape reflects a compromise across speech, music, and environmental sounds. Speech-only downstream performance is lower than speech-specific SSL models (wav2vec2, HuBERT, Whisper). See the paper's X-ARES tables and the follow-up BEST-RQ-2 (arXiv 2606.30700) for a quantitative comparison.",
55
+ "suggested_alternative_for_speech": "For speech-heavy downstream tasks, consider BEST-RQ-2 (same author, arXiv 2606.30700, MIT weights at ltuncay/BEST-RQ-2) which shares the encoder architecture, or wait for the upcoming BEST-RQ-2.1 with explicit speech-oriented improvements."
56
+ }
57
+ }
inference_example.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal standalone inference for Audio-JEPA (ltuncay/Audio-JEPA).
2
+
3
+ Loads the JEPA.ckpt weights, instantiates the ViT encoder with the correct
4
+ input shape, and produces (T, D) embeddings from a mono waveform.
5
+
6
+ Runs on CPU without installing flash-attn CUDA kernels: a small shim
7
+ substitutes flash_attn.modules.mha.MHA with a torch-native equivalent that
8
+ matches the checkpoint parameter names (qkv, proj).
9
+
10
+ Requires the upstream code cloned locally:
11
+
12
+ git clone --depth 1 https://github.com/LudovicTuncay/Audio-JEPA.git /path/to/audio-jepa
13
+
14
+ Usage:
15
+
16
+ python inference_example.py --audio-jepa-src /path/to/audio-jepa
17
+
18
+ or, from a directory containing this file next to ``audio-jepa/``:
19
+
20
+ python inference_example.py
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import sys
27
+ import time
28
+ import types
29
+ from importlib.machinery import ModuleSpec
30
+ from pathlib import Path
31
+
32
+ import numpy as np
33
+ import torch
34
+ import torch.nn as nn
35
+ import torchaudio
36
+ from huggingface_hub import hf_hub_download
37
+
38
+ # Config values matching the shipped checkpoint (confirmed by the author).
39
+ SAMPLE_RATE = 32_000
40
+ CLIP_LENGTH_S = 10
41
+ N_MELS = 128
42
+ TARGET_TIME_BINS = 256
43
+ PATCH_SIZE = (16, 16)
44
+ EMBED_DIM = 768
45
+ DEPTH = 12
46
+ NUM_HEADS = 12
47
+ MLP_RATIO = 4.0
48
+
49
+
50
+ def install_flash_attn_shim() -> None:
51
+ """Replace ``flash_attn.modules.mha.MHA`` with a CPU-friendly torch class.
52
+
53
+ Matches the checkpoint's parameter naming (``qkv``, ``proj``) so
54
+ ``load_state_dict(..., strict=True)`` succeeds without any CUDA build.
55
+ """
56
+
57
+ class CpuMultiHeadAttention(nn.Module):
58
+ def __init__(
59
+ self,
60
+ embed_dim: int,
61
+ num_heads: int,
62
+ dropout: float = 0.0,
63
+ qkv_proj_bias: bool = True,
64
+ use_flash_attn: bool = False,
65
+ **_ignore,
66
+ ) -> None:
67
+ super().__init__()
68
+ assert embed_dim % num_heads == 0
69
+ self.embed_dim = embed_dim
70
+ self.num_heads = num_heads
71
+ self.head_dim = embed_dim // num_heads
72
+ self.dropout = dropout
73
+ self.qkv = nn.Linear(embed_dim, 3 * embed_dim, bias=qkv_proj_bias)
74
+ self.proj = nn.Linear(embed_dim, embed_dim, bias=True)
75
+
76
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
77
+ b, n, d = x.shape
78
+ qkv = self.qkv(x).reshape(b, n, 3, self.num_heads, self.head_dim)
79
+ q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0)
80
+ out = torch.nn.functional.scaled_dot_product_attention(
81
+ q, k, v, dropout_p=self.dropout if self.training else 0.0
82
+ )
83
+ return self.proj(out.transpose(1, 2).reshape(b, n, d))
84
+
85
+ def make(name: str) -> types.ModuleType:
86
+ m = types.ModuleType(name)
87
+ m.__spec__ = ModuleSpec(name, loader=None)
88
+ return m
89
+
90
+ flash_attn = make("flash_attn")
91
+ flash_attn.__version__ = "0.0.0-cpu-shim"
92
+ modules = make("flash_attn.modules")
93
+ mha = make("flash_attn.modules.mha")
94
+ mha.MHA = CpuMultiHeadAttention
95
+ sys.modules.update(
96
+ {
97
+ "flash_attn": flash_attn,
98
+ "flash_attn.modules": modules,
99
+ "flash_attn.modules.mha": mha,
100
+ }
101
+ )
102
+
103
+
104
+ def stub_upstream_inits(root: Path) -> None:
105
+ """Pre-empt heavy ``__init__.py`` files in the upstream that pull hydra/wandb.
106
+
107
+ Only the leaf model file and the mel-spec transform are needed for inference.
108
+ """
109
+ for cached in list(sys.modules):
110
+ if cached == "src" or cached.startswith("src."):
111
+ del sys.modules[cached]
112
+
113
+ for name in (
114
+ "src",
115
+ "src.utils",
116
+ "src.models",
117
+ "src.models.components",
118
+ "src.masks",
119
+ "src.masks.components",
120
+ "src.data",
121
+ "src.data.components",
122
+ ):
123
+ m = types.ModuleType(name)
124
+ m.__path__ = [str(root / name.replace(".", "/"))]
125
+ sys.modules[name] = m
126
+
127
+
128
+ def compute_mel_spec(waveform: torch.Tensor) -> torch.Tensor:
129
+ """Kaldi-fbank mel spectrogram of shape (1, TARGET_TIME_BINS, N_MELS)."""
130
+ hop_length_ms = (CLIP_LENGTH_S * 1000) / TARGET_TIME_BINS
131
+ frame_length_ms = 2.5 * hop_length_ms
132
+ spec = torchaudio.compliance.kaldi.fbank(
133
+ waveform - waveform.mean(),
134
+ sample_frequency=SAMPLE_RATE,
135
+ frame_length=frame_length_ms,
136
+ frame_shift=hop_length_ms,
137
+ num_mel_bins=N_MELS,
138
+ low_freq=20,
139
+ high_freq=SAMPLE_RATE // 2,
140
+ use_log_fbank=True,
141
+ window_type="hanning",
142
+ )
143
+ if spec.shape[0] < TARGET_TIME_BINS:
144
+ pad = TARGET_TIME_BINS - spec.shape[0]
145
+ spec = torch.cat([spec, torch.zeros(pad, N_MELS)], dim=0)
146
+ elif spec.shape[0] > TARGET_TIME_BINS:
147
+ spec = spec[:TARGET_TIME_BINS]
148
+ return spec.unsqueeze(0)
149
+
150
+
151
+ def main() -> int:
152
+ parser = argparse.ArgumentParser(description="Audio-JEPA CPU inference example.")
153
+ parser.add_argument(
154
+ "--audio-jepa-src",
155
+ default="./audio-jepa",
156
+ help="Path to the cloned LudovicTuncay/Audio-JEPA repo (default: ./audio-jepa).",
157
+ )
158
+ parser.add_argument(
159
+ "--wav",
160
+ default=None,
161
+ help="Optional WAV file to encode (mono, will be resampled to 32 kHz).",
162
+ )
163
+ args = parser.parse_args()
164
+
165
+ root = Path(args.audio_jepa_src).resolve()
166
+ if not (root / "src" / "models" / "components" / "vision_transformer.py").exists():
167
+ print(f"ERROR: Audio-JEPA source not found at {root}", file=sys.stderr)
168
+ print("Run: git clone --depth 1 https://github.com/LudovicTuncay/Audio-JEPA.git", file=sys.stderr)
169
+ return 2
170
+
171
+ install_flash_attn_shim()
172
+ if str(root) not in sys.path:
173
+ sys.path.insert(0, str(root))
174
+ stub_upstream_inits(root)
175
+
176
+ from src.models.components.vision_transformer import VisionTransformer
177
+
178
+ print("Building encoder (input_size=(256, 128), patch=(16, 16))...")
179
+ encoder = VisionTransformer(
180
+ input_size=(TARGET_TIME_BINS, N_MELS),
181
+ patch_size=PATCH_SIZE,
182
+ in_chans=1,
183
+ embed_dim=EMBED_DIM,
184
+ depth=DEPTH,
185
+ num_heads=NUM_HEADS,
186
+ mlp_ratio=MLP_RATIO,
187
+ use_flash_attn=False,
188
+ )
189
+
190
+ print("Downloading checkpoint (JEPA.ckpt, ~350 MB, first run only)...")
191
+ ckpt_path = hf_hub_download("ltuncay/Audio-JEPA", "JEPA.ckpt")
192
+ state = torch.load(ckpt_path, map_location="cpu", weights_only=False)
193
+ raw_sd = state.get("state_dict", state)
194
+ encoder_sd = {
195
+ k[len("encoder.") :]: v
196
+ for k, v in raw_sd.items()
197
+ if k.startswith("encoder.") and not k.startswith("encoder_")
198
+ }
199
+ missing, unexpected = encoder.load_state_dict(encoder_sd, strict=True)
200
+ print(f"Loaded: {len(encoder_sd)} keys, {len(missing)} missing, {len(unexpected)} unexpected.")
201
+ encoder.eval()
202
+
203
+ if args.wav:
204
+ waveform, sr = torchaudio.load(args.wav)
205
+ if waveform.shape[0] > 1:
206
+ waveform = waveform.mean(dim=0, keepdim=True)
207
+ if sr != SAMPLE_RATE:
208
+ waveform = torchaudio.functional.resample(waveform, sr, SAMPLE_RATE)
209
+ else:
210
+ n = SAMPLE_RATE * 5
211
+ t = torch.arange(n).float() / SAMPLE_RATE
212
+ waveform = (0.3 * torch.sin(2 * torch.pi * 440 * t)).unsqueeze(0)
213
+ print(f"No --wav given, using a 5 s synthetic tone at {SAMPLE_RATE} Hz.")
214
+
215
+ duration_s = waveform.shape[1] / SAMPLE_RATE
216
+ print(f"Waveform: {waveform.shape[1]} samples ({duration_s:.2f} s)")
217
+
218
+ spec = compute_mel_spec(waveform).unsqueeze(0)
219
+ t0 = time.perf_counter()
220
+ with torch.inference_mode():
221
+ emb = encoder(spec)
222
+ wall = time.perf_counter() - t0
223
+
224
+ print(f"Embeddings: shape={tuple(emb.shape)} wall={wall*1000:.1f} ms RTF={wall/duration_s:.3f}")
225
+ print("Note: the encoder always produces 128 patches (8 temporal x 16 frequency).")
226
+ return 0
227
+
228
+
229
+ if __name__ == "__main__":
230
+ sys.exit(main())