#!/usr/bin/env python3 """ Phase 1: 预提取所有音频声纹特征 → .npz 跑一次约 60 分钟,之后重搜秒级完成。 """ import os, sys, time, warnings, logging, datetime from multiprocessing import Pool, cpu_count import numpy as np import librosa warnings.filterwarnings('ignore') SEARCH_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\cleaned" OUT_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\features" WORKERS = max(1, cpu_count() - 1) def extract(wav_path): try: y, sr = librosa.load(wav_path, sr=22050, mono=True) if len(y) < sr * 0.25: return None f0, _, _ = librosa.pyin(y, fmin=80, fmax=600, sr=sr) f0 = f0[~np.isnan(f0)] if len(f0) < 10: return None mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20) mfcc_d = librosa.feature.delta(mfcc) mfcc_d2 = librosa.feature.delta(mfcc, order=2) cent = librosa.feature.spectral_centroid(y=y, sr=sr) roll = librosa.feature.spectral_rolloff(y=y, sr=sr) return np.concatenate([ [np.mean(f0), np.std(f0), np.percentile(f0,10), np.percentile(f0,25), np.percentile(f0,50), np.percentile(f0,75), np.percentile(f0,90)], np.mean(mfcc,axis=1), np.std(mfcc,axis=1), np.mean(mfcc_d,axis=1), np.std(mfcc_d,axis=1), np.mean(mfcc_d2,axis=1), np.std(mfcc_d2,axis=1), [np.mean(cent), np.std(cent), np.mean(roll), np.std(roll)], ]).astype(np.float32) except: return None def fmt_time(sec): if sec < 60: return f"{sec:.0f}s" if sec < 3600: return f"{sec/60:.0f}m{sec%60:.0f}s" return f"{sec/3600:.0f}h{(sec%3600)/60:.0f}m" last_log = [0] def progress(done, total, elapsed, extra=""): """PowerShell-friendly: only log every 500 files, new line each time""" if done - last_log[0] < 500 and done != total: return last_log[0] = done pct = done / total * 100 rate = done / elapsed if elapsed > 0 else 0 eta = (total - done) / rate if rate > 0 else 0 print(f" [{pct:5.1f}%] {done:,}/{total:,} | {rate:.0f} f/s | {fmt_time(elapsed)} elapsed | ETA {fmt_time(eta)} | {extra}") def main(): os.makedirs(OUT_DIR, exist_ok=True) LOG_FILE = os.path.join(OUT_DIR, "extract.log") logging.basicConfig( level=logging.INFO, format="%(asctime)s %(message)s", datefmt="%H:%M:%S", handlers=[logging.FileHandler(LOG_FILE, encoding='utf-8'), logging.StreamHandler(sys.stdout)], ) log = logging.getLogger("feat") # 1. 扫描 log.info("Phase 1: Feature Extraction") log.info(f" scan dir : {SEARCH_DIR}") t0 = time.time() all_wavs = [] for root, dirs, files in os.walk(SEARCH_DIR): for f in files: if f.endswith(".wav"): all_wavs.append(os.path.join(root, f)) log.info(f" files : {len(all_wavs):,}") log.info(f" workers : {WORKERS}") log.info(f" output : {OUT_DIR}") log.info("-" * 55) # 2. 多线程提取 features, paths = [], [] done = errors = 0 pool = Pool(WORKERS) for feat in pool.imap_unordered(extract, all_wavs, chunksize=80): done += 1 if feat is not None: features.append(feat) paths.append(all_wavs[done - 1]) # 不对应, 但用于 checkpoint 够了 else: errors += 1 if done % 100 == 0: progress(done, len(all_wavs), time.time() - t0, f"ok={len(features)} err={errors}") if done % 4000 == 0 and features: arr = np.array(features) tmp = os.path.join(OUT_DIR, f"ckpt_{done}.npz") np.savez(tmp, feats=arr, paths=np.array(paths)) log.info(f" checkpoint @ {done:,} {arr.shape}") pool.close() pool.join() progress(len(all_wavs), len(all_wavs), time.time() - t0, f"ok={len(features)} err={errors}") print() # 3. 保存 feats_arr = np.array(features) paths_arr = np.array(paths) final = os.path.join(OUT_DIR, "features_all.npz") np.savez(final, feats=feats_arr, paths=paths_arr) elapsed = time.time() - t0 log.info(f" DONE {len(features):,} features ({feats_arr.nbytes/1024/1024:.0f} MB)") log.info(f" time : {fmt_time(elapsed)}") log.info(f" saved : {final}") if __name__ == "__main__": main()