Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files
README.md
CHANGED
|
@@ -19,8 +19,9 @@ sliders together. The query embedding is edited through sparse inversion in a
|
|
| 19 |
trained BatchTopK SAE (MuQ-MuLan / music4all), and nearest neighbours are retrieved
|
| 20 |
live after each slider move. No audio is hosted — playback is via Spotify embeds.
|
| 21 |
|
| 22 |
-
Runs on **ZeroGPU**: the corpus + MuQ weights are fetched at startup; the
|
| 23 |
-
on
|
|
|
|
| 24 |
|
| 25 |
## Configuration (Space variables / secrets)
|
| 26 |
|
|
|
|
| 19 |
trained BatchTopK SAE (MuQ-MuLan / music4all), and nearest neighbours are retrieved
|
| 20 |
live after each slider move. No audio is hosted — playback is via Spotify embeds.
|
| 21 |
|
| 22 |
+
Runs on **ZeroGPU**: the corpus + MuQ weights are fetched at startup; the text tower is
|
| 23 |
+
used on GPU only while a new slider is instantiated. Sparse inversion, slider edits,
|
| 24 |
+
and retrieval run on CPU from cached slider masks.
|
| 25 |
|
| 26 |
## Configuration (Space variables / secrets)
|
| 27 |
|
app.py
CHANGED
|
@@ -4,9 +4,9 @@ Search for a seed track or draw one at random, make one or more concept sliders,
|
|
| 4 |
and retrieve live as the sliders move. No audio is hosted — playback is via
|
| 5 |
Spotify embeds.
|
| 6 |
|
| 7 |
-
ZeroGPU: the corpus + MuQ weights are fetched at startup (CPU); the
|
| 8 |
-
on
|
| 9 |
-
|
| 10 |
"""
|
| 11 |
import html
|
| 12 |
import os
|
|
@@ -259,7 +259,6 @@ def make_slider(seed_track_id, concept, concepts, *alphas):
|
|
| 259 |
return concepts, note, results_html(results), *_slider_components(concepts, values)
|
| 260 |
|
| 261 |
|
| 262 |
-
@spaces.GPU(duration=120)
|
| 263 |
def live_retrieve(seed_track_id, concepts, *alphas):
|
| 264 |
concepts = list(concepts or [])
|
| 265 |
if not seed_track_id:
|
|
|
|
| 4 |
and retrieve live as the sliders move. No audio is hosted — playback is via
|
| 5 |
Spotify embeds.
|
| 6 |
|
| 7 |
+
ZeroGPU: the corpus + MuQ weights are fetched at startup (CPU); the text tower is
|
| 8 |
+
used on GPU only while a new slider is made. Live slider edits reuse cached CPU
|
| 9 |
+
masks.
|
| 10 |
"""
|
| 11 |
import html
|
| 12 |
import os
|
|
|
|
| 259 |
return concepts, note, results_html(results), *_slider_components(concepts, values)
|
| 260 |
|
| 261 |
|
|
|
|
| 262 |
def live_retrieve(seed_track_id, concepts, *alphas):
|
| 263 |
concepts = list(concepts or [])
|
| 264 |
if not seed_track_id:
|
core.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
"""Shared engine for the demo Space and website example generation.
|
| 2 |
|
| 3 |
Loads a trained BatchTopK SAE + MuQ text tower and a music4all corpus, then steers a
|
| 4 |
-
seed track toward
|
| 5 |
-
|
| 6 |
-
|
| 7 |
|
| 8 |
Configuration (env):
|
| 9 |
SSR_CHECKPOINT local .ckpt path, or a HF model repo id (default: local L0=20 run)
|
|
@@ -16,14 +16,13 @@ from __future__ import annotations
|
|
| 16 |
|
| 17 |
import json
|
| 18 |
import os
|
| 19 |
-
from functools import lru_cache
|
| 20 |
|
| 21 |
import numpy as np
|
| 22 |
import torch
|
|
|
|
| 23 |
|
| 24 |
from steerable_retrieval.steer import Slider
|
| 25 |
from steerable_retrieval.steer.loading import load_steerable_sae
|
| 26 |
-
from steerable_retrieval.steer.retrieval import topk_cosine_neighbors
|
| 27 |
from steerable_retrieval.steer.steering import decode_normalized
|
| 28 |
|
| 29 |
HERE = os.path.dirname(__file__)
|
|
@@ -32,6 +31,7 @@ CORPUS_REPO = os.environ.get("SSR_CORPUS_REPO") # HF dataset repo id, if hosted
|
|
| 32 |
CHECKPOINT = os.environ.get("SSR_CHECKPOINT", os.path.join(HERE, os.pardir, "logs/xps/66a0caf9/checkpoints/last.ckpt"))
|
| 33 |
L0_SUBFOLDER = os.environ.get("SSR_L0") # e.g. "L0-20" when CHECKPOINT is a Hub repo
|
| 34 |
CONFIG = os.environ.get("SSR_CONFIG")
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
def device() -> str:
|
|
@@ -57,17 +57,49 @@ def load_corpus():
|
|
| 57 |
|
| 58 |
class DemoEngine:
|
| 59 |
def __init__(self, corpus=None, dev: str | None = None):
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
| 61 |
embs, ids, meta = corpus if corpus is not None else load_corpus()
|
| 62 |
-
self.embeddings = torch.from_numpy(embs).
|
|
|
|
| 63 |
self.track_ids = ids
|
| 64 |
self.id_to_idx = {t: i for i, t in enumerate(ids)}
|
| 65 |
self.meta = meta
|
| 66 |
-
self.model = load_steerable_sae(CHECKPOINT, device=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
@lru_cache(maxsize=64)
|
| 69 |
def _slider(self, concept: str) -> Slider:
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
def track_meta(self, track_id: str) -> dict:
|
| 73 |
m = dict(self.meta.get(track_id, {}))
|
|
@@ -77,11 +109,21 @@ class DemoEngine:
|
|
| 77 |
def seed_embedding(self, track_id: str) -> torch.Tensor:
|
| 78 |
return self.embeddings[self.id_to_idx[track_id]]
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
def steer_and_retrieve(self, seed_track_id: str, concept: str, alpha: float = 1.0, k: int = 8) -> list[dict]:
|
| 81 |
slider = self._slider(concept)
|
| 82 |
z = self.seed_embedding(seed_track_id)
|
| 83 |
seed_idx = self.id_to_idx[seed_track_id]
|
| 84 |
-
|
|
|
|
| 85 |
return self._format_results(idx, scores)
|
| 86 |
|
| 87 |
def multi_steer_and_retrieve(self, seed_track_id: str, sliders: list[tuple[str, float]], k: int = 8) -> list[dict]:
|
|
@@ -93,19 +135,23 @@ class DemoEngine:
|
|
| 93 |
"""
|
| 94 |
z = self.seed_embedding(seed_track_id).reshape(1, -1)
|
| 95 |
seed_idx = self.id_to_idx[seed_track_id]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
with torch.inference_mode():
|
| 97 |
_, sparse, _, _ = self.model.inference(z)
|
| 98 |
edited_sparse = sparse.clone()
|
| 99 |
-
for
|
| 100 |
-
|
| 101 |
-
alpha = float(alpha)
|
| 102 |
-
if not concept or abs(alpha) < 1e-6:
|
| 103 |
-
continue
|
| 104 |
-
mask = self._slider(concept).mask.to(device=self.device, dtype=edited_sparse.dtype)
|
| 105 |
edited_sparse = edited_sparse + alpha * mask.reshape(1, -1)
|
| 106 |
edited_sparse = edited_sparse.clamp_min(0.0)
|
| 107 |
edited = decode_normalized(self.model, edited_sparse)
|
| 108 |
-
idx, scores =
|
| 109 |
return self._format_results(idx, scores)
|
| 110 |
|
| 111 |
def _format_results(self, idx, scores) -> list[dict]:
|
|
|
|
| 1 |
"""Shared engine for the demo Space and website example generation.
|
| 2 |
|
| 3 |
Loads a trained BatchTopK SAE + MuQ text tower and a music4all corpus, then steers a
|
| 4 |
+
seed track toward free-form concepts and retrieves nearest neighbours. The SAE,
|
| 5 |
+
inversion, edit masks, and retrieval corpus stay on CPU; ZeroGPU is used only while
|
| 6 |
+
embedding a new text concept for slider instantiation.
|
| 7 |
|
| 8 |
Configuration (env):
|
| 9 |
SSR_CHECKPOINT local .ckpt path, or a HF model repo id (default: local L0=20 run)
|
|
|
|
| 16 |
|
| 17 |
import json
|
| 18 |
import os
|
|
|
|
| 19 |
|
| 20 |
import numpy as np
|
| 21 |
import torch
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
|
| 24 |
from steerable_retrieval.steer import Slider
|
| 25 |
from steerable_retrieval.steer.loading import load_steerable_sae
|
|
|
|
| 26 |
from steerable_retrieval.steer.steering import decode_normalized
|
| 27 |
|
| 28 |
HERE = os.path.dirname(__file__)
|
|
|
|
| 31 |
CHECKPOINT = os.environ.get("SSR_CHECKPOINT", os.path.join(HERE, os.pardir, "logs/xps/66a0caf9/checkpoints/last.ckpt"))
|
| 32 |
L0_SUBFOLDER = os.environ.get("SSR_L0") # e.g. "L0-20" when CHECKPOINT is a Hub repo
|
| 33 |
CONFIG = os.environ.get("SSR_CONFIG")
|
| 34 |
+
INVERSION_METHOD = os.environ.get("SSR_INVERSION_METHOD", "fista")
|
| 35 |
|
| 36 |
|
| 37 |
def device() -> str:
|
|
|
|
| 57 |
|
| 58 |
class DemoEngine:
|
| 59 |
def __init__(self, corpus=None, dev: str | None = None):
|
| 60 |
+
# Keep SAE inversion, sparse edits, and retrieval on CPU. ZeroGPU is only
|
| 61 |
+
# needed while embedding new text concepts with MuQ.
|
| 62 |
+
self.device = "cpu"
|
| 63 |
+
self.text_device = dev or device()
|
| 64 |
embs, ids, meta = corpus if corpus is not None else load_corpus()
|
| 65 |
+
self.embeddings = torch.from_numpy(embs).float()
|
| 66 |
+
self.gallery = F.normalize(self.embeddings, dim=-1)
|
| 67 |
self.track_ids = ids
|
| 68 |
self.id_to_idx = {t: i for i, t in enumerate(ids)}
|
| 69 |
self.meta = meta
|
| 70 |
+
self.model = load_steerable_sae(CHECKPOINT, device="cpu", subfolder=L0_SUBFOLDER, config_path=CONFIG)
|
| 71 |
+
self._slider_cache: dict[str, Slider] = {}
|
| 72 |
+
self._text_encoder_device = "cpu"
|
| 73 |
+
|
| 74 |
+
def _slider_key(self, concept: str) -> str:
|
| 75 |
+
return " ".join(concept.strip().lower().split())
|
| 76 |
+
|
| 77 |
+
def has_slider(self, concept: str) -> bool:
|
| 78 |
+
return self._slider_key(concept) in self._slider_cache
|
| 79 |
+
|
| 80 |
+
def _set_text_encoder_device(self, target: str) -> None:
|
| 81 |
+
if self._text_encoder_device == target:
|
| 82 |
+
return
|
| 83 |
+
text_encoder = self.model.text_encoder
|
| 84 |
+
if text_encoder is None:
|
| 85 |
+
return
|
| 86 |
+
text_encoder.to(target)
|
| 87 |
+
if hasattr(text_encoder, "device"):
|
| 88 |
+
text_encoder.device = target
|
| 89 |
+
self._text_encoder_device = target
|
| 90 |
|
|
|
|
| 91 |
def _slider(self, concept: str) -> Slider:
|
| 92 |
+
key = self._slider_key(concept)
|
| 93 |
+
if key not in self._slider_cache:
|
| 94 |
+
target = self.text_device if str(self.text_device).startswith("cuda") and torch.cuda.is_available() else "cpu"
|
| 95 |
+
self._set_text_encoder_device(target)
|
| 96 |
+
try:
|
| 97 |
+
self._slider_cache[key] = Slider(concept.strip(), model=self.model, method=INVERSION_METHOD, device="cpu")
|
| 98 |
+
finally:
|
| 99 |
+
# The cached slider stores its text embedding/mask; live edits no
|
| 100 |
+
# longer need the text tower or any GPU memory.
|
| 101 |
+
self._set_text_encoder_device("cpu")
|
| 102 |
+
return self._slider_cache[key]
|
| 103 |
|
| 104 |
def track_meta(self, track_id: str) -> dict:
|
| 105 |
m = dict(self.meta.get(track_id, {}))
|
|
|
|
| 109 |
def seed_embedding(self, track_id: str) -> torch.Tensor:
|
| 110 |
return self.embeddings[self.id_to_idx[track_id]]
|
| 111 |
|
| 112 |
+
def _retrieve_dense(self, query: torch.Tensor, k: int, *, exclude_idx: int | None = None):
|
| 113 |
+
query = F.normalize(query.reshape(-1), dim=0)
|
| 114 |
+
sims = self.gallery @ query
|
| 115 |
+
if exclude_idx is not None:
|
| 116 |
+
sims[exclude_idx] = -1e9
|
| 117 |
+
n_avail = self.gallery.shape[0] - (1 if exclude_idx is not None else 0)
|
| 118 |
+
vals, idx = torch.topk(sims, k=min(int(k), int(n_avail)), largest=True, sorted=True)
|
| 119 |
+
return idx.detach().cpu(), vals.detach().cpu()
|
| 120 |
+
|
| 121 |
def steer_and_retrieve(self, seed_track_id: str, concept: str, alpha: float = 1.0, k: int = 8) -> list[dict]:
|
| 122 |
slider = self._slider(concept)
|
| 123 |
z = self.seed_embedding(seed_track_id)
|
| 124 |
seed_idx = self.id_to_idx[seed_track_id]
|
| 125 |
+
edited = slider.steer(z, alpha=alpha)
|
| 126 |
+
idx, scores = self._retrieve_dense(edited, k, exclude_idx=seed_idx)
|
| 127 |
return self._format_results(idx, scores)
|
| 128 |
|
| 129 |
def multi_steer_and_retrieve(self, seed_track_id: str, sliders: list[tuple[str, float]], k: int = 8) -> list[dict]:
|
|
|
|
| 135 |
"""
|
| 136 |
z = self.seed_embedding(seed_track_id).reshape(1, -1)
|
| 137 |
seed_idx = self.id_to_idx[seed_track_id]
|
| 138 |
+
slider_masks = []
|
| 139 |
+
for concept, alpha in sliders:
|
| 140 |
+
concept = concept.strip()
|
| 141 |
+
alpha = float(alpha)
|
| 142 |
+
if not concept or abs(alpha) < 1e-6:
|
| 143 |
+
continue
|
| 144 |
+
slider_masks.append((self._slider(concept).mask, alpha))
|
| 145 |
+
|
| 146 |
with torch.inference_mode():
|
| 147 |
_, sparse, _, _ = self.model.inference(z)
|
| 148 |
edited_sparse = sparse.clone()
|
| 149 |
+
for mask, alpha in slider_masks:
|
| 150 |
+
mask = mask.to(device=self.device, dtype=edited_sparse.dtype)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
edited_sparse = edited_sparse + alpha * mask.reshape(1, -1)
|
| 152 |
edited_sparse = edited_sparse.clamp_min(0.0)
|
| 153 |
edited = decode_normalized(self.model, edited_sparse)
|
| 154 |
+
idx, scores = self._retrieve_dense(edited, k, exclude_idx=seed_idx)
|
| 155 |
return self._format_results(idx, scores)
|
| 156 |
|
| 157 |
def _format_results(self, idx, scores) -> list[dict]:
|