Download scripts/extract_features.py from AksharaSharma/voice-authenticity-openenv: direct link, hf CLI and curl.
- Browser
- Download file 14.5 kB
-
https://huggingface.co/spaces/AksharaSharma/voice-authenticity-openenv/resolve/main/scripts/extract_features.py
- Command line
-
hf download hf://spaces/AksharaSharma/voice-authenticity-openenv/scripts/extract_features.py
-
curl -L -o extract_features.py https://huggingface.co/spaces/AksharaSharma/voice-authenticity-openenv/resolve/main/scripts/extract_features.py
14.5 kB
| import numpy as np | |
| import librosa | |
| import parselmouth | |
| from parselmouth.praat import call | |
| import os | |
| import warnings | |
| warnings.filterwarnings("ignore") | |
| REAL_DIR = "data/real" | |
| FAKE_DIR = "data/fake" | |
| OUTPUT_DIR = "environment/data" | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| def extract_features(file_path): | |
| """ | |
| Extract 48-dim feature vector from audio file. | |
| Returns None if file fails. | |
| """ | |
| try: | |
| # Load audio | |
| y, sr = librosa.load(file_path, sr=16000, duration=5.0) | |
| if len(y) < 1600: # skip clips shorter than 0.1s | |
| return None | |
| # ββ MFCC (40 features) ββββββββββββββββββββββββββββββ | |
| mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20) | |
| mfcc_mean = mfcc.mean(axis=1) # 20 values | |
| mfcc_std = mfcc.std(axis=1) # 20 values | |
| # ββ Spectral features (2 features) ββββββββββββββββββ | |
| zcr = librosa.feature.zero_crossing_rate(y).mean() | |
| spec_centroid = librosa.feature.spectral_centroid( | |
| y=y, sr=sr).mean() | |
| # ββ Voice authenticity features (3 features) ββββββββ | |
| # These are the KEY discriminators between real and fake | |
| try: | |
| snd = parselmouth.Sound(file_path) | |
| pp = call(snd, "To PointProcess (periodic, cc)", 75, 500) | |
| jitter = call( | |
| pp, "Get jitter (local)", 0, 0, 0.0001, 0.02, 1.3 | |
| ) | |
| shimmer = call( | |
| [snd, pp], "Get shimmer (local)", | |
| 0, 0, 0.0001, 0.02, 1.3, 1.6 | |
| ) | |
| harmonicity = call( | |
| snd, "To Harmonicity (cc)", 0.01, 75, 0.1, 1.0 | |
| ) | |
| hnr = call(harmonicity, "Get mean", 0, 0) | |
| # Replace NaN/inf with 0 | |
| jitter = float(jitter) if np.isfinite(jitter) else 0.0 | |
| shimmer = float(shimmer) if np.isfinite(shimmer) else 0.0 | |
| hnr = float(hnr) if np.isfinite(hnr) else 0.0 | |
| except Exception: | |
| jitter, shimmer, hnr = 0.0, 0.0, 0.0 | |
| # ββ Compression artifact features (3 features) ββββββ | |
| # Simulates codec degradation for task 2 | |
| spec_bandwidth = librosa.feature.spectral_bandwidth( | |
| y=y, sr=sr).mean() | |
| spec_rolloff = librosa.feature.spectral_rolloff( | |
| y=y, sr=sr).mean() | |
| rms = librosa.feature.rms(y=y).mean() | |
| # ββ Assemble final 48-dim vector βββββββββββββββββββββ | |
| features = np.concatenate([ | |
| mfcc_mean, # 0-19 | |
| mfcc_std, # 20-39 | |
| [zcr, spec_centroid], # 40-41 | |
| [jitter, shimmer, hnr], # 42-44 | |
| [spec_bandwidth, spec_rolloff, rms] # 45-47 | |
| ]) | |
| return features.astype(np.float32) | |
| except Exception as e: | |
| print(f" ERROR on {file_path}: {e}") | |
| return None | |
| def process_directory(directory, label, desc): | |
| files = [ | |
| f for f in os.listdir(directory) | |
| if f.endswith((".wav", ".flac", ".mp3")) | |
| ] | |
| print(f"\nProcessing {desc}: {len(files)} files found") | |
| features_list = [] | |
| labels_list = [] | |
| failed = 0 | |
| for i, fname in enumerate(files): | |
| path = os.path.join(directory, fname) | |
| feat = extract_features(path) | |
| if feat is not None: | |
| features_list.append(feat) | |
| labels_list.append(label) | |
| if (i + 1) % 50 == 0: | |
| print(f" {i+1}/{len(files)} done...") | |
| else: | |
| failed += 1 | |
| print(f" Success: {len(features_list)}, Failed: {failed}") | |
| return features_list, labels_list | |
| def add_compression_artifacts(features, strength=0.3): | |
| """Simulate codec compression degradation.""" | |
| degraded = features.copy() | |
| degraded[20:40] *= (1 - strength * np.random.uniform(0.5, 1.0, 20)) | |
| degraded[42] *= (1 - strength * np.random.uniform(0.3, 0.7)) | |
| degraded[43] *= (1 - strength * np.random.uniform(0.3, 0.7)) | |
| degraded[44] *= (1 + strength * np.random.uniform(0.1, 0.4)) | |
| degraded[45] *= (1 + strength * np.random.uniform(0.3, 0.8)) | |
| degraded[46] *= (1 - strength * np.random.uniform(0.2, 0.6)) | |
| degraded[47] += strength * np.random.uniform(0.1, 0.4) | |
| return degraded | |
| def add_adversarial_perturbation(features, label): | |
| """ | |
| True adversarial: create overlapping distributions. | |
| Fake audio shifted INTO real speech range. | |
| Real audio shifted TOWARD synthetic range. | |
| No clean threshold can separate them. | |
| """ | |
| perturbed = features.copy() | |
| if label == 1: # fake β make it look real | |
| # Push jitter into real range | |
| perturbed[42] += np.random.uniform(0.010, 0.025) | |
| # Push shimmer into real range | |
| perturbed[43] += np.random.uniform(0.020, 0.060) | |
| # Lower HNR toward real range | |
| perturbed[44] -= np.random.uniform(2.0, 5.0) | |
| # Add slight MFCC variation | |
| perturbed[20:30] += np.random.normal(0, 0.3, 10) | |
| elif label == 0: # real β push toward synthetic range | |
| # Suppress jitter slightly | |
| perturbed[42] *= np.random.uniform(0.6, 0.85) | |
| # Suppress shimmer slightly | |
| perturbed[43] *= np.random.uniform(0.6, 0.85) | |
| # Raise HNR slightly | |
| perturbed[44] += np.random.uniform(0.5, 2.0) | |
| # Add 8% label noise β some samples are deliberately mislabeled | |
| # to simulate real-world distribution ambiguity | |
| if np.random.random() < 0.08: | |
| perturbed += np.random.normal(0, 0.5, len(perturbed)) | |
| return perturbed | |
| def add_streaming_degradation(features, label): | |
| """Simulate streaming/partial decode degradation. | |
| Models real-time audio streaming where features are partially decoded: | |
| - MFCC values slightly degraded (simulating partial frame decode) | |
| - Temporal features intact but with mild additive noise | |
| - High-frequency spectral features mildly rolled off | |
| - Overall mild Gaussian noise across all dims | |
| This is the base perturbation for Task 4 (streaming_detection). | |
| The environment also applies step-dependent soft-gated noise at runtime. | |
| """ | |
| degraded = features.copy() | |
| # Partial MFCC decode: higher-order coefficients more degraded | |
| for i in range(20): | |
| degradation = 0.02 * (i / 20) # more degradation on higher coeffs | |
| degraded[i] += np.random.normal(0, degradation + 0.01) | |
| for i in range(20, 40): | |
| degradation = 0.03 * ((i - 20) / 20) | |
| degraded[i] *= (1 - degradation * np.random.uniform(0.3, 0.8)) | |
| # Mild noise on temporal features | |
| degraded[42] += np.random.normal(0, 0.003) # jitter noise | |
| degraded[43] += np.random.normal(0, 0.008) # shimmer noise | |
| degraded[44] += np.random.normal(0, 0.5) # HNR noise | |
| # Mild spectral rolloff | |
| degraded[41] *= np.random.uniform(0.92, 0.98) # spectral centroid | |
| degraded[45] *= np.random.uniform(0.90, 0.97) # spectral bandwidth | |
| degraded[46] *= np.random.uniform(0.88, 0.96) # spectral rolloff | |
| # Global mild noise | |
| degraded += np.random.normal(0, 0.015, len(degraded)) | |
| return degraded | |
| def add_phonecall_degradation(features, label): | |
| """Simulate phone call conditions: heavy codec + background noise. | |
| Models the worst-case real-world scenario: | |
| - Aggressive codec compression (narrowband telephony) | |
| - Background noise injection across all bands | |
| - Severe HNR degradation (noisy channel) | |
| - MFCC high-frequency rolloff (narrowband 300-3400Hz telephony) | |
| - RMS energy fluctuation (network jitter/packet loss) | |
| - Jitter/shimmer partially masked by channel noise | |
| This is the hardest task β designed to be near the limit of detectability. | |
| """ | |
| degraded = features.copy() | |
| # ββ Heavy codec compression (narrowband telephony simulation) βββ | |
| # MFCC means: zero out high-order coefficients (narrowband kills them) | |
| for i in range(12, 20): | |
| degraded[i] *= np.random.uniform(0.1, 0.4) # severe suppression | |
| # MFCC stds: flatten temporal variation (codec smoothing) | |
| degraded[20:40] *= np.random.uniform(0.3, 0.6, 20) | |
| # ββ Background noise injection ββββββββββββββββββββββββββββββββββ | |
| noise_strength = np.random.uniform(0.15, 0.35) | |
| degraded += np.random.normal(0, noise_strength, len(degraded)) | |
| # ββ Severe HNR degradation (noisy channel) βββββββββββββββββββββ | |
| degraded[44] -= np.random.uniform(3.0, 7.0) # massive HNR drop | |
| # ββ Jitter/shimmer partially masked by channel noise βββββββββββ | |
| degraded[42] += np.random.normal(0, 0.015) # large jitter noise | |
| degraded[43] += np.random.normal(0, 0.03) # large shimmer noise | |
| # ββ Spectral degradation (narrowband rolloff) ββββββββββββββββββ | |
| degraded[41] *= np.random.uniform(0.5, 0.75) # centroid drops | |
| degraded[45] *= np.random.uniform(0.4, 0.65) # bandwidth severely narrows | |
| degraded[46] *= np.random.uniform(0.3, 0.55) # rolloff drastically drops | |
| # ββ RMS energy fluctuation (packet loss / AGC) ββββββββββββββββββ | |
| degraded[47] *= np.random.uniform(0.5, 1.5) | |
| # ββ ZCR noise (transmission artifacts) ββββββββββββββββββββββββββ | |
| degraded[40] += np.random.normal(0, 0.02) | |
| return degraded | |
| def main(): | |
| print("=" * 60) | |
| print("Feature Extraction Pipeline (5 Tasks)") | |
| print("=" * 60) | |
| real_feat, real_labels = process_directory( | |
| REAL_DIR, label=0, desc="REAL audio" | |
| ) | |
| fake_feat, fake_labels = process_directory( | |
| FAKE_DIR, label=1, desc="FAKE audio" | |
| ) | |
| all_features = np.array(real_feat + fake_feat, dtype=np.float32) | |
| all_labels = np.array(real_labels + fake_labels, dtype=np.int32) | |
| idx = np.random.permutation(len(all_labels)) | |
| all_features = all_features[idx] | |
| all_labels = all_labels[idx] | |
| mean = all_features.mean(axis=0) | |
| std = all_features.std(axis=0) + 1e-8 | |
| all_features_norm = (all_features - mean) / std | |
| np.save(f"{OUTPUT_DIR}/features.npy", all_features_norm) | |
| # Save raw unnormalized features for env to use | |
| np.save(f"{OUTPUT_DIR}/features_raw.npy", all_features) | |
| np.save(f"{OUTPUT_DIR}/labels.npy", all_labels) | |
| np.save(f"{OUTPUT_DIR}/mean.npy", mean) | |
| np.save(f"{OUTPUT_DIR}/std.npy", std) | |
| print(f"\nTask 1 (clean): {len(all_labels)} samples saved") | |
| # ββ TASK 2: Compressed features βββββββββββββββββββββββββ | |
| raw_combined = real_feat + fake_feat | |
| compressed_features = np.array([ | |
| add_compression_artifacts(f, strength=0.3) | |
| for f in raw_combined | |
| ], dtype=np.float32) | |
| compressed_features = compressed_features[idx] | |
| compressed_norm = (compressed_features - mean) / std | |
| np.save(f"{OUTPUT_DIR}/features_compressed.npy", compressed_norm) | |
| np.save(f"{OUTPUT_DIR}/labels_compressed.npy", all_labels) | |
| print(f"Task 2 (compressed): {len(all_labels)} samples saved") | |
| # ββ TASK 3: Adversarial features ββββββββββββββββββββββββ | |
| raw_labels_combined = real_labels + fake_labels | |
| adversarial_features = np.array([ | |
| add_adversarial_perturbation(f, l) | |
| for f, l in zip(raw_combined, raw_labels_combined) | |
| ], dtype=np.float32) | |
| adversarial_features = adversarial_features[idx] | |
| adversarial_norm = (adversarial_features - mean) / std | |
| np.save(f"{OUTPUT_DIR}/features_adversarial.npy", adversarial_norm) | |
| np.save(f"{OUTPUT_DIR}/labels_adversarial.npy", all_labels) | |
| print(f"Task 3 (adversarial): {len(all_labels)} samples saved") | |
| # ββ TASK 4: Streaming features ββββββββββββββββββββββββββ | |
| streaming_features = np.array([ | |
| add_streaming_degradation(f, l) | |
| for f, l in zip(raw_combined, raw_labels_combined) | |
| ], dtype=np.float32) | |
| streaming_features = streaming_features[idx] | |
| streaming_norm = (streaming_features - mean) / std | |
| np.save(f"{OUTPUT_DIR}/features_streaming.npy", streaming_norm) | |
| np.save(f"{OUTPUT_DIR}/labels_streaming.npy", all_labels) | |
| print(f"Task 4 (streaming): {len(all_labels)} samples saved") | |
| # ββ TASK 5: Phone call features βββββββββββββββββββββββββ | |
| phonecall_features = np.array([ | |
| add_phonecall_degradation(f, l) | |
| for f, l in zip(raw_combined, raw_labels_combined) | |
| ], dtype=np.float32) | |
| phonecall_features = phonecall_features[idx] | |
| phonecall_norm = (phonecall_features - mean) / std | |
| np.save(f"{OUTPUT_DIR}/features_phonecall.npy", phonecall_norm) | |
| np.save(f"{OUTPUT_DIR}/labels_phonecall.npy", all_labels) | |
| print(f"Task 5 (phonecall): {len(all_labels)} samples saved") | |
| print(f"\n{'='*60}") | |
| print("DONE") | |
| print(f"Total samples : {len(all_labels)}") | |
| print(f"Real samples : {all_labels.tolist().count(0)}") | |
| print(f"Fake samples : {all_labels.tolist().count(1)}") | |
| print(f"Feature shape : {all_features_norm.shape}") | |
| print(f"{'='*60}") | |
| print("\nSanity check β jitter/shimmer/HNR comparison:") | |
| for i in range(min(2, len(all_labels))): | |
| raw_i = np.array(raw_combined)[idx][i] | |
| label_str = "REAL" if all_labels[i] == 0 else "FAKE" | |
| print(f"\n [{label_str}]") | |
| print(f" Clean β jitter={raw_i[42]:.4f} shimmer={raw_i[43]:.4f} hnr={raw_i[44]:.4f}") | |
| print(f" Compressed β jitter={compressed_features[i][42]:.4f} shimmer={compressed_features[i][43]:.4f} hnr={compressed_features[i][44]:.4f}") | |
| print(f" Adversarial β jitter={adversarial_features[i][42]:.4f} shimmer={adversarial_features[i][43]:.4f} hnr={adversarial_features[i][44]:.4f}") | |
| print(f" Streaming β jitter={streaming_features[i][42]:.4f} shimmer={streaming_features[i][43]:.4f} hnr={streaming_features[i][44]:.4f}") | |
| print(f" PhoneCall β jitter={phonecall_features[i][42]:.4f} shimmer={phonecall_features[i][43]:.4f} hnr={phonecall_features[i][44]:.4f}") | |
| if __name__ == "__main__": | |
| main() |