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:
2026-06-22 20:48:07 +08:00
parent 70dbb23234
commit 41f653b672
25 changed files with 1609 additions and 82 deletions
+123
View File
@@ -0,0 +1,123 @@
#!/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()