Spaces:
Runtime error
Runtime error
Upload app.py with huggingface_hub
Browse files
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
|
| 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 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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(
|
| 44 |
-
_model =
|
| 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
|
| 59 |
-
"""
|
| 60 |
fe, mdl = _load_model()
|
| 61 |
-
|
| 62 |
-
|
| 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 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 265 |
-
|
| 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 |
-
|
| 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
|
| 356 |
|
| 357 |
-
**Model:** [microsoft/wavlm-base-plus-sv](https://huggingface.co/microsoft/wavlm-base-plus-sv)
|
| 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 |
---
|