fosters commited on
Commit
36ed964
Β·
verified Β·
1 Parent(s): 94535c8

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +100 -59
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import io
2
  import os
3
  import time
 
4
  import threading
5
  import concurrent.futures
6
  import gradio as gr
@@ -18,6 +19,12 @@ os.environ.setdefault("HF_XET_HIGH_PERFORMANCE", "1")
18
 
19
  N_CPUS = os.cpu_count() or 2
20
  BATCH_SIZE = 64 # max clips per ONNX forward pass
 
 
 
 
 
 
21
 
22
  DATASETS_SERVER = "https://datasets-server.huggingface.co"
23
  ONNX_MODEL_ID = "fosters/wavlm-base-plus-sv-onnx"
@@ -66,64 +73,85 @@ def _batch_embed(arrays: list[np.ndarray]) -> np.ndarray:
66
  # ── Audio fetching ────────────────────────────────────────────────────────────
67
 
68
  def _fetch_audio_urls(repo_id: str, n: int, token: str | None) -> tuple[list[str], str]:
 
69
  headers = {"Authorization": f"Bearer {token}"} if token else {}
70
- resp = requests.get(
71
- f"{DATASETS_SERVER}/rows",
72
- params={"dataset": repo_id, "config": "default", "split": "train", "offset": 0, "length": n},
73
- headers=headers,
74
- timeout=30,
75
- )
76
- if not resp.ok:
77
- return [], f"datasets-server {resp.status_code}"
78
-
79
- rows = resp.json().get("rows", [])
80
- if not rows:
81
- return [], "datasets-server: empty rows"
82
-
83
- sample_audio = rows[0]["row"].get("audio", {})
84
- audio_keys = list(sample_audio.keys()) if isinstance(sample_audio, dict) else type(sample_audio).__name__
85
-
86
- urls = []
87
- for row in rows:
88
- audio = row["row"].get("audio", {})
89
- if isinstance(audio, list):
90
- audio = audio[0] if audio else {}
91
- if isinstance(audio, dict) and "src" in audio:
92
- urls.append(audio["src"])
93
-
94
- if not urls:
95
- return [], f"no src in audio (keys={audio_keys})"
96
- return urls, "ok"
97
-
98
-
99
- def _download_audio(url: str, token: str | None, max_sec: int) -> tuple[np.ndarray, int]:
100
- """Download one audio file. Returns (array_at_TARGET_SR, size_kb)."""
 
 
 
 
101
  headers = {"Authorization": f"Bearer {token}"} if token else {}
 
102
  resp = requests.get(url, headers=headers, timeout=30)
103
  resp.raise_for_status()
 
104
  audio_array, sr = sf.read(io.BytesIO(resp.content))
105
- return _to_array(audio_array, sr, max_sec), len(resp.content) // 1024
106
 
107
 
108
  def _fetch_repo_audio(
109
- repo: str, n_samples: int, audio_sec: int, token: str | None
110
- ) -> tuple[str, list[np.ndarray], str]:
111
- """Fetch + download audio for one repo. Returns (repo, arrays, log)."""
 
 
112
  t0 = time.time()
 
113
  try:
 
114
  urls, api_status = _fetch_audio_urls(repo, n_samples, token)
115
  fetch_ms = int((time.time() - t0) * 1000)
116
 
117
  if urls:
 
118
  with concurrent.futures.ThreadPoolExecutor(max_workers=len(urls)) as ex:
119
- results = list(ex.map(lambda u: _download_audio(u, token, audio_sec), urls))
 
120
  arrays = [r[0] for r in results]
121
- avg_kb = int(np.mean([r[1] for r in results]))
122
- dl_ms = int((time.time() - t0) * 1000) - fetch_ms
123
- log = f"api={fetch_ms}ms dl={dl_ms}ms ({avg_kb}KB/file) [{api_status}]"
124
- return repo, arrays, log
 
 
 
 
 
 
125
 
126
  # Streaming fallback
 
 
127
  from datasets import load_dataset, Audio as HFAudio
128
  ds = load_dataset(repo, split="train", streaming=True, token=token)
129
  ds = ds.cast_column("audio", HFAudio(decode=False))
