mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 15:43:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaaea2b24c | ||
|
|
58d0aefae1 | ||
|
|
11c9915a70 | ||
|
|
6cf6b0d645 | ||
|
|
1980a39b73 | ||
|
|
8d3fbeeb22 | ||
|
|
2678644c2c | ||
|
|
25da9fe119 | ||
|
|
03d89d22ec | ||
|
|
100cd25ffc | ||
|
|
6284a74a6e | ||
|
|
978cd8ba8e | ||
|
|
c76f28824c | ||
|
|
703ecfae40 | ||
|
|
6e4e9a002b | ||
|
|
91278b0b3d | ||
|
|
ff9b3ed1a2 | ||
|
|
0c855d4ba8 | ||
|
|
9176e9d255 | ||
|
|
2bab917b3e | ||
|
|
8a4fca9bd5 | ||
|
|
13288a6b3a | ||
|
|
4912f385c6 | ||
|
|
dcb6d0feab | ||
|
|
de8c762d79 | ||
|
|
056659304a | ||
|
|
525bf18a03 | ||
|
|
e3928ffbcc | ||
|
|
7c73f060ed | ||
|
|
f0aa7df3b0 |
+2
-1
@@ -158,4 +158,5 @@ my_wakeup_words.mp3
|
|||||||
main/manager-api/.vscode
|
main/manager-api/.vscode
|
||||||
|
|
||||||
# Ignore webpack cache directory
|
# Ignore webpack cache directory
|
||||||
main/manager-web/.webpack_cache/
|
main/manager-web/.webpack_cache/
|
||||||
|
main/xiaozhi-server/mysql
|
||||||
|
|||||||
@@ -145,42 +145,24 @@ server:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 部署方式 🚀
|
## 部署文档
|
||||||
|
|
||||||
[](./docs/Deployment.md)
|
本项目提供两种部署方式,请根据您的具体需求选择:
|
||||||
|
|
||||||
### 部署文档
|
#### 🚀 部署方式选择
|
||||||
|
|
||||||
本项目支持`最简化`安装和`全模块`安装。请根据具体需求选择安装方式。
|
| 部署方式 | 特点 | 适用场景 | 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) |
|
||||||
|
|
||||||
`最简化`适合在较低资源配置中运行,无需数据库。[请参考-最简化安装架构图](./docs/images/deploy1.png)
|
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
|
||||||
|
|
||||||
`全模块`适合想体验完整功能,过程复杂。[请参考-全模块安装架构图](./docs/images/deploy2.png)
|
|
||||||
|
|
||||||
#### 1、 最简化安装
|
|
||||||
|
|
||||||
`最简化`安装包含:智能对话、IOT等功能,数据存储在`配置文件`。我们提供了两种安装教程,如果你决定使用最简化安装,可选择`Docker只运行Server`或`本地源码只运行Server`任意一种方式。
|
|
||||||
|
|
||||||
##### 1.1. Docker只运行Server
|
|
||||||
|
|
||||||
你可以使用编译好的docker镜像安装,[点击Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)。优点:减少环境配置的烦恼;缺点:版本可能有点旧,镜像文件较大。
|
|
||||||
|
|
||||||
##### 2.2. 本地源码只运行Server
|
|
||||||
|
|
||||||
你可以以本地源码的方式运行,[点击本地源码只运行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)。优点:可以随时更新最新代码;缺点:安装Conda环境较为复杂。
|
|
||||||
|
|
||||||
#### 2、 全模块安装
|
|
||||||
|
|
||||||
`全模块`安装包含:智能对话、IOT、OTA、智控台,数据存储在`数据库`中。我们提供了两种安装教程,如果你决定使用全模块安装,可选择`Docker运行全模块`或`本地源码运行全模块`任意一种方式。
|
|
||||||
|
|
||||||
##### 2.1. Docker运行全模块
|
|
||||||
|
|
||||||
你可以使用编译好的docker镜像安装,[点击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)。优点:减少环境配置的烦恼;缺点:版本可能有点旧,镜像文件较大。
|
|
||||||
|
|
||||||
##### 2.2. 本地源码运行全模块
|
|
||||||
|
|
||||||
你可以以本地源码的方式运行,[点击本地源码运行全模块](./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)。优点:可以随时更新最新代码;缺点:安装Java、Node.js、Conda环境较为复杂。
|
|
||||||
|
|
||||||
|
```
|
||||||
|
智控台地址: https://2662r3426b.vicp.fun
|
||||||
|
OTA接口地址: htts://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
|
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||||
|
```
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题 ❓
|
## 常见问题 ❓
|
||||||
@@ -188,54 +170,41 @@ server:
|
|||||||
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 产品生态 👬
|
|
||||||
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
|
||||||
|
|
||||||
- [小智安卓客户端](https://github.com/TOM88812/xiaozhi-android-client)
|
|
||||||
|
|
||||||
一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。现在是flutter版本,打通IOS、Android端。
|
|
||||||
- [小智电脑客户端](https://github.com/Huang-junsen/py-xiaozhi)
|
|
||||||
|
|
||||||
该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,依然能够体过代码体验小智 AI 的功能。主要功能包括 AI 语音交互、视觉多模态识别、IoT 设备集成、联网音乐播放、语音唤醒、自动对话模式、图形化界面、命令行模式、跨平台支持、音量控制、会话管理、加密音频传输、自动验证码处理等。
|
|
||||||
- [小智Java服务端](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
|
|
||||||
|
|
||||||
小智开源后端服务 Java 版本是一个基于 Java 的开源项目,它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。
|
|
||||||
---
|
|
||||||
## 功能清单 ✨
|
## 功能清单 ✨
|
||||||
|
|
||||||
### 已实现 ✅
|
### 已实现 ✅
|
||||||
|
|
||||||
- **通信协议**
|
| 功能模块 | 描述 |
|
||||||
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
|---------|------|
|
||||||
- **对话交互**
|
| 通信协议 | 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互 |
|
||||||
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
| 对话交互 | 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 |
|
||||||
- **意图识别**
|
| 意图识别 | 支持使用LLM意图识别、function call函数调用,减少硬编码意图判断 |
|
||||||
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
|
| 多语言识别 | 支持国语、粤语、英语、日语、韩语(默认使用 FunASR) |
|
||||||
- **多语言识别**
|
| LLM 模块 | 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口 |
|
||||||
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
| TTS 模块 | 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求 |
|
||||||
- **LLM 模块**
|
| 记忆功能 | 支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求 |
|
||||||
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
| IOT功能 | 支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制 |
|
||||||
- **TTS 模块**
|
| 智控台 | 提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理 |
|
||||||
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
|
||||||
- **记忆功能**
|
|
||||||
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
|
|
||||||
- **IOT功能**
|
|
||||||
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
|
|
||||||
- **智控台**
|
|
||||||
提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理。
|
|
||||||
|
|
||||||
|
|
||||||
### 正在开发 🚧
|
### 正在开发 🚧
|
||||||
|
|
||||||
- 多种心情模式
|
想了解具体开发计划进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
||||||
|
|
||||||
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
|
||||||
|
|
||||||
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 产品生态 👬
|
||||||
|
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
||||||
|
|
||||||
|
| 项目名称 | 项目地址 | 项目描述 |
|
||||||
|
|:---------------------|:--------|:--------|
|
||||||
|
| 小智安卓客户端 | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | 一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。<br/>现在是flutter版本,打通IOS、Android端。 |
|
||||||
|
| 小智电脑客户端 | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | 该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,<br/>依然能够体过代码体验小智 AI 的功能。 |
|
||||||
|
| 小智Java服务端 | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | 小智开源后端服务 Java 版本是一个基于 Java 的开源项目。<br/>它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 本项目支持的平台/组件列表 📋
|
## 本项目支持的平台/组件列表 📋
|
||||||
|
|
||||||
### LLM 语言模型
|
### LLM 语言模型
|
||||||
@@ -298,10 +267,17 @@ server:
|
|||||||
|
|
||||||
## 鸣谢 🙏
|
## 鸣谢 🙏
|
||||||
|
|
||||||
- 本项目受 [百聆语音对话机器人](https://github.com/wwbin2017/bailing) 启发,并在其基础上实现。
|
| Logo | 项目/公司 | 说明 |
|
||||||
- 感谢 [十方融海](https://www.tenclass.com/) 对小智通讯协议提供的详尽文档支持。
|
|:---:|:---:|:---|
|
||||||
|
| <img src="./docs/images/logo_bailing.png" width="160"> | [百聆语音对话机器人](https://github.com/wwbin2017/bailing) | 本项目受[百聆语音对话机器人](https://github.com/wwbin2017/bailing)启发,并在其基础上实现 |
|
||||||
|
| <img src="./docs/images/logo_tenclass.png" width="160"> | [十方融海](https://www.tenclass.com/) | 感谢[十方融海](https://www.tenclass.com/)为小智生态制定了标准的通讯协议、多设备兼容性方案及高并发场景实践示范;为本项目提供了全链路技术文档支持 |
|
||||||
|
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [玄凤科技](https://github.com/Eric0308) | 感谢[玄凤科技](https://github.com/Eric0308)贡献函数调用框架、MCP通信协议及插件化调用机制的实现代码,通过标准化的指令调度体系与动态扩展能力,显著提升了前端设备(IoT)的交互效率和功能延展性 |
|
||||||
|
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [汇远设计](http://ui.kwd988.net/) | 感谢[汇远设计](http://ui.kwd988.net/)为本项目提供专业视觉解决方案,用其服务超千家企业的设计实战经验,赋能本项目产品用户体验 |
|
||||||
|
| <img src="./docs/images/logo_qinren.png" width="160"> | [西安勤人信息科技](https://www.029app.com/) | 感谢[西安勤人信息科技](https://www.029app.com/)深化本项目视觉体系,确保整体设计风格在多场景应用中的一致性和扩展性 |
|
||||||
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||||
|
|||||||
+4
-1
@@ -137,7 +137,10 @@ docker logs -f xiaozhi-esp32-server
|
|||||||
```
|
```
|
||||||
docker stop xiaozhi-esp32-server
|
docker stop xiaozhi-esp32-server
|
||||||
docker rm xiaozhi-esp32-server
|
docker rm xiaozhi-esp32-server
|
||||||
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
```
|
```
|
||||||
|
|
||||||
5.3、重新按docker方式部署
|
5.3、重新按docker方式部署
|
||||||
@@ -221,7 +224,7 @@ python app.py
|
|||||||
## 配置项目
|
## 配置项目
|
||||||
|
|
||||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`
|
||||||
|
|
||||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
||||||
|
|
||||||
|
|||||||
+36
-68
@@ -44,6 +44,8 @@ xiaozhi-server
|
|||||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的
|
||||||
`xiaozhi-server`中。
|
`xiaozhi-server`中。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/docker-compose_all.yml` 下载。
|
||||||
|
|
||||||
下载完后,回到本教程继续往下。
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
##### 1.3.2 下载 config_from_api.yaml
|
##### 1.3.2 下载 config_from_api.yaml
|
||||||
@@ -53,6 +55,8 @@ xiaozhi-server
|
|||||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的
|
||||||
`xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml`。
|
`xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml`。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/config_from_api.yaml` 下载保存。
|
||||||
|
|
||||||
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -67,57 +71,37 @@ xiaozhi-server
|
|||||||
|
|
||||||
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
## 2. 安装Mysql和Redis
|
## 2. 备份数据
|
||||||
|
|
||||||
### 2.1 安装Mysql数据库
|
如果你之前已经成功运行智控台,如果上面保存有你的密钥信息,请先从智控台上拷贝重要数据下来。因为升级过程中,有可能会覆盖原来的数据。
|
||||||
如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
```
|
|
||||||
|
|
||||||
如果还没有MySQL,你可以通过docker安装mysql
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 安装Redis
|
|
||||||
|
|
||||||
如果还没有Redis,你可以通过docker安装redis
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --name xiaozhi-esp32-server-redis -d -p 6379:6379 redis
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 配置docker-compose_all.yml文件文件
|
|
||||||
|
|
||||||
在`xiaozhi-server`目录,打开docker-compose_all.yml,
|
|
||||||
|
|
||||||
1、修改`SPRING_DATASOURCE_DRUID_URL`里,把`192.168.1.25`修改成部署mysql的电脑局域网ip,如果是你本地,你要查看一下你本机的局域网ip是什么
|
|
||||||
|
|
||||||
2、修改`SPRING_DATA_REDIS_HOST`里,把`192.168.1.25`修改成部署redis的电脑局域网ip,如果是你本地,你要查看一下你本机的局域网ip是什么
|
|
||||||
|
|
||||||
3、确认一下mysql的用户名`SPRING_DATASOURCE_DRUID_USERNAME`和密码`SPRING_DATASOURCE_DRUID_PASSWORD`
|
|
||||||
|
|
||||||
## 4. 运行程序
|
|
||||||
|
|
||||||
确认mysql和redis是否运行正常,输入
|
|
||||||
```
|
|
||||||
docker ps
|
|
||||||
```
|
|
||||||
如果你能看到`xiaozhi-esp32-server-redis`和`xiaozhi-esp32-server-db`信息,就可以继续往下,如果看不到,说明你的`mysql`和`redis`没有安装或没有启动。需要继续回到上面的教程,看看哪一步漏了。
|
|
||||||
|
|
||||||
```
|
|
||||||
CONTAINER ID IMAGE COMMAND CREATED TATUS PORTS NAMES
|
|
||||||
xxx redis "xx" xxx xxx 0.0.0.0:6379->6379/tcp xiaozhi-esp32-server-redis
|
|
||||||
xxx mysql:latest "xx" xxx xxx 0.0.0.0:3306->3306/tcp, 33060/tcp xiaozhi-esp32-server-db
|
|
||||||
```
|
|
||||||
|
|
||||||
|
## 3. 清除历史版本镜像和容器
|
||||||
接下来打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
接下来打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker-compose_all.yml up -d
|
docker compose -f docker-compose_all.yml down
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server
|
||||||
|
docker rm xiaozhi-esp32-server
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-db
|
||||||
|
docker rm xiaozhi-esp32-server-db
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-redis
|
||||||
|
docker rm xiaozhi-esp32-server-redis
|
||||||
|
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 运行程序
|
||||||
|
执行以下命令启动新版本容器
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose -f docker-compose_all.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
执行完后,再执行以下命令,查看日志信息。
|
执行完后,再执行以下命令,查看日志信息。
|
||||||
@@ -150,17 +134,16 @@ http://localhost:8002/xiaozhi/doc.html
|
|||||||
|
|
||||||
```
|
```
|
||||||
manager-api:
|
manager-api:
|
||||||
url: http://127.0.0.1:8002/xiaozhi
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
secret: 你的server.secret值
|
secret: 你的server.secret值
|
||||||
```
|
```
|
||||||
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
||||||
|
2、注意,把`url`改成下面的`http://xiaozhi-esp32-server-web/xiaozhi`
|
||||||
2、把`url`里的`127.0.0.1`改成你电脑局域网内的ip
|
|
||||||
|
|
||||||
类似这样的效果
|
类似这样的效果
|
||||||
```
|
```
|
||||||
manager-api:
|
manager-api:
|
||||||
url: http://192.168.1.25:8002/xiaozhi
|
url: http://xiaozhi-esp32-server-web/xiaozhi
|
||||||
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -202,24 +185,6 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
|||||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
|
||||||
## 6. 版本升级操作
|
|
||||||
|
|
||||||
如果后期想升级版本,先备份你的模型密钥。
|
|
||||||
|
|
||||||
进入`xiaozhi-server`目录
|
|
||||||
|
|
||||||
5.1、执行以下命令
|
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose down
|
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
|
||||||
```
|
|
||||||
|
|
||||||
5.2、删掉`docker-compose_all.yaml`文件
|
|
||||||
|
|
||||||
5.3、重新按1.1开始部署
|
|
||||||
|
|
||||||
# 方式二:本地源码运行全模块
|
# 方式二:本地源码运行全模块
|
||||||
|
|
||||||
## 1.安装MySQL数据库
|
## 1.安装MySQL数据库
|
||||||
@@ -388,6 +353,9 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||||
|
|
||||||
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
|
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config_from_api.yaml`文件复制到`data`,并重命名为`.config.yaml`
|
||||||
|
|
||||||
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
+1
-1
@@ -44,7 +44,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
@Override
|
@Override
|
||||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
IPage<AgentEntity> page = agentDao.selectPage(
|
IPage<AgentEntity> page = agentDao.selectPage(
|
||||||
getPage(params, "sort", true),
|
getPage(params, "agent_name", true),
|
||||||
new QueryWrapper<>());
|
new QueryWrapper<>());
|
||||||
return new PageData<>(page.getRecords(), page.getTotal());
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-3
@@ -171,7 +171,13 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
switch (param.getValueType().toLowerCase()) {
|
switch (param.getValueType().toLowerCase()) {
|
||||||
case "number":
|
case "number":
|
||||||
try {
|
try {
|
||||||
current.put(lastKey, Double.parseDouble(value));
|
double doubleValue = Double.parseDouble(value);
|
||||||
|
// 如果数值是整数形式,则转换为Integer
|
||||||
|
if (doubleValue == (int) doubleValue) {
|
||||||
|
current.put(lastKey, (int) doubleValue);
|
||||||
|
} else {
|
||||||
|
current.put(lastKey, doubleValue);
|
||||||
|
}
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
current.put(lastKey, value);
|
current.put(lastKey, value);
|
||||||
}
|
}
|
||||||
@@ -231,8 +237,9 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
boolean isCache) {
|
boolean isCache) {
|
||||||
Map<String, String> selectedModule = new HashMap<>();
|
Map<String, String> selectedModule = new HashMap<>();
|
||||||
|
|
||||||
String[] modelTypes = { "VAD", "ASR", "LLM", "TTS", "Memory", "Intent" };
|
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM" };
|
||||||
String[] modelIds = { vadModelId, asrModelId, llmModelId, ttsModelId, memModelId, intentModelId };
|
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId };
|
||||||
|
String intentLLMModelId = null;
|
||||||
|
|
||||||
for (int i = 0; i < modelIds.length; i++) {
|
for (int i = 0; i < modelIds.length; i++) {
|
||||||
if (modelIds[i] == null) {
|
if (modelIds[i] == null) {
|
||||||
@@ -246,10 +253,27 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if ("TTS".equals(modelTypes[i]) && voice != null) {
|
if ("TTS".equals(modelTypes[i]) && voice != null) {
|
||||||
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
|
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
|
||||||
}
|
}
|
||||||
|
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型
|
||||||
|
if ("Intent".equals(modelTypes[i])) {
|
||||||
|
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
|
||||||
|
if ("intent_llm".equals(map.get("type"))) {
|
||||||
|
intentLLMModelId = (String) map.get("llm");
|
||||||
|
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
||||||
|
intentLLMModelId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
||||||
|
if ("LLM".equals(modelTypes[i]) && intentLLMModelId != null) {
|
||||||
|
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache);
|
||||||
|
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.put(modelTypes[i], typeConfig);
|
result.put(modelTypes[i], typeConfig);
|
||||||
|
|
||||||
selectedModule.put(modelTypes[i], model.getId());
|
selectedModule.put(modelTypes[i], model.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
result.put("selected_module", selectedModule);
|
result.put("selected_module", selectedModule);
|
||||||
if (StringUtils.isNotBlank(prompt)) {
|
if (StringUtils.isNotBlank(prompt)) {
|
||||||
prompt = prompt.replace("{{assistant_name}}", "小智");
|
prompt = prompt.replace("{{assistant_name}}", "小智");
|
||||||
|
|||||||
+1
-1
@@ -221,7 +221,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
params.put(Constant.PAGE, dto.getPage());
|
params.put(Constant.PAGE, dto.getPage());
|
||||||
params.put(Constant.LIMIT, dto.getLimit());
|
params.put(Constant.LIMIT, dto.getLimit());
|
||||||
IPage<DeviceEntity> page = baseDao.selectPage(
|
IPage<DeviceEntity> page = baseDao.selectPage(
|
||||||
getPage(params, "sort", true),
|
getPage(params, "mac_address", true),
|
||||||
// 定义查询条件
|
// 定义查询条件
|
||||||
new QueryWrapper<DeviceEntity>()
|
new QueryWrapper<DeviceEntity>()
|
||||||
// 必须设备关键词查找
|
// 必须设备关键词查找
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public class ShiroConfig {
|
|||||||
filterMap.put("/favicon.ico", "anon");
|
filterMap.put("/favicon.ico", "anon");
|
||||||
filterMap.put("/user/captcha", "anon");
|
filterMap.put("/user/captcha", "anon");
|
||||||
filterMap.put("/user/login", "anon");
|
filterMap.put("/user/login", "anon");
|
||||||
|
filterMap.put("/user/pub-config", "anon");
|
||||||
filterMap.put("/user/register", "anon");
|
filterMap.put("/user/register", "anon");
|
||||||
filterMap.put("/config/server-base", "anon");
|
filterMap.put("/config/server-base", "anon");
|
||||||
filterMap.put("/config/agent-models", "anon");
|
filterMap.put("/config/agent-models", "anon");
|
||||||
|
|||||||
+10
@@ -1,6 +1,8 @@
|
|||||||
package xiaozhi.modules.security.controller;
|
package xiaozhi.modules.security.controller;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -111,4 +113,12 @@ public class LoginController {
|
|||||||
sysUserTokenService.changePassword(userId, passwordDTO);
|
sysUserTokenService.changePassword(userId, passwordDTO);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pub-config")
|
||||||
|
@Operation(summary = "公共配置")
|
||||||
|
public Result<Map<String, Object>> pubConfig() {
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.put("version", "0.3.3");
|
||||||
|
return new Result<Map<String, Object>>().ok(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -37,7 +37,7 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
@Override
|
@Override
|
||||||
public PageData<SysParamsDTO> page(Map<String, Object> params) {
|
public PageData<SysParamsDTO> page(Map<String, Object> params) {
|
||||||
IPage<SysParamsEntity> page = baseDao.selectPage(
|
IPage<SysParamsEntity> page = baseDao.selectPage(
|
||||||
getPage(params, Constant.CREATE_DATE, false),
|
getPage(params, null, false),
|
||||||
getWrapper(params));
|
getWrapper(params));
|
||||||
|
|
||||||
return getPageData(page, SysParamsDTO.class);
|
return getPageData(page, SysParamsDTO.class);
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
|||||||
params.put(Constant.PAGE, dto.getPage());
|
params.put(Constant.PAGE, dto.getPage());
|
||||||
params.put(Constant.LIMIT, dto.getLimit());
|
params.put(Constant.LIMIT, dto.getLimit());
|
||||||
IPage<TimbreEntity> page = baseDao.selectPage(
|
IPage<TimbreEntity> page = baseDao.selectPage(
|
||||||
getPage(params, "sort", true),
|
getPage(params, null, true),
|
||||||
// 定义查询条件
|
// 定义查询条件
|
||||||
new QueryWrapper<TimbreEntity>()
|
new QueryWrapper<TimbreEntity>()
|
||||||
// 必须按照ttsID查找
|
// 必须按照ttsID查找
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 对0.3.0版本之前的参数进行修改
|
||||||
|
update `sys_params` set param_value = '.mp3;.wav;.p3' where param_code = 'plugins.play_music.music_ext';
|
||||||
|
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Intent_intent_llm';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- 对0.3.0版本之前的参数进行修改
|
||||||
|
update `sys_params` set param_value = '.mp3;.wav;.p3' where param_code = 'plugins.play_music.music_ext';
|
||||||
|
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Intent_intent_llm';
|
||||||
|
|
||||||
|
-- 添加edge音色
|
||||||
|
delete from `ai_tts_voice` where tts_model_id = 'TTS_EdgeTTS';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES
|
||||||
|
('TTS_EdgeTTS0001', 'TTS_EdgeTTS', 'EdgeTTS女声-晓晓', 'zh-CN-XiaoxiaoNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0002', 'TTS_EdgeTTS', 'EdgeTTS男声-云扬', 'zh-CN-YunyangNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0003', 'TTS_EdgeTTS', 'EdgeTTS女声-晓伊', 'zh-CN-XiaoyiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0004', 'TTS_EdgeTTS', 'EdgeTTS男声-云健', 'zh-CN-YunjianNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0005', 'TTS_EdgeTTS', 'EdgeTTS男声-云希', 'zh-CN-YunxiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0006', 'TTS_EdgeTTS', 'EdgeTTS男声-云夏', 'zh-CN-YunxiaNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0007', 'TTS_EdgeTTS', 'EdgeTTS女声-辽宁小贝', 'zh-CN-liaoning-XiaobeiNeural', '辽宁', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0008', 'TTS_EdgeTTS', 'EdgeTTS女声-陕西小妮', 'zh-CN-shaanxi-XiaoniNeural', '陕西', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
||||||
@@ -50,4 +50,11 @@ databaseChangeLog:
|
|||||||
changes:
|
changes:
|
||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202504112058.sql
|
path: classpath:db/changelog/202504112058.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504131543
|
||||||
|
author: John
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504131543.sql
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
# 暂时使用群主的接口
|
VUE_APP_API_BASE_URL=/xiaozhi
|
||||||
# VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun/xiaozhi
|
|
||||||
# 如果本地开发,请使用以下接口
|
|
||||||
VUE_APP_API_BASE_URL=http://localhost:8002/xiaozhi
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getServiceUrl } from '../api';
|
import { getServiceUrl } from '../api';
|
||||||
import RequestService from '../httpRequest';
|
import RequestService from '../httpRequest';
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// 获取模型配置列表
|
// 获取模型配置列表
|
||||||
getModelList(params, callback) {
|
getModelList(params, callback) {
|
||||||
@@ -162,21 +161,25 @@ export default {
|
|||||||
// 更新模型配置
|
// 更新模型配置
|
||||||
updateModel(params, callback) {
|
updateModel(params, callback) {
|
||||||
const { modelType, provideCode, id, formData } = params;
|
const { modelType, provideCode, id, formData } = params;
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
configJson: formData.configJson
|
||||||
|
};
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
|
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
|
||||||
.method('PUT')
|
.method('PUT')
|
||||||
.data(formData)
|
.data(payload)
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime();
|
RequestService.clearRequestTime();
|
||||||
callback(res);
|
callback(res);
|
||||||
})
|
})
|
||||||
.fail((err) => {
|
.fail((err) => {
|
||||||
console.error('更新模型失败:', err);
|
console.error('更新模型失败:', err);
|
||||||
this.$message.error(err.msg || '更新模型失败');
|
this.$message.error(err.msg || '更新模型失败');
|
||||||
RequestService.reAjaxFun(() => {
|
RequestService.reAjaxFun(() => {
|
||||||
this.updateModel(params, callback);
|
this.updateModel(params, callback);
|
||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
// 设置默认模型
|
// 设置默认模型
|
||||||
setDefaultModel(id, callback) {
|
setDefaultModel(id, callback) {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default {
|
|||||||
},
|
},
|
||||||
// 修改用户状态
|
// 修改用户状态
|
||||||
changeUserStatus(status, userIds, successCallback) {
|
changeUserStatus(status, userIds, successCallback) {
|
||||||
console.log(555,userIds)
|
console.log(555, userIds)
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/admin/users/changeStatus/${status}`)
|
.url(`${getServiceUrl()}/admin/users/changeStatus/${status}`)
|
||||||
.method('put')
|
.method('put')
|
||||||
@@ -122,4 +122,20 @@ export default {
|
|||||||
})
|
})
|
||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
|
// 获取公共配置
|
||||||
|
getPubConfig(callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/user/pub-config`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取公共配置失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getPubConfig(callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,14 +94,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
|
||||||
const DEFAULT_CONFIG_JSON = {
|
|
||||||
type: "",
|
|
||||||
base_url: "",
|
|
||||||
model_name: "",
|
|
||||||
api_key: "",
|
|
||||||
empty: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ModelEditDialog",
|
name: "ModelEditDialog",
|
||||||
props: {
|
props: {
|
||||||
@@ -132,7 +124,7 @@ export default {
|
|||||||
docLink: "",
|
docLink: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
configJson: JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON))
|
configJson: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -183,8 +175,11 @@ export default {
|
|||||||
docLink: "",
|
docLink: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
configJson: JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON))
|
configJson: {}
|
||||||
};
|
};
|
||||||
|
this.dynamicCallInfoFields.forEach(field => {
|
||||||
|
this.$set(this.form.configJson, field.prop, '');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
resetProviders() {
|
resetProviders() {
|
||||||
this.providers = [];
|
this.providers = [];
|
||||||
@@ -266,6 +261,8 @@ export default {
|
|||||||
this.dynamicCallInfoFields.forEach(field => {
|
this.dynamicCallInfoFields.forEach(field => {
|
||||||
if (!configJson.hasOwnProperty(field.prop)) {
|
if (!configJson.hasOwnProperty(field.prop)) {
|
||||||
configJson[field.prop] = '';
|
configJson[field.prop] = '';
|
||||||
|
} else if (typeof configJson[field.prop] !== 'string') {
|
||||||
|
configJson[field.prop] = String(configJson[field.prop]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -280,7 +277,6 @@ export default {
|
|||||||
remark: model.remark,
|
remark: model.remark,
|
||||||
sort: Number(model.sort) || 0,
|
sort: Number(model.sort) || 0,
|
||||||
configJson: {
|
configJson: {
|
||||||
...DEFAULT_CONFIG_JSON,
|
|
||||||
...configJson
|
...configJson
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div class="copyright">
|
||||||
|
©2025 xiaozhi-esp32-server v{{ version }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Api from '@/apis/api';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VersionFooter',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
version: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getSystemVersion();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getSystemVersion() {
|
||||||
|
const storedVersion = sessionStorage.getItem('systemVersion');
|
||||||
|
if (storedVersion) {
|
||||||
|
this.version = storedVersion;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Api.user.getPubConfig(({ data }) => {
|
||||||
|
if (data.code === 0 && data.data.version) {
|
||||||
|
this.version = data.data.version;
|
||||||
|
sessionStorage.setItem('systemVersion', data.data.version);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -4,10 +4,9 @@
|
|||||||
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<h3 class="device-list-title">设备列表</h3>
|
<h3 class="device-list-title">设备列表</h3>
|
||||||
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
|
<el-table ref="deviceTable" :data="paginatedDeviceList" @selection-change="handleSelectionChange"
|
||||||
+ 添加设备
|
style="width: 100%; margin-top: 20px" border stripe>
|
||||||
</el-button>
|
<el-table-column type="selection" align="center" width="60"></el-table-column>
|
||||||
<el-table :data="paginatedDeviceList" style="width: 100%; margin-top: 20px" border stripe>
|
|
||||||
<el-table-column label="设备型号" prop="model" flex></el-table-column>
|
<el-table-column label="设备型号" prop="model" flex></el-table-column>
|
||||||
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
|
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
|
||||||
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
|
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
|
||||||
@@ -39,12 +38,26 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-pagination class="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange"
|
<div class="table_bottom">
|
||||||
:current-page="currentPage" :page-sizes="[5, 10, 20, 50]" :page-size="pageSize"
|
<div class="ctrl_btn">
|
||||||
layout="total, sizes, prev, pager, next, jumper" :total="deviceList.length"></el-pagination>
|
<el-button size="mini" type="primary" class="select-all-btn" @click="toggleAllSelection">
|
||||||
</div>
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||||
<div class="copyright">
|
</el-button>
|
||||||
©2025 xiaozhi-esp32-server
|
<el-button type="primary" size="mini" class="add-device-btn" @click="handleAddDevice">
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="custom-pagination">
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||||
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||||
|
<span class="total-text">共{{ deviceList.length }}条记录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||||
@refresh="fetchBindDevices(currentAgentId)" />
|
@refresh="fetchBindDevices(currentAgentId)" />
|
||||||
@@ -62,6 +75,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
|
selectedDevices: [],
|
||||||
|
isAllSelected: false,
|
||||||
currentAgentId: this.$route.query.agentId || '',
|
currentAgentId: this.$route.query.agentId || '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 5,
|
||||||
@@ -75,7 +90,25 @@ export default {
|
|||||||
const start = (this.currentPage - 1) * this.pageSize;
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
const end = start + this.pageSize;
|
const end = start + this.pageSize;
|
||||||
return this.deviceList.slice(start, end);
|
return this.deviceList.slice(start, end);
|
||||||
}
|
},
|
||||||
|
pageCount() {
|
||||||
|
return Math.ceil(this.deviceList.length / this.pageSize);
|
||||||
|
},
|
||||||
|
visiblePages() {
|
||||||
|
const pages = [];
|
||||||
|
const maxVisible = 3;
|
||||||
|
let start = Math.max(1, this.currentPage - 1);
|
||||||
|
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||||
|
|
||||||
|
if (end - start + 1 < maxVisible) {
|
||||||
|
start = Math.max(1, end - maxVisible + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const agentId = this.$route.query.agentId;
|
const agentId = this.$route.query.agentId;
|
||||||
@@ -84,6 +117,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
handleSelectionChange(val) {
|
||||||
|
this.selectedDevices = val;
|
||||||
|
this.isAllSelected = val.length === this.paginatedDeviceList.length;
|
||||||
|
},
|
||||||
|
toggleAllSelection() {
|
||||||
|
this.$refs.deviceTable.toggleAllSelection();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
handleAddDevice() {
|
handleAddDevice() {
|
||||||
this.addDeviceDialogVisible = true;
|
this.addDeviceDialogVisible = true;
|
||||||
},
|
},
|
||||||
@@ -115,12 +158,19 @@ export default {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSizeChange(val) {
|
goFirst() {
|
||||||
this.pageSize = val;
|
this.currentPage = 1;
|
||||||
},
|
},
|
||||||
handleCurrentChange(val) {
|
goPrev() {
|
||||||
this.currentPage = val;
|
if (this.currentPage > 1) this.currentPage--;
|
||||||
},
|
},
|
||||||
|
goNext() {
|
||||||
|
if (this.currentPage < this.pageCount) this.currentPage++;
|
||||||
|
},
|
||||||
|
goToPage(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
},
|
||||||
|
|
||||||
fetchBindDevices(agentId) {
|
fetchBindDevices(agentId) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
||||||
@@ -180,21 +230,18 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-device-btn {
|
.add-device-btn {
|
||||||
float: right;
|
background: linear-gradient(135deg, #6b8cff, #a966ff) !important;
|
||||||
background: #409eff;
|
border: none !important;
|
||||||
border: none;
|
color: white !important;
|
||||||
border-radius: 10px;
|
margin-left: 10px;
|
||||||
width: 105px;
|
|
||||||
height: 32px;
|
height: 32px;
|
||||||
display: flex;
|
padding: 7px 12px;
|
||||||
align-items: center;
|
border-radius: 4px !important;
|
||||||
justify-content: center;
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
font-size: 14px;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #3a8ee6;
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +260,83 @@ export default {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.custom-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:first-child,
|
||||||
|
.pagination-btn:nth-child(2),
|
||||||
|
.pagination-btn:nth-last-child(2) {
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:hover {
|
||||||
|
background: #d7dce6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn.active {
|
||||||
|
background: #5f70f3 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-text {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-align: right;
|
}
|
||||||
|
|
||||||
|
.ctrl_btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-left: 26px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 72px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary {
|
||||||
|
background: #5f70f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,21 +3,15 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">{{ modelTypeText }}</h2>
|
<h2 class="page-title">{{ modelTypeText }}</h2>
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<div class="search-group">
|
<div class="search-group">
|
||||||
<el-input
|
<el-input placeholder="请输入模型名称查询" v-model="search" class="search-input" clearable
|
||||||
placeholder="请输入模型名称查询"
|
@keyup.enter.native="handleSearch" style="width: 240px" />
|
||||||
v-model="search"
|
<el-button class="btn-search" @click="handleSearch">
|
||||||
class="search-input"
|
搜索
|
||||||
clearable
|
</el-button>
|
||||||
@keyup.enter.native="handleSearch"
|
</div>
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
<el-button class="btn-search" @click="handleSearch">
|
|
||||||
搜索
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -93,9 +87,9 @@
|
|||||||
|
|
||||||
<div class="table-footer">
|
<div class="table-footer">
|
||||||
<div class="batch-actions">
|
<div class="batch-actions">
|
||||||
<el-button size="mini" type="primary" @click="selectAll" >
|
<el-button size="mini" type="primary" @click="selectAll">
|
||||||
{{ isAllSelected ?
|
{{ isAllSelected ?
|
||||||
'取消全选' : '全选' }}
|
'取消全选' : '全选' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
||||||
新增
|
新增
|
||||||
@@ -125,10 +119,6 @@
|
|||||||
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" />
|
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" />
|
||||||
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
|
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="copyright">
|
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -639,20 +629,6 @@ export default {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyright {
|
|
||||||
text-align: center;
|
|
||||||
color: #979db1;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 30px 0 20px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-wrapper {
|
.title-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -64,8 +64,6 @@
|
|||||||
<!-- 新增/编辑参数对话框 -->
|
<!-- 新增/编辑参数对话框 -->
|
||||||
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
|
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
|
||||||
@cancel="dialogVisible = false" />
|
@cancel="dialogVisible = false" />
|
||||||
|
|
||||||
<div class="copyright">©2025 xiaozhi-esp32-server</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -382,20 +380,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyright {
|
|
||||||
text-align: center;
|
|
||||||
color: #979db1;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 30px 0 20px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination {
|
.custom-pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<div class="table_bottom">
|
<div class="table_bottom">
|
||||||
<div class="ctrl_btn">
|
<div class="ctrl_btn">
|
||||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button>
|
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button>
|
||||||
<el-button size="mini" type="warning" @click="batchDisable"><i
|
<el-button size="mini" type="warning" @click="batchDisable"><i
|
||||||
@@ -69,8 +69,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="copyright">©2025 xiaozhi-esp32-server</div>
|
|
||||||
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
|
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -465,20 +463,6 @@ export default {
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyright {
|
|
||||||
text-align: center;
|
|
||||||
color: #979db1;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 30px 0 20px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination {
|
.custom-pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -35,11 +35,11 @@
|
|||||||
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" />
|
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="copyright">
|
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
||||||
</el-main>
|
</el-main>
|
||||||
|
<el-footer>
|
||||||
|
<version-footer />
|
||||||
|
</el-footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
@@ -49,10 +49,11 @@ import Api from '@/apis/api';
|
|||||||
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
|
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
|
||||||
import DeviceItem from '@/components/DeviceItem.vue';
|
import DeviceItem from '@/components/DeviceItem.vue';
|
||||||
import HeaderBar from '@/components/HeaderBar.vue';
|
import HeaderBar from '@/components/HeaderBar.vue';
|
||||||
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HomePage',
|
name: 'HomePage',
|
||||||
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar },
|
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar, VersionFooter },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
|
|||||||
@@ -51,9 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
<el-footer>
|
<el-footer>
|
||||||
<div class="copyright">
|
<version-footer />
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
</el-footer>
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,11 +59,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
components: {
|
||||||
|
VersionFooter
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeName: "username",
|
activeName: "username",
|
||||||
@@ -76,11 +77,13 @@ export default {
|
|||||||
captchaId: ''
|
captchaId: ''
|
||||||
},
|
},
|
||||||
captchaUuid: '',
|
captchaUuid: '',
|
||||||
captchaUrl: ''
|
captchaUrl: '',
|
||||||
|
version: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
|
this.getSystemVersion();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchCaptcha() {
|
fetchCaptcha() {
|
||||||
@@ -144,6 +147,21 @@ export default {
|
|||||||
|
|
||||||
goToRegister() {
|
goToRegister() {
|
||||||
goToPage('/register')
|
goToPage('/register')
|
||||||
|
},
|
||||||
|
|
||||||
|
getSystemVersion() {
|
||||||
|
const storedVersion = sessionStorage.getItem('systemVersion');
|
||||||
|
if (storedVersion) {
|
||||||
|
this.version = storedVersion;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Api.user.getPubConfig(({ data }) => {
|
||||||
|
if (data.code === 0 && data.data.version) {
|
||||||
|
this.version = data.data.version;
|
||||||
|
sessionStorage.setItem('systemVersion', data.data.version);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,9 +72,7 @@
|
|||||||
|
|
||||||
<!-- 保持相同的页脚 -->
|
<!-- 保持相同的页脚 -->
|
||||||
<el-footer>
|
<el-footer>
|
||||||
<div class="copyright">
|
<version-footer />
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
</el-footer>
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,10 +80,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'register',
|
name: 'register',
|
||||||
|
components: {
|
||||||
|
VersionFooter
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
|
|||||||
@@ -81,9 +81,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="copyright">
|
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
</el-main>
|
</el-main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ module.exports = defineConfig({
|
|||||||
devServer: {
|
devServer: {
|
||||||
port: 8001, // 指定端口为 8001
|
port: 8001, // 指定端口为 8001
|
||||||
proxy: {
|
proxy: {
|
||||||
|
'/xiaozhi': {
|
||||||
|
target: 'http://127.0.0.1:8002',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
overlay: false, // 不显示 webpack 错误覆盖层
|
overlay: false, // 不显示 webpack 错误覆盖层
|
||||||
|
|||||||
@@ -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.1"
|
SERVER_VERSION = "0.3.3"
|
||||||
|
|
||||||
|
|
||||||
def get_module_abbreviation(module_name, module_dict):
|
def get_module_abbreviation(module_name, module_dict):
|
||||||
@@ -25,6 +25,12 @@ def build_module_string(selected_module):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def formatter(record):
|
||||||
|
"""为没有 tag 的日志添加默认值"""
|
||||||
|
record["extra"].setdefault("tag", record["name"])
|
||||||
|
return record["message"]
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
@@ -56,9 +62,14 @@ def setup_logging():
|
|||||||
logger.remove()
|
logger.remove()
|
||||||
|
|
||||||
# 输出到控制台
|
# 输出到控制台
|
||||||
logger.add(sys.stdout, format=log_format, level=log_level)
|
logger.add(sys.stdout, format=log_format, level=log_level, filter=formatter)
|
||||||
|
|
||||||
# 输出到文件
|
# 输出到文件
|
||||||
logger.add(os.path.join(log_dir, log_file), format=log_format_file, level=log_level)
|
logger.add(
|
||||||
|
os.path.join(log_dir, log_file),
|
||||||
|
format=log_format_file,
|
||||||
|
level=log_level,
|
||||||
|
filter=formatter,
|
||||||
|
)
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
|
|||||||
@@ -104,8 +104,6 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
self.close_after_chat = False # 是否在聊天结束后关闭连接
|
self.close_after_chat = False # 是否在聊天结束后关闭连接
|
||||||
self.use_function_call_mode = False
|
self.use_function_call_mode = False
|
||||||
if self.config["selected_module"]["Intent"] == "function_call":
|
|
||||||
self.use_function_call_mode = True
|
|
||||||
|
|
||||||
async def handle_connection(self, ws):
|
async def handle_connection(self, ws):
|
||||||
try:
|
try:
|
||||||
@@ -206,7 +204,7 @@ class ConnectionHandler:
|
|||||||
self.headers.get("device-id", None),
|
self.headers.get("device-id", None),
|
||||||
self.headers.get("client-id", None),
|
self.headers.get("client-id", None),
|
||||||
)
|
)
|
||||||
private_config["delete_audio"] = self.config["delete_audio"]
|
private_config["delete_audio"] = bool(self.config.get("delete_audio", True))
|
||||||
self.logger.bind(tag=TAG).info(f"获取差异化配置成功: {private_config}")
|
self.logger.bind(tag=TAG).info(f"获取差异化配置成功: {private_config}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}")
|
self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}")
|
||||||
@@ -222,37 +220,37 @@ class ConnectionHandler:
|
|||||||
)
|
)
|
||||||
if private_config.get("VAD", None) is not None:
|
if private_config.get("VAD", None) is not None:
|
||||||
init_vad = True
|
init_vad = True
|
||||||
self.config["vad"] = private_config["VAD"]
|
self.config["VAD"] = private_config["VAD"]
|
||||||
self.config["selected_module"]["VAD"] = private_config["selected_module"][
|
self.config["selected_module"]["VAD"] = private_config["selected_module"][
|
||||||
"VAD"
|
"VAD"
|
||||||
]
|
]
|
||||||
if private_config.get("ASR", None) is not None:
|
if private_config.get("ASR", None) is not None:
|
||||||
init_asr = True
|
init_asr = True
|
||||||
self.config["asr"] = private_config["ASR"]
|
self.config["ASR"] = private_config["ASR"]
|
||||||
self.config["selected_module"]["ASR"] = private_config["selected_module"][
|
self.config["selected_module"]["ASR"] = private_config["selected_module"][
|
||||||
"ASR"
|
"ASR"
|
||||||
]
|
]
|
||||||
if private_config.get("LLM", None) is not None:
|
if private_config.get("LLM", None) is not None:
|
||||||
init_llm = True
|
init_llm = True
|
||||||
self.config["llm"] = private_config["LLM"]
|
self.config["LLM"] = private_config["LLM"]
|
||||||
self.config["selected_module"]["LLM"] = private_config["selected_module"][
|
self.config["selected_module"]["LLM"] = private_config["selected_module"][
|
||||||
"LLM"
|
"LLM"
|
||||||
]
|
]
|
||||||
if private_config.get("TTS", None) is not None:
|
if private_config.get("TTS", None) is not None:
|
||||||
init_tts = True
|
init_tts = True
|
||||||
self.config["tts"] = private_config["TTS"]
|
self.config["TTS"] = private_config["TTS"]
|
||||||
self.config["selected_module"]["TTS"] = private_config["selected_module"][
|
self.config["selected_module"]["TTS"] = private_config["selected_module"][
|
||||||
"TTS"
|
"TTS"
|
||||||
]
|
]
|
||||||
if private_config.get("Memory", None) is not None:
|
if private_config.get("Memory", None) is not None:
|
||||||
init_memory = True
|
init_memory = True
|
||||||
self.config["memory"] = private_config["Memory"]
|
self.config["Memory"] = private_config["Memory"]
|
||||||
self.config["selected_module"]["Memory"] = private_config[
|
self.config["selected_module"]["Memory"] = private_config[
|
||||||
"selected_module"
|
"selected_module"
|
||||||
]["Memory"]
|
]["Memory"]
|
||||||
if private_config.get("Intent", None) is not None:
|
if private_config.get("Intent", None) is not None:
|
||||||
init_intent = True
|
init_intent = True
|
||||||
self.config["intent"] = private_config["Intent"]
|
self.config["Intent"] = private_config["Intent"]
|
||||||
self.config["selected_module"]["Intent"] = private_config[
|
self.config["selected_module"]["Intent"] = private_config[
|
||||||
"selected_module"
|
"selected_module"
|
||||||
]["Intent"]
|
]["Intent"]
|
||||||
@@ -287,17 +285,26 @@ class ConnectionHandler:
|
|||||||
self.memory.init_memory(device_id, self.llm)
|
self.memory.init_memory(device_id, self.llm)
|
||||||
|
|
||||||
def _initialize_intent(self):
|
def _initialize_intent(self):
|
||||||
|
if (
|
||||||
|
self.config["Intent"][self.config["selected_module"]["Intent"]]["type"]
|
||||||
|
== "function_call"
|
||||||
|
):
|
||||||
|
self.use_function_call_mode = True
|
||||||
"""初始化意图识别模块"""
|
"""初始化意图识别模块"""
|
||||||
# 获取意图识别配置
|
# 获取意图识别配置
|
||||||
intent_config = self.config["Intent"]
|
intent_config = self.config["Intent"]
|
||||||
intent_type = self.config["selected_module"]["Intent"]
|
intent_type = self.config["Intent"][self.config["selected_module"]["Intent"]][
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
|
||||||
# 如果使用 nointent,直接返回
|
# 如果使用 nointent,直接返回
|
||||||
if intent_type == "nointent":
|
if intent_type == "nointent":
|
||||||
return
|
return
|
||||||
# 使用 intent_llm 模式
|
# 使用 intent_llm 模式
|
||||||
elif intent_type == "intent_llm":
|
elif intent_type == "intent_llm":
|
||||||
intent_llm_name = intent_config["intent_llm"]["llm"]
|
intent_llm_name = intent_config[self.config["selected_module"]["Intent"]][
|
||||||
|
"llm"
|
||||||
|
]
|
||||||
|
|
||||||
if intent_llm_name and intent_llm_name in self.config["LLM"]:
|
if intent_llm_name and intent_llm_name in self.config["LLM"]:
|
||||||
# 如果配置了专用LLM,则创建独立的LLM实例
|
# 如果配置了专用LLM,则创建独立的LLM实例
|
||||||
@@ -328,10 +335,8 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
def change_system_prompt(self, prompt):
|
def change_system_prompt(self, prompt):
|
||||||
self.prompt = prompt
|
self.prompt = prompt
|
||||||
# 找到原来的role==system,替换原来的系统提示
|
# 更新系统prompt至上下文
|
||||||
for m in self.dialogue.dialogue:
|
self.dialogue.update_system_message(self.prompt)
|
||||||
if m.role == "system":
|
|
||||||
m.content = prompt
|
|
||||||
|
|
||||||
def chat(self, query):
|
def chat(self, query):
|
||||||
|
|
||||||
@@ -695,7 +700,7 @@ class ConnectionHandler:
|
|||||||
opus_datas, text_index, tts_file = [], 0, None
|
opus_datas, text_index, tts_file = [], 0, None
|
||||||
try:
|
try:
|
||||||
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
||||||
tts_timeout = self.config.get("tts_timeout", 10)
|
tts_timeout = int(self.config.get("tts_timeout", 10))
|
||||||
tts_file, text, text_index = future.result(timeout=tts_timeout)
|
tts_file, text, text_index = future.result(timeout=tts_timeout)
|
||||||
if text is None or len(text) <= 0:
|
if text is None or len(text) <= 0:
|
||||||
self.logger.bind(tag=TAG).error(
|
self.logger.bind(tag=TAG).error(
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ class FunctionHandler:
|
|||||||
|
|
||||||
def register_config_functions(self):
|
def register_config_functions(self):
|
||||||
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
||||||
for func in self.config["Intent"]["function_call"].get("functions", []):
|
for func in self.config["Intent"][self.config["selected_module"]["Intent"]].get(
|
||||||
|
"functions", []
|
||||||
|
):
|
||||||
self.function_registry.register_function(func)
|
self.function_registry.register_function(func)
|
||||||
|
|
||||||
"""home assistant需要初始化提示词"""
|
"""home assistant需要初始化提示词"""
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ async def process_intent_result(conn, intent_result, original_text):
|
|||||||
if function_name == "continue_chat":
|
if function_name == "continue_chat":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if function_name == "play_music":
|
||||||
|
funcItem = conn.func_handler.get_function(function_name)
|
||||||
|
if not funcItem:
|
||||||
|
conn.func_handler.function_registry.register_function("play_music")
|
||||||
|
|
||||||
function_args = None
|
function_args = None
|
||||||
if "arguments" in intent_data["function_call"]:
|
if "arguments" in intent_data["function_call"]:
|
||||||
function_args = intent_data["function_call"]["arguments"]
|
function_args = intent_data["function_call"]["arguments"]
|
||||||
|
|||||||
@@ -269,14 +269,12 @@ def register_device_type(descriptor):
|
|||||||
|
|
||||||
# 用于接受前端设备推送的搜索iot描述
|
# 用于接受前端设备推送的搜索iot描述
|
||||||
async def handleIotDescriptors(conn, descriptors):
|
async def handleIotDescriptors(conn, descriptors):
|
||||||
if not conn.use_function_call_mode:
|
|
||||||
return
|
|
||||||
wait_max_time = 5
|
wait_max_time = 5
|
||||||
while conn.func_handler is None or not conn.func_handler.finish_init:
|
while conn.func_handler is None or not conn.func_handler.finish_init:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
wait_max_time -= 1
|
wait_max_time -= 1
|
||||||
if wait_max_time <= 0:
|
if wait_max_time <= 0:
|
||||||
logger.bind(tag=TAG).error("连接对象没有func_handler")
|
logger.bind(tag=TAG).debug("连接对象没有func_handler")
|
||||||
return
|
return
|
||||||
"""处理物联网描述"""
|
"""处理物联网描述"""
|
||||||
functions_changed = False
|
functions_changed = False
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ async def no_voice_close_connect(conn):
|
|||||||
conn.client_no_voice_last_time = time.time() * 1000
|
conn.client_no_voice_last_time = time.time() * 1000
|
||||||
else:
|
else:
|
||||||
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
||||||
close_connection_no_voice_time = conn.config.get(
|
close_connection_no_voice_time = int(
|
||||||
"close_connection_no_voice_time", 120
|
conn.config.get("close_connection_no_voice_time", 120)
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
not conn.close_after_chat
|
not conn.close_after_chat
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ class LLMProvider(LLMProviderBase):
|
|||||||
self.session_conversation_map[session_id] = (
|
self.session_conversation_map[session_id] = (
|
||||||
conversation_id # 更新映射
|
conversation_id # 更新映射
|
||||||
)
|
)
|
||||||
if event.get("answer"):
|
# 过滤 message_replace 事件,此事件会全量推一次
|
||||||
|
if event.get("event") != "message_replace" and event.get("answer"):
|
||||||
yield event["answer"]
|
yield event["answer"]
|
||||||
elif self.mode == "workflows/run":
|
elif self.mode == "workflows/run":
|
||||||
for line in r.iter_lines():
|
for line in r.iter_lines():
|
||||||
@@ -73,7 +74,8 @@ class LLMProvider(LLMProviderBase):
|
|||||||
for line in r.iter_lines():
|
for line in r.iter_lines():
|
||||||
if line.startswith(b"data: "):
|
if line.startswith(b"data: "):
|
||||||
event = json.loads(line[6:])
|
event = json.loads(line[6:])
|
||||||
if event.get("answer"):
|
# 过滤 message_replace 事件,此事件会全量推一次
|
||||||
|
if event.get("event") != "message_replace" and event.get("answer"):
|
||||||
yield event["answer"]
|
yield event["answer"]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -15,7 +15,15 @@ class LLMProvider(LLMProviderBase):
|
|||||||
self.base_url = config.get("base_url")
|
self.base_url = config.get("base_url")
|
||||||
else:
|
else:
|
||||||
self.base_url = config.get("url")
|
self.base_url = config.get("url")
|
||||||
self.max_tokens = config.get("max_tokens", 500)
|
max_tokens = config.get("max_tokens")
|
||||||
|
if max_tokens is None or max_tokens == "":
|
||||||
|
max_tokens = 500
|
||||||
|
|
||||||
|
try:
|
||||||
|
max_tokens = int(max_tokens)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
max_tokens = 500
|
||||||
|
self.max_tokens = max_tokens
|
||||||
|
|
||||||
check_model_key("LLM", self.api_key)
|
check_model_key("LLM", self.api_key)
|
||||||
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
|
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||||
|
|||||||
@@ -12,14 +12,20 @@ class TTSProvider(TTSProviderBase):
|
|||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
self.model = config.get("model")
|
self.model = config.get("model")
|
||||||
self.access_token = config.get("access_token")
|
self.access_token = config.get("access_token")
|
||||||
self.voice = config.get("voice")
|
if config.get("private_voice"):
|
||||||
|
self.voice = config.get("private_voice")
|
||||||
|
else:
|
||||||
|
self.voice = config.get("voice")
|
||||||
self.response_format = config.get("response_format")
|
self.response_format = config.get("response_format")
|
||||||
|
|
||||||
self.host = "api.coze.cn"
|
self.host = "api.coze.cn"
|
||||||
self.api_url = f"https://{self.host}/v1/audio/speech"
|
self.api_url = f"https://{self.host}/v1/audio/speech"
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
request_json = {
|
request_json = {
|
||||||
@@ -30,9 +36,11 @@ class TTSProvider(TTSProviderBase):
|
|||||||
}
|
}
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.access_token}",
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
response = requests.request("POST", self.api_url, json=request_json, headers=headers)
|
response = requests.request(
|
||||||
|
"POST", self.api_url, json=request_json, headers=headers
|
||||||
|
)
|
||||||
data = response.content
|
data = response.content
|
||||||
file_to_save = open(output_file, "wb")
|
file_to_save = open(output_file, "wb")
|
||||||
file_to_save.write(data)
|
file_to_save.write(data)
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.appid = config.get("appid")
|
self.appid = config.get("appid")
|
||||||
self.access_token = config.get("access_token")
|
self.access_token = config.get("access_token")
|
||||||
self.cluster = config.get("cluster")
|
self.cluster = config.get("cluster")
|
||||||
self.voice = config.get("voice")
|
|
||||||
|
if config.get("private_voice"):
|
||||||
|
self.voice = config.get("private_voice")
|
||||||
|
else:
|
||||||
|
self.voice = config.get("voice")
|
||||||
|
|
||||||
self.api_url = config.get("api_url")
|
self.api_url = config.get("api_url")
|
||||||
self.authorization = config.get("authorization")
|
self.authorization = config.get("authorization")
|
||||||
self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
|
self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
|
||||||
|
|||||||
@@ -8,20 +8,26 @@ from core.providers.tts.base import TTSProviderBase
|
|||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
self.voice = config.get("voice")
|
if config.get("private_voice"):
|
||||||
|
self.voice = config.get("private_voice")
|
||||||
|
else:
|
||||||
|
self.voice = config.get("voice")
|
||||||
|
|
||||||
def generate_filename(self, extension=".mp3"):
|
def generate_filename(self, extension=".mp3"):
|
||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
communicate = edge_tts.Communicate(text, voice=self.voice)
|
communicate = edge_tts.Communicate(text, voice=self.voice)
|
||||||
# 确保目录存在并创建空文件
|
# 确保目录存在并创建空文件
|
||||||
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||||
with open(output_file, 'wb') as f:
|
with open(output_file, "wb") as f:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 流式写入音频数据
|
# 流式写入音频数据
|
||||||
with open(output_file, 'ab') as f: # 改为追加模式避免覆盖
|
with open(output_file, "ab") as f: # 改为追加模式避免覆盖
|
||||||
async for chunk in communicate.stream():
|
async for chunk in communicate.stream():
|
||||||
if chunk["type"] == "audio": # 只处理音频数据块
|
if chunk["type"] == "audio": # 只处理音频数据块
|
||||||
f.write(chunk["data"])
|
f.write(chunk["data"])
|
||||||
|
|||||||
@@ -89,19 +89,19 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.reference_audio = config.get("reference_audio", [])
|
self.reference_audio = config.get("reference_audio", [])
|
||||||
self.reference_text = config.get("reference_text", [])
|
self.reference_text = config.get("reference_text", [])
|
||||||
self.format = config.get("format", "wav")
|
self.format = config.get("format", "wav")
|
||||||
self.channels = config.get("channels", 1)
|
self.channels = int(config.get("channels", 1))
|
||||||
self.rate = config.get("rate", 44100)
|
self.rate = int(config.get("rate", 44100))
|
||||||
self.api_key = config.get("api_key", "YOUR_API_KEY")
|
self.api_key = config.get("api_key", "YOUR_API_KEY")
|
||||||
have_key = check_model_key("FishSpeech TTS", self.api_key)
|
have_key = check_model_key("FishSpeech TTS", self.api_key)
|
||||||
if not have_key:
|
if not have_key:
|
||||||
return
|
return
|
||||||
self.normalize = config.get("normalize", True)
|
self.normalize = config.get("normalize", True)
|
||||||
self.max_new_tokens = config.get("max_new_tokens", 1024)
|
self.max_new_tokens = int(config.get("max_new_tokens", 1024))
|
||||||
self.chunk_length = config.get("chunk_length", 200)
|
self.chunk_length = int(config.get("chunk_length", 200))
|
||||||
self.top_p = config.get("top_p", 0.7)
|
self.top_p = float(config.get("top_p", 0.7))
|
||||||
self.repetition_penalty = config.get("repetition_penalty", 1.2)
|
self.repetition_penalty = float(config.get("repetition_penalty", 1.2))
|
||||||
self.temperature = config.get("temperature", 0.7)
|
self.temperature = float(config.get("temperature", 0.7))
|
||||||
self.streaming = config.get("streaming", False)
|
self.streaming = bool(config.get("streaming", False))
|
||||||
self.use_memory_cache = config.get("use_memory_cache", "on")
|
self.use_memory_cache = config.get("use_memory_cache", "on")
|
||||||
self.seed = config.get("seed")
|
self.seed = config.get("seed")
|
||||||
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
|
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from core.providers.tts.base import TTSProviderBase
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
@@ -18,23 +19,26 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.ref_audio_path = config.get("ref_audio_path")
|
self.ref_audio_path = config.get("ref_audio_path")
|
||||||
self.prompt_text = config.get("prompt_text")
|
self.prompt_text = config.get("prompt_text")
|
||||||
self.prompt_lang = config.get("prompt_lang", "zh")
|
self.prompt_lang = config.get("prompt_lang", "zh")
|
||||||
self.top_k = config.get("top_k", 5)
|
self.top_k = int(config.get("top_k", 5))
|
||||||
self.top_p = config.get("top_p", 1)
|
self.top_p = float(config.get("top_p", 1))
|
||||||
self.temperature = config.get("temperature", 1)
|
self.temperature = float(config.get("temperature", 1))
|
||||||
self.text_split_method = config.get("text_split_method", "cut0")
|
self.text_split_method = config.get("text_split_method", "cut0")
|
||||||
self.batch_size = config.get("batch_size", 1)
|
self.batch_size = int(config.get("batch_size", 1))
|
||||||
self.batch_threshold = config.get("batch_threshold", 0.75)
|
self.batch_threshold = float(config.get("batch_threshold", 0.75))
|
||||||
self.split_bucket = config.get("split_bucket", True)
|
self.split_bucket = bool(config.get("split_bucket", True))
|
||||||
self.return_fragment = config.get("return_fragment", False)
|
self.return_fragment = bool(config.get("return_fragment", False))
|
||||||
self.speed_factor = config.get("speed_factor", 1.0)
|
self.speed_factor = float(config.get("speed_factor", 1.0))
|
||||||
self.streaming_mode = config.get("streaming_mode", False)
|
self.streaming_mode = bool(config.get("streaming_mode", False))
|
||||||
self.seed = config.get("seed", -1)
|
self.seed = int(config.get("seed", -1))
|
||||||
self.parallel_infer = config.get("parallel_infer", True)
|
self.parallel_infer = bool(config.get("parallel_infer", True))
|
||||||
self.repetition_penalty = config.get("repetition_penalty", 1.35)
|
self.repetition_penalty = float(config.get("repetition_penalty", 1.35))
|
||||||
self.aux_ref_audio_paths = config.get("aux_ref_audio_paths", [])
|
self.aux_ref_audio_paths = config.get("aux_ref_audio_paths", [])
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
request_json = {
|
request_json = {
|
||||||
@@ -56,7 +60,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
"streaming_mode": self.streaming_mode,
|
"streaming_mode": self.streaming_mode,
|
||||||
"seed": self.seed,
|
"seed": self.seed,
|
||||||
"parallel_infer": self.parallel_infer,
|
"parallel_infer": self.parallel_infer,
|
||||||
"repetition_penalty": self.repetition_penalty
|
"repetition_penalty": self.repetition_penalty,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = requests.post(self.url, json=request_json)
|
resp = requests.post(self.url, json=request_json)
|
||||||
@@ -64,4 +68,6 @@ class TTSProvider(TTSProviderBase):
|
|||||||
with open(output_file, "wb") as file:
|
with open(output_file, "wb") as file:
|
||||||
file.write(resp.content)
|
file.write(resp.content)
|
||||||
else:
|
else:
|
||||||
logger.bind(tag=TAG).error(f"GPT_SoVITS_V2 TTS请求失败: {resp.status_code} - {resp.text}")
|
logger.bind(tag=TAG).error(
|
||||||
|
f"GPT_SoVITS_V2 TTS请求失败: {resp.status_code} - {resp.text}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from core.providers.tts.base import TTSProviderBase
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
@@ -16,18 +17,20 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.prompt_text = config.get("prompt_text")
|
self.prompt_text = config.get("prompt_text")
|
||||||
self.prompt_language = config.get("prompt_language")
|
self.prompt_language = config.get("prompt_language")
|
||||||
self.text_language = config.get("text_language", "audo")
|
self.text_language = config.get("text_language", "audo")
|
||||||
self.top_k = config.get("top_k", 15)
|
self.top_k = int(config.get("top_k", 15))
|
||||||
self.top_p = config.get("top_p", 1.0)
|
self.top_p = float(config.get("top_p", 1.0))
|
||||||
self.temperature = config.get("temperature", 1.0)
|
self.temperature = float(config.get("temperature", 1.0))
|
||||||
self.cut_punc = config.get("cut_punc","")
|
self.cut_punc = config.get("cut_punc", "")
|
||||||
self.speed = config.get("speed", 1.0)
|
self.speed = float(config.get("speed", 1.0))
|
||||||
self.inp_refs = config.get("inp_refs",[])
|
self.inp_refs = config.get("inp_refs", [])
|
||||||
self.sample_steps = config.get("sample_steps",32)
|
self.sample_steps = int(config.get("sample_steps", 32))
|
||||||
self.if_sr = config.get("if_sr",False)
|
self.if_sr = bool(config.get("if_sr", False))
|
||||||
|
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
request_params = {
|
request_params = {
|
||||||
@@ -51,4 +54,6 @@ class TTSProvider(TTSProviderBase):
|
|||||||
with open(output_file, "wb") as file:
|
with open(output_file, "wb") as file:
|
||||||
file.write(resp.content)
|
file.write(resp.content)
|
||||||
else:
|
else:
|
||||||
logger.bind(tag=TAG).error(f"GPT_SoVITS_V3 TTS请求失败: {resp.status_code} - {resp.text}")
|
logger.bind(tag=TAG).error(
|
||||||
|
f"GPT_SoVITS_V3 TTS请求失败: {resp.status_code} - {resp.text}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,28 +12,33 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.group_id = config.get("group_id")
|
self.group_id = config.get("group_id")
|
||||||
self.api_key = config.get("api_key")
|
self.api_key = config.get("api_key")
|
||||||
self.model = config.get("model")
|
self.model = config.get("model")
|
||||||
self.voice_id = config.get("voice_id")
|
if config.get("private_voice"):
|
||||||
|
self.voice_id = config.get("private_voice")
|
||||||
|
else:
|
||||||
|
self.voice_id = config.get("voice_id")
|
||||||
|
|
||||||
default_voice_setting = {
|
default_voice_setting = {
|
||||||
"voice_id": "female-shaonv",
|
"voice_id": "female-shaonv",
|
||||||
"speed": 1,
|
"speed": 1,
|
||||||
"vol": 1,
|
"vol": 1,
|
||||||
"pitch": 0,
|
"pitch": 0,
|
||||||
"emotion": "happy"
|
"emotion": "happy",
|
||||||
}
|
|
||||||
default_pronunciation_dict = {
|
|
||||||
"tone": [
|
|
||||||
"处理/(chu3)(li3)", "危险/dangerous"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
default_pronunciation_dict = {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}
|
||||||
defult_audio_setting = {
|
defult_audio_setting = {
|
||||||
"sample_rate": 32000,
|
"sample_rate": 32000,
|
||||||
"bitrate": 128000,
|
"bitrate": 128000,
|
||||||
"format": "mp3",
|
"format": "mp3",
|
||||||
"channel": 1
|
"channel": 1,
|
||||||
|
}
|
||||||
|
self.voice_setting = {
|
||||||
|
**default_voice_setting,
|
||||||
|
**config.get("voice_setting", {}),
|
||||||
|
}
|
||||||
|
self.pronunciation_dict = {
|
||||||
|
**default_pronunciation_dict,
|
||||||
|
**config.get("pronunciation_dict", {}),
|
||||||
}
|
}
|
||||||
self.voice_setting = {**default_voice_setting, **config.get("voice_setting", {})}
|
|
||||||
self.pronunciation_dict = {**default_pronunciation_dict, **config.get("pronunciation_dict", {})}
|
|
||||||
self.audio_setting = {**defult_audio_setting, **config.get("audio_setting", {})}
|
self.audio_setting = {**defult_audio_setting, **config.get("audio_setting", {})}
|
||||||
self.timber_weights = config.get("timber_weights", [])
|
self.timber_weights = config.get("timber_weights", [])
|
||||||
|
|
||||||
@@ -44,11 +49,14 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.api_url = f"https://{self.host}/v1/t2a_v2?GroupId={self.group_id}"
|
self.api_url = f"https://{self.host}/v1/t2a_v2?GroupId={self.group_id}"
|
||||||
self.header = {
|
self.header = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": f"Bearer {self.api_key}"
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def generate_filename(self, extension=".mp3"):
|
def generate_filename(self, extension=".mp3"):
|
||||||
return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
request_json = {
|
request_json = {
|
||||||
@@ -65,13 +73,17 @@ class TTSProvider(TTSProviderBase):
|
|||||||
request_json["voice_setting"]["voice_id"] = ""
|
request_json["voice_setting"]["voice_id"] = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
|
resp = requests.post(
|
||||||
|
self.api_url, json.dumps(request_json), headers=self.header
|
||||||
|
)
|
||||||
# 检查返回请求数据的status_code是否为0
|
# 检查返回请求数据的status_code是否为0
|
||||||
if resp.json()["base_resp"]["status_code"] == 0:
|
if resp.json()["base_resp"]["status_code"] == 0:
|
||||||
data = resp.json()['data']['audio']
|
data = resp.json()["data"]["audio"]
|
||||||
file_to_save = open(output_file, "wb")
|
file_to_save = open(output_file, "wb")
|
||||||
file_to_save.write(bytes.fromhex(data))
|
file_to_save.write(bytes.fromhex(data))
|
||||||
else:
|
else:
|
||||||
raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}")
|
raise Exception(
|
||||||
|
f"{__name__} status_code: {resp.status_code} response: {resp.content}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"{__name__} error: {e}")
|
raise Exception(f"{__name__} error: {e}")
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.api_key = config.get("api_key")
|
self.api_key = config.get("api_key")
|
||||||
self.api_url = config.get("api_url", "https://api.openai.com/v1/audio/speech")
|
self.api_url = config.get("api_url", "https://api.openai.com/v1/audio/speech")
|
||||||
self.model = config.get("model", "tts-1")
|
self.model = config.get("model", "tts-1")
|
||||||
self.voice = config.get("voice", "alloy")
|
if config.get("private_voice"):
|
||||||
|
self.voice = config.get("private_voice")
|
||||||
|
else:
|
||||||
|
self.voice = config.get("voice", "alloy")
|
||||||
self.response_format = "wav"
|
self.response_format = "wav"
|
||||||
self.speed = config.get("speed", 1.0)
|
self.speed = float(config.get("speed", 1.0))
|
||||||
self.output_file = config.get("output_dir", "tmp/")
|
self.output_file = config.get("output_dir", "tmp/")
|
||||||
check_model_key("TTS", self.api_key)
|
check_model_key("TTS", self.api_key)
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,23 @@ class TTSProvider(TTSProviderBase):
|
|||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
self.model = config.get("model")
|
self.model = config.get("model")
|
||||||
self.access_token = config.get("access_token")
|
self.access_token = config.get("access_token")
|
||||||
self.voice = config.get("voice")
|
if config.get("private_voice"):
|
||||||
|
self.voice = config.get("private_voice")
|
||||||
|
else:
|
||||||
|
self.voice = config.get("voice")
|
||||||
self.response_format = config.get("response_format")
|
self.response_format = config.get("response_format")
|
||||||
self.sample_rate = config.get("sample_rate")
|
self.sample_rate = config.get("sample_rate")
|
||||||
self.speed = config.get("speed")
|
self.speed = float(config.get("speed"))
|
||||||
self.gain = config.get("gain")
|
self.gain = config.get("gain")
|
||||||
|
|
||||||
self.host = "api.siliconflow.cn"
|
self.host = "api.siliconflow.cn"
|
||||||
self.api_url = f"https://{self.host}/v1/audio/speech"
|
self.api_url = f"https://{self.host}/v1/audio/speech"
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
request_json = {
|
request_json = {
|
||||||
@@ -31,9 +37,11 @@ class TTSProvider(TTSProviderBase):
|
|||||||
}
|
}
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.access_token}",
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
response = requests.request("POST", self.api_url, json=request_json, headers=headers)
|
response = requests.request(
|
||||||
|
"POST", self.api_url, json=request_json, headers=headers
|
||||||
|
)
|
||||||
data = response.content
|
data = response.content
|
||||||
file_to_save = open(output_file, "wb")
|
file_to_save = open(output_file, "wb")
|
||||||
file_to_save.write(data)
|
file_to_save.write(data)
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.appid = config.get("appid")
|
self.appid = config.get("appid")
|
||||||
self.secret_id = config.get("secret_id")
|
self.secret_id = config.get("secret_id")
|
||||||
self.secret_key = config.get("secret_key")
|
self.secret_key = config.get("secret_key")
|
||||||
self.voice = config.get("voice")
|
if config.get("private_voice"):
|
||||||
|
self.voice = config.get("private_voice")
|
||||||
|
else:
|
||||||
|
self.voice = int(config.get("voice"))
|
||||||
self.api_url = "https://tts.tencentcloudapi.com" # 正确的API端点
|
self.api_url = "https://tts.tencentcloudapi.com" # 正确的API端点
|
||||||
self.region = config.get("region")
|
self.region = config.get("region")
|
||||||
self.output_file = config.get("output_dir")
|
self.output_file = config.get("output_dir")
|
||||||
@@ -25,35 +28,36 @@ class TTSProvider(TTSProviderBase):
|
|||||||
"""生成鉴权请求头"""
|
"""生成鉴权请求头"""
|
||||||
# 获取当前UTC时间戳
|
# 获取当前UTC时间戳
|
||||||
timestamp = int(time.time())
|
timestamp = int(time.time())
|
||||||
|
|
||||||
# 使用UTC时间计算日期
|
# 使用UTC时间计算日期
|
||||||
utc_date = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime('%Y-%m-%d')
|
utc_date = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime(
|
||||||
|
"%Y-%m-%d"
|
||||||
|
)
|
||||||
|
|
||||||
# 服务名称必须是 "tts"
|
# 服务名称必须是 "tts"
|
||||||
service = "tts"
|
service = "tts"
|
||||||
|
|
||||||
# 拼接凭证范围
|
# 拼接凭证范围
|
||||||
credential_scope = f"{utc_date}/{service}/tc3_request"
|
credential_scope = f"{utc_date}/{service}/tc3_request"
|
||||||
|
|
||||||
# 使用TC3-HMAC-SHA256签名方法
|
# 使用TC3-HMAC-SHA256签名方法
|
||||||
algorithm = "TC3-HMAC-SHA256"
|
algorithm = "TC3-HMAC-SHA256"
|
||||||
|
|
||||||
# 构建规范请求字符串
|
# 构建规范请求字符串
|
||||||
http_request_method = "POST"
|
http_request_method = "POST"
|
||||||
canonical_uri = "/"
|
canonical_uri = "/"
|
||||||
canonical_querystring = ""
|
canonical_querystring = ""
|
||||||
|
|
||||||
# 请求头必须包含host和content-type,且按字典序排列
|
# 请求头必须包含host和content-type,且按字典序排列
|
||||||
canonical_headers = (
|
canonical_headers = (
|
||||||
f"content-type:application/json\n"
|
f"content-type:application/json\n" f"host:tts.tencentcloudapi.com\n"
|
||||||
f"host:tts.tencentcloudapi.com\n"
|
|
||||||
)
|
)
|
||||||
signed_headers = "content-type;host"
|
signed_headers = "content-type;host"
|
||||||
|
|
||||||
# 请求体哈希值
|
# 请求体哈希值
|
||||||
payload = json.dumps(request_body)
|
payload = json.dumps(request_body)
|
||||||
payload_hash = hashlib.sha256(payload.encode('utf-8')).hexdigest()
|
payload_hash = hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
# 构建规范请求字符串
|
# 构建规范请求字符串
|
||||||
canonical_request = (
|
canonical_request = (
|
||||||
f"{http_request_method}\n"
|
f"{http_request_method}\n"
|
||||||
@@ -63,10 +67,12 @@ class TTSProvider(TTSProviderBase):
|
|||||||
f"{signed_headers}\n"
|
f"{signed_headers}\n"
|
||||||
f"{payload_hash}"
|
f"{payload_hash}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 计算规范请求的哈希值
|
# 计算规范请求的哈希值
|
||||||
hashed_canonical_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
|
hashed_canonical_request = hashlib.sha256(
|
||||||
|
canonical_request.encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
# 构建待签名字符串
|
# 构建待签名字符串
|
||||||
string_to_sign = (
|
string_to_sign = (
|
||||||
f"{algorithm}\n"
|
f"{algorithm}\n"
|
||||||
@@ -74,19 +80,19 @@ class TTSProvider(TTSProviderBase):
|
|||||||
f"{credential_scope}\n"
|
f"{credential_scope}\n"
|
||||||
f"{hashed_canonical_request}"
|
f"{hashed_canonical_request}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 计算签名密钥
|
# 计算签名密钥
|
||||||
secret_date = self._hmac_sha256(f"TC3{self.secret_key}".encode('utf-8'), utc_date)
|
secret_date = self._hmac_sha256(
|
||||||
|
f"TC3{self.secret_key}".encode("utf-8"), utc_date
|
||||||
|
)
|
||||||
secret_service = self._hmac_sha256(secret_date, service)
|
secret_service = self._hmac_sha256(secret_date, service)
|
||||||
secret_signing = self._hmac_sha256(secret_service, "tc3_request")
|
secret_signing = self._hmac_sha256(secret_service, "tc3_request")
|
||||||
|
|
||||||
# 计算签名
|
# 计算签名
|
||||||
signature = hmac.new(
|
signature = hmac.new(
|
||||||
secret_signing,
|
secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256
|
||||||
string_to_sign.encode('utf-8'),
|
|
||||||
hashlib.sha256
|
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
|
||||||
# 构建授权头
|
# 构建授权头
|
||||||
authorization = (
|
authorization = (
|
||||||
f"{algorithm} "
|
f"{algorithm} "
|
||||||
@@ -94,7 +100,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
f"SignedHeaders={signed_headers}, "
|
f"SignedHeaders={signed_headers}, "
|
||||||
f"Signature={signature}"
|
f"Signature={signature}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 构建请求头
|
# 构建请求头
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -104,19 +110,22 @@ class TTSProvider(TTSProviderBase):
|
|||||||
"X-TC-Timestamp": str(timestamp),
|
"X-TC-Timestamp": str(timestamp),
|
||||||
"X-TC-Version": "2019-08-23",
|
"X-TC-Version": "2019-08-23",
|
||||||
"X-TC-Region": self.region,
|
"X-TC-Region": self.region,
|
||||||
"X-TC-Language": "zh-CN"
|
"X-TC-Language": "zh-CN",
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
def _hmac_sha256(self, key, msg):
|
def _hmac_sha256(self, key, msg):
|
||||||
"""HMAC-SHA256加密"""
|
"""HMAC-SHA256加密"""
|
||||||
if isinstance(msg, str):
|
if isinstance(msg, str):
|
||||||
msg = msg.encode('utf-8')
|
msg = msg.encode("utf-8")
|
||||||
return hmac.new(key, msg, hashlib.sha256).digest()
|
return hmac.new(key, msg, hashlib.sha256).digest()
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
# 构建请求体
|
# 构建请求体
|
||||||
@@ -129,19 +138,23 @@ class TTSProvider(TTSProviderBase):
|
|||||||
try:
|
try:
|
||||||
# 获取请求头(每次请求都重新生成,以确保时间戳和签名是最新的)
|
# 获取请求头(每次请求都重新生成,以确保时间戳和签名是最新的)
|
||||||
headers = self._get_auth_headers(request_json)
|
headers = self._get_auth_headers(request_json)
|
||||||
|
|
||||||
# 发送请求
|
# 发送请求
|
||||||
resp = requests.post(self.api_url, json.dumps(request_json), headers=headers)
|
resp = requests.post(
|
||||||
|
self.api_url, json.dumps(request_json), headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
# 检查响应
|
# 检查响应
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
response_data = resp.json()
|
response_data = resp.json()
|
||||||
|
|
||||||
# 检查是否成功
|
# 检查是否成功
|
||||||
if response_data.get("Response", {}).get("Error") is not None:
|
if response_data.get("Response", {}).get("Error") is not None:
|
||||||
error_info = response_data["Response"]["Error"]
|
error_info = response_data["Response"]["Error"]
|
||||||
raise Exception(f"API返回错误: {error_info['Code']}: {error_info['Message']}")
|
raise Exception(
|
||||||
|
f"API返回错误: {error_info['Code']}: {error_info['Message']}"
|
||||||
|
)
|
||||||
|
|
||||||
# 提取音频数据
|
# 提取音频数据
|
||||||
audio_data = response_data["Response"].get("Audio")
|
audio_data = response_data["Response"].get("Audio")
|
||||||
if audio_data:
|
if audio_data:
|
||||||
@@ -151,6 +164,8 @@ class TTSProvider(TTSProviderBase):
|
|||||||
else:
|
else:
|
||||||
raise Exception(f"{__name__}: 没有返回音频数据: {response_data}")
|
raise Exception(f"{__name__}: 没有返回音频数据: {response_data}")
|
||||||
else:
|
else:
|
||||||
raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}")
|
raise Exception(
|
||||||
|
f"{__name__} status_code: {resp.status_code} response: {resp.content}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"{__name__} error: {e}")
|
raise Exception(f"{__name__} error: {e}")
|
||||||
|
|||||||
@@ -10,48 +10,62 @@ from core.providers.tts.base import TTSProviderBase
|
|||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
self.url = config.get("url", "https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=")
|
self.url = config.get(
|
||||||
self.voice_id = config.get("voice_id", 1695)
|
"url",
|
||||||
|
"https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=",
|
||||||
|
)
|
||||||
|
if config.get("private_voice"):
|
||||||
|
self.voice_id = int(config.get("private_voice"))
|
||||||
|
else:
|
||||||
|
self.voice_id = int(config.get("voice_id", 1695))
|
||||||
self.token = config.get("token")
|
self.token = config.get("token")
|
||||||
self.to_lang = config.get("to_lang")
|
self.to_lang = config.get("to_lang")
|
||||||
self.volume_change_dB = config.get("volume_change_dB", 0)
|
self.volume_change_dB = int(config.get("volume_change_dB", 0))
|
||||||
self.speed_factor = config.get("speed_factor", 1)
|
self.speed_factor = int(config.get("speed_factor", 1))
|
||||||
self.stream = config.get("stream", False)
|
self.stream = bool(config.get("stream", False))
|
||||||
self.output_file = config.get("output_dir")
|
self.output_file = config.get("output_dir")
|
||||||
self.pitch_factor = config.get("pitch_factor", 0)
|
self.pitch_factor = int(config.get("pitch_factor", 0))
|
||||||
self.format = config.get("format", "mp3")
|
self.format = config.get("format", "mp3")
|
||||||
self.emotion = config.get("emotion", 1)
|
self.emotion = int(config.get("emotion", 1))
|
||||||
self.header = {
|
self.header = {"Content-Type": "application/json"}
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
def generate_filename(self, extension=".mp3"):
|
def generate_filename(self, extension=".mp3"):
|
||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
url = f'{self.url}{self.token}'
|
url = f"{self.url}{self.token}"
|
||||||
result = "firefly"
|
result = "firefly"
|
||||||
payload = json.dumps({
|
payload = json.dumps(
|
||||||
"to_lang": self.to_lang,
|
{
|
||||||
"text": text,
|
"to_lang": self.to_lang,
|
||||||
"emotion": self.emotion,
|
"text": text,
|
||||||
"format": self.format,
|
"emotion": self.emotion,
|
||||||
"volume_change_dB": self.volume_change_dB,
|
"format": self.format,
|
||||||
"voice_id": self.voice_id,
|
"volume_change_dB": self.volume_change_dB,
|
||||||
"pitch_factor": self.pitch_factor,
|
"voice_id": self.voice_id,
|
||||||
"speed_factor": self.speed_factor,
|
"pitch_factor": self.pitch_factor,
|
||||||
"token": self.token
|
"speed_factor": self.speed_factor,
|
||||||
})
|
"token": self.token,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
resp = requests.request("POST", url, data=payload)
|
resp = requests.request("POST", url, data=payload)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
return None
|
return None
|
||||||
resp_json = resp.json()
|
resp_json = resp.json()
|
||||||
try:
|
try:
|
||||||
result = resp_json['url'] + ':' + str(
|
result = (
|
||||||
resp_json[
|
resp_json["url"]
|
||||||
'port']) + '/flashsummary/retrieveFileData?stream=True&token=' + self.token + '&voice_audio_path=' + \
|
+ ":"
|
||||||
resp_json['voice_path']
|
+ str(resp_json["port"])
|
||||||
|
+ "/flashsummary/retrieveFileData?stream=True&token="
|
||||||
|
+ self.token
|
||||||
|
+ "&voice_audio_path="
|
||||||
|
+ resp_json["voice_path"]
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("error:", e)
|
print("error:", e)
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class VADProvider(VADProviderBase):
|
|||||||
(get_speech_timestamps, _, _, _, _) = self.utils
|
(get_speech_timestamps, _, _, _, _) = self.utils
|
||||||
|
|
||||||
self.decoder = opuslib_next.Decoder(16000, 1)
|
self.decoder = opuslib_next.Decoder(16000, 1)
|
||||||
self.vad_threshold = config.get("threshold")
|
self.vad_threshold = float(config.get("threshold", 0.5))
|
||||||
self.silence_threshold_ms = config.get("min_silence_duration_ms")
|
self.silence_threshold_ms = int(config.get("min_silence_duration_ms", 1000))
|
||||||
|
|
||||||
def is_vad(self, conn, opus_packet):
|
def is_vad(self, conn, opus_packet):
|
||||||
try:
|
try:
|
||||||
@@ -42,7 +42,8 @@ class VADProvider(VADProviderBase):
|
|||||||
audio_tensor = torch.from_numpy(audio_float32)
|
audio_tensor = torch.from_numpy(audio_float32)
|
||||||
|
|
||||||
# 检测语音活动
|
# 检测语音活动
|
||||||
speech_prob = self.model(audio_tensor, 16000).item()
|
with torch.no_grad():
|
||||||
|
speech_prob = self.model(audio_tensor, 16000).item()
|
||||||
client_have_voice = speech_prob >= self.vad_threshold
|
client_have_voice = speech_prob >= self.vad_threshold
|
||||||
|
|
||||||
# 如果之前有声音,但本次没有声音,且与上次有声音的时间查已经超过了静默阈值,则认为已经说完一句话
|
# 如果之前有声音,但本次没有声音,且与上次有声音的时间查已经超过了静默阈值,则认为已经说完一句话
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ def initialize_modules(
|
|||||||
modules["tts"] = tts.create_instance(
|
modules["tts"] = tts.create_instance(
|
||||||
tts_type,
|
tts_type,
|
||||||
config["TTS"][config["selected_module"]["TTS"]],
|
config["TTS"][config["selected_module"]["TTS"]],
|
||||||
config["delete_audio"],
|
bool(config.get("delete_audio", True)),
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: tts成功")
|
logger.bind(tag=TAG).info(f"初始化组件: tts成功")
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@ def initialize_modules(
|
|||||||
modules["asr"] = asr.create_instance(
|
modules["asr"] = asr.create_instance(
|
||||||
asr_type,
|
asr_type,
|
||||||
config["ASR"][config["selected_module"]["ASR"]],
|
config["ASR"][config["selected_module"]["ASR"]],
|
||||||
config["delete_audio"],
|
bool(config.get("delete_audio", True)),
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: asr成功")
|
logger.bind(tag=TAG).info(f"初始化组件: asr成功")
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class WebSocketServer:
|
|||||||
async def start(self):
|
async def start(self):
|
||||||
server_config = self.config["server"]
|
server_config = self.config["server"]
|
||||||
host = server_config["ip"]
|
host = server_config["ip"]
|
||||||
port = server_config["port"]
|
port = int(server_config.get("port", 8000))
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
"Server is running at ws://{}:{}/xiaozhi/v1/", get_local_ip(), port
|
"Server is running at ws://{}:{}/xiaozhi/v1/", get_local_ip(), port
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
# Docker安装全模块
|
# Docker安装全模块
|
||||||
|
|
||||||
# 1、安装mysql
|
|
||||||
# |- 如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
|
|
||||||
# |- 如果还没有MySQL,你可以通过docker安装mysql,执行以下一句话
|
|
||||||
# |- docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
|
||||||
# |- 记得修改下方SPRING_DATASOURCE_DRUID_URL的IP,ip不能写127.0.0.1或localhost,否则容器无法访问,要写你电脑局域网ip
|
|
||||||
|
|
||||||
# 2、安装redis
|
|
||||||
# |- 如果本机已经安装了Redis,看一下你安装的redis端口、密码,然后修改下方redis的地址和端口
|
|
||||||
# |- 如果还没有Redis,你可以通过docker安装redis,执行以下一句话
|
|
||||||
# |- docker run --name xiaozhi-esp32-server-redis -d -p 6379:6379 redis
|
|
||||||
# |- 记得修改SPRING_DATA_REDIS_HOST的IP,ip不能写127.0.0.1或localhost,否则容器无法访问,要写你电脑局域网ip
|
|
||||||
|
|
||||||
|
|
||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
# Server模块
|
# Server模块
|
||||||
xiaozhi-esp32-server:
|
xiaozhi-esp32-server:
|
||||||
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
container_name: xiaozhi-esp32-server
|
container_name: xiaozhi-esp32-server
|
||||||
|
depends_on:
|
||||||
|
- xiaozhi-esp32-server-db
|
||||||
|
- xiaozhi-esp32-server-redis
|
||||||
restart: always
|
restart: always
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
ports:
|
ports:
|
||||||
# ws服务端
|
# ws服务端
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
@@ -38,14 +30,55 @@ services:
|
|||||||
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
container_name: xiaozhi-esp32-server-web
|
container_name: xiaozhi-esp32-server-web
|
||||||
restart: always
|
restart: always
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
depends_on:
|
||||||
|
xiaozhi-esp32-server-db:
|
||||||
|
condition: service_healthy
|
||||||
|
xiaozhi-esp32-server-redis:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
# 智控台
|
# 智控台
|
||||||
- "8002:8002"
|
- "8002:8002"
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
##记得改mysql和redis IP 密码
|
##记得改mysql和redis IP 密码
|
||||||
- SPRING_DATASOURCE_DRUID_URL=jdbc:mysql://192.168.1.25:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
|
- SPRING_DATASOURCE_DRUID_URL=jdbc:mysql://xiaozhi-esp32-server-db:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&connectTimeout=30000&socketTimeout=30000&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
|
||||||
- SPRING_DATASOURCE_DRUID_USERNAME=root
|
- SPRING_DATASOURCE_DRUID_USERNAME=root
|
||||||
- SPRING_DATASOURCE_DRUID_PASSWORD=123456
|
- SPRING_DATASOURCE_DRUID_PASSWORD=123456
|
||||||
- SPRING_DATA_REDIS_HOST=192.168.1.25
|
- SPRING_DATA_REDIS_HOST=xiaozhi-esp32-server-redis
|
||||||
- SPRING_DATA_REDIS_PORT=6379
|
- SPRING_DATA_REDIS_PORT=6379
|
||||||
|
xiaozhi-esp32-server-db:
|
||||||
|
image: mysql:latest
|
||||||
|
container_name: xiaozhi-esp32-server-db
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
|
||||||
|
timeout: 45s
|
||||||
|
interval: 10s
|
||||||
|
retries: 10
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
expose:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- ./mysql/data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
- MYSQL_ROOT_PASSWORD=123456
|
||||||
|
- MYSQL_DATABASE=xiaozhi_esp32_server
|
||||||
|
- MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
|
||||||
|
xiaozhi-esp32-server-redis:
|
||||||
|
image: redis
|
||||||
|
expose:
|
||||||
|
- 6379
|
||||||
|
container_name: xiaozhi-esp32-server-redis
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
|||||||
@@ -1,34 +1,43 @@
|
|||||||
from plugins_func.register import register_function,ToolType, ActionResponse, Action
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
handle_exit_intent_function_desc = {
|
handle_exit_intent_function_desc = {
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "handle_exit_intent",
|
"name": "handle_exit_intent",
|
||||||
"description": "当用户想结束对话或需要退出系统时调用",
|
"description": "当用户想结束对话或需要退出系统时调用",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"say_goodbye": {
|
"say_goodbye": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "和用户友好结束对话的告别语"
|
"description": "和用户友好结束对话的告别语",
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["say_goodbye"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"required": ["say_goodbye"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@register_function('handle_exit_intent', handle_exit_intent_function_desc, ToolType.SYSTEM_CTL)
|
|
||||||
def handle_exit_intent(conn, say_goodbye: str):
|
@register_function(
|
||||||
|
"handle_exit_intent", handle_exit_intent_function_desc, ToolType.SYSTEM_CTL
|
||||||
|
)
|
||||||
|
def handle_exit_intent(conn, say_goodbye: str | None = None):
|
||||||
# 处理退出意图
|
# 处理退出意图
|
||||||
try:
|
try:
|
||||||
|
if say_goodbye is None:
|
||||||
|
say_goodbye = "再见,祝您生活愉快!"
|
||||||
conn.close_after_chat = True
|
conn.close_after_chat = True
|
||||||
logger.bind(tag=TAG).info(f"退出意图已处理:{say_goodbye}")
|
logger.bind(tag=TAG).info(f"退出意图已处理:{say_goodbye}")
|
||||||
return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=say_goodbye)
|
return ActionResponse(
|
||||||
|
action=Action.RESPONSE, result="退出意图已处理", response=say_goodbye
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"处理退出意图错误: {e}")
|
logger.bind(tag=TAG).error(f"处理退出意图错误: {e}")
|
||||||
return ActionResponse(action=Action.NONE, result="退出意图处理失败", response="")
|
return ActionResponse(
|
||||||
|
action=Action.NONE, result="退出意图处理失败", response=""
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ HASS_CACHE = {}
|
|||||||
|
|
||||||
def append_devices_to_prompt(conn):
|
def append_devices_to_prompt(conn):
|
||||||
if conn.use_function_call_mode:
|
if conn.use_function_call_mode:
|
||||||
funcs = conn.config["Intent"]["function_call"].get("functions", [])
|
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
|
||||||
|
"functions", []
|
||||||
|
)
|
||||||
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
||||||
prompt = "下面是我家智能设备,可以通过homeassistant控制\n"
|
prompt = "下面是我家智能设备,可以通过homeassistant控制\n"
|
||||||
devices = conn.config["plugins"]["home_assistant"].get("devices", [])
|
devices = conn.config["plugins"]["home_assistant"].get("devices", [])
|
||||||
@@ -26,7 +28,9 @@ def initialize_hass_handler(conn):
|
|||||||
global HASS_CACHE
|
global HASS_CACHE
|
||||||
if HASS_CACHE == {}:
|
if HASS_CACHE == {}:
|
||||||
if conn.use_function_call_mode:
|
if conn.use_function_call_mode:
|
||||||
funcs = conn.config["Intent"]["function_call"].get("functions", [])
|
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
|
||||||
|
"functions", []
|
||||||
|
)
|
||||||
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
||||||
HASS_CACHE["base_url"] = conn.config["plugins"]["home_assistant"].get(
|
HASS_CACHE["base_url"] = conn.config["plugins"]["home_assistant"].get(
|
||||||
"base_url"
|
"base_url"
|
||||||
|
|||||||
Reference in New Issue
Block a user