fosters commited on
Commit
28d361f
Β·
verified Β·
1 Parent(s): adf923d

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +34 -35
app.py CHANGED
@@ -1,6 +1,5 @@
1
  import io
2
  import os
3
- import tempfile
4
  import time
5
  import datetime
6
  import threading
@@ -13,22 +12,25 @@ import soundfile as sf
13
  import torchaudio
14
  import torch
15
  from sklearn.cluster import AgglomerativeClustering
16
- from optimum.onnxruntime import ORTModelForAudioXVector
17
- from transformers import Wav2Vec2FeatureExtractor
18
 
19
  os.environ.setdefault("HF_XET_HIGH_PERFORMANCE", "1")
20
 
21
  N_CPUS = os.cpu_count() or 2
22
- BATCH_SIZE = 64 # max clips per ONNX forward pass
23
- API_TIMEOUT = 12 # seconds per datasets-server attempt (one try per endpoint)
24
- STREAMING_TIMEOUT = 120 # seconds before giving up on streaming fallback
 
 
 
 
25
 
26
 
27
  def _ts() -> str:
28
  return datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
29
 
30
  DATASETS_SERVER = "https://datasets-server.huggingface.co"
31
- ONNX_MODEL_ID = "fosters/wavlm-base-plus-sv-onnx"
32
  TARGET_SR = 16000
33
 
34
  _feature_extractor = None
@@ -40,13 +42,12 @@ def _load_model():
40
  global _feature_extractor, _model
41
  with _init_lock:
42
  if _model is None:
43
- _feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(ONNX_MODEL_ID)
44
- _model = ORTModelForAudioXVector.from_pretrained(ONNX_MODEL_ID)
45
  return _feature_extractor, _model
46
 
47
 
48
  def _to_array(audio_array: np.ndarray, sr: int, max_sec: int) -> np.ndarray:
49
- """Resample to TARGET_SR, convert to mono, trim to max_sec."""
50
  waveform = torch.tensor(audio_array, dtype=torch.float32)
51
  if waveform.ndim == 2:
52
  waveform = waveform.mean(0)
@@ -55,20 +56,26 @@ def _to_array(audio_array: np.ndarray, sr: int, max_sec: int) -> np.ndarray:
55
  return waveform[: max_sec * TARGET_SR].numpy().astype(np.float32)
56
 
57
 
58
- def _batch_embed(arrays: list[np.ndarray]) -> np.ndarray:
59
- """Run all clips through the model in batches. Returns (N, D) embeddings."""
60
  fe, mdl = _load_model()
61
- all_embs = []
62
- for i in range(0, len(arrays), BATCH_SIZE):
63
- batch = arrays[i : i + BATCH_SIZE]
64
- inputs = fe(batch, sampling_rate=TARGET_SR, return_tensors="pt", padding=True)
65
  out = mdl(**inputs)
66
- # embeddings shape: (batch, D)
67
- embs = out.embeddings.detach().numpy()
68
- if embs.ndim == 1:
69
- embs = embs[np.newaxis]
70
- all_embs.append(embs)
71
- return np.concatenate(all_embs, axis=0)
 
 
 
 
 
 
 
 
72
 
73
 
74
  # ── Audio fetching ────────────────────────────────────────────────────────────