@@ -135,11 +163,14 @@ def _fetch_repo_audio(
135
  audio_bytes = raw.get("bytes") or open(raw["path"], "rb").read()
136
  a, sr = sf.read(io.BytesIO(audio_bytes))
137
  arrays.append(_to_array(a, sr, audio_sec))
 
138
  total_ms = int((time.time() - t0) * 1000)
139
- return repo, arrays, f"streaming fallback total={total_ms}ms [reason: {api_status}]"
 
140
 
141
  except Exception as exc:
142
- return repo, [], f"ERROR: {exc}"
 
143
 
144
 
145
  # ── Main ──────────────────────────────────────────────────────────────────────
@@ -158,35 +189,44 @@ def identify_speakers(
158
 
159
  token = hf_token.strip() or os.environ.get("HF_TOKEN") or None
160
 
 
 
 
 
 
 
161
  progress(0, desc="Loading model…")
 
162
  _load_model()
 
163
 
164
  # ── Phase 1: download all audio in parallel (I/O bound) ──────────────────
 
165
  progress(0.05, desc=f"Downloading audio from {len(repos)} datasets…")
166
  repo_arrays: dict[str, list[np.ndarray]] = {}
167
- fetch_logs: list[str] = []
168
- errors: list[str] = []
169
 
170
  with concurrent.futures.ThreadPoolExecutor(max_workers=N_CPUS * 2) as ex:
171
  futures = {
172
- ex.submit(_fetch_repo_audio, repo, int(samples_per_book), int(audio_sec), token): repo
173
  for repo in repos
174
  }
175
  done = 0
176
  for future in concurrent.futures.as_completed(futures):
177
- repo, arrays, log = future.result()
178
  done += 1
179
  progress(0.05 + 0.55 * done / len(repos), desc=f"[{done}/{len(repos)}] {repo.split('/')[-1]}")
180
  if arrays:
181
  repo_arrays[repo] = arrays
182
- fetch_logs.append(f"{repo.split('/')[-1]}: {log}")
183
- else:
184
- errors.append(f"{repo.split('/')[-1]}: {log}")
 
185
 
186
  if not repo_arrays:
187
- return pd.DataFrame(), "No audio downloaded.", "\n".join(errors)
188
 
189
  # ── Phase 2: batch embed all clips in one shot ───────────────────────────
 
190
  progress(0.60, desc="Batch embedding all clips…")
191
  all_arrays: list[np.ndarray] = []
192
  repo_slices: dict[str, tuple[int, int]] = {}
@@ -197,15 +237,17 @@ def identify_speakers(
197
  all_arrays.extend(repo_arrays[repo])
198
  repo_slices[repo] = (start, len(all_arrays))
199
 
 
200
  t_embed = time.time()
201
- all_embeddings = _batch_embed(all_arrays) # (N_clips, D)
202
  embed_ms = int((time.time() - t_embed) * 1000)
203
- fetch_logs.append(
204
- f"batch embed: {len(all_arrays)} clips in {embed_ms}ms "
205
- f"({embed_ms // len(all_arrays)}ms/clip avg)"
206
  )
207
 
208
  # ── Phase 3: average per repo, cluster ───────────────────────────────────
 
209
  progress(0.95, desc="Clustering…")
210
  embeddings: dict[str, np.ndarray] = {}
211
  for repo, (start, end) in repo_slices.items():
@@ -249,12 +291,11 @@ def identify_speakers(
249
 
250
  df = pd.DataFrame(rows).sort_values(["speaker_id", "dataset"]).reset_index(drop=True)
251
  n_speakers = len(set(labels))
252
- summary = f"βœ… {len(repo_names)} books β†’ {n_speakers} unique speakers"
 
 
253
 
254
- debug = "\n".join(fetch_logs)
255
- if errors:
256
- debug += "\n\nERRORS:\n" + "\n".join(errors)
257
- return df, summary, debug
258
 
259
 
260
  DESCRIPTION = """
 
1
  import io
2
  import os
3
  import time
4
+ import datetime
5
  import threading
6
  import concurrent.futures
7
  import gradio as gr
 
19
 
20
  N_CPUS = os.cpu_count() or 2
21
  BATCH_SIZE = 64 # max clips per ONNX forward pass
22
+ API_TIMEOUT = 20 # seconds per datasets-server attempt
23
+ API_RETRIES = 2
24
+
25
+
26
+ def _ts() -> str:
27
+ return datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
28
 
29
  DATASETS_SERVER = "https://datasets-server.huggingface.co"
30
  ONNX_MODEL_ID = "fosters/wavlm-base-plus-sv-onnx"
 
73
  # ── Audio fetching ────────────────────────────────────────────────────────────
74
 
75
  def _fetch_audio_urls(repo_id: str, n: int, token: str | None) -> tuple[list[str], str]:
76
+ """Fetch audio URLs from datasets-server with retry. Returns (urls, status)."""
77
  headers = {"Authorization": f"Bearer {token}"} if token else {}
78
+ params = {"dataset": repo_id, "config": "default", "split": "train", "offset": 0, "length": n}
79
+ last_err = ""
80
+ for attempt in range(1, API_RETRIES + 1):
81
+ try:
82
+ t0 = time.time()
83
+ resp = requests.get(
84
+ f"{DATASETS_SERVER}/rows", params=params,
85
+ headers=headers, timeout=API_TIMEOUT,
86
+ )
87
+ elapsed = int((time.time() - t0) * 1000)
88
+ if not resp.ok:
89
+ last_err = f"HTTP {resp.status_code} (attempt {attempt}, {elapsed}ms)"
90
+ continue
91
+ rows = resp.json().get("rows", [])
92
+ if not rows:
93
+ return [], f"empty rows ({elapsed}ms)"
94
+ urls = []
95
+ for row in rows:
96
+ audio = row["row"].get("audio", {})
97
+ if isinstance(audio, list):
98
+ audio = audio[0] if audio else {}
99
+ if isinstance(audio, dict) and "src" in audio:
100
+ urls.append(audio["src"])
101
+ if not urls:
102
+ return [], f"no src field ({elapsed}ms)"
103
+ return urls, f"api={elapsed}ms"
104
+ except requests.Timeout:
105
+ last_err = f"timeout>{API_TIMEOUT}s (attempt {attempt})"
106
+ except Exception as exc:
107
+ last_err = f"{exc} (attempt {attempt})"
108
+ return [], last_err
109
+
110
+
111
+ def _download_audio(url: str, token: str | None, max_sec: int) -> tuple[np.ndarray, int, int]:
112
+ """Download one audio file. Returns (array, size_kb, dl_ms)."""
113
  headers = {"Authorization": f"Bearer {token}"} if token else {}
114
+ t0 = time.time()
115
  resp = requests.get(url, headers=headers, timeout=30)
116
  resp.raise_for_status()
117
+ dl_ms = int((time.time() - t0) * 1000)
118
  audio_array, sr = sf.read(io.BytesIO(resp.content))
119
+ return _to_array(audio_array, sr, max_sec), len(resp.content) // 1024, dl_ms
120
 
121
 
122
  def _fetch_repo_audio(
123
+ repo: str, n_samples: int, audio_sec: int, token: str | None,
124
+ log: list[str],
125
+ ) -> tuple[str, list[np.ndarray]]:
126
+ """Fetch + download audio for one repo. Appends timestamped entries to log."""
127
+ short = repo.split("/")[-1]
128
  t0 = time.time()
129
+
130
  try:
131
+ log.append(f"[{_ts()}] {short}: fetching URLs from datasets-server…")
132
  urls, api_status = _fetch_audio_urls(repo, n_samples, token)
133
  fetch_ms = int((time.time() - t0) * 1000)
134
 
135
  if urls:
136
+ log.append(f"[{_ts()}] {short}: {api_status} β†’ {len(urls)} URLs")
137
  with concurrent.futures.ThreadPoolExecutor(max_workers=len(urls)) as ex:
138
+ futures = [ex.submit(_download_audio, u, token, audio_sec) for u in urls]
139
+ results = [f.result() for f in futures]
140
  arrays = [r[0] for r in results]
141
+ sizes = [r[1] for r in results]
142
+ dls = [r[2] for r in results]
143
+ total_ms = int((time.time() - t0) * 1000)
144
+ log.append(
145
+ f"[{_ts()}] {short}: βœ“ {len(arrays)} clips "
146
+ f"dl=[{', '.join(str(d)+'ms' for d in dls)}] "
147
+ f"size=[{', '.join(str(s)+'KB' for s in sizes)}] "
148
+ f"total={total_ms}ms"
149
+ )
150
+ return repo, arrays
151
 
152
  # Streaming fallback
153
+ log.append(f"[{_ts()}] {short}: ⚠ datasets-server failed ({api_status}) β†’ streaming fallback")
154
+ t1 = time.time()
155
  from datasets import load_dataset, Audio as HFAudio
156
  ds = load_dataset(repo, split="train", streaming=True, token=token)
157
  ds = ds.cast_column("audio", HFAudio(decode=False))
 
163
  audio_bytes = raw.get("bytes") or open(raw["path"], "rb").read()
164
  a, sr = sf.read(io.BytesIO(audio_bytes))
165
  arrays.append(_to_array(a, sr, audio_sec))
166
+ log.append(f"[{_ts()}] {short}: streaming clip {j+1}/{n_samples} ({len(audio_bytes)//1024}KB, {int((time.time()-t1)*1000)}ms so far)")
167
  total_ms = int((time.time() - t0) * 1000)
168
+ log.append(f"[{_ts()}] {short}: βœ“ streaming done, {len(arrays)} clips, total={total_ms}ms")
169
+ return repo, arrays
170
 
171
  except Exception as exc:
172
+ log.append(f"[{_ts()}] {short}: βœ— {exc} (total={int((time.time()-t0)*1000)}ms)")
173
+ return repo, []
174
 
175
 
176
  # ── Main ──────────────────────────────────────────────────────────────────────
 
189
 
190
  token = hf_token.strip() or os.environ.get("HF_TOKEN") or None
191
 
192
+ log: list[str] = []
193
+ t_total = time.time()
194
+
195
+ log.append(f"[{_ts()}] === START: {len(repos)} repos, {samples_per_book} sample/book, {audio_sec}s/clip ===")
196
+ log.append(f"[{_ts()}] CPU count: {N_CPUS}, batch size: {BATCH_SIZE}")
197
+
198
  progress(0, desc="Loading model…")
199
+ t_model = time.time()
200
  _load_model()
201
+ log.append(f"[{_ts()}] model loaded in {int((time.time()-t_model)*1000)}ms")
202
 
203
  # ── Phase 1: download all audio in parallel (I/O bound) ──────────────────
204
+ log.append(f"[{_ts()}] --- Phase 1: download ({N_CPUS*2} workers) ---")
205
  progress(0.05, desc=f"Downloading audio from {len(repos)} datasets…")
206
  repo_arrays: dict[str, list[np.ndarray]] = {}
 
 
207
 
208
  with concurrent.futures.ThreadPoolExecutor(max_workers=N_CPUS * 2) as ex:
209
  futures = {
210
+ ex.submit(_fetch_repo_audio, repo, int(samples_per_book), int(audio_sec), token, log): repo
211
  for repo in repos
212
  }
213
  done = 0
214
  for future in concurrent.futures.as_completed(futures):
215
+ repo, arrays = future.result()
216
  done += 1
217
  progress(0.05 + 0.55 * done / len(repos), desc=f"[{done}/{len(repos)}] {repo.split('/')[-1]}")
218
  if arrays:
219
  repo_arrays[repo] = arrays
220
+
221
+ ok = len(repo_arrays)
222
+ failed = len(repos) - ok
223
+ log.append(f"[{_ts()}] Phase 1 done: {ok} ok, {failed} failed, phase_total={int((time.time()-t_total)*1000)}ms")
224
 
225
  if not repo_arrays:
226
+ return pd.DataFrame(), "No audio downloaded.", "\n".join(log)
227
 
228
  # ── Phase 2: batch embed all clips in one shot ───────────────────────────
229
+ log.append(f"[{_ts()}] --- Phase 2: batch embed ---")
230
  progress(0.60, desc="Batch embedding all clips…")
231
  all_arrays: list[np.ndarray] = []
232
  repo_slices: dict[str, tuple[int, int]] = {}
 
237
  all_arrays.extend(repo_arrays[repo])
238
  repo_slices[repo] = (start, len(all_arrays))
239
 
240
+ log.append(f"[{_ts()}] embedding {len(all_arrays)} clips in batches of {BATCH_SIZE}…")
241
  t_embed = time.time()
242
+ all_embeddings = _batch_embed(all_arrays)
243
  embed_ms = int((time.time() - t_embed) * 1000)
244
+ log.append(
245
+ f"[{_ts()}] batch embed done: {len(all_arrays)} clips, "
246
+ f"{embed_ms}ms total, {embed_ms // max(len(all_arrays),1)}ms/clip avg"
247
  )
248
 
249
  # ── Phase 3: average per repo, cluster ───────────────────────────────────
250
+ log.append(f"[{_ts()}] --- Phase 3: cluster ({len(repo_slices)} repos) ---")
251
  progress(0.95, desc="Clustering…")
252
  embeddings: dict[str, np.ndarray] = {}
253
  for repo, (start, end) in repo_slices.items():
 
291
 
292
  df = pd.DataFrame(rows).sort_values(["speaker_id", "dataset"]).reset_index(drop=True)
293
  n_speakers = len(set(labels))
294
+ total_ms = int((time.time() - t_total) * 1000)
295
+ summary = f"βœ… {len(repo_names)} books β†’ {n_speakers} unique speakers ({total_ms/1000:.1f}s total)"
296
+ log.append(f"[{_ts()}] === DONE: {total_ms}ms total ===")
297
 
298
+ return df, summary, "\n".join(log)
 
 
 
299
 
300
 
301
  DESCRIPTION = """