From d86a2cf9de36e1e9c65b8c63835ad66ed8f1636a Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Thu, 29 May 2025 10:38:01 +0800
Subject: [PATCH 1/6] =?UTF-8?q?update:=E6=8A=BD=E7=A6=BB=E5=85=AC=E5=85=B1?=
=?UTF-8?q?=E6=96=B9=E6=B3=95save=5Faudio=5Fto=5Ffile=E5=88=B0=E7=88=B6?=
=?UTF-8?q?=E7=B1=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../core/providers/asr/aliyun.py | 17 -------------
.../core/providers/asr/baidu.py | 14 -----------
.../xiaozhi-server/core/providers/asr/base.py | 20 ++++++++++++----
.../core/providers/asr/doubao.py | 24 +++----------------
.../core/providers/asr/fun_local.py | 14 -----------
.../core/providers/asr/fun_server.py | 14 -----------
.../core/providers/asr/sherpa_onnx_local.py | 14 -----------
.../core/providers/asr/tencent.py | 16 -------------
8 files changed, 19 insertions(+), 114 deletions(-)
diff --git a/main/xiaozhi-server/core/providers/asr/aliyun.py b/main/xiaozhi-server/core/providers/asr/aliyun.py
index 74fb4091..5fa18253 100644
--- a/main/xiaozhi-server/core/providers/asr/aliyun.py
+++ b/main/xiaozhi-server/core/providers/asr/aliyun.py
@@ -3,8 +3,6 @@ import json
import asyncio
from typing import Optional, Tuple, List
import opuslib_next
-import wave
-import io
import os
import uuid
import hmac
@@ -165,21 +163,6 @@ class ASRProvider(ASRProviderBase):
request += "&enable_voice_detection=false"
return request
- def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
- """PCM数据保存为WAV文件"""
- module_name = __name__.split(".")[-1]
- file_name = f"asr_{module_name}_{session_id}_{uuid.uuid4()}.wav"
- file_path = os.path.join(self.output_dir, file_name)
-
- with wave.open(file_path, "wb") as wf:
- wf.setnchannels(1) # 单声道
- wf.setsampwidth(2) # 16-bit
- wf.setframerate(self.sample_rate)
- wf.writeframes(b"".join(pcm_data))
-
- logger.bind(tag=TAG).debug(f"音频文件已保存至: {file_path}")
- return file_path
-
async def _send_request(self, pcm_data: bytes) -> Optional[str]:
"""发送请求到阿里云ASR服务"""
try:
diff --git a/main/xiaozhi-server/core/providers/asr/baidu.py b/main/xiaozhi-server/core/providers/asr/baidu.py
index fe73fd45..87cb6a71 100644
--- a/main/xiaozhi-server/core/providers/asr/baidu.py
+++ b/main/xiaozhi-server/core/providers/asr/baidu.py
@@ -36,20 +36,6 @@ class ASRProvider(ASRProviderBase):
# 确保输出目录存在
os.makedirs(self.output_dir, exist_ok=True)
- def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
- """PCM数据保存为WAV文件"""
- module_name = __name__.split(".")[-1]
- file_name = f"asr_{module_name}_{session_id}_{uuid.uuid4()}.wav"
- file_path = os.path.join(self.output_dir, file_name)
-
- 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
-
async def speech_to_text(
self, opus_data: List[bytes], session_id: str
) -> Tuple[Optional[str], Optional[str]]:
diff --git a/main/xiaozhi-server/core/providers/asr/base.py b/main/xiaozhi-server/core/providers/asr/base.py
index 9d974180..153f40e2 100644
--- a/main/xiaozhi-server/core/providers/asr/base.py
+++ b/main/xiaozhi-server/core/providers/asr/base.py
@@ -1,7 +1,10 @@
-from abc import ABC, abstractmethod
-from typing import Optional, Tuple, List
+import os
+import uuid
+import wave
import opuslib_next
+from abc import ABC, abstractmethod
from config.logger import setup_logging
+from typing import Optional, Tuple, List
TAG = __name__
logger = setup_logging()
@@ -11,10 +14,19 @@ class ASRProviderBase(ABC):
def __init__(self):
self.audio_format = "opus"
- @abstractmethod
def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
"""PCM数据保存为WAV文件"""
- pass
+ module_name = __name__.split(".")[-1]
+ file_name = f"asr_{module_name}_{session_id}_{uuid.uuid4()}.wav"
+ file_path = os.path.join(self.output_dir, file_name)
+
+ 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
@abstractmethod
async def speech_to_text(
diff --git a/main/xiaozhi-server/core/providers/asr/doubao.py b/main/xiaozhi-server/core/providers/asr/doubao.py
index 403c181f..bd33af40 100644
--- a/main/xiaozhi-server/core/providers/asr/doubao.py
+++ b/main/xiaozhi-server/core/providers/asr/doubao.py
@@ -1,17 +1,13 @@
import time
-import io
-import wave
import os
-from typing import Optional, Tuple, List
import uuid
-import websockets
import json
import gzip
-
-import opuslib_next
+import websockets
+from config.logger import setup_logging
+from typing import Optional, Tuple, List
from core.providers.asr.base import ASRProviderBase
-from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
@@ -102,20 +98,6 @@ class ASRProvider(ASRProviderBase):
# 确保输出目录存在
os.makedirs(self.output_dir, exist_ok=True)
- def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
- """PCM数据保存为WAV文件"""
- module_name = __name__.split(".")[-1]
- file_name = f"asr_{module_name}_{session_id}_{uuid.uuid4()}.wav"
- file_path = os.path.join(self.output_dir, file_name)
-
- 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 _generate_header(
message_type=CLIENT_FULL_REQUEST, message_type_specific_flags=NO_SEQUENCE
diff --git a/main/xiaozhi-server/core/providers/asr/fun_local.py b/main/xiaozhi-server/core/providers/asr/fun_local.py
index c8446574..e3b7fc83 100644
--- a/main/xiaozhi-server/core/providers/asr/fun_local.py
+++ b/main/xiaozhi-server/core/providers/asr/fun_local.py
@@ -53,20 +53,6 @@ class ASRProvider(ASRProviderBase):
# device="cuda:0", # 启用GPU加速
)
- def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
- """PCM数据保存为WAV文件"""
- module_name = __name__.split(".")[-1]
- file_name = f"asr_{module_name}_{session_id}_{uuid.uuid4()}.wav"
- file_path = os.path.join(self.output_dir, file_name)
-
- 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
-
async def speech_to_text(
self, opus_data: List[bytes], session_id: str
) -> Tuple[Optional[str], Optional[str]]:
diff --git a/main/xiaozhi-server/core/providers/asr/fun_server.py b/main/xiaozhi-server/core/providers/asr/fun_server.py
index 97c9fdc9..ff411340 100644
--- a/main/xiaozhi-server/core/providers/asr/fun_server.py
+++ b/main/xiaozhi-server/core/providers/asr/fun_server.py
@@ -43,20 +43,6 @@ class ASRProvider(ASRProviderBase):
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE
- def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
- """PCM数据保存为WAV文件"""
- module_name = __name__.split(".")[-1]
- file_name = f"asr_{module_name}_{session_id}_{uuid.uuid4()}.wav"
- file_path = os.path.join(self.output_dir, file_name)
-
- 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
-
async def _receive_responses(self, ws) -> None:
"""
Asynchronous generator to receive messages from the WebSocket.
diff --git a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py
index 667e1606..24522d83 100644
--- a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py
+++ b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py
@@ -84,20 +84,6 @@ class ASRProvider(ASRProviderBase):
use_itn=True,
)
- def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
- """PCM数据保存为WAV文件"""
- module_name = __name__.split(".")[-1]
- file_name = f"asr_{module_name}_{session_id}_{uuid.uuid4()}.wav"
- file_path = os.path.join(self.output_dir, file_name)
-
- 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
-
def read_wave(self, wave_filename: str) -> Tuple[np.ndarray, int]:
"""
Args:
diff --git a/main/xiaozhi-server/core/providers/asr/tencent.py b/main/xiaozhi-server/core/providers/asr/tencent.py
index 5ab1befb..22a7ed94 100644
--- a/main/xiaozhi-server/core/providers/asr/tencent.py
+++ b/main/xiaozhi-server/core/providers/asr/tencent.py
@@ -8,8 +8,6 @@ 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
@@ -33,20 +31,6 @@ class ASRProvider(ASRProviderBase):
# 确保输出目录存在
os.makedirs(self.output_dir, exist_ok=True)
- def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
- """PCM数据保存为WAV文件"""
- module_name = __name__.split(".")[-1]
- file_name = f"asr_{module_name}_{session_id}_{uuid.uuid4()}.wav"
- file_path = os.path.join(self.output_dir, file_name)
-
- 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
-
async def speech_to_text(
self, opus_data: List[bytes], session_id: str
) -> Tuple[Optional[str], Optional[str]]:
From bb42095ca0a6ec7afb71d740e211548be0fb3eea Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Thu, 29 May 2025 23:56:34 +0800
Subject: [PATCH 2/6] =?UTF-8?q?update:=E5=85=BC=E5=AE=B9=E8=B1=86=E5=8C=85?=
=?UTF-8?q?=E6=B5=81=E5=BC=8FASR?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-server/core/connection.py | 38 +-
.../core/handle/intentHandler.py | 2 -
.../core/handle/receiveAudioHandle.py | 59 +-
.../core/handle/sendAudioHandle.py | 5 +
main/xiaozhi-server/core/handle/textHandle.py | 5 +-
main/xiaozhi-server/core/ota_server.py | 4 +-
.../core/providers/asr/aliyun.py | 3 +-
.../core/providers/asr/baidu.py | 11 +-
.../xiaozhi-server/core/providers/asr/base.py | 53 ++
.../core/providers/asr/doubao.py | 584 +++++++++++-------
.../core/providers/asr/dto/dto.py | 9 +
.../core/providers/asr/fun_local.py | 4 +-
.../core/providers/asr/fun_server.py | 6 +-
.../core/providers/asr/sherpa_onnx_local.py | 4 +-
.../core/providers/asr/tencent.py | 4 +-
.../core/utils/modules_initialize.py | 128 ++++
main/xiaozhi-server/core/utils/util.py | 118 ----
main/xiaozhi-server/core/websocket_server.py | 3 +-
18 files changed, 630 insertions(+), 410 deletions(-)
create mode 100644 main/xiaozhi-server/core/providers/asr/dto/dto.py
create mode 100644 main/xiaozhi-server/core/utils/modules_initialize.py
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index 04c352bf..b8f06f70 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -12,18 +12,22 @@ import subprocess
import websockets
from core.utils.util import (
extract_json_from_string,
- initialize_modules,
check_vad_update,
check_asr_update,
filter_sensitive_info,
- initialize_tts,
)
from typing import Dict, Any
from core.mcp.manager import MCPManager
+from core.utils.modules_initialize import (
+ initialize_modules,
+ initialize_tts,
+ initialize_asr,
+)
from core.handle.reportHandle import report
from core.providers.tts.default import DefaultTTS
from concurrent.futures import ThreadPoolExecutor
from core.utils.dialogue import Message, Dialogue
+from core.providers.asr.dto.dto import InterfaceType
from core.handle.textHandle import handleTextMessage
from core.handle.functionHandler import FunctionHandler
from plugins_func.loadplugins import auto_import_modules
@@ -112,7 +116,6 @@ class ConnectionHandler:
# asr相关变量
self.asr_audio = []
- self.asr_server_receive = True
# llm相关变量
self.llm_finish_task = True
@@ -315,10 +318,14 @@ class ConnectionHandler:
if self.vad is None:
self.vad = self._vad
if self.asr is None:
- self.asr = self._asr
+ self.asr = self._initialize_asr()
+ # 打开语音识别通道
+ asyncio.run_coroutine_threadsafe(
+ self.asr.open_audio_channels(self), self.loop
+ )
if self.tts is None:
self.tts = self._initialize_tts()
- # 使用事件循环运行异步方法
+ # 打开语音合成通道
asyncio.run_coroutine_threadsafe(
self.tts.open_audio_channels(self), self.loop
)
@@ -356,6 +363,19 @@ class ConnectionHandler:
return tts
+ def _initialize_asr(self):
+ """初始化ASR"""
+ if self._asr.interface_type == InterfaceType.LOCAL:
+ # 如果公共ASR是本地服务,则直接返回
+ # 因为本地一个实例ASR,可以被多个连接共享
+ asr = self._asr
+ else:
+ # 如果公共ASR是远程服务,则初始化一个新实例
+ # 因为远程ASR,涉及到websocket连接和接收线程,需要每个连接一个实例
+ asr = initialize_asr(self.config)
+
+ return asr
+
def _initialize_private_config(self):
"""如果是从配置文件获取,则进行二次实例化"""
if not self.read_config_from_api:
@@ -597,6 +617,8 @@ class ConnectionHandler:
text_index = 0
for response in llm_responses:
+ if self.client_abort:
+ break
if functions is not None:
content, tools_call = response
if "content" in response:
@@ -622,9 +644,6 @@ class ConnectionHandler:
if content is not None and len(content) > 0:
if not tool_call_flag:
response_message.append(content)
- if self.client_abort:
- break
-
if text_index == 0:
self.tts.tts_text_queue.put(
TTSMessageDTO(
@@ -801,9 +820,7 @@ class ConnectionHandler:
item = self.report_queue.get(timeout=1)
if item is None: # 检测毒丸对象
break
-
type, text, audio_data, report_time = item
-
try:
# 检查线程池状态
if self.executor is None:
@@ -834,7 +851,6 @@ class ConnectionHandler:
def clearSpeakStatus(self):
self.logger.bind(tag=TAG).debug(f"清除服务端讲话状态")
- self.asr_server_receive = True
async def close(self, ws=None):
"""资源清理方法"""
diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py
index 4c876550..cb2e3c69 100644
--- a/main/xiaozhi-server/core/handle/intentHandler.py
+++ b/main/xiaozhi-server/core/handle/intentHandler.py
@@ -1,4 +1,3 @@
-from config.logger import setup_logging
import json
import uuid
from core.handle.sendAudioHandle import send_stt_message
@@ -146,4 +145,3 @@ async def process_intent_result(conn, intent_result, original_text):
def speak_txt(conn, text):
conn.tts.tts_one_sentence(conn, ContentType.TEXT, content_detail=text)
conn.dialogue.put(Message(role="assistant", content=text))
-
diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
index 18482a04..6776dd0f 100644
--- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py
+++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
@@ -1,10 +1,7 @@
-import time
-import copy
-from core.utils.util import remove_punctuation_and_length
from core.handle.sendAudioHandle import send_stt_message
from core.handle.intentHandler import handle_user_intent
from core.utils.output_counter import check_device_output_limit
-from core.handle.reportHandle import enqueue_asr_report
+import time
from core.handle.sendAudioHandle import SentenceType
from core.utils.util import audio_to_data
@@ -13,46 +10,17 @@ TAG = __name__
async def handleAudioMessage(conn, audio):
if conn.vad is None:
+ conn.logger.bind(tag=TAG).warning("VAD模块未初始化,继续等待")
return
- if not conn.asr_server_receive:
- conn.logger.bind(tag=TAG).debug(f"前期数据处理中,暂停接收")
+ if conn.asr is None:
+ conn.logger.bind(tag=TAG).warning("ASR模块未初始化,继续等待")
return
- if conn.client_listen_mode == "auto" or conn.client_listen_mode == "realtime":
- have_voice = conn.vad.is_vad(conn, audio)
- else:
- have_voice = conn.client_have_voice
-
- # 如果本次没有声音,本段也没声音,就把声音丢弃了
- if have_voice == False and conn.client_have_voice == False:
- await no_voice_close_connect(conn)
- conn.asr_audio.append(audio)
- conn.asr_audio = conn.asr_audio[
- -10:
- ] # 保留最新的10帧音频内容,解决ASR句首丢字问题
- return
- conn.client_no_voice_last_time = 0.0
- conn.asr_audio.append(audio)
- # 如果本段有声音,且已经停止了
- if conn.client_voice_stop:
- conn.client_abort = False
- conn.asr_server_receive = False
- # 音频太短了,无法识别
- if len(conn.asr_audio) < 15:
- conn.asr_server_receive = True
- else:
- raw_text, _ = await conn.asr.speech_to_text(
- conn.asr_audio, conn.session_id
- ) # 确保ASR模块返回原始文本
- conn.logger.bind(tag=TAG).info(f"识别文本: {raw_text}")
- text_len, _ = remove_punctuation_and_length(raw_text)
- if text_len > 0:
- # 使用自定义模块进行上报
- await startToChat(conn, raw_text)
- enqueue_asr_report(conn, raw_text, copy.deepcopy(conn.asr_audio))
- else:
- conn.asr_server_receive = True
- conn.asr_audio.clear()
- conn.reset_vad_states()
+ # 当前片段是否有人说话
+ have_voice = conn.vad.is_vad(conn, audio)
+ # 设备长时间空闲检测,用于say goodbye
+ await no_voice_close_connect(conn, have_voice)
+ # 接收音频
+ await conn.asr.receive_audio(audio, have_voice)
async def startToChat(conn, text):
@@ -73,7 +41,6 @@ async def startToChat(conn, text):
if intent_handled:
# 如果意图已被处理,不再进行聊天
- conn.asr_server_receive = True
return
# 意图未被处理,继续常规聊天流程
@@ -81,7 +48,10 @@ async def startToChat(conn, text):
conn.executor.submit(conn.chat, text)
-async def no_voice_close_connect(conn):
+async def no_voice_close_connect(conn, have_voice):
+ if have_voice:
+ conn.client_no_voice_last_time = 0.0
+ return
if conn.client_no_voice_last_time == 0.0:
conn.client_no_voice_last_time = time.time() * 1000
else:
@@ -95,7 +65,6 @@ async def no_voice_close_connect(conn):
):
conn.close_after_chat = True
conn.client_abort = False
- conn.asr_server_receive = False
end_prompt = conn.config.get("end_prompt", {})
if end_prompt and end_prompt.get("enable", True) is False:
conn.logger.bind(tag=TAG).info("结束对话,无需发送结束提示语")
diff --git a/main/xiaozhi-server/core/handle/sendAudioHandle.py b/main/xiaozhi-server/core/handle/sendAudioHandle.py
index 5694c618..6625838c 100644
--- a/main/xiaozhi-server/core/handle/sendAudioHandle.py
+++ b/main/xiaozhi-server/core/handle/sendAudioHandle.py
@@ -131,6 +131,11 @@ async def send_tts_message(conn, state, text=None):
async def send_stt_message(conn, text):
+ end_prompt_str = conn.config.get("end_prompt", {}).get("prompt")
+ if end_prompt_str and end_prompt_str == text:
+ await send_tts_message(conn, "start")
+ return
+
"""发送 STT 状态消息"""
stt_text = get_string_no_punctuation_or_emoji(text)
await conn.websocket.send(
diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py
index da6ede47..602f72aa 100644
--- a/main/xiaozhi-server/core/handle/textHandle.py
+++ b/main/xiaozhi-server/core/handle/textHandle.py
@@ -41,12 +41,13 @@ async def handleTextMessage(conn, message):
if len(conn.asr_audio) > 0:
await handleAudioMessage(conn, b"")
elif msg_json["state"] == "detect":
- conn.asr_server_receive = False
conn.client_have_voice = False
conn.asr_audio.clear()
if "text" in msg_json:
original_text = msg_json["text"] # 保留原始文本
- filtered_len, filtered_text = remove_punctuation_and_length(original_text)
+ filtered_len, filtered_text = remove_punctuation_and_length(
+ original_text
+ )
# 识别是否是唤醒词
is_wakeup_words = filtered_text in conn.config.get("wakeup_words")
diff --git a/main/xiaozhi-server/core/ota_server.py b/main/xiaozhi-server/core/ota_server.py
index 89e36869..5f89a503 100644
--- a/main/xiaozhi-server/core/ota_server.py
+++ b/main/xiaozhi-server/core/ota_server.py
@@ -3,8 +3,8 @@ import time
import asyncio
from aiohttp import web
from config.logger import setup_logging
-from core.connection import ConnectionHandler
-from core.utils.util import get_local_ip, initialize_modules
+from core.utils.util import get_local_ip
+from core.utils.modules_initialize import initialize_modules
TAG = __name__
diff --git a/main/xiaozhi-server/core/providers/asr/aliyun.py b/main/xiaozhi-server/core/providers/asr/aliyun.py
index 5fa18253..7ed24c00 100644
--- a/main/xiaozhi-server/core/providers/asr/aliyun.py
+++ b/main/xiaozhi-server/core/providers/asr/aliyun.py
@@ -2,7 +2,6 @@ import http.client
import json
import asyncio
from typing import Optional, Tuple, List
-import opuslib_next
import os
import uuid
import hmac
@@ -14,6 +13,7 @@ import time
from datetime import datetime
from config.logger import setup_logging
from core.providers.asr.base import ASRProviderBase
+from core.providers.asr.dto.dto import InterfaceType
TAG = __name__
logger = setup_logging()
@@ -90,6 +90,7 @@ class AccessToken:
class ASRProvider(ASRProviderBase):
def __init__(self, config: dict, delete_audio_file: bool):
super().__init__()
+ self.interface_type = InterfaceType.NON_STREAM
"""阿里云ASR初始化"""
# 新增空值判断逻辑
self.access_key_id = config.get("access_key_id")
diff --git a/main/xiaozhi-server/core/providers/asr/baidu.py b/main/xiaozhi-server/core/providers/asr/baidu.py
index 87cb6a71..7df94ff3 100644
--- a/main/xiaozhi-server/core/providers/asr/baidu.py
+++ b/main/xiaozhi-server/core/providers/asr/baidu.py
@@ -1,18 +1,10 @@
-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
-
from aip import AipSpeech
from core.providers.asr.base import ASRProviderBase
from config.logger import setup_logging
+from core.providers.asr.dto.dto import InterfaceType
TAG = __name__
logger = setup_logging()
@@ -21,6 +13,7 @@ logger = setup_logging()
class ASRProvider(ASRProviderBase):
def __init__(self, config: dict, delete_audio_file: bool = True):
super().__init__()
+ self.interface_type = InterfaceType.NON_STREAM
self.app_id = config.get("app_id")
self.api_key = config.get("api_key")
self.secret_key = config.get("secret_key")
diff --git a/main/xiaozhi-server/core/providers/asr/base.py b/main/xiaozhi-server/core/providers/asr/base.py
index 153f40e2..d3ce5ee0 100644
--- a/main/xiaozhi-server/core/providers/asr/base.py
+++ b/main/xiaozhi-server/core/providers/asr/base.py
@@ -1,10 +1,15 @@
import os
+import time
+import copy
import uuid
import wave
import opuslib_next
from abc import ABC, abstractmethod
from config.logger import setup_logging
from typing import Optional, Tuple, List
+from core.utils.util import remove_punctuation_and_length
+from core.handle.reportHandle import enqueue_asr_report
+from core.handle.receiveAudioHandle import startToChat
TAG = __name__
logger = setup_logging()
@@ -13,6 +18,54 @@ logger = setup_logging()
class ASRProviderBase(ABC):
def __init__(self):
self.audio_format = "opus"
+ self.conn = None
+
+ # 打开音频通道
+ # 这里默认是非流式的处理方式
+ # 流式处理方式请在子类中重写
+ async def open_audio_channels(self, conn):
+ self.conn = conn
+
+ # 接收音频
+ # 这里默认是非流式的处理方式
+ # 流式处理方式请在子类中重写
+ async def receive_audio(self, audio, audio_have_voice):
+ if (
+ self.conn.client_listen_mode == "auto"
+ or self.conn.client_listen_mode == "realtime"
+ ):
+ have_voice = audio_have_voice
+ else:
+ have_voice = self.conn.client_have_voice
+ # 如果本次没有声音,本段也没声音,就把声音丢弃了
+ if have_voice == False and self.conn.client_have_voice == False:
+ self.conn.asr_audio.append(audio)
+ self.conn.asr_audio = self.conn.asr_audio[-10:]
+ return
+
+ # 如果本段有声音,且已经停止了
+ if self.conn.client_voice_stop:
+ self.conn.client_abort = False
+ # 音频太短了,无法识别
+ if len(self.conn.asr_audio) < 15:
+ self.conn.asr_audio.clear()
+ self.conn.reset_vad_states()
+ else:
+ await self.handle_voice_stop()
+
+ # 处理语音停止
+ async def handle_voice_stop(self):
+ raw_text, _ = await self.speech_to_text(
+ self.conn.asr_audio, self.conn.session_id
+ ) # 确保ASR模块返回原始文本
+ self.conn.logger.bind(tag=TAG).info(f"识别文本: {raw_text}")
+ text_len, _ = remove_punctuation_and_length(raw_text)
+ if text_len > 0:
+ # 使用自定义模块进行上报
+ await startToChat(self.conn, raw_text)
+ enqueue_asr_report(self.conn, raw_text, copy.deepcopy(self.conn.asr_audio))
+ self.conn.asr_audio.clear()
+ self.conn.reset_vad_states()
def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
"""PCM数据保存为WAV文件"""
diff --git a/main/xiaozhi-server/core/providers/asr/doubao.py b/main/xiaozhi-server/core/providers/asr/doubao.py
index bd33af40..d738f508 100644
--- a/main/xiaozhi-server/core/providers/asr/doubao.py
+++ b/main/xiaozhi-server/core/providers/asr/doubao.py
@@ -1,265 +1,431 @@
-import time
-import os
-import uuid
import json
import gzip
+import uuid
+import asyncio
import websockets
-from config.logger import setup_logging
-from typing import Optional, Tuple, List
+import opuslib_next
from core.providers.asr.base import ASRProviderBase
-
+from config.logger import setup_logging
+from core.providers.asr.dto.dto import InterfaceType
+import threading
TAG = __name__
logger = setup_logging()
CLIENT_FULL_REQUEST = 0b0001
CLIENT_AUDIO_ONLY_REQUEST = 0b0010
-
-NO_SEQUENCE = 0b0000
-NEG_SEQUENCE = 0b0010
-
SERVER_FULL_RESPONSE = 0b1001
SERVER_ACK = 0b1011
SERVER_ERROR_RESPONSE = 0b1111
-
-NO_SERIALIZATION = 0b0000
-JSON = 0b0001
-THRIFT = 0b0011
-CUSTOM_TYPE = 0b1111
-NO_COMPRESSION = 0b0000
-GZIP = 0b0001
-CUSTOM_COMPRESSION = 0b1111
-
-
-def parse_response(res):
- """
- protocol_version(4 bits), header_size(4 bits),
- message_type(4 bits), message_type_specific_flags(4 bits)
- serialization_method(4 bits) message_compression(4 bits)
- reserved (8bits) 保留字段
- header_extensions 扩展头(大小等于 8 * 4 * (header_size - 1) )
- payload 类似与http 请求体
- """
- protocol_version = res[0] >> 4
- header_size = res[0] & 0x0F
- message_type = res[1] >> 4
- message_type_specific_flags = res[1] & 0x0F
- serialization_method = res[2] >> 4
- message_compression = res[2] & 0x0F
- reserved = res[3]
- header_extensions = res[4 : header_size * 4]
- payload = res[header_size * 4 :]
- result = {}
- payload_msg = None
- payload_size = 0
- if message_type == SERVER_FULL_RESPONSE:
- payload_size = int.from_bytes(payload[:4], "big", signed=True)
- payload_msg = payload[4:]
- elif message_type == SERVER_ACK:
- seq = int.from_bytes(payload[:4], "big", signed=True)
- result["seq"] = seq
- if len(payload) >= 8:
- payload_size = int.from_bytes(payload[4:8], "big", signed=False)
- payload_msg = payload[8:]
- elif message_type == SERVER_ERROR_RESPONSE:
- code = int.from_bytes(payload[:4], "big", signed=False)
- result["code"] = code
- payload_size = int.from_bytes(payload[4:8], "big", signed=False)
- payload_msg = payload[8:]
- if payload_msg is None:
- return result
- if message_compression == GZIP:
- payload_msg = gzip.decompress(payload_msg)
- if serialization_method == JSON:
- payload_msg = json.loads(str(payload_msg, "utf-8"))
- elif serialization_method != NO_SERIALIZATION:
- payload_msg = str(payload_msg, "utf-8")
- result["payload_msg"] = payload_msg
- result["payload_size"] = payload_size
- return result
+NO_SEQUENCE = 0b0000
+NEG_SEQUENCE = 0b0010
+JSON_SERIALIZATION = 0b0001
+GZIP_COMPRESSION = 0b0001
+PROTOCOL_VERSION = 0b0001
class ASRProvider(ASRProviderBase):
- def __init__(self, config: dict, delete_audio_file: bool):
+ def __init__(self, config, delete_audio_file):
super().__init__()
- self.appid = config.get("appid")
+ self.interface_type = InterfaceType.STREAM
+ self.config = config
+ self.text = ""
+ self.max_retries = 3
+ self.retry_delay = 2 # 重试延迟秒数
+ self.recv_lock = asyncio.Lock() # 添加接收锁
+
+ self.appid = str(config.get("appid"))
self.cluster = config.get("cluster")
self.access_token = config.get("access_token")
self.boosting_table_name = config.get("boosting_table_name", "")
self.correct_table_name = config.get("correct_table_name", "")
- self.output_dir = config.get("output_dir")
+ self.output_dir = config.get("output_dir", "temp/")
self.delete_audio_file = delete_audio_file
- self.host = "openspeech.bytedance.com"
- self.ws_url = f"wss://{self.host}/api/v2/asr"
- self.success_code = 1000
- self.seg_duration = 15000
+ self.ws_url = "wss://openspeech.bytedance.com/api/v2/asr"
+ self.uid = config.get("uid", "streaming_asr_service")
+ self.workflow = config.get(
+ "workflow", "audio_in,resample,partition,vad,fe,decode,itn,nlu_punctuate"
+ )
+ self.result_type = config.get("result_type", "single")
+ self.format = config.get("format", "raw")
+ self.codec = config.get("codec", "pcm")
+ self.rate = config.get("sample_rate", 16000)
+ self.language = config.get("language", "zh-CN")
+ self.bits = config.get("bits", 16)
+ self.channel = config.get("channel", 1)
+ self.auth_method = config.get("auth_method", "token")
+ self.secret = config.get("secret", "access_secret")
+ self.decoder = opuslib_next.Decoder(16000, 1)
+ self.asr_ws = None
+ self.forward_task = None
+ self.conn = None
+ self.asr_thread = None
- # 确保输出目录存在
- os.makedirs(self.output_dir, exist_ok=True)
+ ###################################################################################
+ # 豆包流式ASR重写父类的方法--开始
+ ###################################################################################
+ async def open_audio_channels(self, conn):
+ await super().open_audio_channels(conn)
- @staticmethod
- def _generate_header(
- message_type=CLIENT_FULL_REQUEST, message_type_specific_flags=NO_SEQUENCE
- ) -> bytearray:
- """Generate protocol header."""
- header = bytearray()
- header_size = 1
- header.append((0b0001 << 4) | header_size) # Protocol version
- header.append((message_type << 4) | message_type_specific_flags)
- header.append((0b0001 << 4) | 0b0001) # JSON serialization & GZIP compression
- header.append(0x00) # reserved
- return header
+ retry_count = 0
+ while retry_count < self.max_retries:
+ try:
+ # 确保关闭旧的连接
+ if self.asr_ws is not None:
+ try:
+ await self.asr_ws.close()
+ except Exception as e:
+ logger.bind(tag=TAG).warning(f"关闭旧连接时发生错误: {e}")
+ finally:
+ self.asr_ws = None
- def _construct_request(self, reqid) -> dict:
- """Construct the request payload."""
- return {
+ headers = self.token_auth() if self.auth_method == "token" else None
+ self.asr_ws = await websockets.connect(
+ self.ws_url,
+ additional_headers=headers,
+ max_size=1000000000,
+ ping_interval=None, # 禁用ping,因为服务器可能不支持
+ ping_timeout=None,
+ close_timeout=10,
+ )
+
+ # 发送初始化请求
+ request_params = self.construct_request(str(uuid.uuid4()))
+ try:
+ payload_bytes = str.encode(json.dumps(request_params))
+ payload_bytes = gzip.compress(payload_bytes)
+ full_client_request = self.generate_header()
+ full_client_request.extend((len(payload_bytes)).to_bytes(4, "big"))
+ full_client_request.extend(payload_bytes)
+ await self.asr_ws.send(full_client_request)
+ logger.bind(tag=TAG).debug(f"发送初始化请求: {request_params}")
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"发送初始化请求失败: {e}")
+ raise e
+
+ # 等待初始化响应
+ try:
+ init_res = await self.asr_ws.recv()
+ logger.bind(tag=TAG).debug(f"收到原始响应: {init_res}")
+ result = self.parse_response(init_res)
+ logger.bind(tag=TAG).info(f"ASR服务初始化响应: {result}")
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"ASR服务初始化失败: {e}")
+ raise e
+
+ # 启动接收ASR结果的异步任务
+ asr_priority = threading.Thread(
+ target=self._start_monitor_asr_response_thread, daemon=True
+ )
+ asr_priority.start()
+ return
+
+ except websockets.exceptions.WebSocketException as e:
+ retry_count += 1
+ if retry_count < self.max_retries:
+ logger.bind(tag=TAG).warning(
+ f"WebSocket连接失败,正在进行第{retry_count}次重试: {e}"
+ )
+ await asyncio.sleep(self.retry_delay)
+ else:
+ logger.bind(tag=TAG).error(
+ f"WebSocket连接失败,已达到最大重试次数: {e}"
+ )
+ raise
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"WebSocket连接发生未知错误: {e}")
+ raise
+
+ async def receive_audio(self, audio, _):
+ if not isinstance(audio, bytes):
+ return
+
+ try:
+ # 解码opus得到PCM数据
+ pcm_frame = self.decoder.decode(audio, 960)
+ payload = gzip.compress(pcm_frame)
+ audio_request = bytearray(self.generate_audio_default_header())
+ audio_request.extend(len(payload).to_bytes(4, "big"))
+ audio_request.extend(payload)
+ if self.asr_ws:
+ await self.asr_ws.send(audio_request)
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"发送音频数据时发生错误: {e}")
+
+ ###################################################################################
+ # 豆包流式ASR重写父类的方法--结束
+ ###################################################################################
+
+ def construct_request(self, reqid):
+ req = {
"app": {
- "appid": f"{self.appid}",
+ "appid": self.appid,
"cluster": self.cluster,
"token": self.access_token,
},
- "user": {
- "uid": str(uuid.uuid4()),
- },
+ "user": {"uid": self.uid},
"request": {
"reqid": reqid,
- "show_utterances": False,
+ "workflow": self.workflow,
+ "show_utterances": True,
+ "result_type": self.result_type,
"sequence": 1,
"boosting_table_name": self.boosting_table_name,
"correct_table_name": self.correct_table_name,
},
"audio": {
- "format": "raw",
- "rate": 16000,
- "language": "zh-CN",
- "bits": 16,
- "channel": 1,
- "codec": "raw",
+ "format": self.format,
+ "codec": self.codec,
+ "rate": self.rate,
+ "language": self.language,
+ "bits": self.bits,
+ "channel": self.channel,
},
}
+ return req
- async def _send_request(
- self, audio_data: List[bytes], segment_size: int
- ) -> Optional[str]:
- """Send request to Volcano ASR service."""
+ def token_auth(self):
+ return {"Authorization": f"Bearer; {self.access_token}"}
+
+ def generate_header(
+ self,
+ version=PROTOCOL_VERSION,
+ message_type=CLIENT_FULL_REQUEST,
+ message_type_specific_flags=NO_SEQUENCE,
+ serial_method=JSON_SERIALIZATION,
+ compression_type=GZIP_COMPRESSION,
+ reserved_data=0x00,
+ extension_header: bytes = b"",
+ ):
+ """
+ 生成协议头:
+ - 第1字节:高4位:协议版本,低4位:头部大小(单位 4 字节)
+ - 第2字节:高4位:消息类型,低4位:消息类型特定标志
+ - 第3字节:高4位:序列化方式,低4位:压缩方式
+ - 第4字节:保留字段
+ - 后续:扩展头(如果有)
+ """
+ header = bytearray()
+ header_size = int(len(extension_header) / 4) + 1
+ header.append((version << 4) | header_size)
+ header.append((message_type << 4) | message_type_specific_flags)
+ header.append((serial_method << 4) | compression_type)
+ header.append(reserved_data)
+ header.extend(extension_header)
+ return header
+
+ def generate_full_default_header(self):
+ # full client request 默认头
+ return self.generate_header(
+ version=PROTOCOL_VERSION,
+ message_type=CLIENT_FULL_REQUEST,
+ message_type_specific_flags=NO_SEQUENCE,
+ serial_method=JSON_SERIALIZATION,
+ compression_type=GZIP_COMPRESSION,
+ )
+
+ def generate_audio_default_header(self):
+ # 普通音频片段请求
+ return self.generate_header(
+ version=PROTOCOL_VERSION,
+ message_type=CLIENT_AUDIO_ONLY_REQUEST,
+ message_type_specific_flags=NO_SEQUENCE,
+ serial_method=JSON_SERIALIZATION,
+ compression_type=GZIP_COMPRESSION,
+ )
+
+ def generate_last_audio_default_header(self):
+ # 最后一个音频片段标志
+ return self.generate_header(
+ version=PROTOCOL_VERSION,
+ message_type=CLIENT_AUDIO_ONLY_REQUEST,
+ message_type_specific_flags=NEG_SEQUENCE, # 用 NEG_SEQUENCE 表示结束
+ serial_method=JSON_SERIALIZATION,
+ compression_type=GZIP_COMPRESSION,
+ )
+
+ def _start_monitor_asr_response_thread(self):
+ # 初始化链接
+ asyncio.run_coroutine_threadsafe(
+ self._forward_asr_results(), loop=self.conn.loop
+ )
+
+ async def _forward_asr_results(self):
try:
- auth_header = {"Authorization": "Bearer; {}".format(self.access_token)}
- async with websockets.connect(
- self.ws_url, additional_headers=auth_header
- ) as websocket:
- # Prepare request data
- request_params = self._construct_request(str(uuid.uuid4()))
- payload_bytes = str.encode(json.dumps(request_params))
- payload_bytes = gzip.compress(payload_bytes)
- full_client_request = self._generate_header()
- full_client_request.extend(
- (len(payload_bytes)).to_bytes(4, "big")
- ) # payload size(4 bytes)
- full_client_request.extend(payload_bytes) # payload
+ while not self.conn.stop_event.is_set():
+ try:
+ if self.asr_ws is None:
+ logger.bind(tag=TAG).info("尝试重新连接ASR服务...")
+ await self.open_audio_channels(self.conn)
+ continue
- # Send header and metadata
- # full_client_request
- await websocket.send(full_client_request)
- res = await websocket.recv()
- result = parse_response(res)
- if (
- "payload_msg" in result
- and result["payload_msg"]["code"] != self.success_code
- ):
- logger.bind(tag=TAG).error(f"ASR error: {result}")
- return None
-
- for seq, (chunk, last) in enumerate(
- self.slice_data(audio_data, segment_size), 1
- ):
- if last:
- audio_only_request = self._generate_header(
- message_type=CLIENT_AUDIO_ONLY_REQUEST,
- message_type_specific_flags=NEG_SEQUENCE,
+ # 使用锁来确保同一时间只有一个协程在接收数据
+ async with self.recv_lock:
+ response = await self.asr_ws.recv()
+ result = self.parse_response(response)
+ # 检查是否需要重连
+ if result.get("need_reconnect", False):
+ logger.bind(tag=TAG).info(
+ "检测到需要重连的错误,准备重新连接..."
)
- else:
- audio_only_request = self._generate_header(
- message_type=CLIENT_AUDIO_ONLY_REQUEST
- )
- payload_bytes = gzip.compress(chunk)
- audio_only_request.extend(
- (len(payload_bytes)).to_bytes(4, "big")
- ) # payload size(4 bytes)
- audio_only_request.extend(payload_bytes) # payload
- # Send audio data
- await websocket.send(audio_only_request)
+ if self.asr_ws is not None:
+ try:
+ await self.asr_ws.close()
+ except Exception as e:
+ logger.bind(tag=TAG).warning(
+ f"关闭旧连接时发生错误: {e}"
+ )
+ finally:
+ self.asr_ws = None
+ continue
- # Receive response
- response = await websocket.recv()
- result = parse_response(response)
-
- if (
- "payload_msg" in result
- and result["payload_msg"]["code"] == self.success_code
- ):
- if len(result["payload_msg"]["result"]) > 0:
- return result["payload_msg"]["result"][0]["text"]
- return None
- else:
- logger.bind(tag=TAG).error(f"ASR error: {result}")
- return None
+ if "payload_msg" in result:
+ if "result" in result["payload_msg"]:
+ # 检查是否有utterances并且definite为True
+ utterances = result["payload_msg"]["result"][0].get(
+ "utterances", []
+ )
+ for utterance in utterances:
+ if utterance.get("definite", False):
+ self.text = utterance["text"]
+ await self.handle_voice_stop()
+ break
+ except websockets.ConnectionClosed:
+ logger.bind(tag=TAG).debug("ASR服务连接已关闭,准备重连...")
+ # 确保关闭旧连接
+ if self.asr_ws is not None:
+ try:
+ await self.asr_ws.close()
+ except Exception as e:
+ logger.bind(tag=TAG).warning(f"关闭旧连接时发生错误: {e}")
+ finally:
+ self.asr_ws = None
+ retry_count = 0
+ while (
+ retry_count < self.max_retries
+ and not self.conn.stop_event.is_set()
+ ):
+ try:
+ logger.bind(tag=TAG).info(
+ f"正在进行第{retry_count + 1}次重连尝试..."
+ )
+ await self.open_audio_channels(self.conn)
+ break
+ except Exception as e:
+ retry_count += 1
+ if retry_count < self.max_retries:
+ logger.bind(tag=TAG).warning(
+ f"重连失败,等待{self.retry_delay}秒后重试: {e}"
+ )
+ await asyncio.sleep(self.retry_delay)
+ else:
+ logger.bind(tag=TAG).error(
+ f"重连失败,已达到最大重试次数: {e}"
+ )
+ await asyncio.sleep(
+ self.retry_delay
+ ) # 继续等待,以便后续重试
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"处理ASR结果时发生错误: {e}")
+ if not self.conn.stop_event.is_set():
+ await asyncio.sleep(2) # 增加重试延迟
except Exception as e:
- logger.bind(tag=TAG).error(f"ASR request failed: {e}", exc_info=True)
- return None
+ logger.bind(tag=TAG).error(f"ASR监听线程发生错误: {e}")
+ # 确保在发生严重错误时也能继续尝试重连
+ if not self.conn.stop_event.is_set():
+ await asyncio.sleep(self.retry_delay)
+ await self._forward_asr_results() # 递归重试
- @staticmethod
- def slice_data(data: bytes, chunk_size: int) -> (list, bool):
+ async def speech_to_text(self, opus_data, session_id):
+ result = self.text
+ self.text = "" # 清空text
+ return result, None
+
+ def parse_response(self, res: bytes) -> dict:
"""
- slice data
- :param data: wav data
- :param chunk_size: the segment size in one request
- :return: segment data, last flag
+ 解析 ASR 服务返回的二进制响应。
+ 根据协议格式解析头部和 payload,若采用 GZIP 压缩则先解压,再根据 JSON 反序列化。
"""
- data_len = len(data)
- offset = 0
- while offset + chunk_size < data_len:
- yield data[offset : offset + chunk_size], False
- offset += chunk_size
+ protocol_version = res[0] >> 4
+ header_size = res[0] & 0x0F
+ message_type = res[1] >> 4
+ serialization_method = res[2] >> 4
+ message_compression = res[2] & 0x0F
+ payload = res[header_size * 4 :]
+ result = {}
+ payload_msg = None
+ payload_size = 0
+
+ if message_type == SERVER_FULL_RESPONSE:
+ payload_size = int.from_bytes(payload[:4], "big", signed=True)
+ payload_msg = payload[4:]
+ elif message_type == SERVER_ACK:
+ seq = int.from_bytes(payload[:4], "big", signed=True)
+ result["seq"] = seq
+ if len(payload) >= 8:
+ payload_size = int.from_bytes(payload[4:8], "big", signed=False)
+ payload_msg = payload[8:]
+ elif message_type == SERVER_ERROR_RESPONSE:
+ code = int.from_bytes(payload[:4], "big", signed=False)
+ result["code"] = code
+ payload_size = int.from_bytes(payload[4:8], "big", signed=False)
+ payload_msg = payload[8:]
+
+ if payload_msg is None:
+ return result
+ if message_compression == GZIP_COMPRESSION:
+ payload_msg = gzip.decompress(payload_msg)
+ if serialization_method == JSON_SERIALIZATION:
+ payload_msg = json.loads(payload_msg.decode("utf-8"))
else:
- yield data[offset:data_len], True
+ payload_msg = payload_msg.decode("utf-8")
+ result["payload_msg"] = payload_msg
+ result["payload_size"] = payload_size
- async def speech_to_text(
- self, opus_data: List[bytes], session_id: str
- ) -> Tuple[Optional[str], Optional[str]]:
- """将语音数据转换为文本"""
+ # 错误码处理
+ if "code" in result:
+ error_code = result["code"]
+ error_message = ""
- file_path = None
- try:
- # 合并所有opus数据包
- if self.audio_format == "pcm":
- pcm_data = opus_data
+ if error_code == 1000:
+ error_message = "成功"
+ elif error_code == 1001:
+ error_message = "请求参数无效:请求参数缺失必需字段/字段值无效/重复请求"
+ elif error_code == 1002:
+ error_message = "无访问权限:token无效/过期/无权访问指定服务"
+ elif error_code == 1003:
+ error_message = "访问超频:当前appid访问QPS超出设定阈值"
+ elif error_code == 1004:
+ error_message = "访问超额:当前appid访问次数超出限制"
+ elif error_code == 1005:
+ error_message = "服务器繁忙:服务过载,无法处理当前请求"
+ elif error_code == 1010:
+ error_message = "音频过长:音频数据时长超出阈值"
+ elif error_code == 1011:
+ error_message = "音频过大:音频数据大小超出阈值"
+ elif error_code == 1012:
+ error_message = "音频格式无效:音频header有误/无法进行音频解码"
+ elif error_code == 1013:
+ error_message = "音频静音:音频未识别出任何文本结果"
+ elif error_code >= 1020 and error_code <= 1022:
+ error_message = "识别相关错误:需要重连"
+ if error_code == 1020:
+ error_message = "识别等待超时:等待下一包就绪超时"
+ elif error_code == 1021:
+ error_message = "识别处理超时:识别处理过程超时"
+ elif error_code == 1022:
+ error_message = "识别错误:识别过程中发生错误"
else:
- pcm_data = self.decode_opus(opus_data)
- combined_pcm_data = b"".join(pcm_data)
+ error_message = "未知错误:未归类错误"
- # 判断是否保存为WAV文件
- if self.delete_audio_file:
- pass
- else:
- file_path = self.save_audio_to_file(pcm_data, session_id)
+ logger.bind(tag=TAG).debug(
+ f"ASR错误: {error_message} (错误码: {error_code})"
+ )
- # 直接使用PCM数据
- # 计算分段大小 (单声道, 16bit, 16kHz采样率)
- size_per_sec = 1 * 2 * 16000 # nchannels * sampwidth * framerate
- segment_size = int(size_per_sec * self.seg_duration / 1000)
+ # 如果是识别相关错误(>=1020),标记需要重连
+ if error_code >= 1020:
+ result["need_reconnect"] = True
- # 语音识别
- start_time = time.time()
- text = await self._send_request(combined_pcm_data, segment_size)
- if text:
- logger.bind(tag=TAG).debug(
- f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}"
- )
- return text, file_path
- return "", file_path
-
- except Exception as e:
- logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True)
- return "", file_path
+ return result
diff --git a/main/xiaozhi-server/core/providers/asr/dto/dto.py b/main/xiaozhi-server/core/providers/asr/dto/dto.py
new file mode 100644
index 00000000..85b2d7b2
--- /dev/null
+++ b/main/xiaozhi-server/core/providers/asr/dto/dto.py
@@ -0,0 +1,9 @@
+from enum import Enum
+from typing import Union, Optional
+
+
+class InterfaceType(Enum):
+ # 接口类型
+ STREAM = "STREAM" # 流式接口
+ NON_STREAM = "NON_STREAM" # 非流式接口
+ LOCAL = "LOCAL" # 本地服务
diff --git a/main/xiaozhi-server/core/providers/asr/fun_local.py b/main/xiaozhi-server/core/providers/asr/fun_local.py
index e3b7fc83..8c7305c6 100644
--- a/main/xiaozhi-server/core/providers/asr/fun_local.py
+++ b/main/xiaozhi-server/core/providers/asr/fun_local.py
@@ -1,15 +1,14 @@
import time
-import wave
import os
import sys
import io
from config.logger import setup_logging
from typing import Optional, Tuple, List
-import uuid
from core.providers.asr.base import ASRProviderBase
from funasr import AutoModel
from funasr.utils.postprocess_utils import rich_transcription_postprocess
import shutil
+from core.providers.asr.dto.dto import InterfaceType
TAG = __name__
logger = setup_logging()
@@ -38,6 +37,7 @@ class CaptureOutput:
class ASRProvider(ASRProviderBase):
def __init__(self, config: dict, delete_audio_file: bool):
super().__init__()
+ self.interface_type = InterfaceType.LOCAL
self.model_dir = config.get("model_dir")
self.output_dir = config.get("output_dir") # 修正配置键名
self.delete_audio_file = delete_audio_file
diff --git a/main/xiaozhi-server/core/providers/asr/fun_server.py b/main/xiaozhi-server/core/providers/asr/fun_server.py
index ff411340..60d5e425 100644
--- a/main/xiaozhi-server/core/providers/asr/fun_server.py
+++ b/main/xiaozhi-server/core/providers/asr/fun_server.py
@@ -1,11 +1,8 @@
from typing import Optional, Tuple, List
-import opuslib_next
from core.providers.asr.base import ASRProviderBase
-import os
+from core.providers.asr.dto.dto import InterfaceType
import ssl
import json
-import uuid
-import wave
import websockets
from config.logger import setup_logging
import asyncio
@@ -23,6 +20,7 @@ class ASRProvider(ASRProviderBase):
:param delete_audio_file: Boolean to indicate whether to delete audio files after processing.
"""
super().__init__()
+ self.interface_type = InterfaceType.NON_STREAM
self.host = config.get("host", "localhost")
self.port = config.get("port", 10095)
self.api_key = config.get("api_key", "none")
diff --git a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py
index 24522d83..a27640dd 100644
--- a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py
+++ b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py
@@ -5,8 +5,7 @@ import sys
import io
from config.logger import setup_logging
from typing import Optional, Tuple, List
-import uuid
-import opuslib_next
+from core.providers.asr.dto.dto import InterfaceType
from core.providers.asr.base import ASRProviderBase
import numpy as np
@@ -38,6 +37,7 @@ class CaptureOutput:
class ASRProvider(ASRProviderBase):
def __init__(self, config: dict, delete_audio_file: bool):
super().__init__()
+ self.interface_type = InterfaceType.LOCAL
self.model_dir = config.get("model_dir")
self.output_dir = config.get("output_dir")
self.delete_audio_file = delete_audio_file
diff --git a/main/xiaozhi-server/core/providers/asr/tencent.py b/main/xiaozhi-server/core/providers/asr/tencent.py
index 22a7ed94..03301568 100644
--- a/main/xiaozhi-server/core/providers/asr/tencent.py
+++ b/main/xiaozhi-server/core/providers/asr/tencent.py
@@ -5,9 +5,8 @@ import json
import time
from datetime import datetime, timezone
import os
-import uuid
from typing import Optional, Tuple, List
-import wave
+from core.providers.asr.dto.dto import InterfaceType
import requests
from core.providers.asr.base import ASRProviderBase
from config.logger import setup_logging
@@ -23,6 +22,7 @@ class ASRProvider(ASRProviderBase):
def __init__(self, config: dict, delete_audio_file: bool = True):
super().__init__()
+ self.interface_type = InterfaceType.NON_STREAM
self.secret_id = config.get("secret_id")
self.secret_key = config.get("secret_key")
self.output_dir = config.get("output_dir")
diff --git a/main/xiaozhi-server/core/utils/modules_initialize.py b/main/xiaozhi-server/core/utils/modules_initialize.py
new file mode 100644
index 00000000..f2e3968e
--- /dev/null
+++ b/main/xiaozhi-server/core/utils/modules_initialize.py
@@ -0,0 +1,128 @@
+from typing import Dict, Any
+from config.logger import setup_logging
+from core.utils import tts, llm, intent, memory, vad, asr
+
+TAG = __name__
+logger = setup_logging()
+
+
+def initialize_modules(
+ logger,
+ config: Dict[str, Any],
+ init_vad=False,
+ init_asr=False,
+ init_llm=False,
+ init_tts=False,
+ init_memory=False,
+ init_intent=False,
+) -> Dict[str, Any]:
+ """
+ 初始化所有模块组件
+
+ Args:
+ config: 配置字典
+
+ Returns:
+ Dict[str, Any]: 包含所有初始化后的模块的字典
+ """
+ modules = {}
+
+ # 初始化TTS模块
+ if init_tts:
+ select_tts_module = config["selected_module"]["TTS"]
+ modules["tts"] = initialize_tts(config)
+ logger.bind(tag=TAG).info(f"初始化组件: tts成功 {select_tts_module}")
+
+ # 初始化LLM模块
+ if init_llm:
+ select_llm_module = config["selected_module"]["LLM"]
+ llm_type = (
+ select_llm_module
+ if "type" not in config["LLM"][select_llm_module]
+ else config["LLM"][select_llm_module]["type"]
+ )
+ modules["llm"] = llm.create_instance(
+ llm_type,
+ config["LLM"][select_llm_module],
+ )
+ logger.bind(tag=TAG).info(f"初始化组件: llm成功 {select_llm_module}")
+
+ # 初始化Intent模块
+ if init_intent:
+ select_intent_module = config["selected_module"]["Intent"]
+ intent_type = (
+ select_intent_module
+ if "type" not in config["Intent"][select_intent_module]
+ else config["Intent"][select_intent_module]["type"]
+ )
+ modules["intent"] = intent.create_instance(
+ intent_type,
+ config["Intent"][select_intent_module],
+ )
+ logger.bind(tag=TAG).info(f"初始化组件: intent成功 {select_intent_module}")
+
+ # 初始化Memory模块
+ if init_memory:
+ select_memory_module = config["selected_module"]["Memory"]
+ memory_type = (
+ select_memory_module
+ if "type" not in config["Memory"][select_memory_module]
+ else config["Memory"][select_memory_module]["type"]
+ )
+ modules["memory"] = memory.create_instance(
+ memory_type,
+ config["Memory"][select_memory_module],
+ config.get("summaryMemory", None),
+ )
+ logger.bind(tag=TAG).info(f"初始化组件: memory成功 {select_memory_module}")
+
+ # 初始化VAD模块
+ if init_vad:
+ select_vad_module = config["selected_module"]["VAD"]
+ vad_type = (
+ select_vad_module
+ if "type" not in config["VAD"][select_vad_module]
+ else config["VAD"][select_vad_module]["type"]
+ )
+ modules["vad"] = vad.create_instance(
+ vad_type,
+ config["VAD"][select_vad_module],
+ )
+ logger.bind(tag=TAG).info(f"初始化组件: vad成功 {select_vad_module}")
+
+ # 初始化ASR模块
+ if init_asr:
+ select_asr_module = config["selected_module"]["ASR"]
+ modules["asr"] = initialize_asr(config)
+ logger.bind(tag=TAG).info(f"初始化组件: asr成功 {select_asr_module}")
+ return modules
+
+
+def initialize_tts(config):
+ select_tts_module = config["selected_module"]["TTS"]
+ tts_type = (
+ select_tts_module
+ if "type" not in config["TTS"][select_tts_module]
+ else config["TTS"][select_tts_module]["type"]
+ )
+ new_tts = tts.create_instance(
+ tts_type,
+ config["TTS"][select_tts_module],
+ str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
+ )
+ return new_tts
+
+
+def initialize_asr(config):
+ select_asr_module = config["selected_module"]["ASR"]
+ asr_type = (
+ select_asr_module
+ if "type" not in config["ASR"][select_asr_module]
+ else config["ASR"][select_asr_module]["type"]
+ )
+ new_asr = asr.create_instance(
+ asr_type,
+ config["ASR"][select_asr_module],
+ str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
+ )
+ return new_asr
diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py
index 8f5ded4b..ff861aa9 100644
--- a/main/xiaozhi-server/core/utils/util.py
+++ b/main/xiaozhi-server/core/utils/util.py
@@ -7,8 +7,6 @@ import numpy as np
import requests
import opuslib_next
from pydub import AudioSegment
-from typing import Dict, Any
-from core.utils import tts, llm, intent, memory, vad, asr
import copy
TAG = __name__
@@ -245,122 +243,6 @@ def extract_json_from_string(input_string):
return None
-def initialize_modules(
- logger,
- config: Dict[str, Any],
- init_vad=False,
- init_asr=False,
- init_llm=False,
- init_tts=False,
- init_memory=False,
- init_intent=False,
-) -> Dict[str, Any]:
- """
- 初始化所有模块组件
-
- Args:
- config: 配置字典
-
- Returns:
- Dict[str, Any]: 包含所有初始化后的模块的字典
- """
- modules = {}
-
- # 初始化TTS模块
- if init_tts:
- select_tts_module = config["selected_module"]["TTS"]
- modules["tts"] = initialize_tts(config)
- logger.bind(tag=TAG).info(f"初始化组件: tts成功 {select_tts_module}")
-
- # 初始化LLM模块
- if init_llm:
- select_llm_module = config["selected_module"]["LLM"]
- llm_type = (
- select_llm_module
- if "type" not in config["LLM"][select_llm_module]
- else config["LLM"][select_llm_module]["type"]
- )
- modules["llm"] = llm.create_instance(
- llm_type,
- config["LLM"][select_llm_module],
- )
- logger.bind(tag=TAG).info(f"初始化组件: llm成功 {select_llm_module}")
-
- # 初始化Intent模块
- if init_intent:
- select_intent_module = config["selected_module"]["Intent"]
- intent_type = (
- select_intent_module
- if "type" not in config["Intent"][select_intent_module]
- else config["Intent"][select_intent_module]["type"]
- )
- modules["intent"] = intent.create_instance(
- intent_type,
- config["Intent"][select_intent_module],
- )
- logger.bind(tag=TAG).info(f"初始化组件: intent成功 {select_intent_module}")
-
- # 初始化Memory模块
- if init_memory:
- select_memory_module = config["selected_module"]["Memory"]
- memory_type = (
- select_memory_module
- if "type" not in config["Memory"][select_memory_module]
- else config["Memory"][select_memory_module]["type"]
- )
- modules["memory"] = memory.create_instance(
- memory_type,
- config["Memory"][select_memory_module],
- config.get("summaryMemory", None),
- )
- logger.bind(tag=TAG).info(f"初始化组件: memory成功 {select_memory_module}")
-
- # 初始化VAD模块
- if init_vad:
- select_vad_module = config["selected_module"]["VAD"]
- vad_type = (
- select_vad_module
- if "type" not in config["VAD"][select_vad_module]
- else config["VAD"][select_vad_module]["type"]
- )
- modules["vad"] = vad.create_instance(
- vad_type,
- config["VAD"][select_vad_module],
- )
- logger.bind(tag=TAG).info(f"初始化组件: vad成功 {select_vad_module}")
-
- # 初始化ASR模块
- if init_asr:
- select_asr_module = config["selected_module"]["ASR"]
- asr_type = (
- select_asr_module
- if "type" not in config["ASR"][select_asr_module]
- else config["ASR"][select_asr_module]["type"]
- )
- modules["asr"] = asr.create_instance(
- asr_type,
- config["ASR"][select_asr_module],
- str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
- )
- logger.bind(tag=TAG).info(f"初始化组件: asr成功 {select_asr_module}")
- return modules
-
-
-def initialize_tts(config):
- select_tts_module = config["selected_module"]["TTS"]
- tts_type = (
- select_tts_module
- if "type" not in config["TTS"][select_tts_module]
- else config["TTS"][select_tts_module]["type"]
- )
- new_tts = tts.create_instance(
- tts_type,
- config["TTS"][select_tts_module],
- str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
- )
- return new_tts
-
-
def analyze_emotion(text):
"""
分析文本情感并返回对应的emoji名称(支持中英文)
diff --git a/main/xiaozhi-server/core/websocket_server.py b/main/xiaozhi-server/core/websocket_server.py
index e60f0c87..1a1ba4c0 100644
--- a/main/xiaozhi-server/core/websocket_server.py
+++ b/main/xiaozhi-server/core/websocket_server.py
@@ -2,8 +2,9 @@ import asyncio
import websockets
from config.logger import setup_logging
from core.connection import ConnectionHandler
-from core.utils.util import initialize_modules, check_vad_update, check_asr_update
from config.config_loader import get_config_from_api
+from core.utils.modules_initialize import initialize_modules
+from core.utils.util import check_vad_update, check_asr_update
TAG = __name__
From 8f48e9ac0c014567aa105098eb13c56955652810 Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Fri, 30 May 2025 02:05:34 +0800
Subject: [PATCH 3/6] =?UTF-8?q?update:=E8=B0=83=E9=80=9A=E6=B5=81=E5=BC=8F?=
=?UTF-8?q?ASR?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-server/app.py | 10 +++++-----
.../xiaozhi-server/core/providers/asr/base.py | 20 +++++++++----------
.../core/providers/asr/doubao.py | 2 +-
3 files changed, 15 insertions(+), 17 deletions(-)
diff --git a/main/xiaozhi-server/app.py b/main/xiaozhi-server/app.py
index 63849c58..1ae61cbb 100644
--- a/main/xiaozhi-server/app.py
+++ b/main/xiaozhi-server/app.py
@@ -1,13 +1,13 @@
-import asyncio
import sys
import signal
+import asyncio
+from aioconsole import ainput
from config.settings import load_config
-from core.websocket_server import WebSocketServer
-from core.ota_server import SimpleOtaServer
-from core.utils.util import check_ffmpeg_installed
from config.logger import setup_logging
from core.utils.util import get_local_ip
-from aioconsole import ainput
+from core.ota_server import SimpleOtaServer
+from core.websocket_server import WebSocketServer
+from core.utils.util import check_ffmpeg_installed
TAG = __name__
logger = setup_logging()
diff --git a/main/xiaozhi-server/core/providers/asr/base.py b/main/xiaozhi-server/core/providers/asr/base.py
index d3ce5ee0..1a3a384d 100644
--- a/main/xiaozhi-server/core/providers/asr/base.py
+++ b/main/xiaozhi-server/core/providers/asr/base.py
@@ -38,34 +38,32 @@ class ASRProviderBase(ABC):
else:
have_voice = self.conn.client_have_voice
# 如果本次没有声音,本段也没声音,就把声音丢弃了
+ self.conn.asr_audio.append(audio)
if have_voice == False and self.conn.client_have_voice == False:
- self.conn.asr_audio.append(audio)
self.conn.asr_audio = self.conn.asr_audio[-10:]
return
# 如果本段有声音,且已经停止了
if self.conn.client_voice_stop:
+ asr_audio_task = copy.deepcopy(self.conn.asr_audio)
+ self.conn.asr_audio.clear()
self.conn.client_abort = False
# 音频太短了,无法识别
- if len(self.conn.asr_audio) < 15:
- self.conn.asr_audio.clear()
- self.conn.reset_vad_states()
- else:
- await self.handle_voice_stop()
+ self.conn.reset_vad_states()
+ if len(asr_audio_task) > 15:
+ await self.handle_voice_stop(asr_audio_task)
# 处理语音停止
- async def handle_voice_stop(self):
+ async def handle_voice_stop(self, asr_audio_task):
raw_text, _ = await self.speech_to_text(
- self.conn.asr_audio, self.conn.session_id
+ asr_audio_task, self.conn.session_id
) # 确保ASR模块返回原始文本
self.conn.logger.bind(tag=TAG).info(f"识别文本: {raw_text}")
text_len, _ = remove_punctuation_and_length(raw_text)
if text_len > 0:
# 使用自定义模块进行上报
await startToChat(self.conn, raw_text)
- enqueue_asr_report(self.conn, raw_text, copy.deepcopy(self.conn.asr_audio))
- self.conn.asr_audio.clear()
- self.conn.reset_vad_states()
+ enqueue_asr_report(self.conn, raw_text, asr_audio_task)
def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
"""PCM数据保存为WAV文件"""
diff --git a/main/xiaozhi-server/core/providers/asr/doubao.py b/main/xiaozhi-server/core/providers/asr/doubao.py
index d738f508..96abdff3 100644
--- a/main/xiaozhi-server/core/providers/asr/doubao.py
+++ b/main/xiaozhi-server/core/providers/asr/doubao.py
@@ -289,7 +289,7 @@ class ASRProvider(ASRProviderBase):
for utterance in utterances:
if utterance.get("definite", False):
self.text = utterance["text"]
- await self.handle_voice_stop()
+ await self.handle_voice_stop(None)
break
except websockets.ConnectionClosed:
logger.bind(tag=TAG).debug("ASR服务连接已关闭,准备重连...")
From a0b0a0e1e8c8a34c7069a00d2a7f966bfaca62f7 Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Fri, 30 May 2025 02:16:39 +0800
Subject: [PATCH 4/6] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=97=A5=E5=BF=97?=
=?UTF-8?q?=E6=89=93=E5=8D=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-server/core/handle/reportHandle.py | 3 ++-
main/xiaozhi-server/core/providers/asr/doubao.py | 2 --
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/main/xiaozhi-server/core/handle/reportHandle.py b/main/xiaozhi-server/core/handle/reportHandle.py
index b1214fd5..7b30f79c 100644
--- a/main/xiaozhi-server/core/handle/reportHandle.py
+++ b/main/xiaozhi-server/core/handle/reportHandle.py
@@ -8,6 +8,7 @@ TTS上报功能已集成到ConnectionHandler类中。
具体实现请参考core/connection.py中的相关代码。
"""
+
import time
import opuslib_next
@@ -145,4 +146,4 @@ def enqueue_asr_report(conn, text, opus_data):
f"ASR数据已加入上报队列: {conn.device_id}, 不上报音频"
)
except Exception as e:
- conn.logger.bind(tag=TAG).error(f"加入ASR上报队列失败: {text}, {e}")
+ conn.logger.bind(tag=TAG).debug(f"加入ASR上报队列失败: {text}, {e}")
diff --git a/main/xiaozhi-server/core/providers/asr/doubao.py b/main/xiaozhi-server/core/providers/asr/doubao.py
index 96abdff3..9ef1d411 100644
--- a/main/xiaozhi-server/core/providers/asr/doubao.py
+++ b/main/xiaozhi-server/core/providers/asr/doubao.py
@@ -107,7 +107,6 @@ class ASRProvider(ASRProviderBase):
# 等待初始化响应
try:
init_res = await self.asr_ws.recv()
- logger.bind(tag=TAG).debug(f"收到原始响应: {init_res}")
result = self.parse_response(init_res)
logger.bind(tag=TAG).info(f"ASR服务初始化响应: {result}")
except Exception as e:
@@ -328,7 +327,6 @@ class ASRProvider(ASRProviderBase):
self.retry_delay
) # 继续等待,以便后续重试
except Exception as e:
- logger.bind(tag=TAG).error(f"处理ASR结果时发生错误: {e}")
if not self.conn.stop_event.is_set():
await asyncio.sleep(2) # 增加重试延迟
except Exception as e:
From c4a240cb045b280e542ef3446e805b4e1898edbe Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Fri, 30 May 2025 09:32:11 +0800
Subject: [PATCH 5/6] =?UTF-8?q?update:=E6=B7=BB=E5=8A=A0=E9=85=8D=E7=BD=AE?=
=?UTF-8?q?=E8=AF=B4=E6=98=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 29 ++++++++++++++----
.../xiaozhi/common/constant/Constant.java | 2 +-
main/xiaozhi-server/config/logger.py | 30 ++++++++++++++-----
3 files changed, 48 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 27c8c759..62a3f78b 100644
--- a/README.md
+++ b/README.md
@@ -141,11 +141,10 @@
本项目提供两种部署方式,请根据您的具体需求选择:
#### 🚀 部署方式选择
-
-| 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 | 视频教程 |
+| 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 |
|---------|------|---------|---------|---------|---------|
-| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| - |
-| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
+| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - |
+| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
@@ -158,9 +157,29 @@ OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
```
+#### 🚩 配置说明和推荐
+
+本项目默认的配置是`入门全免费`设置,如果想效果更优,推荐使用`全流式配置`。
+
+本项目自`0.5.2`版本,已支持整个生命周期全流式,相比`0.5`版本以前,响应速度提升约`2.5秒`
+
+> [!Note]
+> 1、输入流式(自有流式)
+> 2、语音识别流式(DoubaoASR(火山流式语音识别))
+> 3、大模型流式(所有支持openai、ollma接口的大模型)
+> 4、语音合成双流式(HuoshanDoubleStreamTTS(火山双流式语音合成))
+> 5、输出音频流式(自有流式)
+
+| 模块名称 | 入门全免费设置 | 全流式配置 |
+|---------|---------|------|
+| ASR(语音识别) | FunASR(本地) | ✅DoubaoASR(火山流式语音识别) |
+| LLM(大模型) | ChatGLMLLM(智谱glm-4-flash) | ✅DoubaoLLM(火山doubao-1-5-pro-32k-250115) |
+| TTS(语音合成) | EdgeTTS(微软语音) | ✅HuoshanDoubleStreamTTS(火山双流式语音合成) |
+| Intent(意图识别) | function_call(函数调用) | ✅function_call(函数调用) |
+| Memory(记忆功能) | mem_local_short(本地短期记忆) | ✅mem_local_short(本地短期记忆) |
+
---
## 功能清单 ✨
-
### 已实现 ✅
| 功能模块 | 描述 |
diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
index abac385b..2e3174af 100644
--- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
+++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
@@ -227,7 +227,7 @@ public interface Constant {
/**
* 版本号
*/
- public static final String VERSION = "0.5.1";
+ public static final String VERSION = "0.5.2";
/**
* 无效固件URL
diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py
index e5fec630..5f107f5d 100644
--- a/main/xiaozhi-server/config/logger.py
+++ b/main/xiaozhi-server/config/logger.py
@@ -4,7 +4,7 @@ from loguru import logger
from config.config_loader import load_config
from config.settings import check_config_file
-SERVER_VERSION = "0.5.1"
+SERVER_VERSION = "0.5.2"
_logger_initialized = False
@@ -48,7 +48,11 @@ def setup_logging():
# 第一次初始化时配置日志
if not _logger_initialized:
- logger.configure(extra={"selected_module": log_config.get("selected_module", "00000000000000")}) # 新增配置
+ logger.configure(
+ extra={
+ "selected_module": log_config.get("selected_module", "00000000000000")
+ }
+ ) # 新增配置
log_format = log_config.get(
"log_format",
"{time:YYMMDD HH:mm:ss}[{version}_{extra[selected_module]}][{extra[tag]}]-{level}-{message}",
@@ -62,7 +66,9 @@ def setup_logging():
log_format = log_format.replace("{version}", SERVER_VERSION)
log_format = log_format.replace("{selected_module}", selected_module_str)
log_format_file = log_format_file.replace("{version}", SERVER_VERSION)
- log_format_file = log_format_file.replace("{selected_module}", selected_module_str)
+ log_format_file = log_format_file.replace(
+ "{selected_module}", selected_module_str
+ )
log_level = log_config.get("log_level", "INFO")
log_dir = log_config.get("log_dir", "tmp")
@@ -116,15 +122,25 @@ def update_module_string(selected_module_str):
log_format = log_format.replace("{version}", SERVER_VERSION)
log_format = log_format.replace("{selected_module}", selected_module_str)
log_format_file = log_format_file.replace("{version}", SERVER_VERSION)
- log_format_file = log_format_file.replace("{selected_module}", selected_module_str)
+ log_format_file = log_format_file.replace(
+ "{selected_module}", selected_module_str
+ )
logger.remove()
- logger.add(sys.stdout, format=log_format, level=log_config.get("log_level", "INFO"), filter=formatter)
logger.add(
- os.path.join(log_config.get("log_dir", "tmp"), log_config.get("log_file", "server.log")),
+ sys.stdout,
+ format=log_format,
+ level=log_config.get("log_level", "INFO"),
+ filter=formatter,
+ )
+ logger.add(
+ os.path.join(
+ log_config.get("log_dir", "tmp"),
+ log_config.get("log_file", "server.log"),
+ ),
format=log_format_file,
level=log_config.get("log_level", "INFO"),
- filter=formatter
+ filter=formatter,
)
except Exception as e:
From 2fd0bb4912a22985cdfa06049f29be560d685349 Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Fri, 30 May 2025 09:37:13 +0800
Subject: [PATCH 6/6] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 14 ++++----------
1 file changed, 4 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 62a3f78b..a94c89f2 100644
--- a/README.md
+++ b/README.md
@@ -158,17 +158,11 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
```
#### 🚩 配置说明和推荐
-
-本项目默认的配置是`入门全免费`设置,如果想效果更优,推荐使用`全流式配置`。
-
-本项目自`0.5.2`版本,已支持整个生命周期全流式,相比`0.5`版本以前,响应速度提升约`2.5秒`
-
> [!Note]
-> 1、输入流式(自有流式)
-> 2、语音识别流式(DoubaoASR(火山流式语音识别))
-> 3、大模型流式(所有支持openai、ollma接口的大模型)
-> 4、语音合成双流式(HuoshanDoubleStreamTTS(火山双流式语音合成))
-> 5、输出音频流式(自有流式)
+> 本项目默认的配置是`入门全免费`设置,如果想效果更优,推荐使用`全流式配置`。
+>
+> 本项目自`0.5.2`版本,已支持整个生命周期全流式,相比`0.5`版本以前,响应速度提升约`2.5秒`
+
| 模块名称 | 入门全免费设置 | 全流式配置 |
|---------|---------|------|