"""Zero-shot semantic listening with CLAP. The DSP layer knows a band is 4 dB hot. It does not know the sound is a reese. CLAP scores the audio against a bank of sound-design descriptors, so the report can say "gritty distorted reese bass, over-compressed drums" instead of only quoting numbers. Loads in a background thread so the Space boots immediately, and fails soft: if the model never arrives, everything else still works. """ from __future__ import annotations import threading import numpy as np MODEL_ID = "laion/clap-htsat-unfused" CLAP_SR = 48_000 # Grouped so the report can show one line per axis rather than a flat top-k. BANK: dict[str, list[str]] = { "character": [ "a gritty distorted reese bass", "a clean deep sine sub bass", "a metallic screaming growl bass", "an aggressive detuned saw lead", "a warm analog pad", "a plucky short synth stab", "a bright supersaw chord stack", "a wobbling filtered bass", "a soft mellow electric piano", "an acoustic guitar", "a male vocal", "a female vocal", ], "drums": [ "a punchy tight kick drum", "a boomy undamped kick drum", "a sharp cracking snare", "a boxy resonant snare", "crisp hi hats", "a heavily compressed drum break", "a loose live drum kit", ], "problem": [ "a muddy boomy cluttered mix", "a harsh sibilant painful mix", "an over-compressed lifeless mix", "a thin tinny weak mix", "a clipping distorted overloaded mix", "a clean balanced professional mix", "a hissy noisy recording", "a phasey hollow comb-filtered sound", ], "space": [ "a dry close-miked sound with no reverb", "a tight small room reverb", "a huge cavernous hall reverb", "a long washed-out ambient reverb tail", "a slapback delay", ], "energy": [ "a quiet sparse intro section", "a building tense riser", "a full loud drop section", "a calm breakdown section", ], } _FLAT: list[tuple[str, str]] = [(g, t) for g, items in BANK.items() for t in items] def _features(raw, projection): """Normalise CLAP's feature output across transformers versions. 4.x returns the projected tensor directly. 5.x returns a BaseModelOutputWithPooling, so the projection has to be applied here — which is exactly what 4.x did internally. """ if hasattr(raw, "shape"): return raw pooled = getattr(raw, "pooler_output", None) if pooled is None: pooled = raw.last_hidden_state[:, 0] return projection(pooled) class _Semantic: def __init__(self) -> None: self.ready = False self.error: str | None = None self._model = None self._processor = None self._text_emb = None self._lock = threading.Lock() def start(self) -> None: threading.Thread(target=self._load, daemon=True).start() def _load(self) -> None: try: import torch from transformers import ClapModel, ClapProcessor torch.set_num_threads(2) model = ClapModel.from_pretrained(MODEL_ID) model.eval() processor = ClapProcessor.from_pretrained(MODEL_ID) texts = [t for _, t in _FLAT] with torch.no_grad(): inputs = processor(text=texts, return_tensors="pt", padding=True) emb = _features(model.get_text_features(**inputs), model.text_projection) emb = emb / emb.norm(dim=-1, keepdim=True) self._model, self._processor, self._text_emb = model, processor, emb self.ready = True except Exception as exc: # noqa: BLE001 - fail soft, the app still works self.error = f"{type(exc).__name__}: {exc}" def status(self) -> str: if self.ready: return "ready" if self.error: return f"unavailable ({self.error})" return "warming up" def describe(self, mono48: np.ndarray, top_k: int = 2) -> dict[str, list[tuple[str, float]]]: """Score the clip against every descriptor, grouped by axis.""" if not self.ready or mono48.size < CLAP_SR // 2: return {} import torch # CLAP was trained on 10 s windows; take the loudest one. want = CLAP_SR * 10 if mono48.size > want: hop = CLAP_SR best_s, best_e = 0, -1.0 for s in range(0, mono48.size - want + 1, hop): e = float(np.mean(mono48[s : s + want] ** 2)) if e > best_e: best_e, best_s = e, s mono48 = mono48[best_s : best_s + want] clip = mono48.astype(np.float32) with self._lock, torch.no_grad(): # transformers 4.x takes `audios`, 5.x renamed it to `audio`. try: inputs = self._processor(audio=clip, sampling_rate=CLAP_SR, return_tensors="pt") except TypeError: inputs = self._processor(audios=clip, sampling_rate=CLAP_SR, return_tensors="pt") audio_emb = _features(self._model.get_audio_features(**inputs), self._model.audio_projection) audio_emb = audio_emb / audio_emb.norm(dim=-1, keepdim=True) sims = (audio_emb @ self._text_emb.T).squeeze(0).cpu().numpy() grouped: dict[str, list[tuple[str, float]]] = {} for group in BANK: idx = [i for i, (g, _) in enumerate(_FLAT) if g == group] local = sims[idx] # Softmax within the group — cross-group absolute scores are not # comparable, ranking inside a group is. e = np.exp((local - local.max()) * 20.0) probs = e / e.sum() order = np.argsort(-probs)[:top_k] grouped[group] = [(_FLAT[idx[o]][1], float(probs[o])) for o in order] return grouped SEMANTIC = _Semantic() def tags_line(grouped: dict[str, list[tuple[str, float]]], min_conf: float = 0.30) -> str: """Flatten the grouped scores into one readable sentence.""" picks = [items[0][0] for items in grouped.values() if items and items[0][1] >= min_conf] return ", ".join(picks) if picks else ""