diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index 476f09cd..5586b9f0 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -298,6 +298,20 @@ public class ConfigServiceImpl implements ConfigService { Map voiceprintConfig = new HashMap<>(); voiceprintConfig.put("url", voiceprintUrl); voiceprintConfig.put("speakers", speakers); + + // 获取声纹识别相似度阈值,默认0.4 + String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true); + if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) { + try { + double threshold = Double.parseDouble(thresholdStr); + voiceprintConfig.put("similarity_threshold", threshold); + } catch (NumberFormatException e) { + // 如果解析失败,使用默认值0.4 + voiceprintConfig.put("similarity_threshold", 0.4); + } + } else { + voiceprintConfig.put("similarity_threshold", 0.4); + } result.put("voiceprint", voiceprintConfig); } catch (Exception e) { diff --git a/main/manager-api/src/main/resources/db/changelog/202508271113.sql b/main/manager-api/src/main/resources/db/changelog/202508271113.sql new file mode 100644 index 00000000..6d020803 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202508271113.sql @@ -0,0 +1,24 @@ +-- VOSK ASR模型供应器 +delete from `ai_model_provider` where id = 'SYSTEM_ASR_VoskASR'; +INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +('SYSTEM_ASR_VoskASR', 'ASR', 'vosk', 'VOSK离线语音识别', '[{"key": "model_path", "type": "string", "label": "模型路径"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]', 11, 1, NOW(), 1, NOW()); + +-- VOSK ASR模型配置 +delete from `ai_model_config` where id = 'ASR_VoskASR'; +INSERT INTO `ai_model_config` VALUES ('ASR_VoskASR', 'ASR', 'VoskASR', 'VOSK离线语音识别', 0, 1, '{\"type\": \"vosk\", \"model_path\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 11, NULL, NULL, NULL, NULL); + +-- 更新VOSK ASR配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://alphacephei.com/vosk/', +`remark` = 'VOSK ASR配置说明: +1. VOSK是一个离线语音识别库,支持多种语言 +2. 需要先下载模型文件:https://alphacephei.com/vosk/models +3. 中文模型推荐使用vosk-model-small-cn-0.22或vosk-model-cn-0.22 +4. 完全离线运行,无需网络连接 +5. 输出文件保存在tmp/目录 +使用步骤: +1. 访问 https://alphacephei.com/vosk/models 下载中文模型 +2. 解压模型文件到项目目录下的models/vosk/文件夹 +3. 在配置中指定正确的模型路径 +4. 注意:VOSK中文模型输出不带标点符号,词与词之间会有空格 +' WHERE `id` = 'ASR_VoskASR'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/202509081140.sql b/main/manager-api/src/main/resources/db/changelog/202509081140.sql new file mode 100644 index 00000000..9dec9d04 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202509081140.sql @@ -0,0 +1,4 @@ +-- 添加声纹识别相似度阈值参数配置 +delete from `sys_params` where id = 115; +INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) +VALUES (115, 'server.voiceprint_similarity_threshold', '0.4', 'string', 1, '声纹识别相似度阈值,范围0.0-1.0,默认0.4,数值越高越严格'); diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index 5dec58bf..e7b19f0d 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -303,6 +303,13 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202508131557.sql + - changeSet: + id: 202508271113 + author: cgd + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202508271113.sql - changeSet: id: 202509051745 author: RanChen @@ -310,10 +317,17 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202509051745.sql + - changeSet: + id: 202509081140 + author: cgd + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202509081140.sql - changeSet: id: 202509091042 author: cgd changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202509091042.sql + path: classpath:db/changelog/202509091042.sql \ No newline at end of file diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 23b9f2e4..1cfdd964 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -114,7 +114,10 @@ plugins: # 想稳定一点就自行申请替换,每天有1000次免费调用 # 申请地址:https://console.qweather.com/#/apps/create-key/over # 申请后通过这个链接可以找到自己的apihost:https://console.qweather.com/setting?lang=zh - get_weather: {"api_host":"mj7p3y7naa.re.qweatherapi.com", "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" } + get_weather: + api_host: "mj7p3y7naa.re.qweatherapi.com" + api_key: "a861d0d5e7bf4ee1a83d9a9e4f96d4da" + default_location: "广州" # 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻 # 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/ get_news_from_chinanews: @@ -148,6 +151,9 @@ voiceprint: - "test1,张三,张三是一个程序员" - "test2,李四,李四是一个产品经理" - "test3,王五,王五是一个设计师" + # 声纹识别相似度阈值,范围0.0-1.0,默认0.4 + # 数值越高越严格,减少误识别但可能增加拒识率 + similarity_threshold: 0.4 # ##################################################################################### # ################################以下是角色模型配置###################################### @@ -389,6 +395,22 @@ ASR: base_url: https://api.groq.com/openai/v1/audio/transcriptions model_name: whisper-large-v3-turbo output_dir: tmp/ + VoskASR: + # 官方网站:https://alphacephei.com/vosk/ + # 配置说明: + # 1. VOSK是一个离线语音识别库,支持多种语言 + # 2. 需要先下载模型文件:https://alphacephei.com/vosk/models + # 3. 中文模型推荐使用vosk-model-small-cn-0.22或vosk-model-cn-0.22 + # 4. 完全离线运行,无需网络连接 + # 5. 输出文件保存在tmp/目录 + # 使用步骤: + # 1. 访问 https://alphacephei.com/vosk/models 下载对应的模型 + # 2. 解压模型文件到项目目录下的models/vosk/文件夹 + # 3. 在配置中指定正确的模型路径 + # 4. 注意:VOSK中文模型输出不带标点符号,词与词之间会有空格 + type: vosk + model_path: 你的模型路径,如:models/vosk/vosk-model-small-cn-0.22 + output_dir: tmp/ @@ -884,4 +906,4 @@ TTS: audio_format: "pcm" # 默认音色,如需其他音色可到项目assets文件夹下注册 voice: "jay_klee" - output_dir: tmp/ \ No newline at end of file + output_dir: tmp/ diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 4b3b9d1c..324237dd 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -432,10 +432,14 @@ class ConnectionHandler: try: voiceprint_config = self.config.get("voiceprint", {}) if voiceprint_config: - self.voiceprint_provider = VoiceprintProvider(voiceprint_config) - self.logger.bind(tag=TAG).info("声纹识别功能已在连接时动态启用") + voiceprint_provider = VoiceprintProvider(voiceprint_config) + if voiceprint_provider is not None and voiceprint_provider.enabled: + self.voiceprint_provider = voiceprint_provider + self.logger.bind(tag=TAG).info("声纹识别功能已在连接时动态启用") + else: + self.logger.bind(tag=TAG).warning("声纹识别功能启用但配置不完整") else: - self.logger.bind(tag=TAG).info("声纹识别功能未启用或配置不完整") + self.logger.bind(tag=TAG).info("声纹识别功能未启用") except Exception as e: self.logger.bind(tag=TAG).warning(f"声纹识别初始化失败: {str(e)}") diff --git a/main/xiaozhi-server/core/providers/asr/vosk.py b/main/xiaozhi-server/core/providers/asr/vosk.py new file mode 100644 index 00000000..19d31822 --- /dev/null +++ b/main/xiaozhi-server/core/providers/asr/vosk.py @@ -0,0 +1,114 @@ +import os +import json +import time +from typing import Optional, Tuple, List +from .base import ASRProviderBase +from config.logger import setup_logging +from core.providers.asr.dto.dto import InterfaceType +import vosk + +TAG = __name__ +logger = setup_logging() + +class ASRProvider(ASRProviderBase): + def __init__(self, config: dict, delete_audio_file: bool = True): + super().__init__() + self.interface_type = InterfaceType.LOCAL + self.model_path = config.get("model_path") + self.output_dir = config.get("output_dir", "tmp/") + self.delete_audio_file = delete_audio_file + + # 初始化VOSK模型 + self.model = None + self.recognizer = None + self._load_model() + + # 确保输出目录存在 + os.makedirs(self.output_dir, exist_ok=True) + + def _load_model(self): + """加载VOSK模型""" + try: + if not os.path.exists(self.model_path): + raise FileNotFoundError(f"VOSK模型路径不存在: {self.model_path}") + + logger.bind(tag=TAG).info(f"正在加载VOSK模型: {self.model_path}") + self.model = vosk.Model(self.model_path) + + # 初始化VOSK识别器(采样率必须为16kHz) + self.recognizer = vosk.KaldiRecognizer(self.model, 16000) + + logger.bind(tag=TAG).info("VOSK模型加载成功") + except Exception as e: + logger.bind(tag=TAG).error(f"加载VOSK模型失败: {e}") + raise + + async def speech_to_text( + self, audio_data: List[bytes], session_id: str, audio_format: str = "opus" + ) -> Tuple[Optional[str], Optional[str]]: + """将语音数据转换为文本""" + file_path = None + try: + # 检查模型是否加载成功 + if not self.model: + logger.bind(tag=TAG).error("VOSK模型未加载,无法进行识别") + return "", None + + # 解码音频(如果原始格式是Opus) + if audio_format == "pcm": + pcm_data = audio_data + else: + pcm_data = self.decode_opus(audio_data) + + if not pcm_data: + logger.bind(tag=TAG).warning("解码后的PCM数据为空,无法进行识别") + return "", None + + # 合并PCM数据 + combined_pcm_data = b"".join(pcm_data) + if len(combined_pcm_data) == 0: + logger.bind(tag=TAG).warning("合并后的PCM数据为空") + return "", None + + # 判断是否保存为WAV文件 + if not self.delete_audio_file: + file_path = self.save_audio_to_file(pcm_data, session_id) + + start_time = time.time() + + + # 进行识别(VOSK推荐每次送入2000字节的数据) + chunk_size = 2000 + text_result = "" + + for i in range(0, len(combined_pcm_data), chunk_size): + chunk = combined_pcm_data[i:i+chunk_size] + if self.recognizer.AcceptWaveform(chunk): + result = json.loads(self.recognizer.Result()) + text = result.get('text', '') + if text: + text_result += text + " " + + # 获取最终结果 + final_result = json.loads(self.recognizer.FinalResult()) + final_text = final_result.get('text', '') + if final_text: + text_result += final_text + + logger.bind(tag=TAG).debug( + f"VOSK语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text_result.strip()}" + ) + + return text_result.strip(), file_path + + except Exception as e: + logger.bind(tag=TAG).error(f"VOSK语音识别失败: {e}") + return "", None + finally: + # 文件清理逻辑 + if self.delete_audio_file and file_path and os.path.exists(file_path): + try: + os.remove(file_path) + logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}") + except Exception as e: + logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}") diff --git a/main/xiaozhi-server/core/providers/tools/server_mcp/mcp_client.py b/main/xiaozhi-server/core/providers/tools/server_mcp/mcp_client.py index 8b60ac1b..6dd14195 100644 --- a/main/xiaozhi-server/core/providers/tools/server_mcp/mcp_client.py +++ b/main/xiaozhi-server/core/providers/tools/server_mcp/mcp_client.py @@ -167,14 +167,13 @@ class ServerMCPClient: # 建立SSEClient elif "url" in self.config: + headers = dict(self.config.get("headers", {})) + # TODO 兼容旧版本 if "API_ACCESS_TOKEN" in self.config: - headers = { - "Authorization": f"Bearer {self.config['API_ACCESS_TOKEN']}" - } - else: - headers = {} + headers["Authorization"] = f"Bearer {self.config['API_ACCESS_TOKEN']}" + self.logger.bind(tag=TAG).warning(f"你正在使用旧过时的配置 API_ACCESS_TOKEN ,请在.mcp_server_settings.json中将API_ACCESS_TOKEN直接设置在headers中,例如 'Authorization': 'Bearer API_ACCESS_TOKEN'") sse_r, sse_w = await stack.enter_async_context( - sse_client(self.config["url"], headers=headers) + sse_client(self.config["url"], headers=headers, timeout=self.config.get("timeout", 5), sse_read_timeout=self.config.get("sse_read_timeout", 60 * 5)) ) read_stream, write_stream = sse_r, sse_w diff --git a/main/xiaozhi-server/core/utils/voiceprint_provider.py b/main/xiaozhi-server/core/utils/voiceprint_provider.py index 8e788172..ee4f06ca 100644 --- a/main/xiaozhi-server/core/utils/voiceprint_provider.py +++ b/main/xiaozhi-server/core/utils/voiceprint_provider.py @@ -19,6 +19,8 @@ class VoiceprintProvider: 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 @@ -62,7 +64,7 @@ class VoiceprintProvider: # 进行健康检查,验证服务器是否可用 if self._check_server_health(): self.enabled = True - logger.bind(tag=TAG).info(f"声纹识别已启用: API={self.api_url}, 说话人={len(self.speaker_ids)}个") + 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}") @@ -169,12 +171,14 @@ class VoiceprintProvider: logger.bind(tag=TAG).info(f"声纹识别耗时: {total_elapsed_time:.3f}s") - # 置信度检查 - if score < 0.5: - logger.bind(tag=TAG).warning(f"声纹识别置信度较低: {score:.3f}") + # 相似度阈值检查 + 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}") diff --git a/main/xiaozhi-server/mcp_server_settings.json b/main/xiaozhi-server/mcp_server_settings.json index a1bddf06..70607a2c 100644 --- a/main/xiaozhi-server/mcp_server_settings.json +++ b/main/xiaozhi-server/mcp_server_settings.json @@ -36,6 +36,12 @@ "command": "npx", "args": ["-y", "@simonb97/server-win-cli"], "link": "https://github.com/SimonB97/win-cli-mcp-server" + }, + "sse-mcp-server": { + "url": "http://localhost:8080/sse", + "headers": { + "Authorization": "Bearer YOUR TOKEN" + } } } }