Spaces:
Sleeping
Sleeping
File size: 8,604 Bytes
c4a7a0e 4dc7304 cdfb7d6 875a0a4 cdfb7d6 4dc7304 cdfb7d6 c4a7a0e 4dc7304 c4a7a0e 5e96656 cdfb7d6 4dc7304 5e96656 4dc7304 5e96656 4dc7304 5e96656 4dc7304 5e96656 4dc7304 5e96656 cdfb7d6 4dc7304 875a0a4 4dc7304 c4a7a0e 4dc7304 cdfb7d6 c4a7a0e 4dc7304 c4a7a0e 4dc7304 5e96656 4dc7304 5e96656 4dc7304 cdfb7d6 4dc7304 5e96656 4dc7304 cdfb7d6 4dc7304 cdfb7d6 5e96656 4dc7304 5e96656 cdfb7d6 5e96656 4dc7304 5e96656 cdfb7d6 5e96656 4dc7304 5e96656 4dc7304 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | """
Extract embeddings for Arabic Native vs Non-Native classifier.
Emphasizes PROSODIC features: pitch, rhythm, timing, fluency — key for Native vs Non-Native.
Uses XLS-R 53 EARLY-TO-MID layers (4,6,8,10): prosody/speaker-rich, less lexical than final layer.
"""
import pandas as pd
import numpy as np
import torch
import librosa
import os
from transformers import Wav2Vec2Model, AutoFeatureExtractor
from tqdm import tqdm
from scipy.signal import butter, lfilter
# XLS-R 53 (multilingual, includes Arabic)
# model_name = "facebook/wav2vec2-large-xlsr-53"
model_name = "facebook/wav2vec2-lv-60-espeak-cv-ft"
# Prosody-focused layers: early-mid (4,6,8,10) encode pitch, rhythm, timing, speaker
# Final layers encode lexicon; we want prosodic cues for nativeness
PROSODY_LAYERS = (4, 6, 8, 10)
# Lazy-loaded so "from extract_embeddings import get_mfcc_from_audio, ..." doesn't load the big model (avoids 2x GPU OOM in compare_models_on_test)
_feature_extractor = None
_model = None
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def apply_whisper_filter(audio_array, sr=16000, cutoff_hz=700):
"""
Applies a High-Pass Butterworth filter to strip fundamental pitch
and isolate high-frequency phonetic articulation.
"""
nyq = 0.5 * sr
normal_cutoff = cutoff_hz / nyq
b, a = butter(5, normal_cutoff, btype="high", analog=False)
whispered_audio = lfilter(b, a, audio_array)
return whispered_audio.astype(np.float32)
def _get_wav2vec():
global _feature_extractor, _model
if _model is None:
_feature_extractor = AutoFeatureExtractor.from_pretrained(model_name)
_model = Wav2Vec2Model.from_pretrained(model_name)
_model.eval()
_model.to(device)
return _feature_extractor, _model
# Embedding dims: paper uses mean + std + F0 → 1024+1024+3 = 2051; optional MFCC → 2090
WAV2VEC_DIM = 1024
PITCH_DIM = 3 # mean, std, max of F0 (voiced)
PROSODY_EMBED_DIM = WAV2VEC_DIM * 2 + PITCH_DIM # 2051 (mean + std + pitch)
MFCC_DIM = 39 # 13 + 13 + 13
EMBED_DIM_WITH_MFCC = PROSODY_EMBED_DIM + MFCC_DIM # 2090
def get_pitch_stats(speech, sr=16000, fmin=50, fmax=500):
"""F0 (pitch) stats for prosody: mean, std, max over voiced frames (paper: explicit pitch helps dialect/nativity)."""
try:
f0 = librosa.yin(
speech.astype(np.float64), fmin=float(fmin), fmax=float(fmax), sr=sr
)
voiced = (f0 > 0) & np.isfinite(f0)
if np.any(voiced):
f0_v = f0[voiced]
return np.array(
[float(np.mean(f0_v)), float(np.std(f0_v)), float(np.max(f0_v))],
dtype=np.float32,
)
except Exception:
pass
return np.zeros(PITCH_DIM, dtype=np.float32)
def get_mfcc_from_audio(speech, sr=16000, n_mfcc=13):
"""Classical 39-d feature: MFCC mean + delta + delta2 (paper: MFCC >> LPCC/PLP for accent)."""
mfcc = librosa.feature.mfcc(y=speech, sr=sr, n_mfcc=n_mfcc)
mfcc_mean = mfcc.mean(axis=1)
mfcc_delta = librosa.feature.delta(mfcc).mean(axis=1)
mfcc_delta2 = librosa.feature.delta(mfcc, order=2).mean(axis=1)
return np.concatenate([mfcc_mean, mfcc_delta, mfcc_delta2]).astype(np.float32)
def get_embeddings(audio_path, l2_norm=True, return_audio=False):
"""Prosody embedding: wav2vec mean + std (temporal variance) + F0 stats (pitch) → 2051-d.
Paper: explicit pitch and time-derivatives (std) help dialect/nativity. Optional return_audio for MFCC."""
feature_extractor, model = _get_wav2vec()
try:
speech, sr = librosa.load(audio_path, sr=16000)
# 1. Calculate REAL pitch from the unfiltered audio
pitch = get_pitch_stats(speech, sr=sr)
# 2. Apply WHISPER FILTER to blind the model to speaker identity
# whispered_speech = apply_whisper_filter(speech, sr=sr)
whispered_speech = speech
# 3. Pass the WHISPERED speech to Wav2Vec2
input_values = feature_extractor(
whispered_speech, return_tensors="pt", sampling_rate=16000
).input_values.to(device)
with torch.no_grad():
outputs = model(input_values, output_hidden_states=True)
h = outputs.hidden_states
num_layers = len(h) - 1
layer_idx = [min(max(l, 1), num_layers) for l in PROSODY_LAYERS]
embs = [h[i] for i in layer_idx]
# (1, T, 1024) after stack+mean over layers; keep time for mean+std
time_seq = torch.stack(embs, dim=0).mean(dim=0).squeeze(0) # (T, 1024)
emb_mean = time_seq.mean(dim=0).cpu().numpy().astype(np.float32)
emb_std = time_seq.std(dim=0).cpu().numpy().astype(np.float32)
np.nan_to_num(emb_std, copy=False, nan=0.0, posinf=0.0, neginf=0.0)
emb = np.concatenate([emb_mean, emb_std, pitch])
if l2_norm:
n = np.linalg.norm(emb)
if n > 1e-8:
emb = emb / n
if return_audio:
# Return the WHISPERED speech so the MFCC extractor also uses the filtered version
return emb, whispered_speech, sr
return emb
except Exception as e:
print(f"Error on {audio_path}: {e}")
return None
# CLI: optional (manifest_in, embedding_dir, manifest_out) for e.g. Iqra data
if __name__ == "__main__":
import argparse
ap = argparse.ArgumentParser(
description="Extract XLS-R prosody embeddings from chunk manifest."
)
ap.add_argument(
"--manifest",
default="manifests/processed_manifest.csv",
help="Input manifest CSV (chunk_path, original_id, label, language)",
)
ap.add_argument(
"--output-dir", default="embeddings", help="Folder for .npy embeddings"
)
ap.add_argument(
"--output-manifest",
default=None,
help="Output manifest CSV (default: final_training_manifest.csv or derived from --manifest)",
)
ap.add_argument(
"--mfcc",
action="store_true",
help="Append 39-d MFCC → 2090-d (default prosody is 2051-d: mean+std+pitch)",
)
args = ap.parse_args()
manifest_path = args.manifest
output_folder = args.output_dir
use_mfcc = args.mfcc
if args.output_manifest:
output_manifest_path = args.output_manifest
else:
# Main pipeline: processed_manifest -> final_training_manifest; else e.g. iqra_processed_manifest -> iqra_embeddings_manifest
if manifest_path.strip() == "manifests/processed_manifest.csv":
output_manifest_path = "manifests/final_training_manifest.csv"
else:
d = os.path.dirname(manifest_path)
base = (
os.path.basename(manifest_path)
.replace(".csv", "")
.replace("_processed", "_embeddings")
)
output_manifest_path = os.path.join(d, base + ".csv")
os.makedirs(output_folder, exist_ok=True)
df = pd.read_csv(manifest_path)
embedding_paths = []
feat_desc = f"prosody mean+std+pitch ({PROSODY_EMBED_DIM}d)" + (
f" + MFCC({MFCC_DIM}d)" if use_mfcc else ""
)
print(f"Extracting embeddings for {len(df)} chunks ({feat_desc}, L2-normalized)...")
for idx, row in tqdm(df.iterrows(), total=len(df)):
if use_mfcc:
out = get_embeddings(row["chunk_path"], l2_norm=False, return_audio=True)
if out is None:
embedding_paths.append(None)
continue
emb, speech, sr = out
mfcc_39 = get_mfcc_from_audio(speech, sr=sr)
emb = np.concatenate([emb, mfcc_39]).astype(np.float32)
n = np.linalg.norm(emb)
if n > 1e-8:
emb = emb / n
else:
emb = get_embeddings(row["chunk_path"], l2_norm=True)
if emb is not None:
emb_filename = os.path.basename(row["chunk_path"]).replace(".wav", ".npy")
emb_path = os.path.join(output_folder, emb_filename)
np.save(emb_path, emb)
embedding_paths.append(emb_path)
else:
embedding_paths.append(None)
df["embedding_path"] = embedding_paths
df.dropna(subset=["embedding_path"], inplace=True)
df.to_csv(output_manifest_path, index=False)
embed_dim = EMBED_DIM_WITH_MFCC if use_mfcc else PROSODY_EMBED_DIM
dim_file = os.path.join(os.path.dirname(output_manifest_path), "embedding_dim.txt")
with open(dim_file, "w") as f:
f.write(str(embed_dim))
print(
f"Done! Embeddings in {output_folder}, manifest at {output_manifest_path}, dim={embed_dim} (saved to {dim_file})."
)
|