diff --git a/README.md b/README.md
index 6feb1fc7..e8958e61 100644
--- a/README.md
+++ b/README.md
@@ -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)
-
-`全模块`适合想体验完整功能,过程复杂。[请参考-全模块安装架构图](./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环境较为复杂。
+> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
+```
+智控台地址: 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)。
---
-
-## 产品生态 👬
-小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
-
-- [小智安卓客户端](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 实现数据交互。
-- **对话交互**
- 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
-- **意图识别**
- 支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
-- **多语言识别**
- 支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
-- **LLM 模块**
- 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
-- **TTS 模块**
- 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
-- **记忆功能**
- 支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
-- **IOT功能**
- 支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
-- **智控台**
- 提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理。
-
+| 功能模块 | 描述 |
+|---------|------|
+| 通信协议 | 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互 |
+| 对话交互 | 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 |
+| 意图识别 | 支持使用LLM意图识别、function call函数调用,减少硬编码意图判断 |
+| 多语言识别 | 支持国语、粤语、英语、日语、韩语(默认使用 FunASR) |
+| LLM 模块 | 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口 |
+| TTS 模块 | 支持 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),欢迎加入!
---
+## 产品生态 👬
+小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
+
+| 项目名称 | 项目地址 | 项目描述 |
+|:---------------------|:--------|:--------|
+| 小智安卓客户端 | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | 一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。
现在是flutter版本,打通IOS、Android端。 |
+| 小智电脑客户端 | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | 该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,
依然能够体过代码体验小智 AI 的功能。 |
+| 小智Java服务端 | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | 小智开源后端服务 Java 版本是一个基于 Java 的开源项目。
它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。 |
+
+---
+
## 本项目支持的平台/组件列表 📋
### LLM 语言模型
@@ -298,10 +267,17 @@ server:
## 鸣谢 🙏
-- 本项目受 [百聆语音对话机器人](https://github.com/wwbin2017/bailing) 启发,并在其基础上实现。
-- 感谢 [十方融海](https://www.tenclass.com/) 对小智通讯协议提供的详尽文档支持。
+| Logo | 项目/公司 | 说明 |
+|:---:|:---:|:---|
+|
| [百聆语音对话机器人](https://github.com/wwbin2017/bailing) | 本项目受[百聆语音对话机器人](https://github.com/wwbin2017/bailing)启发,并在其基础上实现 |
+|
| [十方融海](https://www.tenclass.com/) | 感谢[十方融海](https://www.tenclass.com/)为小智生态制定了标准的通讯协议、多设备兼容性方案及高并发场景实践示范;为本项目提供了全链路技术文档支持 |
+|
| [玄凤科技](https://github.com/Eric0308) | 感谢[玄凤科技](https://github.com/Eric0308)贡献函数调用框架、MCP通信协议及插件化调用机制的实现代码,通过标准化的指令调度体系与动态扩展能力,显著提升了前端设备(IoT)的交互效率和功能延展性 |
+|
| [汇远设计](http://ui.kwd988.net/) | 感谢[汇远设计](http://ui.kwd988.net/)为本项目提供专业视觉解决方案,用其服务超千家企业的设计实战经验,赋能本项目产品用户体验 |
+|
| [西安勤人信息科技](https://www.029app.com/) | 感谢[西安勤人信息科技](https://www.029app.com/)深化本项目视觉体系,确保整体设计风格在多场景应用中的一致性和扩展性 |
+
+
diff --git a/docs/images/logo_bailing.png b/docs/images/logo_bailing.png
new file mode 100644
index 00000000..22ab6e11
Binary files /dev/null and b/docs/images/logo_bailing.png differ
diff --git a/docs/images/logo_huiyuan.png b/docs/images/logo_huiyuan.png
new file mode 100644
index 00000000..6e441913
Binary files /dev/null and b/docs/images/logo_huiyuan.png differ
diff --git a/docs/images/logo_qinren.png b/docs/images/logo_qinren.png
new file mode 100644
index 00000000..a01af841
Binary files /dev/null and b/docs/images/logo_qinren.png differ
diff --git a/docs/images/logo_tenclass.png b/docs/images/logo_tenclass.png
new file mode 100644
index 00000000..86a6dc4a
Binary files /dev/null and b/docs/images/logo_tenclass.png differ
diff --git a/docs/images/logo_xuanfeng.png b/docs/images/logo_xuanfeng.png
new file mode 100644
index 00000000..552ee78a
Binary files /dev/null and b/docs/images/logo_xuanfeng.png differ
diff --git a/main/manager-api/src/main/resources/db/changelog/202504131543.sql b/main/manager-api/src/main/resources/db/changelog/202504131543.sql
new file mode 100644
index 00000000..55c69912
--- /dev/null
+++ b/main/manager-api/src/main/resources/db/changelog/202504131543.sql
@@ -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);
\ No newline at end of file
diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
index 41f65488..37797d3f 100755
--- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
+++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -52,9 +52,9 @@ databaseChangeLog:
encoding: utf8
path: classpath:db/changelog/202504112058.sql
- changeSet:
- id: 202504131542
+ id: 202504131543
author: John
changes:
- sqlFile:
encoding: utf8
- path: classpath:db/changelog/202504131542.sql
\ No newline at end of file
+ path: classpath:db/changelog/202504131543.sql
diff --git a/main/xiaozhi-server/core/providers/tts/cozecn.py b/main/xiaozhi-server/core/providers/tts/cozecn.py
index 74de9ab3..1f9244f6 100644
--- a/main/xiaozhi-server/core/providers/tts/cozecn.py
+++ b/main/xiaozhi-server/core/providers/tts/cozecn.py
@@ -12,14 +12,20 @@ class TTSProvider(TTSProviderBase):
super().__init__(config, delete_audio_file)
self.model = config.get("model")
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.host = "api.coze.cn"
self.api_url = f"https://{self.host}/v1/audio/speech"
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):
request_json = {
@@ -30,9 +36,11 @@ class TTSProvider(TTSProviderBase):
}
headers = {
"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
file_to_save = open(output_file, "wb")
file_to_save.write(data)
diff --git a/main/xiaozhi-server/core/providers/tts/doubao.py b/main/xiaozhi-server/core/providers/tts/doubao.py
index b40121e3..0051d53e 100644
--- a/main/xiaozhi-server/core/providers/tts/doubao.py
+++ b/main/xiaozhi-server/core/providers/tts/doubao.py
@@ -18,7 +18,12 @@ class TTSProvider(TTSProviderBase):
self.appid = config.get("appid")
self.access_token = config.get("access_token")
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.authorization = config.get("authorization")
self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
diff --git a/main/xiaozhi-server/core/providers/tts/edge.py b/main/xiaozhi-server/core/providers/tts/edge.py
index 289961cb..b86e4087 100644
--- a/main/xiaozhi-server/core/providers/tts/edge.py
+++ b/main/xiaozhi-server/core/providers/tts/edge.py
@@ -8,20 +8,26 @@ from core.providers.tts.base import TTSProviderBase
class TTSProvider(TTSProviderBase):
def __init__(self, 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"):
- 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):
communicate = edge_tts.Communicate(text, voice=self.voice)
# 确保目录存在并创建空文件
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
-
+
# 流式写入音频数据
- with open(output_file, 'ab') as f: # 改为追加模式避免覆盖
+ with open(output_file, "ab") as f: # 改为追加模式避免覆盖
async for chunk in communicate.stream():
if chunk["type"] == "audio": # 只处理音频数据块
f.write(chunk["data"])
diff --git a/main/xiaozhi-server/core/providers/tts/minimax.py b/main/xiaozhi-server/core/providers/tts/minimax.py
index 1fe7be50..2b63ea7e 100644
--- a/main/xiaozhi-server/core/providers/tts/minimax.py
+++ b/main/xiaozhi-server/core/providers/tts/minimax.py
@@ -12,28 +12,33 @@ class TTSProvider(TTSProviderBase):
self.group_id = config.get("group_id")
self.api_key = config.get("api_key")
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 = {
"voice_id": "female-shaonv",
"speed": 1,
"vol": 1,
"pitch": 0,
- "emotion": "happy"
- }
- default_pronunciation_dict = {
- "tone": [
- "处理/(chu3)(li3)", "危险/dangerous"
- ]
+ "emotion": "happy",
}
+ default_pronunciation_dict = {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}
defult_audio_setting = {
"sample_rate": 32000,
"bitrate": 128000,
"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.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.header = {
"Content-Type": "application/json",
- "Authorization": f"Bearer {self.api_key}"
+ "Authorization": f"Bearer {self.api_key}",
}
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):
request_json = {
@@ -65,13 +73,17 @@ class TTSProvider(TTSProviderBase):
request_json["voice_setting"]["voice_id"] = ""
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
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.write(bytes.fromhex(data))
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:
- raise Exception(f"{__name__} error: {e}")
\ No newline at end of file
+ raise Exception(f"{__name__} error: {e}")
diff --git a/main/xiaozhi-server/core/providers/tts/openai.py b/main/xiaozhi-server/core/providers/tts/openai.py
index 8d967df6..18daf29b 100644
--- a/main/xiaozhi-server/core/providers/tts/openai.py
+++ b/main/xiaozhi-server/core/providers/tts/openai.py
@@ -16,7 +16,10 @@ class TTSProvider(TTSProviderBase):
self.api_key = config.get("api_key")
self.api_url = config.get("api_url", "https://api.openai.com/v1/audio/speech")
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.speed = config.get("speed", 1.0)
self.output_file = config.get("output_dir", "tmp/")
diff --git a/main/xiaozhi-server/core/providers/tts/siliconflow.py b/main/xiaozhi-server/core/providers/tts/siliconflow.py
index 4c0cc356..76d131bb 100644
--- a/main/xiaozhi-server/core/providers/tts/siliconflow.py
+++ b/main/xiaozhi-server/core/providers/tts/siliconflow.py
@@ -10,7 +10,10 @@ class TTSProvider(TTSProviderBase):
super().__init__(config, delete_audio_file)
self.model = config.get("model")
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.sample_rate = config.get("sample_rate")
self.speed = config.get("speed")
@@ -20,7 +23,10 @@ class TTSProvider(TTSProviderBase):
self.api_url = f"https://{self.host}/v1/audio/speech"
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):
request_json = {
@@ -31,9 +37,11 @@ class TTSProvider(TTSProviderBase):
}
headers = {
"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
file_to_save = open(output_file, "wb")
file_to_save.write(data)
diff --git a/main/xiaozhi-server/core/providers/tts/tencent.py b/main/xiaozhi-server/core/providers/tts/tencent.py
index ed4db622..c05261fd 100644
--- a/main/xiaozhi-server/core/providers/tts/tencent.py
+++ b/main/xiaozhi-server/core/providers/tts/tencent.py
@@ -16,7 +16,10 @@ class TTSProvider(TTSProviderBase):
self.appid = config.get("appid")
self.secret_id = config.get("secret_id")
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 = config.get("voice")
self.api_url = "https://tts.tencentcloudapi.com" # 正确的API端点
self.region = config.get("region")
self.output_file = config.get("output_dir")
@@ -25,35 +28,36 @@ class TTSProvider(TTSProviderBase):
"""生成鉴权请求头"""
# 获取当前UTC时间戳
timestamp = int(time.time())
-
+
# 使用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"
service = "tts"
-
+
# 拼接凭证范围
credential_scope = f"{utc_date}/{service}/tc3_request"
-
+
# 使用TC3-HMAC-SHA256签名方法
algorithm = "TC3-HMAC-SHA256"
-
+
# 构建规范请求字符串
http_request_method = "POST"
canonical_uri = "/"
canonical_querystring = ""
-
+
# 请求头必须包含host和content-type,且按字典序排列
canonical_headers = (
- f"content-type:application/json\n"
- f"host:tts.tencentcloudapi.com\n"
+ f"content-type:application/json\n" f"host:tts.tencentcloudapi.com\n"
)
signed_headers = "content-type;host"
-
+
# 请求体哈希值
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 = (
f"{http_request_method}\n"
@@ -63,10 +67,12 @@ class TTSProvider(TTSProviderBase):
f"{signed_headers}\n"
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 = (
f"{algorithm}\n"
@@ -74,19 +80,19 @@ class TTSProvider(TTSProviderBase):
f"{credential_scope}\n"
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_signing = self._hmac_sha256(secret_service, "tc3_request")
-
+
# 计算签名
signature = hmac.new(
- secret_signing,
- string_to_sign.encode('utf-8'),
- hashlib.sha256
+ secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256
).hexdigest()
-
+
# 构建授权头
authorization = (
f"{algorithm} "
@@ -94,7 +100,7 @@ class TTSProvider(TTSProviderBase):
f"SignedHeaders={signed_headers}, "
f"Signature={signature}"
)
-
+
# 构建请求头
headers = {
"Content-Type": "application/json",
@@ -104,19 +110,22 @@ class TTSProvider(TTSProviderBase):
"X-TC-Timestamp": str(timestamp),
"X-TC-Version": "2019-08-23",
"X-TC-Region": self.region,
- "X-TC-Language": "zh-CN"
+ "X-TC-Language": "zh-CN",
}
-
+
return headers
-
+
def _hmac_sha256(self, key, msg):
"""HMAC-SHA256加密"""
if isinstance(msg, str):
- msg = msg.encode('utf-8')
+ msg = msg.encode("utf-8")
return hmac.new(key, msg, hashlib.sha256).digest()
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):
# 构建请求体
@@ -129,19 +138,23 @@ class TTSProvider(TTSProviderBase):
try:
# 获取请求头(每次请求都重新生成,以确保时间戳和签名是最新的)
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:
response_data = resp.json()
-
+
# 检查是否成功
if response_data.get("Response", {}).get("Error") is not None:
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")
if audio_data:
@@ -151,6 +164,8 @@ class TTSProvider(TTSProviderBase):
else:
raise Exception(f"{__name__}: 没有返回音频数据: {response_data}")
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:
- raise Exception(f"{__name__} error: {e}")
\ No newline at end of file
+ raise Exception(f"{__name__} error: {e}")
diff --git a/main/xiaozhi-server/core/providers/tts/ttson.py b/main/xiaozhi-server/core/providers/tts/ttson.py
index e9fee109..ba0a1222 100644
--- a/main/xiaozhi-server/core/providers/tts/ttson.py
+++ b/main/xiaozhi-server/core/providers/tts/ttson.py
@@ -10,8 +10,14 @@ from core.providers.tts.base import TTSProviderBase
class TTSProvider(TTSProviderBase):
def __init__(self, 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.voice_id = config.get("voice_id", 1695)
+ self.url = config.get(
+ "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.to_lang = config.get("to_lang")
self.volume_change_dB = config.get("volume_change_dB", 0)
@@ -21,37 +27,45 @@ class TTSProvider(TTSProviderBase):
self.pitch_factor = config.get("pitch_factor", 0)
self.format = config.get("format", "mp3")
self.emotion = config.get("emotion", 1)
- self.header = {
- "Content-Type": "application/json"
- }
+ self.header = {"Content-Type": "application/json"}
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):
- url = f'{self.url}{self.token}'
+ url = f"{self.url}{self.token}"
result = "firefly"
- payload = json.dumps({
- "to_lang": self.to_lang,
- "text": text,
- "emotion": self.emotion,
- "format": self.format,
- "volume_change_dB": self.volume_change_dB,
- "voice_id": self.voice_id,
- "pitch_factor": self.pitch_factor,
- "speed_factor": self.speed_factor,
- "token": self.token
- })
+ payload = json.dumps(
+ {
+ "to_lang": self.to_lang,
+ "text": text,
+ "emotion": self.emotion,
+ "format": self.format,
+ "volume_change_dB": self.volume_change_dB,
+ "voice_id": self.voice_id,
+ "pitch_factor": self.pitch_factor,
+ "speed_factor": self.speed_factor,
+ "token": self.token,
+ }
+ )
resp = requests.request("POST", url, data=payload)
if resp.status_code != 200:
return None
resp_json = resp.json()
try:
- result = resp_json['url'] + ':' + str(
- resp_json[
- 'port']) + '/flashsummary/retrieveFileData?stream=True&token=' + self.token + '&voice_audio_path=' + \
- resp_json['voice_path']
+ result = (
+ resp_json["url"]
+ + ":"
+ + str(resp_json["port"])
+ + "/flashsummary/retrieveFileData?stream=True&token="
+ + self.token
+ + "&voice_audio_path="
+ + resp_json["voice_path"]
+ )
except Exception as e:
print("error:", e)