633 | Copyright (C) 2022-2099 AstrBot Plugin Authors
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 
5 |
6 | # astrbot_plugin_GPT_SoVITS
7 |
8 | _✨ [astrbot](https://github.com/AstrBotDevs/AstrBot) GPT_SoVITS对接插件 ✨_
9 |
10 | [](https://opensource.org/licenses/MIT)
11 | [](https://www.python.org/)
12 | [](https://github.com/Soulter/AstrBot)
13 | [](https://github.com/Zhalslar)
14 |
15 |
16 |
17 | ## 🐔 介绍
18 |
19 | **astrbot_plugin_GPT_SoVITS** 是一个 astrbot 插件,用于对接 [GPT-SoVITS](https://github.com/RVC-Boss/GPT-SoVITS),该插件实现了 TTS(文本到语音)的功能。
20 |
21 | ## 📦 安装
22 |
23 | ### 第一步,本地部署 GPT_SoVITS、
24 |
25 | - 安装步骤请看[GPT_SoVITS仓库](https://github.com/RVC-Boss/GPT-SoVITS)(安装包6G+,装完后10G+)
26 | - 配合[GPT_SoVITS指南](https://www.yuque.com/baicaigongchang1145haoyuangong/ib3g1e)来看
27 |
28 | ### 第二步,安装本插件
29 |
30 | - 可以直接在astrbot的插件市场搜索astrbot_plugin_GPT_SoVITS,点击安装,耐心等待安装完成即可
31 |
32 | - 或者可以直接克隆源码到插件文件夹:
33 |
34 | ```bash
35 | # 克隆仓库到插件目录
36 | cd /AstrBot/data/plugins
37 | git clone https://github.com/Zhalslar/astrbot_plugin_GPT_SoVITS
38 |
39 | # 控制台重启AstrBot
40 | ```
41 |
42 | ## ⚙️ 配置
43 |
44 | 请在astrbot面板配置,插件管理 -> astrbot_plugin_memelite -> 操作 -> 插件配置
45 | 
46 |
47 | - GPT-SoVITS API 的 URL(base_url):必填!GPT_SoVITS官方整合包默认为 第三方整合包可能不同
48 | - GPT模型文件路径(gpt_weights_path):即“.ckpt”后缀的文件,请使用绝对路径!路径两端不要带双引号!!不填则默认用GPT_SoVITS内置的GPT模型
49 | - SoVITS模型文件路径(sovits_weights_path):即“.pth”后缀的文件,请使用绝对路径,路径两端不要带双引号!!不填则默认用GPT_SoVITS内置的SoVITS模型
50 | - 默认使用的情绪(default_emotion):内置情绪有:温柔地说、开心地说、惊讶地说、生气地说,每种情绪都可以自定义,如下图
51 | 
52 |
53 | ## 🐔 使用说明
54 |
55 | ### 第一步,启动GPT_SoVITS的API服务
56 |
57 | - Windows下,编写一个bat批处理文件放在GPT_SoVITS整合包的根目录里,文件内容:
58 |
59 | ```bash
60 | runtime\python.exe api_v2.py
61 | pause
62 | ```
63 |
64 | 然后双击bat文件即可在终端启动GPT_SoVITS的API服务
65 | 
66 |
67 | - Windows或者Linux下,可以直接通过命令行启动GPT_SoVITS的API服务,比如:
68 |
69 | ```bash
70 | python api_v2.py
71 | 也可能是
72 | python3 api_v2.py
73 |
74 | ```
75 |
76 | ### 第二步,调用
77 |
78 | - 自动调用:调用LLM得到的文本有概率会自动转成语音发送,概率可在配置里调
79 | - 指令调用:
80 |
81 | ```bash
82 | /{emotion} {text}` # 生成语音,emotion为情绪,text为文本
83 | /说 怎么啦? # 示例1,使用默认情绪,注意用空格隔空参数
84 | /生气地说 我再也不理你了 # 示例2,使用指定情绪,注意用空格隔空参数
85 | ```
86 |
87 | ## 🤝 TODO
88 |
89 | - [x] 对接GPT_SoVITS,实现基本TTS的功能
90 | - [x] bot发送消息前,一定概率自动触发TTS
91 | - [x] 支持多情绪,并自动切换
92 | - [ ] 支持多模型
93 | - [ ] 支持多语言自动处理
94 | - [ ] 改成astrbot服务商,内嵌在astrbot框架中
95 |
96 | ## 👥 贡献指南
97 |
98 | - 🌟 Star 这个项目!(点右上角的星星,感谢支持!)
99 | - 🐛 提交 Issue 报告问题
100 | - 💡 提出新功能建议
101 | - 🔧 提交 Pull Request 改进代码
102 |
103 | ## 📌 注意事项
104 |
105 | - 本项目优先兼容官方整合包,第三方整合包只要不是大改的基本也能对接
106 | - GPT_SoVITS的部署目前我仅测试了Windows环境,更多环境下的部署请自行查阅[GPT_SoVITS官方文档](https://github.com/RVC-Boss/GPT-SoVITS/blob/main/docs/cn/README.md)
107 | - 想第一时间得到反馈的可以来作者的插件反馈群(QQ群):460973561(不点star不给进)
108 |
--------------------------------------------------------------------------------
/_conf_schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "base_setting": {
3 | "description": "基础配置",
4 | "type": "object",
5 | "hint": "",
6 | "items": {
7 | "base_url": {
8 | "description": "GPT-SoVITS API 的 URL",
9 | "type": "string",
10 | "hint": "必填!GPT_SoVITS官方整合包默认为http://127.0.0.1:9880,第三方整合包可能不同",
11 | "default": "http://127.0.0.1:9880"
12 | }
13 | }
14 | },
15 | "auto_config": {
16 | "description": "主动调用配置",
17 | "type": "object",
18 | "hint": "本插件有一定概率主动将bot本来要发送的文本转成语音发送",
19 | "items": {
20 | "send_record_probability": {
21 | "description": "主动转语音发送的概率",
22 | "type": "float",
23 | "hint": "",
24 | "default": 0.15
25 | },
26 | "max_resp_text_len": {
27 | "description": "文本长度限制",
28 | "type": "int",
29 | "hint": "超过此长度的文本不会被转成语音发送",
30 | "default": 50
31 | }
32 | }
33 | },
34 | "role": {
35 | "description": "角色配置",
36 | "type": "object",
37 | "hint": "",
38 | "items": {
39 | "gpt_weights_path": {
40 | "description": "GPT模型文件路径",
41 | "type": "string",
42 | "hint": "即“.ckpt”后缀的文件,请使用绝对路径!路径两端不要带双引号!!不填则默认用GPT_SoVITS内置的GPT模型",
43 | "default": ""
44 | },
45 | "sovits_weights_path": {
46 | "description": "SoVITS模型文件路径",
47 | "type": "string",
48 | "hint": "即“.pth”后缀的文件,请使用绝对路径,路径两端不要带双引号!!不填则默认用GPT_SoVITS内置的SoVITS模型",
49 | "default": ""
50 | },
51 | "default_emotion": {
52 | "description": "默认使用的情绪",
53 | "type": "string",
54 | "hint": "在没有触发关键词或者未指定情绪时,使用默认的内置情绪,内置情绪有:温柔地说、开心地说、生气地说、惊讶地说",
55 | "options": [
56 | "温柔地说",
57 | "开心地说",
58 | "生气地说",
59 | "惊讶地说"
60 | ],
61 | "default": "惊讶地说"
62 | }
63 | }
64 | },
65 | "emotions": {
66 | "description": "情绪配置",
67 | "type": "object",
68 | "hint": "",
69 | "items": {
70 | "gently": {
71 | "description": "【温柔地说】参数配置",
72 | "type": "object",
73 | "hint": "",
74 | "items": {
75 | "ref_audio_path": {
76 | "description": "参考音频文件路径",
77 | "type": "string",
78 | "hint": "请使用绝对路径!路径两端不要带双引号!!不填则使用插件内置的参考音频文件",
79 | "default": ""
80 | },
81 | "prompt_text": {
82 | "description": "提示文本",
83 | "type": "string",
84 | "hint": "影响语音合成的情绪",
85 | "default": ""
86 | },
87 | "prompt_lang": {
88 | "description": "提示文本的语言",
89 | "type": "string",
90 | "hint": "默认为中文",
91 | "default": "zh"
92 | },
93 | "speed_factor": {
94 | "description": "语音播放速度",
95 | "type": "float",
96 | "hint": "1为原始语速",
97 | "default": 1
98 | },
99 | "fragment_interval": {
100 | "description": "语音片段之间的间隔时间",
101 | "type": "float",
102 | "hint": "间隔越短,语气越急促(单位:秒)",
103 | "default": 0.7
104 | },
105 | "keywords": {
106 | "description": "触发词此情绪的关键词",
107 | "type": "list",
108 | "hint": "用户发送的文本 或者 AI回答的文本中包含这些关键词时,使用此情绪合成语音",
109 | "default": [
110 | "温柔",
111 | "深情"
112 | ]
113 | }
114 | }
115 | },
116 | "happily": {
117 | "description": "【开心地说】参数配置",
118 | "type": "object",
119 | "hint": "",
120 | "items": {
121 | "ref_audio_path": {
122 | "description": "参考音频文件路径",
123 | "type": "string",
124 | "hint": "请使用绝对路径!路径两端不要带双引号!!不填则使用插件内置的参考音频文件",
125 | "default": ""
126 | },
127 | "prompt_text": {
128 | "description": "提示文本",
129 | "type": "string",
130 | "hint": "影响语音合成的情绪",
131 | "default": ""
132 | },
133 | "prompt_lang": {
134 | "description": "提示文本的语言",
135 | "type": "string",
136 | "hint": "默认为中文",
137 | "default": "zh"
138 | },
139 | "speed_factor": {
140 | "description": "语音播放速度",
141 | "type": "float",
142 | "hint": "1为原始语速",
143 | "default": 1.1
144 | },
145 | "fragment_interval": {
146 | "description": "语音片段之间的间隔时间",
147 | "type": "float",
148 | "hint": "间隔越短,语气越急促(单位:秒)",
149 | "default": 0.3
150 | },
151 | "keywords": {
152 | "description": "触发词此情绪的关键词",
153 | "type": "list",
154 | "hint": "用户发送的文本 或者 AI回答的文本中包含这些关键词时,使用此情绪合成语音",
155 | "default": [
156 | "开心",
157 | "高兴"
158 | ]
159 | }
160 | }
161 | },
162 | "angrily": {
163 | "description": "【生气地说】参数配置",
164 | "type": "object",
165 | "hint": "",
166 | "items": {
167 | "ref_audio_path": {
168 | "description": "参考音频文件路径",
169 | "type": "string",
170 | "hint": "请使用绝对路径!路径两端不要带双引号!!不填则使用插件内置的参考音频文件",
171 | "default": ""
172 | },
173 | "prompt_text": {
174 | "description": "提示文本",
175 | "type": "string",
176 | "hint": "影响语音合成的情绪",
177 | "default": ""
178 | },
179 | "prompt_lang": {
180 | "description": "提示文本的语言",
181 | "type": "string",
182 | "hint": "默认为中文",
183 | "default": "zh"
184 | },
185 | "speed_factor": {
186 | "description": "语音播放速度",
187 | "type": "float",
188 | "hint": "1为原始语速",
189 | "default": 1.2
190 | },
191 | "fragment_interval": {
192 | "description": "语音片段之间的间隔时间",
193 | "type": "float",
194 | "hint": "间隔越短,语气越急促(单位:秒)",
195 | "default": 0.5
196 | },
197 | "keywords": {
198 | "description": "触发词此情绪的关键词",
199 | "type": "list",
200 | "hint": "用户发送的文本 或者 AI回答的文本中包含这些关键词时,使用此情绪合成语音",
201 | "default": [
202 | "生气",
203 | "哼",
204 | "尼玛",
205 | "妈",
206 | "操",
207 | "艹",
208 | "逼",
209 | "爹",
210 | "玩意",
211 | "滚",
212 | "鸡巴",
213 | "老子",
214 | "屎",
215 | "死"
216 | ]
217 | }
218 | }
219 | },
220 | "surprise": {
221 | "description": "【惊讶地说】参数配置",
222 | "type": "object",
223 | "hint": "",
224 | "items": {
225 | "ref_audio_path": {
226 | "description": "参考音频文件路径",
227 | "type": "string",
228 | "hint": "请使用绝对路径!路径两端不要带双引号!!不填则使用插件内置的参考音频文件",
229 | "default": ""
230 | },
231 | "prompt_text": {
232 | "description": "提示文本",
233 | "type": "string",
234 | "hint": "影响语音合成的情绪",
235 | "default": ""
236 | },
237 | "prompt_lang": {
238 | "description": "提示文本的语言",
239 | "type": "string",
240 | "hint": "默认为中文",
241 | "default": "zh"
242 | },
243 | "speed_factor": {
244 | "description": "语音播放速度",
245 | "type": "float",
246 | "hint": "1为原始语速",
247 | "default": 1
248 | },
249 | "fragment_interval": {
250 | "description": "语音片段之间的间隔时间",
251 | "type": "float",
252 | "hint": "间隔越短,语气越急促(单位:秒)",
253 | "default": 0.6
254 | },
255 | "keywords": {
256 | "description": "触发词此情绪的关键词",
257 | "type": "list",
258 | "hint": "用户发送的文本 或者 AI回答的文本中包含这些关键词时,使用此情绪合成语音",
259 | "default": [
260 | "惊讶",
261 | "啊"
262 | ]
263 | }
264 | }
265 | }
266 | }
267 | },
268 | "default_params": {
269 | "description": "TTS额外参数配置",
270 | "type": "object",
271 | "hint": "一般情况下保持默认即可,如果你有一定的GPT_SoVITS基础,可根据个人喜好进行更改",
272 | "items": {
273 | "text_lang": {
274 | "description": "文本语言",
275 | "type": "string",
276 | "hint": "默认为中文",
277 | "default": "zh"
278 | },
279 | "aux_ref_audio_paths": {
280 | "description": "辅助参考音频文件路径列表",
281 | "type": "string",
282 | "hint": "不填也行",
283 | "default": ""
284 | },
285 | "top_k": {
286 | "description": "生成语音的多样性",
287 | "type": "int",
288 | "hint": "",
289 | "default": 5
290 | },
291 | "top_p": {
292 | "description": "核采样的阈值",
293 | "type": "float",
294 | "hint": "",
295 | "default": 1.0
296 | },
297 | "temperature": {
298 | "description": "生成语音的随机性",
299 | "type": "float",
300 | "hint": "",
301 | "default": 1.0
302 | },
303 | "text_split_method": {
304 | "description": "切分文本的方法",
305 | "type": "string",
306 | "hint": "可选值: `cut0`:不切分 `cut1`:四句一切 `cut2`:50字一切 `cut3`:按中文句号切 `cut4`:按英文句号切 `cut5`:按标点符号切",
307 | "default": "cut3"
308 | },
309 | "batch_size": {
310 | "description": "批处理大小",
311 | "type": "int",
312 | "hint": "",
313 | "default": 1
314 | },
315 | "batch_threshold": {
316 | "description": "批处理阈值",
317 | "type": "float",
318 | "hint": "",
319 | "default": 0.75
320 | },
321 | "split_bucket": {
322 | "description": "将文本分割成桶以便并行处理",
323 | "type": "bool",
324 | "hint": "",
325 | "default": true
326 | },
327 | "speed_factor": {
328 | "description": "语音播放速度",
329 | "type": "float",
330 | "hint": "1为原始语速",
331 | "default": 1
332 | },
333 | "fragment_interval": {
334 | "description": "语音片段之间的间隔时间",
335 | "type": "float",
336 | "hint": "",
337 | "default": 0.3
338 | },
339 | "streaming_mode": {
340 | "description": "启用流模式",
341 | "type": "bool",
342 | "hint": "",
343 | "default": false
344 | },
345 | "seed": {
346 | "description": "随机种子",
347 | "type": "int",
348 | "hint": "用于结果的可重复性",
349 | "default": -1
350 | },
351 | "parallel_infer": {
352 | "description": "并行执行推理",
353 | "type": "bool",
354 | "hint": "",
355 | "default": true
356 | },
357 | "repetition_penalty": {
358 | "description": "重复惩罚因子",
359 | "type": "float",
360 | "hint": "",
361 | "default": 1.35
362 | },
363 | "media_type": {
364 | "description": "输出媒体的类型",
365 | "type": "string",
366 | "hint": "建议用wav",
367 | "default": "wav"
368 | }
369 | }
370 | }
371 | }
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import re
3 | import random
4 | import aiohttp
5 | from astrbot import logger
6 | from astrbot.api.event import filter
7 | from astrbot.api.star import Context, Star, register
8 | from astrbot.core import AstrBotConfig
9 | from astrbot.core.message.components import Record
10 | from astrbot.core.platform import AstrMessageEvent
11 | import astrbot.core.message.components as Comp
12 | from pathlib import Path
13 | from typing import Dict
14 |
15 | SAVED_AUDIO_DIR = Path(
16 | "./data/plugins_data/astrbot_plugin_GPT_SoVITS"
17 | ) # 语音文件保存目录
18 | REFERENCE_AUDIO_DIR: Path = (
19 | Path(__file__).resolve().parent / "reference_audio"
20 | ) # 参考音频文件目录
21 |
22 | SAVED_AUDIO_DIR.mkdir(parents=True, exist_ok=True)
23 | REFERENCE_AUDIO_DIR.mkdir(parents=True, exist_ok=True)
24 |
25 |
26 | @register("astrbot_plugin_GPT_SoVITS", "Zhalslar", "GPT_SoVITS对接插件", "1.1.3")
27 | class GPTSoVITSPlugin(Star):
28 | def __init__(self, context: Context, config: AstrBotConfig):
29 | super().__init__(context)
30 | base_setting: Dict = config.get("base_setting", {})
31 | self.base_url: str = base_setting.get("base_url", "")
32 |
33 | auto_config: Dict = config.get("auto_config", {})
34 | self.send_record_probability: float = auto_config.get(
35 | "send_record_probability", 0.15
36 | )
37 | self.max_resp_text_len: int = auto_config.get("max_resp_text_len", 50)
38 |
39 | role_config: Dict = config.get("role", {})
40 | self.default_emotion: str = role_config.get(
41 | "default_emotion", "生气地"
42 | ) # 默认情绪预设
43 | self.gpt_weights_path: str = role_config.get("gpt_weights_path", "")
44 | self.sovits_weights_path: str = role_config.get("sovits_weights_path", "")
45 | asyncio.create_task(self._set_model_weights())
46 |
47 | emotions_config = config.get("emotions", {})
48 | gently_config = emotions_config.get("gently", {})
49 | happily_config = emotions_config.get("happily", {})
50 | angrily_config = emotions_config.get("angrily", {})
51 | surprise_config = emotions_config.get("surprise", {})
52 | self.preset_emotions: Dict = {
53 | "温柔地说": {
54 | "ref_audio_path": gently_config.get("ref_audio_path")
55 | or str(REFERENCE_AUDIO_DIR / "不要害怕,也不要哭了.wav"),
56 | "prompt_text": gently_config.get("prompt_text")
57 | or "不要害怕,也不要哭了",
58 | "prompt_lang": gently_config.get("prompt_lang"),
59 | "speed_factor": gently_config.get("speed_factor"),
60 | "fragment_interval": gently_config.get("fragment_interval"),
61 | },
62 | "开心地说": {
63 | "ref_audio_path": happily_config.get("ref_audio_path")
64 | or str(REFERENCE_AUDIO_DIR / "它好像在等另一只蕈兽_心情很好的样子.wav"),
65 | "prompt_text": happily_config.get("prompt_text")
66 | or "它好像在等另一只蕈兽_心情很好的样子",
67 | "prompt_lang": happily_config.get("prompt_lang"),
68 | "speed_factor": happily_config.get("speed_factor"),
69 | "fragment_interval": happily_config.get("fragment_interval"),
70 | },
71 | "生气地说": {
72 | "ref_audio_path": angrily_config.get("ref_audio_path")
73 | or str(
74 | REFERENCE_AUDIO_DIR
75 | / "你还会选择现在的位置吗?到那时,你觉得自己又会是什么呢.wav"
76 | ),
77 | "prompt_text": angrily_config.get("prompt_text")
78 | or "你还会选择现在的位置吗?到那时,你觉得自己又会是什么呢",
79 | "prompt_lang": angrily_config.get("prompt_lang"),
80 | "speed_factor": angrily_config.get("speed_factor"),
81 | "fragment_interval": angrily_config.get("fragment_interval"),
82 | },
83 | "惊讶地说": {
84 | "ref_audio_path": surprise_config.get("ref_audio_path")
85 | or str(
86 | REFERENCE_AUDIO_DIR / "就算是这样,也不至于直接碎掉啊,除非.wav"
87 | ),
88 | "prompt_text": surprise_config.get("prompt_text")
89 | or "就算是这样,也不至于直接碎掉啊,除非",
90 | "prompt_lang": surprise_config.get("prompt_lang"),
91 | "speed_factor": surprise_config.get("speed_factor"),
92 | "fragment_interval": surprise_config.get("fragment_interval"),
93 | },
94 | }
95 | self.preset_emotions_set = set(self.preset_emotions.keys())
96 |
97 | self.keywords_dict = {
98 | "温柔地说": gently_config.get("keywords"),
99 | "开心地说": happily_config.get("keywords"),
100 | "生气地说": angrily_config.get("keywords"),
101 | "惊讶地说": surprise_config.get("keywords"),
102 | }
103 |
104 | self.default_params: Dict = config.get("default_params", {}) # 额外参数
105 |
106 | async def _make_request(
107 | self,
108 | endpoint: str,
109 | params=None,
110 | ) -> None | bytes:
111 | """通用的异步请求方法"""
112 | if params:
113 | params = {
114 | k: str(v).lower() if isinstance(v, bool) else v
115 | for k, v in params.items()
116 | }
117 | async with aiohttp.ClientSession() as session:
118 | async with session.request("GET", endpoint, params=params) as response:
119 | if response.status != 200:
120 | return None
121 | audio_bytes = await response.read()
122 | return audio_bytes
123 |
124 | async def _set_model_weights(self):
125 | """设置模型"""
126 | try:
127 | # 设置 GPT 模型
128 | if self.gpt_weights_path:
129 | gpt_endpoint = f"{self.base_url}/set_gpt_weights"
130 | gpt_params = {"weights_path": self.gpt_weights_path}
131 | if await self._make_request(endpoint=gpt_endpoint, params=gpt_params):
132 | logger.info(f"成功设置 GPT 模型路径:{self.gpt_weights_path}")
133 | else:
134 | logger.info("GPT 模型路径未配置,将使用GPT_SoVITS内置的GPT模型")
135 |
136 | # 设置 SoVITS 模型
137 | if self.sovits_weights_path:
138 | sovits_endpoint = f"{self.base_url}/set_sovits_weights"
139 | sovits_params = {"weights_path": self.sovits_weights_path}
140 | if await self._make_request(
141 | endpoint=sovits_endpoint, params=sovits_params
142 | ):
143 | logger.info(f"成功设置 SoVITS 模型路径:{self.sovits_weights_path}")
144 | else:
145 | logger.info("SoVITS 模型路径未配置,将使用GPT_SoVITS内置的SoVITS模型")
146 | except aiohttp.ClientError as e:
147 | logger.error(f"设置模型路径时发生错误:{e}")
148 | except Exception as e:
149 | logger.error(f"发生未知错误:{e}")
150 |
151 | # 在发送消息前,会触发 on_decorating_result 钩子
152 | @filter.on_decorating_result()
153 | async def on_decorating_result(self, event: AstrMessageEvent):
154 | """将LLM生成的文本按概率生成语音并发送"""
155 | if random.random() > self.send_record_probability: # 概率控制
156 | return
157 |
158 | chain = event.get_result().chain
159 | seg = chain[0]
160 |
161 | # 仅允许只含有单条文本的消息链通过
162 | if not (len(chain) == 1 and isinstance(seg, Comp.Plain)):
163 | return
164 |
165 | resp_text = seg.text # bot将要发送的的文本
166 |
167 | # 仅允许一定长度以下的文本通过
168 | if len(resp_text) > self.max_resp_text_len:
169 | return
170 |
171 | send_text = event.message_str # 用户发送的文本
172 |
173 | # 根据 ai生成的文本 和 用户发送的文本 匹配关键词,从而选择情绪
174 | emotion = self.default_emotion
175 | for emo, keywords in self.keywords_dict.items():
176 | for keyword in keywords:
177 | if keyword in send_text or keyword in resp_text:
178 | emotion = emo
179 | break
180 | else:
181 | continue
182 | break
183 |
184 | params = self.default_params.copy()
185 | params.update(self.preset_emotions[emotion]) # 传递情绪参数
186 | params["text"] = resp_text # 传递文本参数
187 |
188 | file_name = self.generate_file_name(event, params=params) # 生成文件名
189 | save_path = await self.tts_inference(
190 | params=params, file_name=file_name
191 | ) # 生成语音
192 |
193 | if save_path is None:
194 | logger.error("TTS任务执行失败!")
195 | return
196 |
197 | chain.clear() # 清空消息段
198 | chain.append(Record.fromFileSystem(save_path)) # 新增语音消息段
199 |
200 | @filter.command(
201 | "说",
202 | alias={
203 | "温柔地说",
204 | "开心地说",
205 | "生气地说",
206 | "惊讶地说",
207 | },
208 | )
209 | async def on_regex(
210 | self, event: AstrMessageEvent, send_text: str | int | None = None
211 | ):
212 | """/xx地说 xxx,直接调用TTS,发送合成后的语音"""
213 | if not send_text:
214 | yield event.plain_result("未提供文本")
215 | return
216 | send_text = str(send_text)
217 | emotion = next(
218 | (emo for emo in self.preset_emotions_set if emo in event.get_message_str()),
219 | self.default_emotion,
220 | )
221 | params = self.default_params.copy()
222 | params.update(self.preset_emotions[emotion])
223 | params["text"] = send_text
224 |
225 | if not emotion or not send_text:
226 | return
227 |
228 | file_name = self.generate_file_name(event, params=params)
229 | save_path = await self.tts_inference(params=params, file_name=file_name)
230 |
231 | if save_path is None:
232 | logger.error("TTS任务执行失败!")
233 | return
234 |
235 | chain = [Record.fromFileSystem(save_path)]
236 | yield event.chain_result(chain) # type: ignore
237 |
238 | def generate_file_name(self, event: AstrMessageEvent, params) -> str:
239 | """生成文件名"""
240 | group_id = event.get_group_id() or "0"
241 | sender_id = event.get_sender_id() or "0"
242 | sanitized_text = re.sub(r"[^a-zA-Z0-9\u4e00-\u9fff\s]", "", params["text"])
243 | limit_text = sanitized_text.strip()[:30] # 限制长度
244 | media_type = self.default_params["media_type"]
245 | file_name = f"{group_id}_{sender_id}_{limit_text}.{media_type}"
246 | return file_name
247 |
248 | async def tts_inference(self, params, file_name: str) -> str | None:
249 | """发送TTS请求,获取音频内容"""
250 | endpoint = f"{self.base_url}/tts"
251 | save_path = str(SAVED_AUDIO_DIR / file_name)
252 | audio_bytes = await self._make_request(endpoint=endpoint, params=params)
253 | if audio_bytes:
254 | with open(save_path, "wb") as audio_file:
255 | audio_file.write(audio_bytes)
256 | return save_path
257 |
258 | @filter.command("重启TTS", alias={"重启tts"})
259 | async def tts_control(self, event: AstrMessageEvent):
260 | """重启GPT_SoVITS"""
261 | yield event.plain_result("重启TTS中...(报错信息请忽略,等待一会即可完成重启)")
262 | endpoint = f"{self.base_url}/control"
263 | params = {"command": "restart"}
264 | await self._make_request(endpoint=endpoint, params=params)
265 |
--------------------------------------------------------------------------------
/metadata.yaml:
--------------------------------------------------------------------------------
1 | name: astrbot_plugin_GPT_SoVITS # 这是你的插件的唯一识别名。
2 | desc: 对接本地部署的GPT_SoVITS,为astrbot提供文本转语音(TTS)服务,支持自定义音色和情绪 # 插件简短描述
3 | help: /说 xxx # 插件的帮助信息
4 | version: v1.1.7 # 插件版本号。格式:v1.1.1 或者 v1.1
5 | author: Zhalslar # 作者
6 | repo: https://github.com/Zhalslar/astrbot_plugin_GPT_SoVITS # 插件的仓库地址
7 |
--------------------------------------------------------------------------------
/reference_audio/不要害怕,也不要哭了.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zhalslar/astrbot_plugin_GPT_SoVITS/d68714e6d4c43f3012ede8544cb15e18ec44c35a/reference_audio/不要害怕,也不要哭了.wav
--------------------------------------------------------------------------------
/reference_audio/你还会选择现在的位置吗?到那时,你觉得自己又会是什么呢.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zhalslar/astrbot_plugin_GPT_SoVITS/d68714e6d4c43f3012ede8544cb15e18ec44c35a/reference_audio/你还会选择现在的位置吗?到那时,你觉得自己又会是什么呢.wav
--------------------------------------------------------------------------------
/reference_audio/它好像在等另一只蕈兽_心情很好的样子.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zhalslar/astrbot_plugin_GPT_SoVITS/d68714e6d4c43f3012ede8544cb15e18ec44c35a/reference_audio/它好像在等另一只蕈兽_心情很好的样子.wav
--------------------------------------------------------------------------------
/reference_audio/就算是这样,也不至于直接碎掉啊,除非.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zhalslar/astrbot_plugin_GPT_SoVITS/d68714e6d4c43f3012ede8544cb15e18ec44c35a/reference_audio/就算是这样,也不至于直接碎掉啊,除非.wav
--------------------------------------------------------------------------------