fosters commited on
Commit
140fcd4
·
verified ·
1 Parent(s): 5821253

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +167 -0
app.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import numpy as np
4
+ import pandas as pd
5
+ import torch
6
+ import torchaudio
7
+ from datasets import load_dataset
8
+ from sklearn.cluster import AgglomerativeClustering
9
+ from transformers import Wav2Vec2FeatureExtractor, WavLMForXVector
10
+
11
+ MODEL_ID = "microsoft/wavlm-base-plus-sv"
12
+ TARGET_SR = 16000
13
+ MAX_AUDIO_SEC = 20 # trim long clips to keep inference fast
14
+
15
+ _feature_extractor = None
16
+ _model = None
17
+
18
+
19
+ def _load_model():
20
+ global _feature_extractor, _model
21
+ if _model is None:
22
+ _feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(MODEL_ID)
23
+ _model = WavLMForXVector.from_pretrained(MODEL_ID)
24
+ _model.eval()
25
+ return _feature_extractor, _model
26
+
27
+
28
+ def _embed(audio_array: np.ndarray, sr: int) -> np.ndarray:
29
+ fe, mdl = _load_model()
30
+ waveform = torch.tensor(audio_array, dtype=torch.float32)
31
+ if waveform.ndim == 2:
32
+ waveform = waveform.mean(0)
33
+ if sr != TARGET_SR:
34
+ waveform = torchaudio.functional.resample(waveform, sr, TARGET_SR)
35
+ waveform = waveform[: MAX_AUDIO_SEC * TARGET_SR]
36
+ inputs = fe(waveform.numpy(), sampling_rate=TARGET_SR, return_tensors="pt")
37
+ with torch.no_grad():
38
+ out = mdl(**inputs)
39
+ return out.embeddings.squeeze().numpy()
40
+
41
+
42
+ def identify_speakers(
43
+ repo_ids_text: str,
44
+ samples_per_book: int,
45
+ threshold: float,
46
+ hf_token: str,
47
+ progress=gr.Progress(),
48
+ ):
49
+ repos = [r.strip() for r in repo_ids_text.strip().splitlines() if r.strip()]
50
+ if not repos:
51
+ return pd.DataFrame(), "No repos provided.", ""
52
+
53
+ token = hf_token.strip() or os.environ.get("HF_TOKEN") or None
54
+
55
+ progress(0, desc="Loading model…")
56
+ _load_model()
57
+
58
+ embeddings: dict[str, np.ndarray] = {}
59
+ errors: list[str] = []
60
+
61
+ for i, repo in enumerate(repos):
62
+ short = repo.split("/")[-1]
63
+ progress((i + 0.5) / len(repos), desc=f"[{i+1}/{len(repos)}] {short}")
64
+ try:
65
+ ds = load_dataset(repo, split="train", streaming=True, token=token)
66
+ embs = []
67
+ for j, row in enumerate(ds):
68
+ if j >= int(samples_per_book):
69
+ break
70
+ audio = row["audio"]
71
+ embs.append(_embed(np.array(audio["array"]), audio["sampling_rate"]))
72
+ if embs:
73
+ embeddings[repo] = np.mean(embs, axis=0)
74
+ else:
75
+ errors.append(f"{repo}: no rows found")
76
+ except Exception as exc:
77
+ errors.append(f"{repo}: {exc}")
78
+
79
+ if not embeddings:
80
+ return pd.DataFrame(), "No embeddings extracted.", "\n".join(errors)
81
+
82
+ repo_names = list(embeddings.keys())
83
+ emb_matrix = np.stack([embeddings[r] for r in repo_names])
84
+ emb_matrix = emb_matrix / np.linalg.norm(emb_matrix, axis=1, keepdims=True)
85
+ sim_matrix = np.clip(emb_matrix @ emb_matrix.T, -1.0, 1.0)
86
+ dist_matrix = 1.0 - sim_matrix
87
+ np.fill_diagonal(dist_matrix, 0.0)
88
+
89
+ n = len(repo_names)
90
+ if n == 1:
91
+ labels = [0]
92
+ else:
93
+ labels = AgglomerativeClustering(
94
+ n_clusters=None,
95
+ distance_threshold=1.0 - float(threshold),
96
+ metric="precomputed",
97
+ linkage="average",
98
+ ).fit_predict(dist_matrix).tolist()
99
+
100
+ rows = []
101
+ for i, repo in enumerate(repo_names):
102
+ cluster = labels[i]
103
+ same_idx = [j for j, l in enumerate(labels) if l == cluster and j != i]
104
+ intra_sim = float(np.mean([sim_matrix[i][j] for j in same_idx])) if same_idx else 1.0
105
+ other_sorted = sorted([j for j in range(n) if j != i], key=lambda j: -sim_matrix[i][j])
106
+ closest = (
107
+ f"{repo_names[other_sorted[0]].split('/')[-1]} ({sim_matrix[i][other_sorted[0]]:.2f})"
108
+ if other_sorted else "-"
109
+ )
110
+ rows.append({
111
+ "dataset": repo.split("/")[-1],
112
+ "speaker_id": f"speaker_{cluster + 1:02d}",
113
+ "books_with_speaker": sum(1 for l in labels if l == cluster),
114
+ "intra_sim": round(intra_sim, 3),
115
+ "closest_match": closest,
116
+ })
117
+
118
+ df = pd.DataFrame(rows).sort_values(["speaker_id", "dataset"]).reset_index(drop=True)
119
+ n_speakers = len(set(labels))
120
+ summary = f"✅ {len(repo_names)} books → {n_speakers} unique speakers"
121
+ return df, summary, "\n".join(errors) if errors else "None"
122
+
123
+
124
+ with gr.Blocks(title="Speaker Identifier") as demo:
125
+ gr.Markdown(
126
+ """
127
+ # 🎙️ Speaker Identifier
128
+ Extract speaker embeddings from HF audio datasets and cluster into unique speakers.
129
+ Uses **WavLM-Base+** (`microsoft/wavlm-base-plus-sv`) — language-agnostic, works for any language.
130
+ """
131
+ )
132
+ with gr.Row():
133
+ with gr.Column(scale=2):
134
+ repo_input = gr.Textbox(
135
+ label="Dataset repo IDs (one per line, owner/name)",
136
+ placeholder="fosters/some_audiobook_output\nfosters/another_audiobook_output",
137
+ lines=14,
138
+ )
139
+ with gr.Column(scale=1):
140
+ samples = gr.Slider(1, 10, value=3, step=1, label="Samples per book")
141
+ threshold = gr.Slider(
142
+ 0.60, 0.98, value=0.82, step=0.01,
143
+ label="Same-speaker threshold (cosine similarity)",
144
+ info="Higher = stricter matching, more clusters",
145
+ )
146
+ hf_token = gr.Textbox(
147
+ label="HF Token (only for private repos)",
148
+ type="password",
149
+ placeholder="hf_…",
150
+ )
151
+ run_btn = gr.Button("Identify Speakers", variant="primary")
152
+
153
+ summary_out = gr.Textbox(label="Summary", interactive=False)
154
+ table_out = gr.Dataframe(
155
+ label="Results — sorted by speaker_id",
156
+ headers=["dataset", "speaker_id", "books_with_speaker", "intra_sim", "closest_match"],
157
+ wrap=True,
158
+ )
159
+ errors_out = gr.Textbox(label="Errors / Skipped", interactive=False)
160
+
161
+ run_btn.click(
162
+ identify_speakers,
163
+ inputs=[repo_input, samples, threshold, hf_token],
164
+ outputs=[table_out, summary_out, errors_out],
165
+ )
166
+
167
+ demo.launch()