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,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()
|
||||
Reference in New Issue
Block a user