This repository has been archived on 2026-08-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Cyrene/scripts/voice/find_similar_voice.py
T
AskaEth 41f653b672 refactor: QQ → OBv11 重命名 + 平台格式统一抽象
- 所有对外称呼从 QQ 改为 OBv11(注释/提示词/日志/配置项)
- 新增 PlatformFormat 结构体,统一管理平台消息标记格式
- defaultPlatformFormats() 注册表替代硬编码 qqTargetRe
- extractProactiveMessage 改为 Thinker 方法,遍历格式注册表匹配
- 配置项重命名: QQ_BOT_PORT → OBV11_BOT_PORT, QQBotPort → OBv11BotPort
- 标记格式: 【QQ群聊】→【OBv11群聊】、【QQ私聊】→【OBv11私聊】

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-22 20:48:07 +08:00

131 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""
声纹聚类:使用 pitch + delta-MFCC + 谱特征找出相似声音。
"""
import os, sys, json, warnings
import numpy as np
import librosa
warnings.filterwarnings('ignore')
REF_FILE = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\cleaned\VoBanks29\VoBanks29_0036_001a1127.wav"
SEARCH_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\cleaned"
def extract_voiceprint(wav_path):
"""提取多维度声纹特征向量"""
try:
y, sr = librosa.load(wav_path, sr=22050, mono=True)
if len(y) < sr * 0.3:
return None
# 1. Pitch (F0) 统计 — 最区分说话人的特征
f0, voiced_flag, _ = librosa.pyin(y, fmin=80, fmax=600, sr=sr)
f0 = f0[~np.isnan(f0)]
if len(f0) < 10:
return None
pitch_features = [
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),
]
# 2. MFCC delta (语音动态特征)
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
mfcc_delta = librosa.feature.delta(mfcc)
mfcc_delta2 = librosa.feature.delta(mfcc, order=2)
mfcc_features = np.concatenate([
np.mean(mfcc, axis=1), np.std(mfcc, axis=1),
np.mean(mfcc_delta, axis=1), np.std(mfcc_delta, axis=1),
np.mean(mfcc_delta2, axis=1), np.std(mfcc_delta2, axis=1),
])
# 3. 频谱特征
spectral = librosa.feature.spectral_centroid(y=y, sr=sr)
rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)
spec_features = [
np.mean(spectral), np.std(spectral),
np.mean(rolloff), np.std(rolloff),
]
return np.concatenate([pitch_features, mfcc_features, spec_features])
except Exception:
return None
def cosine_sim(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8)
print("提取参考音频特征...")
ref_feat = extract_voiceprint(REF_FILE)
if ref_feat is None:
print("错误: 无法提取参考音频特征")
sys.exit(1)
print(f" 特征维度: {len(ref_feat)}")
print(f" Pitch: mean={ref_feat[0]:.1f}Hz std={ref_feat[1]:.1f}Hz")
# 收集文件
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"\n搜索范围: {len(wav_files)} 个 VoBanks 文件\n")
# 提取并比较
results = []
pitch_stats = []
for i, wav in enumerate(wav_files):
feat = extract_voiceprint(wav)
if feat is not None:
sim = cosine_sim(ref_feat, feat)
results.append((sim, wav, feat[0])) # feat[0] = mean pitch
pitch_stats.append(feat[0])
if (i + 1) % 200 == 0:
print(f" 进度: {i+1}/{len(wav_files)}")
results.sort(key=lambda x: x[0], reverse=True)
# 全局音高分布
all_pitches = [r[2] for r in results]
print(f"\n全局音高: mean={np.mean(all_pitches):.0f}Hz, 范围=[{np.min(all_pitches):.0f}, {np.max(all_pitches):.0f}]")
print(f"参考音高: {ref_feat[0]:.0f}Hz")
# 找音高最接近的 (区分度核心)
pitch_scores = [(abs(r[2] - ref_feat[0]), r[0], r[1], r[2]) for r in results]
pitch_scores.sort()
print(f"\n=== 音高最接近的 Top 20 (参考={ref_feat[0]:.0f}Hz) ===")
for rank, (pdiff, sim, path, pitch) in enumerate(pitch_scores[:20], 1):
fname = os.path.basename(path)
parent = os.path.basename(os.path.dirname(path))
marker = "" if sim > 0.98 else ""
print(f" {rank:2d}. [{pitch:.0f}Hz Δ={pdiff:.0f} sim={sim:.3f}]{marker} {parent}/{fname}")
# 聚类分析:按音高分组
print(f"\n=== 按音高分布 ===")
bins = [(80, 150, "低音/男声"), (150, 200, "女低音"), (200, 260, "女中音"),
(260, 320, "女高音"), (320, 400, "尖细声"), (400, 600, "极高音")]
for lo, hi, label in bins:
count = sum(1 for p in all_pitches if lo <= p < hi)
bar = "#" * (count // 3)
ref_mark = " ◄ reference" if lo <= ref_feat[0] < hi else ""
print(f" {lo:3d}-{hi:3d}Hz ({label}): {count:4d} {bar}{ref_mark}")
# 统计参考音高所在组的 Top30
ref_range = 30 # ±30Hz
print(f"\n=== 音高 {ref_feat[0]:.0f}±{ref_range}Hz 内的文件 ===")
close = [(s, p, os.path.basename(p2), os.path.basename(os.path.dirname(p2)))
for s, p2, p in results if abs(p - ref_feat[0]) < ref_range]
close.sort(key=lambda x: x[0], reverse=True)
for rank, (sim, path, fname, parent) in enumerate(close[:30], 1):
print(f" {rank:2d}. [{sim:.4f}] {parent}/{fname}")
print(f" ... 共 {len(close)} 个文件在 ±{ref_range}Hz 范围内")
# 保存
out = os.path.join(os.path.dirname(REF_FILE), 'voice_cluster_results.json')
with open(out, 'w') as f:
json.dump([(float(s), p, float(pp)) for s, p, pp in results], f)
print(f"\n结果已保存: {out}")