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,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Phase 2: 加载预提取特征 → 秒级搜索昔涟。
|
||||
前提: 先跑完 extract_features.py
|
||||
用法: python search_cyrene_v2.py
|
||||
"""
|
||||
import os, sys, shutil, json, time, logging
|
||||
import numpy as np
|
||||
|
||||
FEAT_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\features"
|
||||
OUT_DIR = r"D:\Project\Code\Uni\Cyrene-Voice-Model\data\cyrene_round2"
|
||||
FEAT_FILE = os.path.join(FEAT_DIR, "features_all.npz")
|
||||
|
||||
# ── 用户确认的昔涟样本 (53 个) ──
|
||||
CONFIRMED = [
|
||||
"External_del_3.5_chapter_2/External_del_3.5_chapter_2_0358_02ff22a9.wav",
|
||||
"External_del_3.5_chapter_2/External_del_3.5_chapter_2_0369_0315d90a.wav",
|
||||
"External_del_3.5_chapter_2/External_del_3.5_chapter_2_0370_0317b5bf.wav",
|
||||
"External_del_3.5_chapter_2/External_del_3.5_chapter_2_0391_033cbeea.wav",
|
||||
"External_del_3.5_chapter_2/External_del_3.5_chapter_2_0392_033d9e2e.wav",
|
||||
"External_del_3.5_chapter_2/External_del_3.5_chapter_2_0398_0346fa30.wav",
|
||||
"External_del_3.5_chapter_2/External_del_3.5_chapter_2_0417_0367cbc5.wav",
|
||||
"External_del_3.5_chapter_2/External_del_3.5_chapter_2_0330_02cb864d.wav",
|
||||
"External_del_3.5_chapter_2/External_del_3.5_chapter_2_0357_02fd4ab1.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0106_009dfe25.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0107_009fba53.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0112_00a89eb2.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0124_00c08c68.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0128_00c6ba19.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0365_026cf0de.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0371_0278f79f.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0374_02803ab5.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0409_02bcd547.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0410_02bec9f4.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0437_02e57ff0.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0440_02e7d98f.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0467_031daa9e.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0102_0095ba46.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0109_00a458cc.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0126_00c44990.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0130_00c95298.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0359_02670f55.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0364_026b8023.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0366_026f3922.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0369_0274d62a.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0372_027b67ef.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0392_029e92a6.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0413_02c1a418.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0415_02c466f1.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0417_02c8ffc0.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0430_02dbad24.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0431_02dda165.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0434_02e216cf.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0443_02eb0bc4.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0455_03048430.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0462_0316bcd9.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0463_03181c56.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0464_03195142.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0468_031e8bd6.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0469_03208c8a.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0484_03339e50.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0488_033a7664.wav",
|
||||
"External_del_3.4_chapter_0/External_del_3.4_chapter_0_0489_033b5c44.wav",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
LOG_FILE = os.path.join(OUT_DIR, "search.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("search")
|
||||
|
||||
log.info("Phase 2: Cyrene Voice Search (Round 2)")
|
||||
log.info(f" refs : {len(CONFIRMED)} confirmed samples")
|
||||
log.info(f" input: {FEAT_FILE}")
|
||||
|
||||
# 加载特征
|
||||
t0 = time.time()
|
||||
data = np.load(FEAT_FILE, allow_pickle=True)
|
||||
feats = data["feats"]
|
||||
paths = data["paths"]
|
||||
log.info(f" loaded: {len(feats):,} features ({feats.nbytes/1024/1024:.0f} MB) in {time.time()-t0:.1f}s")
|
||||
|
||||
# 构建参考模板
|
||||
ref_indices = []
|
||||
for i, p in enumerate(paths):
|
||||
for cf in CONFIRMED:
|
||||
if p.endswith(cf.replace("/", os.sep)):
|
||||
ref_indices.append(i)
|
||||
break
|
||||
|
||||
log.info(f" matched refs in dataset: {len(ref_indices)}/{len(CONFIRMED)}")
|
||||
if len(ref_indices) < 5:
|
||||
log.error(" too few refs matched, check paths!")
|
||||
return
|
||||
|
||||
template = np.mean(feats[ref_indices], axis=0)
|
||||
log.info(f" template pitch: {template[0]:.0f}Hz dim: {len(template)}")
|
||||
|
||||
# 全量比对 (向量化, 秒级)
|
||||
t1 = time.time()
|
||||
norm_feats = feats / (np.linalg.norm(feats, axis=1, keepdims=True) + 1e-8)
|
||||
norm_template = template / (np.linalg.norm(template) + 1e-8)
|
||||
sims = np.dot(norm_feats, norm_template)
|
||||
|
||||
pitches = feats[:, 0]
|
||||
penalty = 1.0 / (1.0 + np.abs(pitches - template[0]) / 100)
|
||||
combos = sims * 0.6 + penalty * 0.4
|
||||
log.info(f" compared {len(combos):,} vectors in {time.time()-t1:.1f}s")
|
||||
|
||||
# 排序
|
||||
order = np.argsort(-combos)
|
||||
results = [(combos[i], sims[i], pitches[i], paths[i]) for i in order]
|
||||
log.info(f" sorted in {time.time()-t1:.1f}s")
|
||||
|
||||
# ── 输出 ──
|
||||
log.info(f"\n{'='*55}")
|
||||
log.info(f"Top 50 Candidates (round 2)")
|
||||
log.info("=" * 55)
|
||||
for rank, (combo, sim, pitch, path) in enumerate(results[:50], 1):
|
||||
star = " *" if combo > 0.90 else ""
|
||||
d = os.path.basename(os.path.dirname(path))
|
||||
f = os.path.basename(path)
|
||||
log.info(f" {rank:2d}. [{combo:.4f}]{star} {d}/{f}")
|
||||
|
||||
# 来源分布
|
||||
log.info(f"\nSource distribution (combo > 0.85):")
|
||||
srcs = {}
|
||||
for combo, sim, pitch, path in results:
|
||||
if combo > 0.85:
|
||||
d = os.path.basename(os.path.dirname(path))
|
||||
srcs[d] = srcs.get(d, 0) + 1
|
||||
for d in sorted(srcs):
|
||||
log.info(f" {d}: {srcs[d]}")
|
||||
|
||||
# 分级导出
|
||||
tiers = [
|
||||
("tier1_095_100", 0.95),
|
||||
("tier2_092_095", 0.92),
|
||||
("tier3_090_092", 0.90),
|
||||
("tier4_085_090", 0.85),
|
||||
]
|
||||
total_copied = 0
|
||||
for tier_name, threshold in tiers:
|
||||
tier_dir = os.path.join(OUT_DIR, tier_name)
|
||||
os.makedirs(tier_dir, exist_ok=True)
|
||||
n = 0
|
||||
for combo, sim, pitch, path in results:
|
||||
if combo >= threshold:
|
||||
dst = os.path.join(tier_dir, os.path.basename(path))
|
||||
if os.path.exists(path) and not os.path.exists(dst):
|
||||
shutil.copy2(path, dst)
|
||||
n += 1
|
||||
else:
|
||||
break # results are sorted, stop when below threshold
|
||||
log.info(f" {tier_name}: {n} files")
|
||||
total_copied += n
|
||||
|
||||
# 保存结果
|
||||
rp = os.path.join(OUT_DIR, "results.json")
|
||||
with open(rp, 'w') as f:
|
||||
json.dump([(float(s), float(p), w) for s, _, p, w in results], f, ensure_ascii=False)
|
||||
|
||||
log.info(f"\n total exported: {total_copied}")
|
||||
log.info(f" results JSON : {rp}")
|
||||
log.info(f" log file : {LOG_FILE}")
|
||||
log.info(f" DONE ({time.time()-t0:.1f}s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user