#!/usr/bin/env python3 """ 用 Group 01 的 33 个文件作为声纹模板,在全量 11K 文件中搜昔涟。 """ import os, sys, json, warnings import numpy as np import librosa warnings.filterwarnings('ignore') SEARCH_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\cleaned" OUTPUT_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\cyrene_voice" # Group 01 的文件列表 (从聚类结果获取) GROUP01_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\cleaned" # 我们需要重建 Group 01 的成员——从之前的聚类结果 # 先手动提取 Group 01 所有文件 # 最简单: 用聚类 center 最近的 N 个文件 print("Step 1: 重建 Group 01 成员...") import subprocess # Re-run clustering focused on VoBanks to get exact Group 01 members VOICEPRINTS = {} ref_files = [] print(" 提取所有 VoBanks 声纹...") wav_files = [] for root, dirs, files in os.walk(SEARCH_DIR): for f in files: if f.endswith('.wav') and 'VoBanks' in root: wav_files.append(os.path.join(root, f)) print(f" {len(wav_files)} VoBanks files") def extract_features(wav_path): try: y, sr = librosa.load(wav_path, sr=22050, mono=True) if len(y) < sr * 0.3: 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=13) mfcc_d = librosa.feature.delta(mfcc) feat = np.concatenate([ [np.mean(f0), np.std(f0), np.percentile(f0, 25), np.percentile(f0, 50), np.percentile(f0, 75)], np.mean(mfcc, axis=1), np.std(mfcc, axis=1), np.mean(mfcc_d, axis=1), np.std(mfcc_d, axis=1), ]) return feat.astype(np.float64) except: return None features = [] valid_files = [] for i, wav in enumerate(wav_files): feat = extract_features(wav) if feat is not None: valid_files.append(wav) features.append(feat) if (i+1) % 200 == 0: print(f" {i+1}/{len(wav_files)}") X = np.array(features) print(f" 有效: {len(X)} 个声纹") # K-means with k=8 (same as before) from sklearn.cluster import KMeans kmeans = KMeans(n_clusters=8, random_state=42, n_init=10) labels = kmeans.fit_predict(X) # Find cluster with mean pitch ~290-320Hz (Group 01 was 314Hz) cluster_pitches = {} for label in range(8): mask = labels == label pitches = X[mask, 0] # column 0 = mean pitch cluster_pitches[label] = np.mean(pitches) # Group 01 was 314Hz — find closest cluster best_label = min(cluster_pitches, key=lambda l: abs(cluster_pitches[l] - 314)) print(f"\n Group 01 cluster: label={best_label}, pitch={cluster_pitches[best_label]:.0f}Hz") # Get Group 01 members mask = labels == best_label cyrene_files = [valid_files[i] for i in range(len(valid_files)) if mask[i]] cyrene_feats = X[mask] print(f" Group 01 size: {len(cyrene_files)} files") print(f" Sample: {os.path.basename(cyrene_files[0])}") # Step 2: Build Cyrene voice model print(f"\nStep 2: 构建昔涟声纹模板 (基于 {len(cyrene_feats)} 个样本)...") cyrene_center = np.mean(cyrene_feats, axis=0) print(f" 模板音高: {cyrene_center[0]:.0f}Hz") def cosine_sim(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8) # Step 3: Search ALL files print("\nStep 3: 全量搜索...") 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)) print(f" 搜索范围: {len(all_wavs)} 个 WAV 文件") results = [] for i, wav in enumerate(all_wavs): feat = extract_features(wav) if feat is not None: sim = cosine_sim(cyrene_center, feat) pitch = feat[0] results.append((sim, pitch, wav)) if (i+1) % 2000 == 0: print(f" {i+1}/{len(all_wavs)}") results.sort(key=lambda x: x[0], reverse=True) # Step 4: Show results by source print(f"\n=== 昔涟声纹搜索结果 ===") print(f"Top 50 文件:") for rank, (sim, pitch, path) in enumerate(results[:50], 1): fname = os.path.basename(path) parent = os.path.basename(os.path.dirname(path)) print(f" {rank:2d}. [{sim:.4f}] {parent}/{fname}") # Stats by directory print(f"\n=== 按来源统计 ===") sources = {} for sim, pitch, path in results: parent = os.path.basename(os.path.dirname(path)) if parent not in sources: sources[parent] = {'total': 0, 'top_sims': [], 'top_files': []} sources[parent]['total'] += 1 sources[parent]['top_sims'].append(sim) sources[parent]['top_files'].append((sim, os.path.basename(path))) for src in sorted(sources.keys()): s = sources[src] top5_avg = np.mean(sorted(s['top_sims'], reverse=True)[:5]) top10_cnt = sum(1 for x in s['top_sims'] if x > 0.92) print(f" {src}: total={s['total']}, top5_avg={top5_avg:.4f}, high_match(>0.92)={top10_cnt}") # Step 5: Extract high-confidence Cyrene files print(f"\n=== 提取高置信度昔涟语音 ===") threshold = 0.92 high_conf = [(s, p, w) for s, p, w in results if s > threshold] print(f" 阈值 >{threshold}: {len(high_conf)} 个文件") os.makedirs(OUTPUT_DIR, exist_ok=True) for sim, pitch, path in high_conf: fname = os.path.basename(path) dst = os.path.join(OUTPUT_DIR, fname) if not os.path.exists(dst): try: import shutil shutil.copy2(path, dst) except: pass print(f" 已复制到: {OUTPUT_DIR}") print(f" 实际文件数: {len(os.listdir(OUTPUT_DIR))}") # Save results with open(os.path.join(OUTPUT_DIR, 'search_results.json'), 'w') as f: json.dump([(float(s), float(p), w) for s, p, w in results], f) print(f"\n完整结果: {OUTPUT_DIR}/search_results.json")