fosters commited on
Commit
2dc5eca
·
verified ·
1 Parent(s): 3499f02

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +122 -37
app.py CHANGED
@@ -1,28 +1,36 @@
1
  import io
2
  import os
 
 
 
3
  import gradio as gr
4
  import numpy as np
5
  import pandas as pd
 
6
  import soundfile as sf
7
  import torch
8
  import torchaudio
9
- from datasets import load_dataset, Audio
10
  from sklearn.cluster import AgglomerativeClustering
11
  from transformers import Wav2Vec2FeatureExtractor, WavLMForXVector
12
 
 
 
 
13
  MODEL_ID = "microsoft/wavlm-base-plus-sv"
14
  TARGET_SR = 16000
15
 
16
  _feature_extractor = None
17
  _model = None
 
18
 
19
 
20
  def _load_model():
21
  global _feature_extractor, _model
22
- if _model is None:
23
- _feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(MODEL_ID)
24
- _model = WavLMForXVector.from_pretrained(MODEL_ID)
25
- _model.eval()
 
26
  return _feature_extractor, _model
27
 
28
 
@@ -35,11 +43,84 @@ def _embed(audio_array: np.ndarray, sr: int, max_sec: int) -> np.ndarray:
35
  waveform = torchaudio.functional.resample(waveform, sr, TARGET_SR)
36
  waveform = waveform[: max_sec * TARGET_SR]
37
  inputs = fe(waveform.numpy(), sampling_rate=TARGET_SR, return_tensors="pt")
38
- with torch.no_grad():
39
- out = mdl(**inputs)
 
