PregoPal / modules /voiceprint.py
J.B-Lin
CLine 界面初版
0f2d3d7
Raw
History Blame Contribute Delete
5.83 kB
"""
PregoPal - 声纹识别模块
========================
家庭成员声纹注册、识别、管理。
当前方案:频谱特征相似度(轻量 baseline)
后续升级:Whisper-medium encoder embedding(MiniCPM-o 内置)
"""
import json
import numpy as np
import datetime
import hashlib
from pathlib import Path
from config import VOICE_DIR, FAMILY_FILE, VOICEPRINT_SIMILARITY_THRESHOLD
class VoiceprintManager:
"""声纹识别与家庭成员管理"""
def __init__(self):
self.family_file = FAMILY_FILE
self.family_data = self._load_family()
def _load_family(self):
"""加载家庭成员数据"""
if self.family_file.exists():
with open(self.family_file, 'r', encoding='utf-8') as f:
return json.load(f)
return {"members": [], "voiceprints": {}}
def _save_family(self):
"""保存家庭成员数据"""
with open(self.family_file, 'w', encoding='utf-8') as f:
json.dump(self.family_data, f, ensure_ascii=False, indent=2)
def register_member(self, name: str, relation: str, audio_path_input):
"""注册家庭成员声纹"""
if audio_path_input is None:
return None, "请先录制或上传语音"
# 生成声纹ID
member_id = hashlib.md5(f"{name}_{datetime.datetime.now()}".encode()).hexdigest()[:8]
# 保存音频文件到数据目录
audio_path = VOICE_DIR / f"{member_id}.wav"
try:
import shutil
shutil.copy2(audio_path_input, audio_path)
except Exception as e:
return None, f"保存音频失败: {str(e)}"
# 提取简单的声纹特征(使用音频的频谱特征作为简化方案)
features = self._extract_features_from_file(audio_path)
# 保存成员信息
member_info = {
"id": member_id,
"name": name,
"relation": relation,
"registered_at": datetime.datetime.now().isoformat(),
"audio_path": str(audio_path),
"features": features
}
self.family_data["members"].append(member_info)
self.family_data["voiceprints"][member_id] = features
self._save_family()
return member_info, f"✅ 成功注册 {relation} - {name}!"
def _extract_features_from_file(self, audio_path):
"""
从音频文件提取声纹特征
当前:频谱统计特征(轻量 baseline)
后续:Whisper-medium encoder embedding(更鲁棒)
"""
try:
import soundfile as sf
data, samplerate = sf.read(audio_path)
if len(data.shape) > 1:
data = data.mean(axis=1)
# 提取简单特征:MFCC-like 统计量
features = {
"mean": float(np.mean(data)),
"std": float(np.std(data)),
"max": float(np.max(data)),
"min": float(np.min(data)),
"zero_crossing_rate": float(np.sum(np.abs(np.diff(np.sign(data)))) / len(data)),
"energy": float(np.sum(data ** 2) / len(data)),
"duration": float(len(data) / samplerate)
}
return features
except Exception:
# 如果无法解析音频,返回占位特征
return {
"mean": 0.0, "std": 0.0, "max": 0.0, "min": 0.0,
"zero_crossing_rate": 0.0, "energy": 0.0, "duration": 0.0
}
def identify_speaker(self, audio_path_input):
"""识别说话人身份"""
if audio_path_input is None:
return None, "请先录制或上传语音"
if not self.family_data["members"]:
return None, "⚠️ 尚未注册任何家庭成员,请先注册"
features = self._extract_features_from_file(audio_path_input)
# 计算与每个注册成员的相似度
best_match = None
best_score = -1
for member in self.family_data["members"]:
stored = member["features"]
score = self._compute_similarity(features, stored)
if score > best_score:
best_score = score
best_match = member
# 相似度阈值判断
threshold = VOICEPRINT_SIMILARITY_THRESHOLD
if best_score >= threshold:
return best_match, f"🔊 识别结果: {best_match['relation']} - {best_match['name']} (置信度: {best_score:.2f})"
else:
return None, f"🔊 未能识别说话人 (最高匹配: {best_score:.2f},需≥{threshold})"
def _compute_similarity(self, f1, f2):
"""计算两个声纹特征的余弦相似度"""
keys = ["mean", "std", "max", "min", "zero_crossing_rate", "energy"]
v1 = np.array([f1.get(k, 0) for k in keys])
v2 = np.array([f2.get(k, 0) for k in keys])
# 归一化
norm1 = np.linalg.norm(v1)
norm2 = np.linalg.norm(v2)
if norm1 == 0 or norm2 == 0:
return 0
return float(np.dot(v1, v2) / (norm1 * norm2))
def get_members_list(self):
"""获取家庭成员列表"""
if not self.family_data["members"]:
return "📋 暂无注册成员"
lines = ["📋 已注册家庭成员:"]
for m in self.family_data["members"]:
lines.append(f" • {m['relation']} - {m['name']} (注册于 {m['registered_at'][:10]})")
return "\n".join(lines)
def delete_member(self, member_id: str):
"""删除家庭成员"""
self.family_data["members"] = [m for m in self.family_data["members"] if m["id"] != member_id]
self.family_data["voiceprints"].pop(member_id, None)
self._save_family()
return f"已删除成员 {member_id}"