mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 23:23:55 +08:00
199 lines
8.7 KiB
Python
199 lines
8.7 KiB
Python
import asyncio
|
|
import time
|
|
import aiohttp
|
|
import requests
|
|
from urllib.parse import urlparse, parse_qs
|
|
from typing import Optional, Dict
|
|
from config.logger import setup_logging
|
|
from core.utils.cache.manager import cache_manager
|
|
from core.utils.cache.config import CacheType
|
|
|
|
TAG = __name__
|
|
logger = setup_logging()
|
|
|
|
|
|
class VoiceprintProvider:
|
|
"""声纹识别服务提供者"""
|
|
|
|
def __init__(self, config: dict):
|
|
self.original_url = config.get("url", "")
|
|
self.speakers = config.get("speakers", [])
|
|
self.speaker_map = self._parse_speakers()
|
|
# 声纹识别相似度阈值,默认0.4
|
|
self.similarity_threshold = float(config.get("similarity_threshold", 0.4))
|
|
|
|
# 解析API地址和密钥
|
|
self.api_url = None
|
|
self.api_key = None
|
|
self.speaker_ids = []
|
|
|
|
if not self.original_url:
|
|
logger.bind(tag=TAG).warning("声纹识别URL未配置,声纹识别将被禁用")
|
|
self.enabled = False
|
|
else:
|
|
# 解析URL和key
|
|
parsed_url = urlparse(self.original_url)
|
|
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
|
|
# 从查询参数中提取key
|
|
query_params = parse_qs(parsed_url.query)
|
|
self.api_key = query_params.get('key', [''])[0]
|
|
|
|
if not self.api_key:
|
|
logger.bind(tag=TAG).error("URL中未找到key参数,声纹识别将被禁用")
|
|
self.enabled = False
|
|
else:
|
|
# 构造identify接口地址
|
|
self.api_url = f"{base_url}/voiceprint/identify"
|
|
|
|
# 提取speaker_ids
|
|
for speaker_str in self.speakers:
|
|
try:
|
|
parts = speaker_str.split(",", 2)
|
|
if len(parts) >= 1:
|
|
speaker_id = parts[0].strip()
|
|
self.speaker_ids.append(speaker_id)
|
|
except Exception:
|
|
continue
|
|
|
|
# 检查是否有有效的说话人配置
|
|
if not self.speaker_ids:
|
|
logger.bind(tag=TAG).warning("未配置有效的说话人,声纹识别将被禁用")
|
|
self.enabled = False
|
|
else:
|
|
# 进行健康检查,验证服务器是否可用
|
|
if self._check_server_health():
|
|
self.enabled = True
|
|
logger.bind(tag=TAG).info(f"声纹识别已启用: API={self.api_url}, 说话人={len(self.speaker_ids)}个, 相似度阈值={self.similarity_threshold}")
|
|
else:
|
|
self.enabled = False
|
|
logger.bind(tag=TAG).warning(f"声纹识别服务器不可用,声纹识别已禁用: {self.api_url}")
|
|
|
|
def _parse_speakers(self) -> Dict[str, Dict[str, str]]:
|
|
"""解析说话人配置"""
|
|
speaker_map = {}
|
|
for speaker_str in self.speakers:
|
|
try:
|
|
parts = speaker_str.split(",", 2)
|
|
if len(parts) >= 3:
|
|
speaker_id, name, description = parts[0].strip(), parts[1].strip(), parts[2].strip()
|
|
speaker_map[speaker_id] = {
|
|
"name": name,
|
|
"description": description
|
|
}
|
|
except Exception as e:
|
|
logger.bind(tag=TAG).warning(f"解析说话人配置失败: {speaker_str}, 错误: {e}")
|
|
return speaker_map
|
|
|
|
def _check_server_health(self) -> bool:
|
|
"""检查声纹识别服务器健康状态"""
|
|
if not self.api_url or not self.api_key:
|
|
return False
|
|
|
|
cache_key = f"{self.api_url}:{self.api_key}"
|
|
|
|
# 检查缓存
|
|
cached_result = cache_manager.get(CacheType.VOICEPRINT_HEALTH, cache_key)
|
|
if cached_result is not None:
|
|
logger.bind(tag=TAG).debug(f"使用缓存的健康状态: {cached_result}")
|
|
return cached_result
|
|
|
|
# 缓存过期或不存在
|
|
logger.bind(tag=TAG).info("执行声纹服务器健康检查")
|
|
|
|
try:
|
|
# 健康检查URL
|
|
parsed_url = urlparse(self.api_url)
|
|
health_url = f"{parsed_url.scheme}://{parsed_url.netloc}/voiceprint/health?key={self.api_key}"
|
|
|
|
# 发送健康检查请求
|
|
response = requests.get(health_url, timeout=3)
|
|
|
|
if response.status_code == 200:
|
|
result = response.json()
|
|
if result.get("status") == "healthy":
|
|
logger.bind(tag=TAG).info("声纹识别服务器健康检查通过")
|
|
is_healthy = True
|
|
else:
|
|
logger.bind(tag=TAG).warning(f"声纹识别服务器状态异常: {result}")
|
|
is_healthy = False
|
|
else:
|
|
logger.bind(tag=TAG).warning(f"声纹识别服务器健康检查失败: HTTP {response.status_code}")
|
|
is_healthy = False
|
|
|
|
except requests.exceptions.ConnectTimeout:
|
|
logger.bind(tag=TAG).warning("声纹识别服务器连接超时")
|
|
is_healthy = False
|
|
except requests.exceptions.ConnectionError:
|
|
logger.bind(tag=TAG).warning("声纹识别服务器连接被拒绝")
|
|
is_healthy = False
|
|
except Exception as e:
|
|
logger.bind(tag=TAG).warning(f"声纹识别服务器健康检查异常: {e}")
|
|
is_healthy = False
|
|
|
|
# 使用全局缓存管理器缓存结果
|
|
cache_manager.set(CacheType.VOICEPRINT_HEALTH, cache_key, is_healthy)
|
|
logger.bind(tag=TAG).info(f"健康检查结果已缓存: {is_healthy}")
|
|
|
|
return is_healthy
|
|
|
|
async def identify_speaker(self, audio_data: bytes, session_id: str) -> Optional[str]:
|
|
"""识别说话人"""
|
|
if not self.enabled or not self.api_url or not self.api_key:
|
|
logger.bind(tag=TAG).debug("声纹识别功能已禁用或未配置,跳过识别")
|
|
return None
|
|
|
|
try:
|
|
api_start_time = time.monotonic()
|
|
|
|
# 准备请求头
|
|
headers = {
|
|
'Authorization': f'Bearer {self.api_key}',
|
|
'Accept': 'application/json'
|
|
}
|
|
|
|
# 准备multipart/form-data数据
|
|
data = aiohttp.FormData()
|
|
data.add_field('speaker_ids', ','.join(self.speaker_ids))
|
|
data.add_field('file', audio_data, filename='audio.wav', content_type='audio/wav')
|
|
|
|
timeout = aiohttp.ClientTimeout(total=10)
|
|
|
|
# 网络请求
|
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
async with session.post(self.api_url, headers=headers, data=data) as response:
|
|
|
|
if response.status == 200:
|
|
result = await response.json()
|
|
speaker_id = result.get("speaker_id")
|
|
score = result.get("score", 0)
|
|
total_elapsed_time = time.monotonic() - api_start_time
|
|
|
|
logger.bind(tag=TAG).info(f"声纹识别耗时: {total_elapsed_time:.3f}s")
|
|
|
|
# 相似度阈值检查
|
|
if score < self.similarity_threshold:
|
|
logger.bind(tag=TAG).warning(f"声纹识别相似度{score:.3f}低于阈值{self.similarity_threshold}")
|
|
return "未知说话人"
|
|
|
|
if speaker_id and speaker_id in self.speaker_map:
|
|
result_name = self.speaker_map[speaker_id]["name"]
|
|
logger.bind(tag=TAG).info(f"声纹识别成功: {result_name} (相似度: {score:.3f})")
|
|
return result_name
|
|
else:
|
|
logger.bind(tag=TAG).warning(f"未识别的说话人ID: {speaker_id}")
|
|
return "未知说话人"
|
|
else:
|
|
logger.bind(tag=TAG).error(f"声纹识别API错误: HTTP {response.status}")
|
|
return None
|
|
|
|
except asyncio.TimeoutError:
|
|
elapsed = time.monotonic() - api_start_time
|
|
logger.bind(tag=TAG).error(f"声纹识别超时: {elapsed:.3f}s")
|
|
return None
|
|
except Exception as e:
|
|
elapsed = time.monotonic() - api_start_time
|
|
logger.bind(tag=TAG).error(f"声纹识别失败: {e}")
|
|
return None
|
|
|