From d2d97149fce5045e08bd09e23ae4bb829c210ec8 Mon Sep 17 00:00:00 2001 From: JoeyZhou Date: Mon, 31 Mar 2025 15:19:29 +0800 Subject: [PATCH 01/18] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E4=BA=91ASR=E8=AF=AD=E9=9F=B3=E8=AF=86=E5=88=AB=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/providers/asr/tencent.py | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 main/xiaozhi-server/core/providers/asr/tencent.py diff --git a/main/xiaozhi-server/core/providers/asr/tencent.py b/main/xiaozhi-server/core/providers/asr/tencent.py new file mode 100644 index 00000000..8221f9df --- /dev/null +++ b/main/xiaozhi-server/core/providers/asr/tencent.py @@ -0,0 +1,251 @@ +import base64 +import hashlib +import hmac +import json +import time +from datetime import datetime, timezone +import os +import uuid +from typing import Optional, Tuple, List +import wave +import opuslib_next + +import requests +from core.providers.asr.base import ASRProviderBase +from config.logger import setup_logging + +TAG = __name__ +logger = setup_logging() + +class ASRProvider(ASRProviderBase): + API_URL = "https://asr.tencentcloudapi.com" + API_VERSION = "2019-06-14" + FORMAT = "pcm" # 支持的音频格式:pcm, wav, mp3 + + def __init__(self, config: dict, delete_audio_file: bool = True): + self.secret_id = config.get("secret_id") + self.secret_key = config.get("secret_key") + self.output_dir = config.get("output_dir") + + # 确保输出目录存在 + os.makedirs(self.output_dir, exist_ok=True) + + def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: + """将Opus音频数据解码并保存为WAV文件""" + + file_name = f"tencent_asr_{session_id}_{uuid.uuid4()}.wav" + file_path = os.path.join(self.output_dir, file_name) + + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + with wave.open(file_path, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) # 2 bytes = 16-bit + wf.setframerate(16000) + wf.writeframes(b"".join(pcm_data)) + + return file_path + + @staticmethod + def decode_opus(opus_data: List[bytes]) -> bytes: + """将Opus音频数据解码为PCM数据""" + import opuslib_next + + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + return b"".join(pcm_data) + + async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: + """将语音数据转换为文本""" + if not opus_data: + logger.bind(tag=TAG).warn("音频数据为空!") + return None, None + + try: + # 检查配置是否已设置 + if not self.secret_id or not self.secret_key: + logger.bind(tag=TAG).error("腾讯云语音识别配置未设置,无法进行识别") + return None, None + + # 将Opus音频数据解码为PCM + pcm_data = self.decode_opus(opus_data) + + # 将音频数据转换为Base64编码 + base64_audio = base64.b64encode(pcm_data).decode('utf-8') + + # 构建请求体 + request_body = self._build_request_body(base64_audio) + + # 获取认证头 + timestamp, authorization = self._get_auth_headers(request_body) + + # 发送请求 + start_time = time.time() + result = self._send_request(request_body, timestamp, authorization) + + if result: + logger.bind(tag=TAG).debug(f"腾讯云语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result}") + + return result, None + + except Exception as e: + logger.bind(tag=TAG).error(f"处理音频时发生错误!{e}", exc_info=True) + return None, None + + def _build_request_body(self, base64_audio: str) -> str: + """构建请求体""" + request_map = { + "ProjectId": 0, + "SubServiceType": 2, # 一句话识别 + "EngSerViceType": "16k_zh", # 中文普通话通用 + "SourceType": 1, # 音频数据来源为语音文件 + "VoiceFormat": self.FORMAT, # 音频格式 + "Data": base64_audio, # Base64编码的音频数据 + "DataLen": len(base64_audio) # 数据长度 + } + return json.dumps(request_map) + + def _get_auth_headers(self, request_body: str) -> Tuple[str, str]: + """获取认证头""" + try: + # 获取当前UTC时间戳 + now = datetime.now(timezone.utc) + timestamp = str(int(now.timestamp())) + date = now.strftime("%Y-%m-%d") + + # 服务名称必须是 "asr" + service = "asr" + + # 拼接凭证范围 + credential_scope = f"{date}/{service}/tc3_request" + + # 使用TC3-HMAC-SHA256签名方法 + algorithm = "TC3-HMAC-SHA256" + + # 构建规范请求字符串 + http_request_method = "POST" + canonical_uri = "/" + canonical_query_string = "" + + # 注意:头部信息需要按照ASCII升序排列,且key和value都转为小写 + # 必须包含content-type和host头部 + content_type = "application/json; charset=utf-8" + host = "asr.tencentcloudapi.com" + action = "SentenceRecognition" # 接口名称 + + # 构建规范头部信息,注意顺序和格式 + canonical_headers = f"content-type:{content_type.lower()}\n" + \ + f"host:{host.lower()}\n" + \ + f"x-tc-action:{action.lower()}\n" + + signed_headers = "content-type;host;x-tc-action" + + # 请求体哈希值 + payload_hash = self._sha256_hex(request_body) + + # 构建规范请求字符串 + canonical_request = f"{http_request_method}\n" + \ + f"{canonical_uri}\n" + \ + f"{canonical_query_string}\n" + \ + f"{canonical_headers}\n" + \ + f"{signed_headers}\n" + \ + f"{payload_hash}" + + # 计算规范请求的哈希值 + hashed_canonical_request = self._sha256_hex(canonical_request) + + # 构建待签名字符串 + string_to_sign = f"{algorithm}\n" + \ + f"{timestamp}\n" + \ + f"{credential_scope}\n" + \ + f"{hashed_canonical_request}" + + # 计算签名密钥 + secret_date = self._hmac_sha256(f"TC3{self.secret_key}", date) + secret_service = self._hmac_sha256(secret_date, service) + secret_signing = self._hmac_sha256(secret_service, "tc3_request") + + # 计算签名 + signature = self._bytes_to_hex(self._hmac_sha256(secret_signing, string_to_sign)) + + # 构建授权头 + authorization = f"{algorithm} " + \ + f"Credential={self.secret_id}/{credential_scope}, " + \ + f"SignedHeaders={signed_headers}, " + \ + f"Signature={signature}" + + return timestamp, authorization + + except Exception as e: + logger.bind(tag=TAG).error(f"生成认证头失败: {e}", exc_info=True) + raise RuntimeError(f"生成认证头失败: {e}") + + def _send_request(self, request_body: str, timestamp: str, authorization: str) -> Optional[str]: + """发送请求到腾讯云API""" + headers = { + "Content-Type": "application/json; charset=utf-8", + "Host": "asr.tencentcloudapi.com", + "Authorization": authorization, + "X-TC-Action": "SentenceRecognition", + "X-TC-Version": self.API_VERSION, + "X-TC-Timestamp": timestamp, + "X-TC-Region": "ap-shanghai" + } + + try: + response = requests.post(self.API_URL, headers=headers, data=request_body) + + if not response.ok: + raise IOError(f"请求失败: {response.status_code} {response.reason}") + + response_json = response.json() + + # 检查是否有错误 + if "Response" in response_json and "Error" in response_json["Response"]: + error = response_json["Response"]["Error"] + error_code = error["Code"] + error_message = error["Message"] + raise IOError(f"API返回错误: {error_code}: {error_message}") + + # 提取识别结果 + if "Response" in response_json and "Result" in response_json["Response"]: + return response_json["Response"]["Result"] + else: + logger.bind(tag=TAG).warn(f"响应中没有识别结果: {response_json}") + return "" + + except Exception as e: + logger.bind(tag=TAG).error(f"发送请求失败: {e}", exc_info=True) + return None + + def _sha256_hex(self, data: str) -> str: + """计算字符串的SHA256哈希值""" + digest = hashlib.sha256(data.encode('utf-8')).digest() + return self._bytes_to_hex(digest) + + def _hmac_sha256(self, key, data: str) -> bytes: + """计算HMAC-SHA256""" + if isinstance(key, str): + key = key.encode('utf-8') + + return hmac.new(key, data.encode('utf-8'), hashlib.sha256).digest() + + def _bytes_to_hex(self, bytes_data: bytes) -> str: + """字节数组转十六进制字符串""" + return ''.join(f"{b:02x}" for b in bytes_data) \ No newline at end of file From ee3808dd2f1ed690cb2c1c8977b4b3abda2aed48 Mon Sep 17 00:00:00 2001 From: JoeyZhou Date: Mon, 31 Mar 2025 15:41:45 +0800 Subject: [PATCH 02/18] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9Evosk=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E8=AF=AD=E9=9F=B3=E8=AF=86=E5=88=AB=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + main/xiaozhi-server/config.yaml | 4 + .../core/providers/asr/vosk_local.py | 133 ++++++++++++++++++ main/xiaozhi-server/requirements.txt | 3 +- 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 main/xiaozhi-server/core/providers/asr/vosk_local.py diff --git a/.gitignore b/.gitignore index 675fb688..7c8abecd 100644 --- a/.gitignore +++ b/.gitignore @@ -155,4 +155,5 @@ main/manager-web/node_modules # model files main/xiaozhi-server/models/SenseVoiceSmall/model.pt main/xiaozhi-server/models/sherpa-onnx* +main/xiaozhi-server/models/vosk-model* my_wakeup_words.mp3 diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 7289743b..9d0a5106 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -162,6 +162,10 @@ ASR: type: fun_local model_dir: models/SenseVoiceSmall output_dir: tmp/ + VoskASR: + type: vosk_local + model_dir: models/vosk-model + output_dir: tmp/ SherpaASR: type: sherpa_onnx_local model_dir: models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17 diff --git a/main/xiaozhi-server/core/providers/asr/vosk_local.py b/main/xiaozhi-server/core/providers/asr/vosk_local.py new file mode 100644 index 00000000..802c5756 --- /dev/null +++ b/main/xiaozhi-server/core/providers/asr/vosk_local.py @@ -0,0 +1,133 @@ +import time +import wave +import os +import sys +import io +import json +from config.logger import setup_logging +from typing import Optional, Tuple, List +import uuid +import opuslib_next +from core.providers.asr.base import ASRProviderBase + +from vosk import Model, KaldiRecognizer, SetLogLevel + +TAG = __name__ +logger = setup_logging() + + +# 捕获标准输出 +class CaptureOutput: + def __enter__(self): + self._output = io.StringIO() + self._original_stdout = sys.stdout + sys.stdout = self._output + + def __exit__(self, exc_type, exc_value, traceback): + sys.stdout = self._original_stdout + self.output = self._output.getvalue() + self._output.close() + + # 将捕获到的内容通过 logger 输出 + if self.output: + logger.bind(tag=TAG).info(self.output.strip()) + + +class ASRProvider(ASRProviderBase): + def __init__(self, config: dict, delete_audio_file: bool): + self.model_dir = config.get("model_dir") + self.output_dir = config.get("output_dir") + self.delete_audio_file = delete_audio_file + self.sample_rate = 16000 + + # 确保输出目录存在 + os.makedirs(self.output_dir, exist_ok=True) + + # 初始化VOSK模型 + with CaptureOutput(): + SetLogLevel(-1) + logger.bind(tag=TAG).info(f"正在加载VOSK模型: {self.model_dir}") + self.model = Model(self.model_dir) + logger.bind(tag=TAG).info("VOSK模型加载完成") + + def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: + """将Opus音频数据解码并保存为WAV文件""" + file_name = f"asr_{session_id}_{uuid.uuid4()}.wav" + file_path = os.path.join(self.output_dir, file_name) + + decoder = opuslib_next.Decoder(self.sample_rate, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + with wave.open(file_path, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) # 2 bytes = 16-bit + wf.setframerate(self.sample_rate) + wf.writeframes(b"".join(pcm_data)) + + return file_path + + async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: + """语音转文本主处理逻辑""" + file_path = None + try: + # 保存音频文件 + start_time = time.time() + file_path = self.save_audio_to_file(opus_data, session_id) + logger.bind(tag=TAG).debug(f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}") + + # 语音识别 + start_time = time.time() + + # 创建识别器 + recognizer = KaldiRecognizer(self.model, self.sample_rate) + recognizer.SetWords(True) # 启用词级时间戳 + + # 读取WAV文件并进行识别 + with wave.open(file_path, "rb") as wf: + # 确保音频格式正确 + if wf.getnchannels() != 1 or wf.getsampwidth() != 2 or wf.getframerate() != self.sample_rate: + logger.bind(tag=TAG).error(f"音频格式不支持: 需要16kHz, 16-bit, 单声道") + return "", file_path + + # 分块处理音频数据 + result_text = "" + while True: + data = wf.readframes(4000) # 读取音频块 + if len(data) == 0: + break + + if recognizer.AcceptWaveform(data): + result_json = json.loads(recognizer.Result()) + if "text" in result_json and result_json["text"].strip(): + result_text += result_json["text"] + " " + + # 获取最终结果 + final_result = json.loads(recognizer.FinalResult()) + if "text" in final_result and final_result["text"].strip(): + result_text += final_result["text"] + + result_text = result_text.strip() + + logger.bind(tag=TAG).debug(f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result_text}") + + return result_text, file_path + + except Exception as e: + logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) + 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}") \ No newline at end of file diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index 51433cb9..eacd6a2f 100755 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -22,4 +22,5 @@ mem0ai==0.1.62 bs4==0.0.2 modelscope==1.23.2 sherpa_onnx==1.11.0 -cnlunar==0.2.0 \ No newline at end of file +cnlunar==0.2.0 +vosk==0.3.45 \ No newline at end of file From f20c17f27d4cc86e1764904a95e5ecc5e315bbe8 Mon Sep 17 00:00:00 2001 From: JoeyZhou Date: Mon, 31 Mar 2025 16:13:32 +0800 Subject: [PATCH 03/18] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9A=E6=94=B9=E8=BF=9B=E6=89=93=E5=8C=85?= =?UTF-8?q?=E5=92=8C=E6=9C=8D=E5=8A=A1=E5=90=AF=E5=8A=A8=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修改打包命令为 mvn clean install,确保构建前清理旧文件 2. 改进服务启动命令,添加日志输出和进程ID保存 3. 规范化操作流程,提高部署可靠性 --- main/manager-api/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/manager-api/README.md b/main/manager-api/README.md index 5eb87ae8..6aa324c2 100644 --- a/main/manager-api/README.md +++ b/main/manager-api/README.md @@ -83,13 +83,13 @@ src/main/java/xiaozhi/AdminApplication.java 执行以下命令生产jar包 ``` -mvn install +mvn clean install ``` 把jar包放在服务器上,执行 ``` -nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.active=dev >/dev/null & +nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.active=dev > xiaozhi-server.log 2>&1 & echo $! > xiaozhi-server.pid ``` # 接口文档 From 48ed5d5c9bf905de3f5f9e163d10d045f2df9066 Mon Sep 17 00:00:00 2001 From: joey <38903653+joey-zhou@users.noreply.github.com> Date: Mon, 31 Mar 2025 21:18:03 +0800 Subject: [PATCH 04/18] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3044433f..e4c2ac00 100644 --- a/.gitignore +++ b/.gitignore @@ -155,6 +155,5 @@ main/manager-web/node_modules # model files main/xiaozhi-server/models/SenseVoiceSmall/model.pt main/xiaozhi-server/models/sherpa-onnx* -main/xiaozhi-server/models/vosk-model* my_wakeup_words.mp3 main/manager-api/.vscode From e085fb1b1023c8546b997bb15df81590b1b09f2d Mon Sep 17 00:00:00 2001 From: joey <38903653+joey-zhou@users.noreply.github.com> Date: Mon, 31 Mar 2025 21:22:13 +0800 Subject: [PATCH 05/18] Update config.yaml --- main/xiaozhi-server/config.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index b2dbd3a9..e2c35e67 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -167,10 +167,6 @@ ASR: type: fun_local model_dir: models/SenseVoiceSmall output_dir: tmp/ - VoskASR: - type: vosk_local - model_dir: models/vosk-model - output_dir: tmp/ SherpaASR: type: sherpa_onnx_local model_dir: models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17 From 44f8254b43d2763771fc9895dafb359c642d5ee9 Mon Sep 17 00:00:00 2001 From: joey <38903653+joey-zhou@users.noreply.github.com> Date: Mon, 31 Mar 2025 21:23:15 +0800 Subject: [PATCH 06/18] Delete main/xiaozhi-server/core/providers/asr/vosk_local.py --- .../core/providers/asr/vosk_local.py | 133 ------------------ 1 file changed, 133 deletions(-) delete mode 100644 main/xiaozhi-server/core/providers/asr/vosk_local.py diff --git a/main/xiaozhi-server/core/providers/asr/vosk_local.py b/main/xiaozhi-server/core/providers/asr/vosk_local.py deleted file mode 100644 index 802c5756..00000000 --- a/main/xiaozhi-server/core/providers/asr/vosk_local.py +++ /dev/null @@ -1,133 +0,0 @@ -import time -import wave -import os -import sys -import io -import json -from config.logger import setup_logging -from typing import Optional, Tuple, List -import uuid -import opuslib_next -from core.providers.asr.base import ASRProviderBase - -from vosk import Model, KaldiRecognizer, SetLogLevel - -TAG = __name__ -logger = setup_logging() - - -# 捕获标准输出 -class CaptureOutput: - def __enter__(self): - self._output = io.StringIO() - self._original_stdout = sys.stdout - sys.stdout = self._output - - def __exit__(self, exc_type, exc_value, traceback): - sys.stdout = self._original_stdout - self.output = self._output.getvalue() - self._output.close() - - # 将捕获到的内容通过 logger 输出 - if self.output: - logger.bind(tag=TAG).info(self.output.strip()) - - -class ASRProvider(ASRProviderBase): - def __init__(self, config: dict, delete_audio_file: bool): - self.model_dir = config.get("model_dir") - self.output_dir = config.get("output_dir") - self.delete_audio_file = delete_audio_file - self.sample_rate = 16000 - - # 确保输出目录存在 - os.makedirs(self.output_dir, exist_ok=True) - - # 初始化VOSK模型 - with CaptureOutput(): - SetLogLevel(-1) - logger.bind(tag=TAG).info(f"正在加载VOSK模型: {self.model_dir}") - self.model = Model(self.model_dir) - logger.bind(tag=TAG).info("VOSK模型加载完成") - - def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: - """将Opus音频数据解码并保存为WAV文件""" - file_name = f"asr_{session_id}_{uuid.uuid4()}.wav" - file_path = os.path.join(self.output_dir, file_name) - - decoder = opuslib_next.Decoder(self.sample_rate, 1) # 16kHz, 单声道 - pcm_data = [] - - for opus_packet in opus_data: - try: - pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms - pcm_data.append(pcm_frame) - except opuslib_next.OpusError as e: - logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) - - with wave.open(file_path, "wb") as wf: - wf.setnchannels(1) - wf.setsampwidth(2) # 2 bytes = 16-bit - wf.setframerate(self.sample_rate) - wf.writeframes(b"".join(pcm_data)) - - return file_path - - async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: - """语音转文本主处理逻辑""" - file_path = None - try: - # 保存音频文件 - start_time = time.time() - file_path = self.save_audio_to_file(opus_data, session_id) - logger.bind(tag=TAG).debug(f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}") - - # 语音识别 - start_time = time.time() - - # 创建识别器 - recognizer = KaldiRecognizer(self.model, self.sample_rate) - recognizer.SetWords(True) # 启用词级时间戳 - - # 读取WAV文件并进行识别 - with wave.open(file_path, "rb") as wf: - # 确保音频格式正确 - if wf.getnchannels() != 1 or wf.getsampwidth() != 2 or wf.getframerate() != self.sample_rate: - logger.bind(tag=TAG).error(f"音频格式不支持: 需要16kHz, 16-bit, 单声道") - return "", file_path - - # 分块处理音频数据 - result_text = "" - while True: - data = wf.readframes(4000) # 读取音频块 - if len(data) == 0: - break - - if recognizer.AcceptWaveform(data): - result_json = json.loads(recognizer.Result()) - if "text" in result_json and result_json["text"].strip(): - result_text += result_json["text"] + " " - - # 获取最终结果 - final_result = json.loads(recognizer.FinalResult()) - if "text" in final_result and final_result["text"].strip(): - result_text += final_result["text"] - - result_text = result_text.strip() - - logger.bind(tag=TAG).debug(f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result_text}") - - return result_text, file_path - - except Exception as e: - logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) - 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}") \ No newline at end of file From 665318164e442da033b75f42e9c4c5362efdf0e9 Mon Sep 17 00:00:00 2001 From: joey <38903653+joey-zhou@users.noreply.github.com> Date: Mon, 31 Mar 2025 21:23:40 +0800 Subject: [PATCH 07/18] Update requirements.txt --- main/xiaozhi-server/requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index 32ba5dcf..485ec14b 100755 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -23,5 +23,4 @@ bs4==0.0.2 modelscope==1.23.2 sherpa_onnx==1.11.0 cnlunar==0.2.0 -vosk==0.3.45 -mcp==1.4.1 \ No newline at end of file +mcp==1.4.1 From d3c8e4badbb3da9e71644db5fed2619eafdefe45 Mon Sep 17 00:00:00 2001 From: JoeyZhou Date: Mon, 31 Mar 2025 22:21:46 +0800 Subject: [PATCH 08/18] =?UTF-8?q?feat:=E5=90=8E=E7=AB=AF=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E6=96=B0=E5=A2=9E=E5=90=AF=E5=8A=A8=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E8=87=AA=E5=8A=A8=E5=BB=BA=E7=AB=8Blog=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + main/manager-api/README.md | 4 +-- main/manager-api/start.sh | 71 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 main/manager-api/start.sh diff --git a/.gitignore b/.gitignore index e4c2ac00..57a2a227 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ docs/_build/ # PyBuilder .pybuilder/ target/ +*.pid # Jupyter Notebook .ipynb_checkpoints diff --git a/main/manager-api/README.md b/main/manager-api/README.md index 6aa324c2..fb363b54 100644 --- a/main/manager-api/README.md +++ b/main/manager-api/README.md @@ -86,10 +86,10 @@ src/main/java/xiaozhi/AdminApplication.java mvn clean install ``` -把jar包放在服务器上,执行 +把jar包与start.sh放在服务器上,执行 ``` -nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.active=dev > xiaozhi-server.log 2>&1 & echo $! > xiaozhi-server.pid +sh start.sh ``` # 接口文档 diff --git a/main/manager-api/start.sh b/main/manager-api/start.sh new file mode 100644 index 00000000..39a6c465 --- /dev/null +++ b/main/manager-api/start.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# 应用名称和JAR包路径 +APP_NAME="xiaozhi-esp32-api" +JAR_FILE="xiaozhi-esp32-api.jar" +PID_FILE="xiaozhi-server.pid" +PROFILE="dev" + +# 创建日志目录 +LOG_DIR="logs" +mkdir -p $LOG_DIR + +# 获取当前日期和时间作为日志文件名(精确到秒) +CURRENT_DATETIME=$(date +"%Y-%m-%d_%H-%M-%S") +LOG_FILE="$LOG_DIR/${APP_NAME}_${CURRENT_DATETIME}.log" + +# 检查JAR文件是否存在,如果不存在则尝试在target目录中查找 +if [ ! -f "$JAR_FILE" ]; then + + # 检查target目录是否存在 + if [ -d "target" ]; then + # 在target目录中查找JAR文件 + TARGET_JAR=$(find target -name "$JAR_FILE" -type f | head -n 1) + + if [ -n "$TARGET_JAR" ]; then + JAR_FILE="$TARGET_JAR" + else + # 如果找不到指定名称的JAR,尝试查找任何JAR文件 + TARGET_JAR=$(find target -name "*.jar" -not -name "*sources.jar" -not -name "*javadoc.jar" -not -name "*tests.jar" -type f | head -n 1) + + if [ -n "$TARGET_JAR" ]; then + JAR_FILE="$TARGET_JAR" + else + echo "错误: 无法找到JAR文件,请确保已编译项目" + exit 1 + fi + fi + else + echo "错误: 未找到target目录,请确保已编译项目" + exit 1 + fi +fi + +# 检查是否已经有实例在运行 +if [ -f "$PID_FILE" ]; then + OLD_PID=$(cat $PID_FILE) + if ps -p $OLD_PID > /dev/null; then + echo "发现已运行的实例 (PID: $OLD_PID),正在停止..." + kill $OLD_PID + sleep 5 + + # 检查进程是否仍在运行,如果是则强制终止 + if ps -p $OLD_PID > /dev/null; then + echo "进程未能正常终止,正在强制终止..." + kill -9 $OLD_PID + sleep 2 + fi + fi +fi + +# 启动应用 +nohup java -jar $JAR_FILE --spring.profiles.active=$PROFILE > $LOG_FILE 2>&1 & echo $! > $PID_FILE +echo "$APP_NAME 已启动,PID: $(cat $PID_FILE)" + +# 创建一个符号链接指向最新的日志文件,方便查看 +LATEST_LOG_LINK="$LOG_DIR/${APP_NAME}_latest.log" +if [ -L "$LATEST_LOG_LINK" ]; then + rm "$LATEST_LOG_LINK" +fi +ln -s "$(basename $LOG_FILE)" "$LATEST_LOG_LINK" +echo "最新日志文件链接: $LATEST_LOG_LINK" From 086dea859498ba0d448b57a1532023e21642224c Mon Sep 17 00:00:00 2001 From: JoeyZhou Date: Mon, 31 Mar 2025 23:58:12 +0800 Subject: [PATCH 09/18] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E7=94=9F=E6=88=90bug?= =?UTF-8?q?=EF=BC=8C=E9=87=87=E7=94=A8logback=E6=8E=A7=E5=88=B6=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 9 ++ .../src/main/resources/logback-spring.xml | 128 ++++++++++++++---- 2 files changed, 109 insertions(+), 28 deletions(-) diff --git a/main/manager-api/src/main/resources/application.yml b/main/manager-api/src/main/resources/application.yml index 902b5198..66662a78 100644 --- a/main/manager-api/src/main/resources/application.yml +++ b/main/manager-api/src/main/resources/application.yml @@ -65,5 +65,14 @@ mybatis-plus: blobType: BLOB boolValue: TRUE +# 添加日志相关配置 +logging: + config: classpath:logback-spring.xml + file: + path: ./logs + level: + root: info + xiaozhi: debug + app: fronted-url: http://localhost:8001 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/logback-spring.xml b/main/manager-api/src/main/resources/logback-spring.xml index 68ea4cf6..add76cf6 100644 --- a/main/manager-api/src/main/resources/logback-spring.xml +++ b/main/manager-api/src/main/resources/logback-spring.xml @@ -1,35 +1,107 @@ - - - - - + + + + + + + + + + + + ${LOG_HOME} + true + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}) - %msg%n + UTF-8 + + + + + + ${LOG_HOME}/xiaozhi-esp32-api.log + + ${LOG_HOME}/xiaozhi-esp32-api.%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 2GB + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + + ${LOG_HOME}/error.log + + ERROR + + + ${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 1GB + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + - - + + + + + + + + - - + + + + + + + + + + + + + + - - - - /system/logs/admin.%d{yyyy-MM-dd}.%i.log - - 1MB - - 7 - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n - - - - - + + + + + + + + + + - - \ No newline at end of file + + + + true + + From a7b5e0df6c0ba4952f3ff95cc6ea5a47c642d248 Mon Sep 17 00:00:00 2001 From: joey <38903653+joey-zhou@users.noreply.github.com> Date: Tue, 1 Apr 2025 00:02:40 +0800 Subject: [PATCH 10/18] Delete main/manager-api/start.sh --- main/manager-api/start.sh | 71 --------------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 main/manager-api/start.sh diff --git a/main/manager-api/start.sh b/main/manager-api/start.sh deleted file mode 100644 index 39a6c465..00000000 --- a/main/manager-api/start.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -# 应用名称和JAR包路径 -APP_NAME="xiaozhi-esp32-api" -JAR_FILE="xiaozhi-esp32-api.jar" -PID_FILE="xiaozhi-server.pid" -PROFILE="dev" - -# 创建日志目录 -LOG_DIR="logs" -mkdir -p $LOG_DIR - -# 获取当前日期和时间作为日志文件名(精确到秒) -CURRENT_DATETIME=$(date +"%Y-%m-%d_%H-%M-%S") -LOG_FILE="$LOG_DIR/${APP_NAME}_${CURRENT_DATETIME}.log" - -# 检查JAR文件是否存在,如果不存在则尝试在target目录中查找 -if [ ! -f "$JAR_FILE" ]; then - - # 检查target目录是否存在 - if [ -d "target" ]; then - # 在target目录中查找JAR文件 - TARGET_JAR=$(find target -name "$JAR_FILE" -type f | head -n 1) - - if [ -n "$TARGET_JAR" ]; then - JAR_FILE="$TARGET_JAR" - else - # 如果找不到指定名称的JAR,尝试查找任何JAR文件 - TARGET_JAR=$(find target -name "*.jar" -not -name "*sources.jar" -not -name "*javadoc.jar" -not -name "*tests.jar" -type f | head -n 1) - - if [ -n "$TARGET_JAR" ]; then - JAR_FILE="$TARGET_JAR" - else - echo "错误: 无法找到JAR文件,请确保已编译项目" - exit 1 - fi - fi - else - echo "错误: 未找到target目录,请确保已编译项目" - exit 1 - fi -fi - -# 检查是否已经有实例在运行 -if [ -f "$PID_FILE" ]; then - OLD_PID=$(cat $PID_FILE) - if ps -p $OLD_PID > /dev/null; then - echo "发现已运行的实例 (PID: $OLD_PID),正在停止..." - kill $OLD_PID - sleep 5 - - # 检查进程是否仍在运行,如果是则强制终止 - if ps -p $OLD_PID > /dev/null; then - echo "进程未能正常终止,正在强制终止..." - kill -9 $OLD_PID - sleep 2 - fi - fi -fi - -# 启动应用 -nohup java -jar $JAR_FILE --spring.profiles.active=$PROFILE > $LOG_FILE 2>&1 & echo $! > $PID_FILE -echo "$APP_NAME 已启动,PID: $(cat $PID_FILE)" - -# 创建一个符号链接指向最新的日志文件,方便查看 -LATEST_LOG_LINK="$LOG_DIR/${APP_NAME}_latest.log" -if [ -L "$LATEST_LOG_LINK" ]; then - rm "$LATEST_LOG_LINK" -fi -ln -s "$(basename $LOG_FILE)" "$LATEST_LOG_LINK" -echo "最新日志文件链接: $LATEST_LOG_LINK" From 19a067f4e842d896f96bb52c1561764c52d9e6ee Mon Sep 17 00:00:00 2001 From: joey <38903653+joey-zhou@users.noreply.github.com> Date: Tue, 1 Apr 2025 00:04:16 +0800 Subject: [PATCH 11/18] Update README.md --- main/manager-api/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main/manager-api/README.md b/main/manager-api/README.md index 0e3e5e79..8788400f 100644 --- a/main/manager-api/README.md +++ b/main/manager-api/README.md @@ -84,7 +84,10 @@ src/main/java/xiaozhi/AdminApplication.java mvn clean install ``` -把jar包与start.sh放在服务器上,执行 +把jar包放在服务器上,执行 + +``` +nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.activate=dev ``` sh start.sh From 5c3ae278be0f93442e384c6aeb59aee0a6fa7179 Mon Sep 17 00:00:00 2001 From: joey <38903653+joey-zhou@users.noreply.github.com> Date: Tue, 1 Apr 2025 00:05:32 +0800 Subject: [PATCH 12/18] Update README.md --- main/manager-api/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/main/manager-api/README.md b/main/manager-api/README.md index 8788400f..98010c62 100644 --- a/main/manager-api/README.md +++ b/main/manager-api/README.md @@ -88,10 +88,8 @@ mvn clean install ``` nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.activate=dev +``` -``` -sh start.sh -``` # 接口文档 启动后打开:http://localhost:8002/xiaozhi-esp32-api/doc.html From 8bc1d09c4a41b98ce3294c2e7d7144a0fddb8fac Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 1 Apr 2025 21:31:50 +0800 Subject: [PATCH 13/18] =?UTF-8?q?fix=EF=BC=9Ahass=5Fset=5Fstate=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E5=88=A4=E6=96=AD=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins_func/functions/hass_init.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/main/xiaozhi-server/plugins_func/functions/hass_init.py b/main/xiaozhi-server/plugins_func/functions/hass_init.py index e09eb67f..17846cfa 100644 --- a/main/xiaozhi-server/plugins_func/functions/hass_init.py +++ b/main/xiaozhi-server/plugins_func/functions/hass_init.py @@ -10,7 +10,7 @@ HASS_CACHE = {} def append_devices_to_prompt(conn): if conn.use_function_call_mode: funcs = conn.config["Intent"]["function_call"].get("functions", []) - if "hass_get_state" in funcs or "hass_get_state" in funcs: + if "hass_get_state" in funcs or "hass_set_state" in funcs: prompt = "下面是我家智能设备,可以通过homeassistant控制\n" devices = conn.config["plugins"]["home_assistant"].get("devices", []) if len(devices) == 0: @@ -27,9 +27,13 @@ def initialize_hass_handler(conn): if HASS_CACHE == {}: if conn.use_function_call_mode: funcs = conn.config["Intent"]["function_call"].get("functions", []) - if "hass_get_state" in funcs or "hass_get_state" in funcs: - HASS_CACHE['base_url'] = conn.config["plugins"]["home_assistant"].get("base_url") - HASS_CACHE['api_key'] = conn.config["plugins"]["home_assistant"].get("api_key") + if "hass_get_state" in funcs or "hass_set_state" in funcs: + HASS_CACHE["base_url"] = conn.config["plugins"]["home_assistant"].get( + "base_url" + ) + HASS_CACHE["api_key"] = conn.config["plugins"]["home_assistant"].get( + "api_key" + ) - check_model_key("home_assistant", HASS_CACHE['api_key']) + check_model_key("home_assistant", HASS_CACHE["api_key"]) return HASS_CACHE From d668917694e7866409081c8d89c992da9c845ead Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 1 Apr 2025 21:49:26 +0800 Subject: [PATCH 14/18] =?UTF-8?q?fix:=E7=94=A8=E6=88=B7=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E9=A1=B5=E7=A0=81=E5=8F=82=E6=95=B0=E9=94=99=E8=AF=AFbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xiaozhi/modules/sys/controller/AdminController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java index ee551400..535fb548 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java @@ -50,7 +50,7 @@ public class AdminController { AdminPageUserDTO dto = new AdminPageUserDTO(); dto.setMobile((String) params.get("mobile")); dto.setLimit((String) params.get(Constant.LIMIT)); - dto.setPage((String) params.get("pages")); + dto.setPage((String) params.get(Constant.PAGE)); ValidatorUtils.validateEntity(dto); PageData page = sysUserService.page(dto); From 86621661df7ae9e923a69bf44220dc98f901bc27 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 1 Apr 2025 22:04:04 +0800 Subject: [PATCH 15/18] =?UTF-8?q?update:=E6=97=A5=E5=BF=97=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=9C=A8logback=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=8D=B3=E5=8F=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-api/src/main/resources/application.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/main/manager-api/src/main/resources/application.yml b/main/manager-api/src/main/resources/application.yml index 66662a78..902b5198 100644 --- a/main/manager-api/src/main/resources/application.yml +++ b/main/manager-api/src/main/resources/application.yml @@ -65,14 +65,5 @@ mybatis-plus: blobType: BLOB boolValue: TRUE -# 添加日志相关配置 -logging: - config: classpath:logback-spring.xml - file: - path: ./logs - level: - root: info - xiaozhi: debug - app: fronted-url: http://localhost:8001 \ No newline at end of file From 6d3f270a8f704935fb5bfcecdfb32637dd086ed3 Mon Sep 17 00:00:00 2001 From: JoeyZhou Date: Tue, 1 Apr 2025 22:07:22 +0800 Subject: [PATCH 16/18] =?UTF-8?q?=E6=B7=BB=E5=8A=A0tencent=20asr=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 7289743b..da279acf 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -172,6 +172,12 @@ ASR: access_token: 你的火山引擎语音合成服务access_token cluster: volcengine_input_common output_dir: tmp/ + TencentASR: + type: tencent + appid: 你的腾讯语音合成服务appid + secret_id: 你的腾讯语音合成服务secret_id + secret_key: 你的腾讯语音合成服务secret_key + output_dir: tmp/ VAD: SileroVAD: threshold: 0.5 From c4d3020a1d32743102c5e47c82c775f31e002f12 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 1 Apr 2025 23:10:10 +0800 Subject: [PATCH 17/18] =?UTF-8?q?update=EF=BC=9A=E5=A2=9E=E5=8A=A0tencenta?= =?UTF-8?q?sr=E9=A2=86=E5=8F=96=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 0ec6778a..d38cdc1f 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -188,6 +188,8 @@ ASR: cluster: volcengine_input_common output_dir: tmp/ TencentASR: + # token申请地址:https://console.cloud.tencent.com/cam/capi + # 免费领取资源:https://console.cloud.tencent.com/asr/resourcebundle type: tencent appid: 你的腾讯语音合成服务appid secret_id: 你的腾讯语音合成服务secret_id From 510ee1b282962119cfb91262f84db0d134c36c98 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 1 Apr 2025 23:14:15 +0800 Subject: [PATCH 18/18] =?UTF-8?q?update=EF=BC=9Amanager-api=E5=B7=B2?= =?UTF-8?q?=E6=8E=A5=E8=BF=91=E5=AE=8C=E6=88=90=EF=BC=8C=E7=8E=B0=E5=9C=A8?= =?UTF-8?q?=E8=BF=9B=E5=85=A5=E6=AD=A3=E5=BC=8F=E6=8E=A5=E5=8F=A3=E8=81=94?= =?UTF-8?q?=E8=B0=83=E9=98=B6=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/main/manager-web/README.md b/main/manager-web/README.md index a81c8ff6..945ea29d 100644 --- a/main/manager-web/README.md +++ b/main/manager-web/README.md @@ -6,8 +6,6 @@ 开发使用代码编辑器,导入项目时,选择`manager-web`文件夹作为项目目录 -参照[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU)开发 - ``` npm install ```