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/cluster_voices.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

89 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""K-means 聚类自动分组声纹,每组抽 1 个样本供试听确认"""
import os, sys, json, warnings, shutil
import numpy as np
import librosa
from sklearn.cluster import KMeans
warnings.filterwarnings('ignore')
SEARCH_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\cleaned"
SAMPLE_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\voice_samples"
N_CLUSTERS = 8 # 预期 3-5 个女声 + 若干男声/杂音组
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.float32)
except:
return None
print("提取声纹特征...")
wav_files = []
features = []
for root, dirs, files in os.walk(SEARCH_DIR):
for f in files:
if f.endswith('.wav') and 'VoBanks' in root:
path = os.path.join(root, f)
feat = extract_features(path)
if feat is not None:
wav_files.append(path)
features.append(feat)
features = np.array(features)
print(f" 有效文件: {len(features)}")
# K-means 聚类
print(f"\nK-means 聚类 (k={N_CLUSTERS})...")
kmeans = KMeans(n_clusters=N_CLUSTERS, random_state=42, n_init=10)
labels = kmeans.fit_predict(features)
# 统计每组
clusters = {}
for i, (label, path) in enumerate(zip(labels, wav_files)):
if label not in clusters:
clusters[label] = []
clusters[label].append((path, features[i]))
# 每组选最接近中心的样本
print(f"\n=== 聚类结果 ===\n")
for label in sorted(clusters.keys()):
group = clusters[label]
center = kmeans.cluster_centers_[label]
# 找离中心最近的
best_idx = min(range(len(group)), key=lambda i: np.linalg.norm(group[i][1] - center))
best_path = group[best_idx][0]
# 统计音高
pitches = [g[1][0] for g in group] # mean pitch
avg_pitch = np.mean(pitches)
voice_type = "" if avg_pitch < 170 else ""
print(f" Group {label+1}: {len(group):4d} files, pitch={avg_pitch:.0f}Hz ({voice_type}), "
f"sample: {os.path.basename(best_path)}")
# 复制每组样本到样本目录
os.makedirs(SAMPLE_DIR, exist_ok=True)
for label in sorted(clusters.keys()):
group = clusters[label]
center = kmeans.cluster_centers_[label]
best_idx = min(range(len(group)), key=lambda i: np.linalg.norm(group[i][1] - center))
src = group[best_idx][0]
dst = os.path.join(SAMPLE_DIR, f"group_{label+1:02d}_{os.path.basename(src)}")
shutil.copy2(src, dst)
print(f"\n每组样本已复制到: {SAMPLE_DIR}")
print("试听每个 group_*.wav,找到昔涟的组,告诉我编号。")