gijs's picture
Initial release
dd45763 verified
Raw History Blame Contribute Delete
3.65 kB
"""Hierarchical audio tagger: frozen Dasheng-0.6B features + an MLP head over a 4,416-node sound taxonomy.
pip install torch dasheng soundfile scipy safetensors
python inference.py clip1.flac clip2.wav ...
Each clip is read from its first 10 s, resampled to 16 kHz mono and zero-padded to 10 s. Dasheng-0.6B frame
features are mean-pooled to 1280-d and fed to the head, which has three outputs: 11 top-level groups, 822 second-level
nodes and 3,718 classes (nodes at depth <= 3). Class scores use the hierarchical readout
logP_L1 + logP_L2 + logP_L3 (each class scored with its own top-level and second-level ancestor).
The Dasheng weights are downloaded by the `dasheng` package on first use.
"""
import json
import sys
from math import gcd
import numpy as np
import soundfile as sf
import torch
import torch.nn as nn
from dasheng import dasheng_06B
from safetensors import safe_open
from safetensors.torch import load_file
from scipy.signal import resample_poly
SR, SECS = 16000, 10
def load_audio(path):
"""First 10 s of `path` as a (160000,) float32 array at 16 kHz mono, zero-padded."""
with sf.SoundFile(path) as f:
sr = f.samplerate
a = f.read(SECS * sr, dtype="float32", always_2d=True).mean(1)
if sr != SR:
g = gcd(sr, SR)
a = resample_poly(a, SR // g, sr // g).astype(np.float32)
x = np.zeros(SECS * SR, np.float32)
x[: min(len(a), SECS * SR)] = a[: SECS * SR]
return x
class TaxonomyClassifier(nn.Module):
def __init__(self, head_path="head.safetensors"):
super().__init__()
with safe_open(head_path, "pt") as f:
meta = f.metadata()
cfg, self.classes = json.loads(meta["config"]), json.loads(meta["classes"])
l1 = [c.split("/")[0] for c in self.classes]
l2 = ["/".join(c.split("/")[:2]) for c in self.classes]
self.top_names, l2_names = sorted(set(l1)), sorted(set(l2))
self.register_buffer("t1", torch.tensor([self.top_names.index(x) for x in l1]), persistent=False)
self.register_buffer("t2", torch.tensor([l2_names.index(x) for x in l2]), persistent=False)
h = cfg["hidden"]
self.trunk = nn.Sequential(nn.Linear(cfg["in_dim"], h), nn.BatchNorm1d(h), nn.GELU(), nn.Dropout(cfg["dropout"]))
self.l3, self.h1, self.h2 = nn.Linear(h, len(self.classes)), nn.Linear(h, len(self.top_names)), nn.Linear(h, len(l2_names))
self.load_state_dict(load_file(head_path))
self.encoder = dasheng_06B()
self.eval()
@torch.inference_mode()
def forward(self, x):
"""x: (B, 160000) float32 at 16 kHz -> (top-level probs (B, 11), class probs (B, 3718))."""
xl = torch.full((x.shape[0],), x.shape[1], dtype=torch.long, device=x.device)
with torch.autocast(x.device.type, dtype=torch.bfloat16, enabled=x.device.type == "cuda"):
feats = self.encoder(x, xl) # (B, T, 1280)
z = self.trunk(feats.float().mean(1))
o3, o1, o2 = self.l3(z), self.h1(z), self.h2(z)
score = o1.log_softmax(1)[:, self.t1] + o2.log_softmax(1)[:, self.t2] + o3.log_softmax(1)
return o1.softmax(1), score.softmax(1)
if __name__ == "__main__":
dev = "cuda" if torch.cuda.is_available() else "cpu"
model = TaxonomyClassifier().to(dev)
paths = sys.argv[1:]
top, cls = model(torch.from_numpy(np.stack([load_audio(p) for p in paths])).to(dev))
for path, pt, pc in zip(paths, top.cpu(), cls.cpu()):
print(f"{path}\n top-level: {model.top_names[int(pt.argmax())]} ({float(pt.max()):.3f})")
for p, i in zip(*pc.topk(5)):
print(f" {float(p):.3f} {model.classes[int(i)]}")