@@ -261,9 +268,8 @@ def identify_speakers(
261
  if not repo_arrays:
262
  return pd.DataFrame(), "No audio downloaded.", "\n".join(log), "", None
263
 
264
- # ── Phase 2: batch embed all clips in one shot ───────────────────────────
265
- log.append(f"[{_ts()}] --- Phase 2: batch embed ---")
266
- progress(0.60, desc="Batch embedding all clips…")
267
  all_arrays: list[np.ndarray] = []
268
  repo_slices: dict[str, tuple[int, int]] = {}
269
 
@@ -273,14 +279,7 @@ def identify_speakers(
273
  all_arrays.extend(repo_arrays[repo])
274
  repo_slices[repo] = (start, len(all_arrays))
275
 
276
- log.append(f"[{_ts()}] embedding {len(all_arrays)} clips in batches of {BATCH_SIZE}…")
277
- t_embed = time.time()
278
- all_embeddings = _batch_embed(all_arrays)
279
- embed_ms = int((time.time() - t_embed) * 1000)
280
- log.append(
281
- f"[{_ts()}] batch embed done: {len(all_arrays)} clips, "
282
- f"{embed_ms}ms total, {embed_ms // max(len(all_arrays),1)}ms/clip avg"
283
- )
284
 
285
  # ── Phase 3: average per repo, cluster ───────────────────────────────────
286
  log.append(f"[{_ts()}] --- Phase 3: cluster ({len(repo_slices)} repos) ---")
@@ -352,9 +351,9 @@ DESCRIPTION = """
352
 
353
  Finds unique speakers across multiple HF audio datasets. Each dataset is assumed to have
354
  **one speaker** (e.g. an audiobook). The app fetches audio directly via the datasets-server API
355
- (no full parquet download), downloads in parallel, then embeds all clips in one batched forward pass.
356
 
357
- **Model:** [microsoft/wavlm-base-plus-sv](https://huggingface.co/microsoft/wavlm-base-plus-sv) (ONNX) β€”
358
  language-agnostic speaker embeddings, works for any language.
359
 
360
  ---
 
1
  import io
2
  import os
 
3
  import time
4
  import datetime
5
  import threading
 
12
  import torchaudio
13
  import torch
14
  from sklearn.cluster import AgglomerativeClustering
15
+ from transformers import Wav2Vec2FeatureExtractor, WavLMForXVector
 
16
 
17
  os.environ.setdefault("HF_XET_HIGH_PERFORMANCE", "1")
18
 
19
  N_CPUS = os.cpu_count() or 2
20
+ N_WORKERS = max(1, N_CPUS // 2) # 8 workers on 16-CPU Space
21
+ EMBED_THREADS = 2 # per-worker thread count β€” NUMA sweet spot
22
+ API_TIMEOUT = 12
23
+ STREAMING_TIMEOUT = 120
24
+
25
+ # Set thread count before model load so ORT/MKL picks it up
26
+ torch.set_num_threads(EMBED_THREADS)
27
 
28
 
29
  def _ts() -> str:
30
  return datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
31
 
32
  DATASETS_SERVER = "https://datasets-server.huggingface.co"
33
+ MODEL_ID = "microsoft/wavlm-base-plus-sv"
34
  TARGET_SR = 16000
35
 
36
  _feature_extractor = None
 
42
  global _feature_extractor, _model
43
  with _init_lock:
44
  if _model is None:
45
+ _feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(MODEL_ID)
46
+ _model = WavLMForXVector.from_pretrained(MODEL_ID).eval()
47
  return _feature_extractor, _model
48
 
49
 
50
  def _to_array(audio_array: np.ndarray, sr: int, max_sec: int) -> np.ndarray:
 
51
  waveform = torch.tensor(audio_array, dtype=torch.float32)
52
  if waveform.ndim == 2:
53
  waveform = waveform.mean(0)
 
56
  return waveform[: max_sec * TARGET_SR].numpy().astype(np.float32)
57
 
58
 
59
+ def _embed_one(array: np.ndarray) -> np.ndarray:
60
+ """Embed a single clip. Thread-safe: eval()+no_grad(), GIL released in C++."""
61
  fe, mdl = _load_model()
62
+ inputs = fe(array, sampling_rate=TARGET_SR, return_tensors="pt")
63
+ with torch.no_grad():
 
 
64
  out = mdl(**inputs)
65
+ return out.embeddings.squeeze().numpy()
66
+
67
+
68
+ def _parallel_embed(arrays: list[np.ndarray], log: list[str]) -> np.ndarray:
69
+ """Embed all clips using N_WORKERS parallel threads (threads=2 each)."""
70
+ t0 = time.time()
71
+ log.append(f"[{_ts()}] --- Phase 2: embed {len(arrays)} clips "
72
+ f"({N_WORKERS} workers Γ— {EMBED_THREADS} threads) ---")
73
+ with concurrent.futures.ThreadPoolExecutor(max_workers=N_WORKERS) as ex:
74
+ futures = [ex.submit(_embed_one, arr) for arr in arrays]
75
+ results = [f.result() for f in futures]
76
+ ms = int((time.time() - t0) * 1000)
77
+ log.append(f"[{_ts()}] embed done: {ms}ms total, {ms//len(arrays)}ms/clip avg")
78
+ return np.stack(results)
79
 
80
 
81
  # ── Audio fetching ────────────────────────────────────────────────────────────
 
268
  if not repo_arrays:
269
  return pd.DataFrame(), "No audio downloaded.", "\n".join(log), "", None
270
 
271
+ # ── Phase 2: parallel embed (N_WORKERS workers Γ— EMBED_THREADS each) ───────
272
+ progress(0.60, desc="Embedding clips…")
 
273
  all_arrays: list[np.ndarray] = []
274
  repo_slices: dict[str, tuple[int, int]] = {}
275
 
 
279
  all_arrays.extend(repo_arrays[repo])
280
  repo_slices[repo] = (start, len(all_arrays))
281
 
282
+ all_embeddings = _parallel_embed(all_arrays, log)
 
 
 
 
 
 
 
283
 
284
  # ── Phase 3: average per repo, cluster ───────────────────────────────────
285
  log.append(f"[{_ts()}] --- Phase 3: cluster ({len(repo_slices)} repos) ---")
 
351
 
352
  Finds unique speakers across multiple HF audio datasets. Each dataset is assumed to have
353
  **one speaker** (e.g. an audiobook). The app fetches audio directly via the datasets-server API
354
+ (no full parquet download), downloads in parallel, then embeds clips across 8 parallel workers.
355
 
356
+ **Model:** [microsoft/wavlm-base-plus-sv](https://huggingface.co/microsoft/wavlm-base-plus-sv) β€”
357
  language-agnostic speaker embeddings, works for any language.
358
 
359
  ---