Merge branch 'xinnan-tech:main' into feature/doubao-tts

This commit is contained in:
GoodyHao
2025-04-29 15:10:42 +08:00
committed by GitHub
14 changed files with 135 additions and 90 deletions
+2
View File
@@ -163,6 +163,8 @@ server:
``` ```
智控台地址: https://2662r3426b.vicp.fun 智控台地址: https://2662r3426b.vicp.fun
服务测试工具: https://2662r3426b.vicp.fun/test/
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/ OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
``` ```
+2 -6
View File
@@ -106,10 +106,6 @@ VAD:
### 8、更多问题,可联系我们反馈 💬 ### 8、更多问题,可联系我们反馈 💬
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y` 可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。 也可以发邮件我们取得联系:huangrongzhuang@xin-nan.com
- 硬件烧录QQ群:适用于硬件烧录问题
- 开源服务端交流群:适用于服务端问题
- 产品建议联系人:适用于产品功能、产品设计等建议
+1 -6
View File
@@ -86,14 +86,9 @@ idf.py set-target esp32s3
idf.py menuconfig idf.py menuconfig
``` ```
![图片](images/build_setting01.png) 进入菜单配置后,再进入`Xiaozhi Assistant`,将`BOARD_TYPE`设置你板子的具体型号
进入菜单配置后,再进入`Xiaozhi Assistant`,将`CONNECTION_TYPE`设置为`Websocket`
回退到主菜单,再进入`Xiaozhi Assistant`,将`BOARD_TYPE`设置你板子的具体型号
保存退出,回到终端命令行。 保存退出,回到终端命令行。
![图片](images/build_setting02.png)
## 第5步 编译固件 ## 第5步 编译固件
``` ```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

@@ -177,5 +177,5 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.3.12"; public static final String VERSION = "0.3.13";
} }
@@ -28,7 +28,6 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO; import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.utils.NetworkUtil;
import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.sys.service.SysParamsService;
@Tag(name = "设备管理", description = "OTA 相关接口") @Tag(name = "设备管理", description = "OTA 相关接口")
@@ -53,7 +52,7 @@ public class OTAController {
clientId = deviceId; clientId = deviceId;
} }
String macAddress = deviceReportReqDTO.getMacAddress(); String macAddress = deviceReportReqDTO.getMacAddress();
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress); boolean macAddressValid = isMacAddressValid(macAddress);
// 设备Id和Mac地址应是一致的, 并且必须需要application字段 // 设备Id和Mac地址应是一致的, 并且必须需要application字段
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) { if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request")); return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
@@ -102,4 +101,19 @@ public class OTAController {
.contentLength(jsonBytes.length) .contentLength(jsonBytes.length)
.body(json); .body(json);
} }
/**
* 简单判断mac地址是否有效(非严格)
*
* @param macAddress
* @return
*/
private boolean isMacAddressValid(String macAddress) {
if (StringUtils.isBlank(macAddress)) {
return false;
}
// MAC地址通常为12位十六进制数字,可以包含冒号或连字符分隔符
String macPattern = "^([0-9A-Za-z]{2}[:-]){5}([0-9A-Za-z]{2})$";
return macAddress.matches(macPattern);
}
} }
@@ -1,34 +0,0 @@
package xiaozhi.modules.device.utils;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
/**
* 网络工具类
*/
public class NetworkUtil {
/**
* MAC地址正则表达式
*/
private static final Pattern macPattern = Pattern.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
/**
* 判断MAC地址是否合法
*/
public static boolean isMacAddressValid(String mac) {
if (StringUtils.isBlank(mac)) {
return false;
}
// 正则校验格式
if (!macPattern.matcher(mac).matches()) {
return false;
}
// 校验MAC地址是否为单播地址
String normalized = mac.toLowerCase();
String[] parts = normalized.split("[:-]");
int firstByte = Integer.parseInt(parts[0], 16);
return (firstByte & 1) == 0; // 最低位为0表示单播地址,合法
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import sys
from loguru import logger from loguru import logger
from config.config_loader import load_config from config.config_loader import load_config
SERVER_VERSION = "0.3.12" SERVER_VERSION = "0.3.13"
def get_module_abbreviation(module_name, module_dict): def get_module_abbreviation(module_name, module_dict):
+60 -3
View File
@@ -214,6 +214,30 @@ class ConnectionHandler:
elif isinstance(message, bytes): elif isinstance(message, bytes):
await handleAudioMessage(self, message) await handleAudioMessage(self, message)
async def handle_config_update(self, message):
"""处理配置更新请求"""
config_model = message.get("model")
new_content = message.get("content")
# 打印更新前的配置
old_value = self.config.get(config_model)
self.logger.bind(tag=TAG).info(f"配置更新: {config_model}{old_value} 更新为 {new_content}")
# 更新配置
self.config[config_model] = new_content
# 如果是模型相关配置,重新初始化模型
if config_model in ["LLM", "TTS", "ASR", "VAD", "Intent", "Memory"]:
self._initialize_components(self.config)
self.logger.bind(tag=TAG).info(f"已使用新配置重新初始化 {config_model} 模块")
# 返回更新确认
await self.websocket.send(json.dumps({
"type": "config_update_response",
"status": "success",
"message": f"{config_model} 配置已更新"
}))
def _initialize_components(self, private_config): def _initialize_components(self, private_config):
"""初始化组件""" """初始化组件"""
if private_config is not None: if private_config is not None:
@@ -241,7 +265,7 @@ class ConnectionHandler:
) )
private_config["delete_audio"] = bool(self.config.get("delete_audio", True)) private_config["delete_audio"] = bool(self.config.get("delete_audio", True))
self.logger.bind(tag=TAG).info( self.logger.bind(tag=TAG).info(
f"{time.time() - begin_time} 秒,获取差异化配置成功: {private_config}" f"{time.time() - begin_time} 秒,获取差异化配置成功: {json.dumps(filter_sensitive_info(private_config), ensure_ascii=False)}"
) )
except DeviceNotFoundException as e: except DeviceNotFoundException as e:
self.need_bind = True self.need_bind = True
@@ -448,7 +472,7 @@ class ConnectionHandler:
pos = current_text.rfind(punct) pos = current_text.rfind(punct)
prev_char = current_text[pos - 1] if pos - 1 >= 0 else "" prev_char = current_text[pos - 1] if pos - 1 >= 0 else ""
# 如果.前面是数字统一判断为小数 # 如果.前面是数字统一判断为小数
if prev_char.isdigit() and punct == '.': if prev_char.isdigit() and punct == ".":
number_flag = False number_flag = False
if pos > last_punct_pos and number_flag: if pos > last_punct_pos and number_flag:
last_punct_pos = pos last_punct_pos = pos
@@ -579,7 +603,7 @@ class ConnectionHandler:
pos = current_text.rfind(punct) pos = current_text.rfind(punct)
prev_char = current_text[pos - 1] if pos - 1 >= 0 else "" prev_char = current_text[pos - 1] if pos - 1 >= 0 else ""
# 如果.前面是数字统一判断为小数 # 如果.前面是数字统一判断为小数
if prev_char.isdigit() and punct == '.': if prev_char.isdigit() and punct == ".":
number_flag = False number_flag = False
if pos > last_punct_pos and number_flag: if pos > last_punct_pos and number_flag:
last_punct_pos = pos last_punct_pos = pos
@@ -945,3 +969,36 @@ class ConnectionHandler:
break break
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"超时检查任务出错: {e}") self.logger.bind(tag=TAG).error(f"超时检查任务出错: {e}")
def filter_sensitive_info(config: dict) -> dict:
"""
过滤配置中的敏感信息
Args:
config: 原始配置字典
Returns:
过滤后的配置字典
"""
sensitive_keys = [
"api_key",
"personal_access_token",
"access_token",
"token",
"access_key_secret",
"secret_key",
]
def _filter_dict(d: dict) -> dict:
filtered = {}
for k, v in d.items():
if any(sensitive in k.lower() for sensitive in sensitive_keys):
filtered[k] = "***"
elif isinstance(v, dict):
filtered[k] = _filter_dict(v)
elif isinstance(v, list):
filtered[k] = [_filter_dict(i) if isinstance(i, dict) else i for i in v]
else:
filtered[k] = v
return filtered
return _filter_dict(copy.deepcopy(config))
@@ -2,51 +2,52 @@ from config.logger import setup_logging
import json import json
import asyncio import asyncio
import time import time
from core.utils.util import ( from core.utils.util import get_string_no_punctuation_or_emoji, analyze_emotion
get_string_no_punctuation_or_emoji,
analyze_emotion
)
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
emoji_map = { emoji_map = {
'neutral': '😶', "neutral": "😶",
'happy': '🙂', "happy": "🙂",
'laughing': '😆', "laughing": "😆",
'funny': '😂', "funny": "😂",
'sad': '😔', "sad": "😔",
'angry': '😠', "angry": "😠",
'crying': '😭', "crying": "😭",
'loving': '😍', "loving": "😍",
'embarrassed': '😳', "embarrassed": "😳",
'surprised': '😲', "surprised": "😲",
'shocked': '😱', "shocked": "😱",
'thinking': '🤔', "thinking": "🤔",
'winking': '😉', "winking": "😉",
'cool': '😎', "cool": "😎",
'relaxed': '😌', "relaxed": "😌",
'delicious': '🤤', "delicious": "🤤",
'kissy': '😘', "kissy": "😘",
'confident': '😏', "confident": "😏",
'sleepy': '😴', "sleepy": "😴",
'silly': '😜', "silly": "😜",
'confused': '🙄' "confused": "🙄",
} }
async def sendAudioMessage(conn, audios, text, text_index=0): async def sendAudioMessage(conn, audios, text, text_index=0):
# 发送句子开始消息 # 发送句子开始消息
emotion = analyze_emotion(text) if text is not None:
emoji = emoji_map.get(emotion, '🙂') # 默认使用笑脸 emotion = analyze_emotion(text)
await conn.websocket.send(json.dumps( emoji = emoji_map.get(emotion, "🙂") # 默认使用笑脸
{ await conn.websocket.send(
"type": "llm", json.dumps(
"text": emoji, {
"emotion": emotion, "type": "llm",
"session_id": conn.session_id, "text": emoji,
} "emotion": emotion,
"session_id": conn.session_id,
}
)
) )
)
if text_index == conn.tts_first_text_index: if text_index == conn.tts_first_text_index:
logger.bind(tag=TAG).info(f"发送第一段语音: {text}") logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
await send_tts_message(conn, "sentence_start", text) await send_tts_message(conn, "sentence_start", text)
@@ -61,5 +61,8 @@ async def handleTextMessage(conn, message):
asyncio.create_task(handleIotDescriptors(conn, msg_json["descriptors"])) asyncio.create_task(handleIotDescriptors(conn, msg_json["descriptors"]))
if "states" in msg_json: if "states" in msg_json:
asyncio.create_task(handleIotStatus(conn, msg_json["states"])) asyncio.create_task(handleIotStatus(conn, msg_json["states"]))
elif msg_json["type"] == "server":
if "model" in msg_json:
await conn.handle_config_update(msg_json)
except json.JSONDecodeError: except json.JSONDecodeError:
await conn.websocket.send(message) await conn.websocket.send(message)
+13 -2
View File
@@ -2,7 +2,7 @@ import asyncio
import websockets import websockets
from config.logger import setup_logging from config.logger import setup_logging
from core.connection import ConnectionHandler from core.connection import ConnectionHandler
from core.utils.util import get_local_ip, initialize_modules from core.utils.util import initialize_modules
TAG = __name__ TAG = __name__
@@ -27,7 +27,9 @@ class WebSocketServer:
host = server_config.get("ip", "0.0.0.0") host = server_config.get("ip", "0.0.0.0")
port = int(server_config.get("port", 8000)) port = int(server_config.get("port", 8000))
async with websockets.serve(self._handle_connection, host, port): async with websockets.serve(
self._handle_connection, host, port, process_request=self._http_response
):
await asyncio.Future() await asyncio.Future()
async def _handle_connection(self, websocket): async def _handle_connection(self, websocket):
@@ -47,3 +49,12 @@ class WebSocketServer:
await handler.handle_connection(websocket) await handler.handle_connection(websocket)
finally: finally:
self.active_connections.discard(handler) self.active_connections.discard(handler)
async def _http_response(self, websocket, request_headers):
# 检查是否为 WebSocket 升级请求
if request_headers.headers.get("connection", "").lower() == "upgrade":
# 如果是 WebSocket 请求,返回 None 允许握手继续
return None
else:
# 如果是普通 HTTP 请求,返回 "server is running"
return websocket.respond(200, "Server is running\n")