40
  return out.embeddings.squeeze().numpy()
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def identify_speakers(
44
  repo_ids_text: str,
45
  samples_per_book: int,
@@ -56,30 +137,28 @@ def identify_speakers(
56
 
57
  progress(0, desc="Loading model…")
58
  _load_model()
 
59
 
60
  embeddings: dict[str, np.ndarray] = {}
 
61
  errors: list[str] = []
 
62
 
63
- for i, repo in enumerate(repos):
64
- short = repo.split("/")[-1]
65
- progress((i + 0.5) / len(repos), desc=f"[{i+1}/{len(repos)}] {short}")
66
- try:
67
- ds = load_dataset(repo, split="train", streaming=True, token=token)
68
- ds = ds.cast_column("audio", Audio(decode=False))
69
- embs = []
70
- for j, row in enumerate(ds):
71
- if j >= int(samples_per_book):
72
- break
73
- raw = row["audio"]
74
- audio_bytes = raw.get("bytes") or open(raw["path"], "rb").read()
75
- audio_array, sr = sf.read(io.BytesIO(audio_bytes))
76
- embs.append(_embed(audio_array, sr, int(audio_sec)))
77
- if embs:
78
- embeddings[repo] = np.mean(embs, axis=0)
79
  else:
80
- errors.append(f"{repo}: no rows found")
81
- except Exception as exc:
82
- errors.append(f"{repo}: {exc}")
83
 
84
  if not embeddings:
85
  return pd.DataFrame(), "No embeddings extracted.", "\n".join(errors)
@@ -123,15 +202,19 @@ def identify_speakers(
123
  df = pd.DataFrame(rows).sort_values(["speaker_id", "dataset"]).reset_index(drop=True)
124
  n_speakers = len(set(labels))
125
  summary = f"✅ {len(repo_names)} books → {n_speakers} unique speakers"
126
- return df, summary, "\n".join(errors) if errors else "None"
 
 
 
 
127
 
128
 
129
  DESCRIPTION = """
130
  # 🎙️ Speaker Identifier
131
 
132
  Finds unique speakers across multiple HF audio datasets. Each dataset is assumed to have
133
- **one speaker** (e.g. an audiobook). The app extracts voice embeddings and clusters datasets
134
- by voice similarity.
135
 
136
  **Model:** [microsoft/wavlm-base-plus-sv](https://huggingface.co/microsoft/wavlm-base-plus-sv) —
137
  language-agnostic speaker embeddings, works for any language.
@@ -142,23 +225,25 @@ language-agnostic speaker embeddings, works for any language.
142
 
143
  1. **Paste dataset repo IDs** (one per line, `owner/name` format) into the left box.
144
  2. **Adjust parameters** if needed (defaults work well for audiobooks):
145
- - *Samples per book* — how many audio chunks to average per dataset. More = more robust, slower.
146
- - *Audio length (sec)* — how many seconds of each chunk to use. 5 sec is enough for a clear voice; increase if results look wrong.
147
- - *Same-speaker threshold* — cosine similarity cutoff. Raise it if too many books are merged into one speaker; lower it if one person gets split into multiple IDs.
148
  - *HF Token* — only needed for **private** repos.
149
- 3. Click **Identify Speakers**.
150
 
151
  ## Output columns
152
 
153
  | Column | Meaning |
154
  |---|---|
155
  | `dataset` | Repo name (short) |
156
- | `speaker_id` | Cluster label — same ID means same voice |
157
  | `books_with_speaker` | How many books share this speaker |
158
- | `intra_sim` | Avg cosine similarity between books with the same speaker (1.0 = only one book; lower = cluster is less tight) |
159
- | `closest_match` | Most similar other book and its similarity score |
160
 
161
  **Tip:** Sort by `speaker_id` to see all books by the same narrator grouped together.
 
 
162
  """
163
 
164
  with gr.Blocks(title="Speaker Identifier") as demo:
@@ -196,7 +281,7 @@ with gr.Blocks(title="Speaker Identifier") as demo:
196
  headers=["dataset", "speaker_id", "books_with_speaker", "intra_sim", "closest_match"],
197
  wrap=True,
198
  )
199
- errors_out = gr.Textbox(label="Errors / Skipped", interactive=False)
200
 
201
  run_btn.click(
202
  identify_speakers,
 
1
  import io
2
  import os
3
+ import time
4
+ import threading
5
+ import concurrent.futures
6
  import gradio as gr
7
  import numpy as np
8
  import pandas as pd
9
+ import requests
10
  import soundfile as sf
11
  import torch
12
  import torchaudio
 
13
  from sklearn.cluster import AgglomerativeClustering
14
  from transformers import Wav2Vec2FeatureExtractor, WavLMForXVector
15
 
16
+ os.environ.setdefault("HF_XET_HIGH_PERFORMANCE", "1")
17
+
18
+ DATASETS_SERVER = "https://datasets-server.huggingface.co"
19
  MODEL_ID = "microsoft/wavlm-base-plus-sv"
20
  TARGET_SR = 16000
21
 
22
  _feature_extractor = None
23
  _model = None
24
+ _embed_lock = threading.Lock() # WavLM inference is not thread-safe
25
 
26
 
27
  def _load_model():
28
  global _feature_extractor, _model
29
+ with _embed_lock:
30
+ if _model is None:
31
+ _feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(MODEL_ID)
32
+ _model = WavLMForXVector.from_pretrained(MODEL_ID)
33
+ _model.eval()
34
  return _feature_extractor, _model
35
 
36
 
 
43
  waveform = torchaudio.functional.resample(waveform, sr, TARGET_SR)
44
  waveform = waveform[: max_sec * TARGET_SR]
45
  inputs = fe(waveform.numpy(), sampling_rate=TARGET_SR, return_tensors="pt")
46
+ with _embed_lock:
47
+ with torch.no_grad():
48
+ out = mdl(**inputs)
49
  return out.embeddings.squeeze().numpy()
50
 
51
 
52
+ def _fetch_audio_urls(repo_id: str, n: int, token: str | None) -> list[str]:
53
+ """Get direct audio URLs via datasets-server — no parquet download needed."""
54
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
55
+ resp = requests.get(
56
+ f"{DATASETS_SERVER}/rows",
57
+ params={"dataset": repo_id, "config": "default", "split": "train", "offset": 0, "length": n},
58
+ headers=headers,
59
+ timeout=30,
60
+ )
61
+ resp.raise_for_status()
62
+ urls = []
63
+ for row in resp.json().get("rows", []):
64
+ audio = row["row"].get("audio", {})
65
+ if isinstance(audio, dict) and "src" in audio:
66
+ urls.append(audio["src"])
67
+ return urls
68
+
69
+
70
+ def _download_one(url: str, token: str | None, max_sec: int) -> tuple[np.ndarray, dict]:
71
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
72
+ t0 = time.time()
73
+ resp = requests.get(url, headers=headers, timeout=30)
74
+ resp.raise_for_status()
75
+ dl_ms = (time.time() - t0) * 1000
76
+
77
+ t1 = time.time()
78
+ audio_array, sr = sf.read(io.BytesIO(resp.content))
79
+ decode_ms = (time.time() - t1) * 1000
80
+
81
+ t2 = time.time()
82
+ emb = _embed(audio_array, sr, max_sec)
83
+ embed_ms = (time.time() - t2) * 1000
84
+
85
+ return emb, {
86
+ "size_kb": len(resp.content) // 1024,
87
+ "dl_ms": int(dl_ms),
88
+ "decode_ms": int(decode_ms),
89
+ "embed_ms": int(embed_ms),
90
+ }
91
+
92
+
93
+ def _process_repo(
94
+ repo: str, n_samples: int, audio_sec: int, token: str | None
95
+ ) -> tuple[str, np.ndarray | None, str]:
96
+ t0 = time.time()
97
+ try:
98
+ urls = _fetch_audio_urls(repo, n_samples, token)
99
+ if not urls:
100
+ return repo, None, f"{repo}: no audio URLs from datasets-server"
101
+ fetch_ms = int((time.time() - t0) * 1000)
102
+
103
+ # Download audio files in parallel (I/O bound)
104
+ with concurrent.futures.ThreadPoolExecutor(max_workers=len(urls)) as ex:
105
+ futures = [ex.submit(_download_one, u, token, audio_sec) for u in urls]
106
+ results = [f.result() for f in futures]
107
+
108
+ embs = [r[0] for r in results]
109
+ # Log stats from first sample as representative
110
+ s = results[0][1]
111
+ total_ms = int((time.time() - t0) * 1000)
112
+ log = (
113
+ f"{repo.split('/')[-1]}: "
114
+ f"api={fetch_ms}ms dl={s['dl_ms']}ms "
115
+ f"decode={s['decode_ms']}ms embed={s['embed_ms']}ms "
116
+ f"total={total_ms}ms ({s['size_kb']}KB/file)"
117
+ )
118
+ return repo, np.mean(embs, axis=0), log
119
+
120
+ except Exception as exc:
121
+ return repo, None, f"{repo}: ERROR {exc}"
122
+
123
+
124
  def identify_speakers(
125
  repo_ids_text: str,
126
  samples_per_book: int,
 
137
 
138
  progress(0, desc="Loading model…")
139
  _load_model()
140
+ progress(0.02, desc=f"Processing {len(repos)} datasets in parallel…")
141
 
142
  embeddings: dict[str, np.ndarray] = {}
143
+ logs: list[str] = []
144
  errors: list[str] = []
145
+ done = 0
146
 
147
+ # Process all repos in parallel (downloads are I/O bound)
148
+ with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
149
+ future_to_repo = {
150
+ ex.submit(_process_repo, repo, int(samples_per_book), int(audio_sec), token): repo
151
+ for repo in repos
152
+ }
153
+ for future in concurrent.futures.as_completed(future_to_repo):
154
+ repo, emb, log = future.result()
155
+ done += 1
156
+ progress(done / len(repos), desc=f"[{done}/{len(repos)}] {repo.split('/')[-1]}")
157
+ if emb is not None:
158
+ embeddings[repo] = emb
159
+ logs.append(log)
 
 
 
160
  else:
161
+ errors.append(log)
 
 
162
 
163
  if not embeddings:
164
  return pd.DataFrame(), "No embeddings extracted.", "\n".join(errors)
 
202
  df = pd.DataFrame(rows).sort_values(["speaker_id", "dataset"]).reset_index(drop=True)
203
  n_speakers = len(set(labels))
204
  summary = f"✅ {len(repo_names)} books → {n_speakers} unique speakers"
205
+
206
+ debug = "\n".join(logs)
207
+ if errors:
208
+ debug += "\n\nERRORS:\n" + "\n".join(errors)
209
+ return df, summary, debug
210
 
211
 
212
  DESCRIPTION = """
213
  # 🎙️ Speaker Identifier
214
 
215
  Finds unique speakers across multiple HF audio datasets. Each dataset is assumed to have
216
+ **one speaker** (e.g. an audiobook). The app fetches audio directly via the datasets-server API
217
+ (no full parquet download), processes datasets in parallel, and clusters by voice similarity.
218
 
219
  **Model:** [microsoft/wavlm-base-plus-sv](https://huggingface.co/microsoft/wavlm-base-plus-sv) —
220
  language-agnostic speaker embeddings, works for any language.
 
225
 
226
  1. **Paste dataset repo IDs** (one per line, `owner/name` format) into the left box.
227
  2. **Adjust parameters** if needed (defaults work well for audiobooks):
228
+ - *Samples per book* — audio chunks to average per dataset. More = more robust, slower. 3 is usually enough.
229
+ - *Audio length (sec)* — seconds of each chunk to use for embedding. 5 sec is sufficient for a clear voice.
230
+ - *Same-speaker threshold* — cosine similarity cutoff. Raise if too many books merge into one speaker; lower if one person gets split across IDs.
231
  - *HF Token* — only needed for **private** repos.
232
+ 3. Click **Identify Speakers**. Datasets are processed in parallel — expect ~30–60 sec for 35 books.
233
 
234
  ## Output columns
235
 
236
  | Column | Meaning |
237
  |---|---|
238
  | `dataset` | Repo name (short) |
239
+ | `speaker_id` | Cluster label — same ID = same voice |
240
  | `books_with_speaker` | How many books share this speaker |
241
+ | `intra_sim` | Avg cosine similarity within cluster (1.0 = only one book; lower = cluster is less tight) |
242
+ | `closest_match` | Most similar other book and similarity score |
243
 
244
  **Tip:** Sort by `speaker_id` to see all books by the same narrator grouped together.
245
+
246
+ The **Errors / Timing** box shows per-dataset timing breakdown (API fetch / download / decode / embed) — useful for diagnosing slow datasets.
247
  """
248
 
249
  with gr.Blocks(title="Speaker Identifier") as demo:
 
281
  headers=["dataset", "speaker_id", "books_with_speaker", "intra_sim", "closest_match"],
282
  wrap=True,
283
  )
284
+ errors_out = gr.Textbox(label="Errors / Timing", interactive=False)
285
 
286
  run_btn.click(
287
  identify_speakers,