Add config.json, README, and CPU inference example
#1
by mmaudet - opened
- README.md +81 -3
- config.json +57 -0
- 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())
|