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>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
#!/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,找到昔涟的组,告诉我编号。")
|
||||
Reference in New Issue
Block a user