sevahu97 commited on
Commit
1b0b1dc
·
verified ·
1 Parent(s): 69e8672

Upload semantic.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. semantic.py +180 -0
semantic.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Zero-shot semantic listening with CLAP.
2
+
3
+ The DSP layer knows a band is 4 dB hot. It does not know the sound is a
4
+ reese. CLAP scores the audio against a bank of sound-design descriptors, so
5
+ the report can say "gritty distorted reese bass, over-compressed drums"
6
+ instead of only quoting numbers.
7
+
8
+ Loads in a background thread so the Space boots immediately, and fails soft:
9
+ if the model never arrives, everything else still works.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import threading
15
+
16
+ import numpy as np
17
+
18
+ MODEL_ID = "laion/clap-htsat-unfused"
19
+ CLAP_SR = 48_000
20
+
21
+ # Grouped so the report can show one line per axis rather than a flat top-k.
22
+ BANK: dict[str, list[str]] = {
23
+ "character": [
24
+ "a gritty distorted reese bass",
25
+ "a clean deep sine sub bass",
26
+ "a metallic screaming growl bass",
27
+ "an aggressive detuned saw lead",
28
+ "a warm analog pad",
29
+ "a plucky short synth stab",
30
+ "a bright supersaw chord stack",
31
+ "a wobbling filtered bass",
32
+ "a soft mellow electric piano",
33
+ "an acoustic guitar",
34
+ "a male vocal",
35
+ "a female vocal",
36
+ ],
37
+ "drums": [
38
+ "a punchy tight kick drum",
39
+ "a boomy undamped kick drum",
40
+ "a sharp cracking snare",
41
+ "a boxy resonant snare",
42
+ "crisp hi hats",
43
+ "a heavily compressed drum break",
44
+ "a loose live drum kit",
45
+ ],
46
+ "problem": [
47
+ "a muddy boomy cluttered mix",
48
+ "a harsh sibilant painful mix",
49
+ "an over-compressed lifeless mix",
50
+ "a thin tinny weak mix",
51
+ "a clipping distorted overloaded mix",
52
+ "a clean balanced professional mix",
53
+ "a hissy noisy recording",
54
+ "a phasey hollow comb-filtered sound",
55
+ ],
56
+ "space": [
57
+ "a dry close-miked sound with no reverb",
58
+ "a tight small room reverb",
59
+ "a huge cavernous hall reverb",
60
+ "a long washed-out ambient reverb tail",
61
+ "a slapback delay",
62
+ ],
63
+ "energy": [
64
+ "a quiet sparse intro section",
65
+ "a building tense riser",
66
+ "a full loud drop section",
67
+ "a calm breakdown section",
68
+ ],
69
+ }
70
+
71
+ _FLAT: list[tuple[str, str]] = [(g, t) for g, items in BANK.items() for t in items]
72
+
73
+
74
+ def _features(raw, projection):
75
+ """Normalise CLAP's feature output across transformers versions.
76
+
77
+ 4.x returns the projected tensor directly. 5.x returns a
78
+ BaseModelOutputWithPooling, so the projection has to be applied here —
79
+ which is exactly what 4.x did internally.
80
+ """
81
+ if hasattr(raw, "shape"):
82
+ return raw
83
+ pooled = getattr(raw, "pooler_output", None)
84
+ if pooled is None:
85
+ pooled = raw.last_hidden_state[:, 0]
86
+ return projection(pooled)
87
+
88
+
89
+ class _Semantic:
90
+ def __init__(self) -> None:
91
+ self.ready = False
92
+ self.error: str | None = None
93
+ self._model = None
94
+ self._processor = None
95
+ self._text_emb = None
96
+ self._lock = threading.Lock()
97
+
98
+ def start(self) -> None:
99
+ threading.Thread(target=self._load, daemon=True).start()
100
+
101
+ def _load(self) -> None:
102
+ try:
103
+ import torch
104
+ from transformers import ClapModel, ClapProcessor
105
+
106
+ torch.set_num_threads(2)
107
+ model = ClapModel.from_pretrained(MODEL_ID)
108
+ model.eval()
109
+ processor = ClapProcessor.from_pretrained(MODEL_ID)
110
+
111
+ texts = [t for _, t in _FLAT]
112
+ with torch.no_grad():
113
+ inputs = processor(text=texts, return_tensors="pt", padding=True)
114
+ emb = _features(model.get_text_features(**inputs), model.text_projection)
115
+ emb = emb / emb.norm(dim=-1, keepdim=True)
116
+
117
+ self._model, self._processor, self._text_emb = model, processor, emb
118
+ self.ready = True
119
+ except Exception as exc: # noqa: BLE001 - fail soft, the app still works
120
+ self.error = f"{type(exc).__name__}: {exc}"
121
+
122
+ def status(self) -> str:
123
+ if self.ready:
124
+ return "ready"
125
+ if self.error:
126
+ return f"unavailable ({self.error})"
127
+ return "warming up"
128
+
129
+ def describe(self, mono48: np.ndarray, top_k: int = 2) -> dict[str, list[tuple[str, float]]]:
130
+ """Score the clip against every descriptor, grouped by axis."""
131
+ if not self.ready or mono48.size < CLAP_SR // 2:
132
+ return {}
133
+
134
+ import torch
135
+
136
+ # CLAP was trained on 10 s windows; take the loudest one.
137
+ want = CLAP_SR * 10
138
+ if mono48.size > want:
139
+ hop = CLAP_SR
140
+ best_s, best_e = 0, -1.0
141
+ for s in range(0, mono48.size - want + 1, hop):
142
+ e = float(np.mean(mono48[s : s + want] ** 2))
143
+ if e > best_e:
144
+ best_e, best_s = e, s
145
+ mono48 = mono48[best_s : best_s + want]
146
+
147
+ clip = mono48.astype(np.float32)
148
+ with self._lock, torch.no_grad():
149
+ # transformers 4.x takes `audios`, 5.x renamed it to `audio`.
150
+ try:
151
+ inputs = self._processor(audio=clip, sampling_rate=CLAP_SR,
152
+ return_tensors="pt")
153
+ except TypeError:
154
+ inputs = self._processor(audios=clip, sampling_rate=CLAP_SR,
155
+ return_tensors="pt")
156
+ audio_emb = _features(self._model.get_audio_features(**inputs),
157
+ self._model.audio_projection)
158
+ audio_emb = audio_emb / audio_emb.norm(dim=-1, keepdim=True)
159
+ sims = (audio_emb @ self._text_emb.T).squeeze(0).cpu().numpy()
160
+
161
+ grouped: dict[str, list[tuple[str, float]]] = {}
162
+ for group in BANK:
163
+ idx = [i for i, (g, _) in enumerate(_FLAT) if g == group]
164
+ local = sims[idx]
165
+ # Softmax within the group — cross-group absolute scores are not
166
+ # comparable, ranking inside a group is.
167
+ e = np.exp((local - local.max()) * 20.0)
168
+ probs = e / e.sum()
169
+ order = np.argsort(-probs)[:top_k]
170
+ grouped[group] = [(_FLAT[idx[o]][1], float(probs[o])) for o in order]
171
+ return grouped
172
+
173
+
174
+ SEMANTIC = _Semantic()
175
+
176
+
177
+ def tags_line(grouped: dict[str, list[tuple[str, float]]], min_conf: float = 0.30) -> str:
178
+ """Flatten the grouped scores into one readable sentence."""
179
+ picks = [items[0][0] for items in grouped.values() if items and items[0][1] >= min_conf]
180
+ return ", ".join(picks) if picks else ""