nonebot-plugin-voicevox-bridge 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_voicevox_bridge-0.1.1/PKG-INFO +252 -0
- nonebot_plugin_voicevox_bridge-0.1.1/README.md +234 -0
- nonebot_plugin_voicevox_bridge-0.1.1/pyproject.toml +138 -0
- nonebot_plugin_voicevox_bridge-0.1.1/src/nonebot_plugin_voicevox_bridge/__init__.py +41 -0
- nonebot_plugin_voicevox_bridge-0.1.1/src/nonebot_plugin_voicevox_bridge/client.py +158 -0
- nonebot_plugin_voicevox_bridge-0.1.1/src/nonebot_plugin_voicevox_bridge/commands.py +155 -0
- nonebot_plugin_voicevox_bridge-0.1.1/src/nonebot_plugin_voicevox_bridge/config.py +23 -0
- nonebot_plugin_voicevox_bridge-0.1.1/src/nonebot_plugin_voicevox_bridge/utils.py +29 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nonebot-plugin-voicevox-bridge
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: 插件描述
|
|
5
|
+
Author: yiyuchichu
|
|
6
|
+
Author-email: yiyuchichu <yiyuchichu@gmail.com>
|
|
7
|
+
Requires-Dist: httpx>=0.27.0,<1.0.0
|
|
8
|
+
Requires-Dist: aiofiles>=23.0.0
|
|
9
|
+
Requires-Dist: nonebot-adapter-onebot>=2.4.6,<3.0.0
|
|
10
|
+
Requires-Dist: nonebot-plugin-localstore>=0.7.4,<1.0.0
|
|
11
|
+
Requires-Dist: pydantic>=2.0.0
|
|
12
|
+
Requires-Dist: nonebot2>=2.5.0,<3.0.0
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Project-URL: Homepage, https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge
|
|
15
|
+
Project-URL: Issues, https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge/issues
|
|
16
|
+
Project-URL: Repository, https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge.git
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
<div align="center">
|
|
20
|
+
<a href="https://v2.nonebot.dev/store">
|
|
21
|
+
<img src="https://raw.githubusercontent.com/fllesser/nonebot-plugin-template/refs/heads/resource/.docs/NoneBotPlugin.svg" width="310" alt="logo"></a>
|
|
22
|
+
|
|
23
|
+
## ✨ nonebot-plugin-voicevox-bridge ✨
|
|
24
|
+
[](./LICENSE)
|
|
25
|
+
[](https://pypi.python.org/pypi/nonebot-plugin-voicevox-bridge)
|
|
26
|
+
[](https://www.python.org)
|
|
27
|
+
[](https://github.com/astral-sh/uv)
|
|
28
|
+
<br/>
|
|
29
|
+
[](https://github.com/astral-sh/ruff)
|
|
30
|
+
[](https://results.pre-commit.ci/latest/github/yiyuchichu/nonebot-plugin-voicevox-bridge/master)
|
|
31
|
+
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
## 📖 介绍
|
|
35
|
+
|
|
36
|
+
本插件基于VOICEVOX的API进行语音合成并通过OneBot v11协议进行通讯。本插件可以通过本地部署[VOICEVOX ENGINE](https://github.com/VOICEVOX/voicevox_engine)并监听其端口实现本地合成语音并转发,也可以通过[WEB版VOICEVOX API(高速)](https://voicevox.su-shiki.com/su-shikiapis/)提供的API进行语音的转发。一般来说VOICEVOX可以处理日语和英语的文本。
|
|
37
|
+
|
|
38
|
+
## 💿 安装
|
|
39
|
+
|
|
40
|
+
<details open>
|
|
41
|
+
<summary>使用 nb-cli 安装</summary>
|
|
42
|
+
在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
|
|
43
|
+
|
|
44
|
+
nb plugin install nonebot-plugin-voicevox-bridge --upgrade
|
|
45
|
+
使用 **pypi** 源安装
|
|
46
|
+
|
|
47
|
+
nb plugin install nonebot-plugin-voicevox-bridge --upgrade -i "https://pypi.org/simple"
|
|
48
|
+
使用**清华源**安装
|
|
49
|
+
|
|
50
|
+
nb plugin install nonebot-plugin-voicevox-bridge --upgrade -i "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
</details>
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary>使用包管理器安装</summary>
|
|
57
|
+
在 nonebot2 项目的插件目录下, 打开命令行, 根据你使用的包管理器, 输入相应的安装命令
|
|
58
|
+
|
|
59
|
+
<details open>
|
|
60
|
+
<summary>uv</summary>
|
|
61
|
+
|
|
62
|
+
uv add nonebot-plugin-voicevox-bridge
|
|
63
|
+
安装仓库 master 分支
|
|
64
|
+
|
|
65
|
+
uv add git+https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge@master
|
|
66
|
+
</details>
|
|
67
|
+
|
|
68
|
+
<details>
|
|
69
|
+
<summary>pdm</summary>
|
|
70
|
+
|
|
71
|
+
pdm add nonebot-plugin-voicevox-bridge
|
|
72
|
+
安装仓库 master 分支
|
|
73
|
+
|
|
74
|
+
pdm add git+https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge@master
|
|
75
|
+
</details>
|
|
76
|
+
<details>
|
|
77
|
+
<summary>poetry</summary>
|
|
78
|
+
|
|
79
|
+
poetry add nonebot-plugin-voicevox-bridge
|
|
80
|
+
安装仓库 master 分支
|
|
81
|
+
|
|
82
|
+
poetry add git+https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge@master
|
|
83
|
+
</details>
|
|
84
|
+
|
|
85
|
+
打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot]` 部分追加写入
|
|
86
|
+
|
|
87
|
+
plugins = ["nonebot_plugin_voicevox_bridge"]
|
|
88
|
+
|
|
89
|
+
</details>
|
|
90
|
+
|
|
91
|
+
<details>
|
|
92
|
+
<summary>使用 nbr 安装(使用 uv 管理依赖可用)</summary>
|
|
93
|
+
|
|
94
|
+
[nbr](https://github.com/fllesser/nbr) 是一个基于 uv 的 nb-cli,可以方便地管理 nonebot2
|
|
95
|
+
|
|
96
|
+
nbr plugin install nonebot-plugin-voicevox-bridge
|
|
97
|
+
使用 **pypi** 源安装
|
|
98
|
+
|
|
99
|
+
nbr plugin install nonebot-plugin-voicevox-bridge -i "https://pypi.org/simple"
|
|
100
|
+
使用**清华源**安装
|
|
101
|
+
|
|
102
|
+
nbr plugin install nonebot-plugin-voicevox-bridge -i "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
103
|
+
|
|
104
|
+
</details>
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## ⚙️ 配置
|
|
108
|
+
|
|
109
|
+
在 nonebot2 项目的`.env`文件中添加下表中的必填配置
|
|
110
|
+
|
|
111
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
112
|
+
| :-----: | :---: | :----: | :------: |
|
|
113
|
+
| VOICEVOX_API_URL | 是 | https://deprecatedapis.tts.quest/v2/voicevox | 需要本地引擎请使用 http://127.0.0.1:50021 |
|
|
114
|
+
| VOICEVOX_TIMEOUT | 是 | 30.0 | 可酌情修改 |
|
|
115
|
+
| VOICEVOX_API_KEY | 否 | 无 | 从[这里](https://voicevox.su-shiki.com/su-shikiapis/)获取,需要谷歌账号 |
|
|
116
|
+
|
|
117
|
+
## 🎉 使用
|
|
118
|
+
### 指令表
|
|
119
|
+
| 指令 | 权限 | 需要@ | 范围 | 说明 |
|
|
120
|
+
| :---: | :---: | :---: | :---: | :------: |
|
|
121
|
+
| /tts <speaker_id> <text> | 群员 | 否 | 全部 | 进行语音合成,speaker_id可通过/speakers获取 |
|
|
122
|
+
| /speakers | 群员 | 否 | 全部 | 获取音源列表 |
|
|
123
|
+
| /voicevox_status | 群员 | 否 | 全部 | 检查引擎状态 |
|
|
124
|
+
| /apilimit | 群员 | 否 | 全部 | 检查api点数(仅限tts.quest) |
|
|
125
|
+
|
|
126
|
+
### 备注
|
|
127
|
+
- APIKEY获取:https://voicevox.su-shiki.com/su-shikiapis/ 以及 https://su-shiki.com/api/
|
|
128
|
+
- 积分规则:1500 + 100 x (UTF-8文字数),每24h重置10,000,000积分。[该网站](https://voicevox.su-shiki.com/su-shikiapis/ttsquest/)也提供了不需要APIKEY的低速API,可按需取用(不过高速的限额其实也很高啦)
|
|
129
|
+
- 本插件是我一时兴起的成过,受[nonebot-plugin-just-enough-katakanas](https://github.com/p0rt39/nonebot-plugin-just-enough-katakanas)启发,~~认为片假名需要念出来才能发挥其威力~~,不过实际上配合效果不太好。这是我的第一个Nonebot插件(难说会不会有下一个),作为计算机小白,代码撰写几乎全部由AI代劳,出发点是自用,功能较少,也难免会出现没有发现的bug,维护恐怕也很难持续,敬请谅解。
|
|
130
|
+
|
|
131
|
+
## VOICEVOX 声源列表(Web API 返回,供参考)
|
|
132
|
+
|
|
133
|
+
| 角色名 | Style ID | 风格名称 |
|
|
134
|
+
|--------|----------|----------|
|
|
135
|
+
| **四国めたん** | 2 | ノーマル |
|
|
136
|
+
| | 0 | あまあま |
|
|
137
|
+
| | 6 | ツンツン |
|
|
138
|
+
| | 4 | セクシー |
|
|
139
|
+
| | 36 | ささやき |
|
|
140
|
+
| | 37 | ヒソヒソ |
|
|
141
|
+
| **ずんだもん** | 3 | ノーマル |
|
|
142
|
+
| | 1 | あまあま |
|
|
143
|
+
| | 7 | ツンツン |
|
|
144
|
+
| | 5 | セクシー |
|
|
145
|
+
| | 22 | ささやき |
|
|
146
|
+
| | 38 | ヒソヒソ |
|
|
147
|
+
| | 75 | ヘロヘロ |
|
|
148
|
+
| | 76 | なみだめ |
|
|
149
|
+
| **春日部つむぎ** | 8 | ノーマル |
|
|
150
|
+
| **雨晴はう** | 10 | ノーマル |
|
|
151
|
+
| **波音リツ** | 9 | ノーマル |
|
|
152
|
+
| | 65 | クイーン |
|
|
153
|
+
| **玄野武宏** | 11 | ノーマル |
|
|
154
|
+
| | 39 | 喜び |
|
|
155
|
+
| | 40 | ツンギレ |
|
|
156
|
+
| | 41 | 悲しみ |
|
|
157
|
+
| **白上虎太郎** | 12 | ふつう |
|
|
158
|
+
| | 32 | わーい |
|
|
159
|
+
| | 33 | びくびく |
|
|
160
|
+
| | 34 | おこ |
|
|
161
|
+
| | 35 | びえーん |
|
|
162
|
+
| **青山龍星** | 13 | ノーマル |
|
|
163
|
+
| | 81 | 熱血 |
|
|
164
|
+
| | 82 | 不機嫌 |
|
|
165
|
+
| | 83 | 喜び |
|
|
166
|
+
| | 84 | しっとり |
|
|
167
|
+
| | 85 | かなしみ |
|
|
168
|
+
| | 86 | 囁き |
|
|
169
|
+
| **冥鳴ひまり** | 14 | ノーマル |
|
|
170
|
+
| **九州そら** | 16 | ノーマル |
|
|
171
|
+
| | 15 | あまあま |
|
|
172
|
+
| | 18 | ツンツン |
|
|
173
|
+
| | 17 | セクシー |
|
|
174
|
+
| | 19 | ささやき |
|
|
175
|
+
| **もち子さん** | 20 | ノーマル |
|
|
176
|
+
| | 66 | セクシー/あん子 |
|
|
177
|
+
| | 77 | 泣き |
|
|
178
|
+
| | 78 | 怒り |
|
|
179
|
+
| | 79 | 喜び |
|
|
180
|
+
| | 80 | のんびり |
|
|
181
|
+
| **剣崎雌雄** | 21 | ノーマル |
|
|
182
|
+
| **WhiteCUL** | 23 | ノーマル |
|
|
183
|
+
| | 24 | たのしい |
|
|
184
|
+
| | 25 | かなしい |
|
|
185
|
+
| | 26 | びえーん |
|
|
186
|
+
| **後鬼** | 27 | 人間ver. |
|
|
187
|
+
| | 28 | ぬいぐるみver. |
|
|
188
|
+
| | 87 | 人間(怒り)ver. |
|
|
189
|
+
| | 88 | 鬼ver. |
|
|
190
|
+
| **No.7** | 29 | ノーマル |
|
|
191
|
+
| | 30 | アナウンス |
|
|
192
|
+
| | 31 | 読み聞かせ |
|
|
193
|
+
| **ちび式じい** | 42 | ノーマル |
|
|
194
|
+
| **櫻歌ミコ** | 43 | ノーマル |
|
|
195
|
+
| | 44 | 第二形態 |
|
|
196
|
+
| | 45 | ロリ |
|
|
197
|
+
| **小夜/SAYO** | 46 | ノーマル |
|
|
198
|
+
| **ナースロボ_タイプT** | 47 | ノーマル |
|
|
199
|
+
| | 48 | 楽々 |
|
|
200
|
+
| | 49 | 恐怖 |
|
|
201
|
+
| | 50 | 内緒話 |
|
|
202
|
+
| **†聖騎士 紅桜†** | 51 | ノーマル |
|
|
203
|
+
| **雀松朱司** | 52 | ノーマル |
|
|
204
|
+
| **麒ヶ島宗麟** | 53 | ノーマル |
|
|
205
|
+
| **春歌ナナ** | 54 | ノーマル |
|
|
206
|
+
| **猫使アル** | 55 | ノーマル |
|
|
207
|
+
| | 56 | おちつき |
|
|
208
|
+
| | 57 | うきうき |
|
|
209
|
+
| | 110 | つよつよ |
|
|
210
|
+
| | 111 | へろへろ |
|
|
211
|
+
| **猫使ビィ** | 58 | ノーマル |
|
|
212
|
+
| | 59 | おちつき |
|
|
213
|
+
| | 60 | 人見知り |
|
|
214
|
+
| | 112 | つよつよ |
|
|
215
|
+
| **中国うさぎ** | 61 | ノーマル |
|
|
216
|
+
| | 62 | おどろき |
|
|
217
|
+
| | 63 | こわがり |
|
|
218
|
+
| | 64 | へろへろ |
|
|
219
|
+
| **栗田まろん** | 67 | ノーマル |
|
|
220
|
+
| **あいえるたん** | 68 | ノーマル |
|
|
221
|
+
| **満別花丸** | 69 | ノーマル |
|
|
222
|
+
| | 70 | 元気 |
|
|
223
|
+
| | 71 | ささやき |
|
|
224
|
+
| | 72 | ぶりっ子 |
|
|
225
|
+
| | 73 | ボーイ |
|
|
226
|
+
| **琴詠ニア** | 74 | ノーマル |
|
|
227
|
+
| **Voidoll** | 89 | ノーマル |
|
|
228
|
+
| **ぞん子** | 90 | ノーマル |
|
|
229
|
+
| | 91 | 低血圧 |
|
|
230
|
+
| | 92 | 覚醒 |
|
|
231
|
+
| | 93 | 実況風 |
|
|
232
|
+
| **中部つるぎ** | 94 | ノーマル |
|
|
233
|
+
| | 95 | 怒り |
|
|
234
|
+
| | 96 | ヒソヒソ |
|
|
235
|
+
| | 97 | おどおど |
|
|
236
|
+
| | 98 | 絶望と敗北 |
|
|
237
|
+
| **離途** | 99 | ノーマル |
|
|
238
|
+
| | 101 | シリアス |
|
|
239
|
+
| **黒沢冴白** | 100 | ノーマル |
|
|
240
|
+
| **ユーレイちゃん** | 102 | ノーマル |
|
|
241
|
+
| | 103 | 甘々 |
|
|
242
|
+
| | 104 | 哀しみ |
|
|
243
|
+
| | 105 | ささやき |
|
|
244
|
+
| | 106 | ツクモちゃん |
|
|
245
|
+
| **東北ずん子** | 107 | ノーマル |
|
|
246
|
+
| **東北きりたん** | 108 | ノーマル |
|
|
247
|
+
| **東北イタコ** | 109 | ノーマル |
|
|
248
|
+
| **あんこもん** | 113 | ノーマル |
|
|
249
|
+
| | 114 | つよつよ |
|
|
250
|
+
| | 115 | よわよわ |
|
|
251
|
+
| | 116 | けだるげ |
|
|
252
|
+
| | 117 | ささやき |
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://v2.nonebot.dev/store">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/fllesser/nonebot-plugin-template/refs/heads/resource/.docs/NoneBotPlugin.svg" width="310" alt="logo"></a>
|
|
4
|
+
|
|
5
|
+
## ✨ nonebot-plugin-voicevox-bridge ✨
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://pypi.python.org/pypi/nonebot-plugin-voicevox-bridge)
|
|
8
|
+
[](https://www.python.org)
|
|
9
|
+
[](https://github.com/astral-sh/uv)
|
|
10
|
+
<br/>
|
|
11
|
+
[](https://github.com/astral-sh/ruff)
|
|
12
|
+
[](https://results.pre-commit.ci/latest/github/yiyuchichu/nonebot-plugin-voicevox-bridge/master)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
## 📖 介绍
|
|
17
|
+
|
|
18
|
+
本插件基于VOICEVOX的API进行语音合成并通过OneBot v11协议进行通讯。本插件可以通过本地部署[VOICEVOX ENGINE](https://github.com/VOICEVOX/voicevox_engine)并监听其端口实现本地合成语音并转发,也可以通过[WEB版VOICEVOX API(高速)](https://voicevox.su-shiki.com/su-shikiapis/)提供的API进行语音的转发。一般来说VOICEVOX可以处理日语和英语的文本。
|
|
19
|
+
|
|
20
|
+
## 💿 安装
|
|
21
|
+
|
|
22
|
+
<details open>
|
|
23
|
+
<summary>使用 nb-cli 安装</summary>
|
|
24
|
+
在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
|
|
25
|
+
|
|
26
|
+
nb plugin install nonebot-plugin-voicevox-bridge --upgrade
|
|
27
|
+
使用 **pypi** 源安装
|
|
28
|
+
|
|
29
|
+
nb plugin install nonebot-plugin-voicevox-bridge --upgrade -i "https://pypi.org/simple"
|
|
30
|
+
使用**清华源**安装
|
|
31
|
+
|
|
32
|
+
nb plugin install nonebot-plugin-voicevox-bridge --upgrade -i "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
</details>
|
|
36
|
+
|
|
37
|
+
<details>
|
|
38
|
+
<summary>使用包管理器安装</summary>
|
|
39
|
+
在 nonebot2 项目的插件目录下, 打开命令行, 根据你使用的包管理器, 输入相应的安装命令
|
|
40
|
+
|
|
41
|
+
<details open>
|
|
42
|
+
<summary>uv</summary>
|
|
43
|
+
|
|
44
|
+
uv add nonebot-plugin-voicevox-bridge
|
|
45
|
+
安装仓库 master 分支
|
|
46
|
+
|
|
47
|
+
uv add git+https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge@master
|
|
48
|
+
</details>
|
|
49
|
+
|
|
50
|
+
<details>
|
|
51
|
+
<summary>pdm</summary>
|
|
52
|
+
|
|
53
|
+
pdm add nonebot-plugin-voicevox-bridge
|
|
54
|
+
安装仓库 master 分支
|
|
55
|
+
|
|
56
|
+
pdm add git+https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge@master
|
|
57
|
+
</details>
|
|
58
|
+
<details>
|
|
59
|
+
<summary>poetry</summary>
|
|
60
|
+
|
|
61
|
+
poetry add nonebot-plugin-voicevox-bridge
|
|
62
|
+
安装仓库 master 分支
|
|
63
|
+
|
|
64
|
+
poetry add git+https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge@master
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot]` 部分追加写入
|
|
68
|
+
|
|
69
|
+
plugins = ["nonebot_plugin_voicevox_bridge"]
|
|
70
|
+
|
|
71
|
+
</details>
|
|
72
|
+
|
|
73
|
+
<details>
|
|
74
|
+
<summary>使用 nbr 安装(使用 uv 管理依赖可用)</summary>
|
|
75
|
+
|
|
76
|
+
[nbr](https://github.com/fllesser/nbr) 是一个基于 uv 的 nb-cli,可以方便地管理 nonebot2
|
|
77
|
+
|
|
78
|
+
nbr plugin install nonebot-plugin-voicevox-bridge
|
|
79
|
+
使用 **pypi** 源安装
|
|
80
|
+
|
|
81
|
+
nbr plugin install nonebot-plugin-voicevox-bridge -i "https://pypi.org/simple"
|
|
82
|
+
使用**清华源**安装
|
|
83
|
+
|
|
84
|
+
nbr plugin install nonebot-plugin-voicevox-bridge -i "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
85
|
+
|
|
86
|
+
</details>
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## ⚙️ 配置
|
|
90
|
+
|
|
91
|
+
在 nonebot2 项目的`.env`文件中添加下表中的必填配置
|
|
92
|
+
|
|
93
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
94
|
+
| :-----: | :---: | :----: | :------: |
|
|
95
|
+
| VOICEVOX_API_URL | 是 | https://deprecatedapis.tts.quest/v2/voicevox | 需要本地引擎请使用 http://127.0.0.1:50021 |
|
|
96
|
+
| VOICEVOX_TIMEOUT | 是 | 30.0 | 可酌情修改 |
|
|
97
|
+
| VOICEVOX_API_KEY | 否 | 无 | 从[这里](https://voicevox.su-shiki.com/su-shikiapis/)获取,需要谷歌账号 |
|
|
98
|
+
|
|
99
|
+
## 🎉 使用
|
|
100
|
+
### 指令表
|
|
101
|
+
| 指令 | 权限 | 需要@ | 范围 | 说明 |
|
|
102
|
+
| :---: | :---: | :---: | :---: | :------: |
|
|
103
|
+
| /tts <speaker_id> <text> | 群员 | 否 | 全部 | 进行语音合成,speaker_id可通过/speakers获取 |
|
|
104
|
+
| /speakers | 群员 | 否 | 全部 | 获取音源列表 |
|
|
105
|
+
| /voicevox_status | 群员 | 否 | 全部 | 检查引擎状态 |
|
|
106
|
+
| /apilimit | 群员 | 否 | 全部 | 检查api点数(仅限tts.quest) |
|
|
107
|
+
|
|
108
|
+
### 备注
|
|
109
|
+
- APIKEY获取:https://voicevox.su-shiki.com/su-shikiapis/ 以及 https://su-shiki.com/api/
|
|
110
|
+
- 积分规则:1500 + 100 x (UTF-8文字数),每24h重置10,000,000积分。[该网站](https://voicevox.su-shiki.com/su-shikiapis/ttsquest/)也提供了不需要APIKEY的低速API,可按需取用(不过高速的限额其实也很高啦)
|
|
111
|
+
- 本插件是我一时兴起的成过,受[nonebot-plugin-just-enough-katakanas](https://github.com/p0rt39/nonebot-plugin-just-enough-katakanas)启发,~~认为片假名需要念出来才能发挥其威力~~,不过实际上配合效果不太好。这是我的第一个Nonebot插件(难说会不会有下一个),作为计算机小白,代码撰写几乎全部由AI代劳,出发点是自用,功能较少,也难免会出现没有发现的bug,维护恐怕也很难持续,敬请谅解。
|
|
112
|
+
|
|
113
|
+
## VOICEVOX 声源列表(Web API 返回,供参考)
|
|
114
|
+
|
|
115
|
+
| 角色名 | Style ID | 风格名称 |
|
|
116
|
+
|--------|----------|----------|
|
|
117
|
+
| **四国めたん** | 2 | ノーマル |
|
|
118
|
+
| | 0 | あまあま |
|
|
119
|
+
| | 6 | ツンツン |
|
|
120
|
+
| | 4 | セクシー |
|
|
121
|
+
| | 36 | ささやき |
|
|
122
|
+
| | 37 | ヒソヒソ |
|
|
123
|
+
| **ずんだもん** | 3 | ノーマル |
|
|
124
|
+
| | 1 | あまあま |
|
|
125
|
+
| | 7 | ツンツン |
|
|
126
|
+
| | 5 | セクシー |
|
|
127
|
+
| | 22 | ささやき |
|
|
128
|
+
| | 38 | ヒソヒソ |
|
|
129
|
+
| | 75 | ヘロヘロ |
|
|
130
|
+
| | 76 | なみだめ |
|
|
131
|
+
| **春日部つむぎ** | 8 | ノーマル |
|
|
132
|
+
| **雨晴はう** | 10 | ノーマル |
|
|
133
|
+
| **波音リツ** | 9 | ノーマル |
|
|
134
|
+
| | 65 | クイーン |
|
|
135
|
+
| **玄野武宏** | 11 | ノーマル |
|
|
136
|
+
| | 39 | 喜び |
|
|
137
|
+
| | 40 | ツンギレ |
|
|
138
|
+
| | 41 | 悲しみ |
|
|
139
|
+
| **白上虎太郎** | 12 | ふつう |
|
|
140
|
+
| | 32 | わーい |
|
|
141
|
+
| | 33 | びくびく |
|
|
142
|
+
| | 34 | おこ |
|
|
143
|
+
| | 35 | びえーん |
|
|
144
|
+
| **青山龍星** | 13 | ノーマル |
|
|
145
|
+
| | 81 | 熱血 |
|
|
146
|
+
| | 82 | 不機嫌 |
|
|
147
|
+
| | 83 | 喜び |
|
|
148
|
+
| | 84 | しっとり |
|
|
149
|
+
| | 85 | かなしみ |
|
|
150
|
+
| | 86 | 囁き |
|
|
151
|
+
| **冥鳴ひまり** | 14 | ノーマル |
|
|
152
|
+
| **九州そら** | 16 | ノーマル |
|
|
153
|
+
| | 15 | あまあま |
|
|
154
|
+
| | 18 | ツンツン |
|
|
155
|
+
| | 17 | セクシー |
|
|
156
|
+
| | 19 | ささやき |
|
|
157
|
+
| **もち子さん** | 20 | ノーマル |
|
|
158
|
+
| | 66 | セクシー/あん子 |
|
|
159
|
+
| | 77 | 泣き |
|
|
160
|
+
| | 78 | 怒り |
|
|
161
|
+
| | 79 | 喜び |
|
|
162
|
+
| | 80 | のんびり |
|
|
163
|
+
| **剣崎雌雄** | 21 | ノーマル |
|
|
164
|
+
| **WhiteCUL** | 23 | ノーマル |
|
|
165
|
+
| | 24 | たのしい |
|
|
166
|
+
| | 25 | かなしい |
|
|
167
|
+
| | 26 | びえーん |
|
|
168
|
+
| **後鬼** | 27 | 人間ver. |
|
|
169
|
+
| | 28 | ぬいぐるみver. |
|
|
170
|
+
| | 87 | 人間(怒り)ver. |
|
|
171
|
+
| | 88 | 鬼ver. |
|
|
172
|
+
| **No.7** | 29 | ノーマル |
|
|
173
|
+
| | 30 | アナウンス |
|
|
174
|
+
| | 31 | 読み聞かせ |
|
|
175
|
+
| **ちび式じい** | 42 | ノーマル |
|
|
176
|
+
| **櫻歌ミコ** | 43 | ノーマル |
|
|
177
|
+
| | 44 | 第二形態 |
|
|
178
|
+
| | 45 | ロリ |
|
|
179
|
+
| **小夜/SAYO** | 46 | ノーマル |
|
|
180
|
+
| **ナースロボ_タイプT** | 47 | ノーマル |
|
|
181
|
+
| | 48 | 楽々 |
|
|
182
|
+
| | 49 | 恐怖 |
|
|
183
|
+
| | 50 | 内緒話 |
|
|
184
|
+
| **†聖騎士 紅桜†** | 51 | ノーマル |
|
|
185
|
+
| **雀松朱司** | 52 | ノーマル |
|
|
186
|
+
| **麒ヶ島宗麟** | 53 | ノーマル |
|
|
187
|
+
| **春歌ナナ** | 54 | ノーマル |
|
|
188
|
+
| **猫使アル** | 55 | ノーマル |
|
|
189
|
+
| | 56 | おちつき |
|
|
190
|
+
| | 57 | うきうき |
|
|
191
|
+
| | 110 | つよつよ |
|
|
192
|
+
| | 111 | へろへろ |
|
|
193
|
+
| **猫使ビィ** | 58 | ノーマル |
|
|
194
|
+
| | 59 | おちつき |
|
|
195
|
+
| | 60 | 人見知り |
|
|
196
|
+
| | 112 | つよつよ |
|
|
197
|
+
| **中国うさぎ** | 61 | ノーマル |
|
|
198
|
+
| | 62 | おどろき |
|
|
199
|
+
| | 63 | こわがり |
|
|
200
|
+
| | 64 | へろへろ |
|
|
201
|
+
| **栗田まろん** | 67 | ノーマル |
|
|
202
|
+
| **あいえるたん** | 68 | ノーマル |
|
|
203
|
+
| **満別花丸** | 69 | ノーマル |
|
|
204
|
+
| | 70 | 元気 |
|
|
205
|
+
| | 71 | ささやき |
|
|
206
|
+
| | 72 | ぶりっ子 |
|
|
207
|
+
| | 73 | ボーイ |
|
|
208
|
+
| **琴詠ニア** | 74 | ノーマル |
|
|
209
|
+
| **Voidoll** | 89 | ノーマル |
|
|
210
|
+
| **ぞん子** | 90 | ノーマル |
|
|
211
|
+
| | 91 | 低血圧 |
|
|
212
|
+
| | 92 | 覚醒 |
|
|
213
|
+
| | 93 | 実況風 |
|
|
214
|
+
| **中部つるぎ** | 94 | ノーマル |
|
|
215
|
+
| | 95 | 怒り |
|
|
216
|
+
| | 96 | ヒソヒソ |
|
|
217
|
+
| | 97 | おどおど |
|
|
218
|
+
| | 98 | 絶望と敗北 |
|
|
219
|
+
| **離途** | 99 | ノーマル |
|
|
220
|
+
| | 101 | シリアス |
|
|
221
|
+
| **黒沢冴白** | 100 | ノーマル |
|
|
222
|
+
| **ユーレイちゃん** | 102 | ノーマル |
|
|
223
|
+
| | 103 | 甘々 |
|
|
224
|
+
| | 104 | 哀しみ |
|
|
225
|
+
| | 105 | ささやき |
|
|
226
|
+
| | 106 | ツクモちゃん |
|
|
227
|
+
| **東北ずん子** | 107 | ノーマル |
|
|
228
|
+
| **東北きりたん** | 108 | ノーマル |
|
|
229
|
+
| **東北イタコ** | 109 | ノーマル |
|
|
230
|
+
| **あんこもん** | 113 | ノーマル |
|
|
231
|
+
| | 114 | つよつよ |
|
|
232
|
+
| | 115 | よわよわ |
|
|
233
|
+
| | 116 | けだるげ |
|
|
234
|
+
| | 117 | ささやき |
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nonebot-plugin-voicevox-bridge"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "插件描述"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
authors = [{ name = "yiyuchichu", email = "yiyuchichu@gmail.com" }]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"httpx>=0.27.0,<1.0.0",
|
|
10
|
+
"aiofiles>=23.0.0",
|
|
11
|
+
"nonebot-adapter-onebot>=2.4.6,<3.0.0",
|
|
12
|
+
"nonebot-plugin-localstore>=0.7.4,<1.0.0", # 存储文件
|
|
13
|
+
"pydantic>=2.0.0", # 数据验证和设置管理
|
|
14
|
+
"nonebot2>=2.5.0,<3.0.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge"
|
|
19
|
+
Issues = "https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge/issues"
|
|
20
|
+
Repository = "https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge.git"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"bump-my-version>=1.2.7",
|
|
25
|
+
"nonebot2[fastapi]>=2.5.0,<3.0.0",
|
|
26
|
+
"poethepoet>=0.42.1",
|
|
27
|
+
"ruff>=0.15.2,<1.0.0",
|
|
28
|
+
{ include-group = "test" },
|
|
29
|
+
]
|
|
30
|
+
test = [
|
|
31
|
+
|
|
32
|
+
"nonebot2[fastapi]>=2.5.0,<3.0.0",
|
|
33
|
+
"nonebug>=0.4.4,<1.0.0",
|
|
34
|
+
"pytest>=9.0.2,<10.0.0",
|
|
35
|
+
"poethepoet>=0.42.1",
|
|
36
|
+
"pytest-asyncio>=1.3.0,<1.4.0",
|
|
37
|
+
"pytest-cov>=7.0.0",
|
|
38
|
+
"pytest-xdist>=3.8.0,<4.0.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["uv_build>=0.10.0,<0.11.0"]
|
|
43
|
+
build-backend = "uv_build"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
[tool.bumpversion]
|
|
47
|
+
current_version = "0.1.1"
|
|
48
|
+
commit = true
|
|
49
|
+
message = "release: bump vesion from {current_version} to {new_version}"
|
|
50
|
+
tag = true
|
|
51
|
+
|
|
52
|
+
[[tool.bumpversion.files]]
|
|
53
|
+
filename = "uv.lock"
|
|
54
|
+
search = "name = \"nonebot-plugin-voicevox-bridge\"\nversion = \"{current_version}\""
|
|
55
|
+
replace = "name = \"nonebot-plugin-voicevox-bridge\"\nversion = \"{new_version}\""
|
|
56
|
+
|
|
57
|
+
[tool.coverage.report]
|
|
58
|
+
exclude_lines = [
|
|
59
|
+
"raise NotImplementedError",
|
|
60
|
+
"if TYPE_CHECKING:",
|
|
61
|
+
"@overload",
|
|
62
|
+
"except ImportError:",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.nonebot]
|
|
66
|
+
plugins = ["nonebot_plugin_voicevox_bridge", "nonebot_plugin_localstore"]
|
|
67
|
+
|
|
68
|
+
[tool.poe.tasks]
|
|
69
|
+
test = "pytest --cov=src --cov-report xml --junitxml=./junit.xml -n auto"
|
|
70
|
+
bump = "bump-my-version bump"
|
|
71
|
+
show-bump = "bump-my-version show-bump"
|
|
72
|
+
|
|
73
|
+
[tool.pyright]
|
|
74
|
+
pythonVersion = "3.10"
|
|
75
|
+
pythonPlatform = "All"
|
|
76
|
+
defineConstant = { PYDANTIC_V2 = true }
|
|
77
|
+
executionEnvironments = [
|
|
78
|
+
{ root = "./tests", extraPaths = [
|
|
79
|
+
"./src",
|
|
80
|
+
] },
|
|
81
|
+
{ root = "./src" },
|
|
82
|
+
]
|
|
83
|
+
typeCheckingMode = "standard"
|
|
84
|
+
disableBytesTypePromotions = true
|
|
85
|
+
|
|
86
|
+
[tool.pytest]
|
|
87
|
+
addopts = [
|
|
88
|
+
"--import-mode=prepend", # 导入模式
|
|
89
|
+
"--strict-markers", # 严格标记模式
|
|
90
|
+
"--tb=short", # 简短的错误回溯
|
|
91
|
+
"-ra", # 显示所有测试结果摘要
|
|
92
|
+
"-s", # 显示打印信息
|
|
93
|
+
"-v", # 详细输出
|
|
94
|
+
]
|
|
95
|
+
pythonpath = ["src"]
|
|
96
|
+
asyncio_mode = "auto"
|
|
97
|
+
asyncio_default_fixture_loop_scope = "session"
|
|
98
|
+
|
|
99
|
+
[tool.ruff]
|
|
100
|
+
line-length = 88
|
|
101
|
+
|
|
102
|
+
[tool.ruff.format]
|
|
103
|
+
line-ending = "lf"
|
|
104
|
+
|
|
105
|
+
[tool.ruff.lint]
|
|
106
|
+
select = [
|
|
107
|
+
"F", # Pyflakes
|
|
108
|
+
"W", # pycodestyle warnings
|
|
109
|
+
"E", # pycodestyle errors
|
|
110
|
+
"I", # isort
|
|
111
|
+
"UP", # pyupgrade
|
|
112
|
+
"ASYNC", # flake8-async
|
|
113
|
+
"C4", # flake8-comprehensions
|
|
114
|
+
"T10", # flake8-debugger
|
|
115
|
+
"T20", # flake8-print
|
|
116
|
+
"PYI", # flake8-pyi
|
|
117
|
+
"PT", # flake8-pytest-style
|
|
118
|
+
"Q", # flake8-quotes
|
|
119
|
+
"TID", # flake8-tidy-imports
|
|
120
|
+
"RUF", # Ruff-specific rules
|
|
121
|
+
]
|
|
122
|
+
ignore = [
|
|
123
|
+
"E402", # module-import-not-at-top-of-file
|
|
124
|
+
"UP037", # quoted-annotation
|
|
125
|
+
"RUF001", # ambiguous-unicode-character-string
|
|
126
|
+
"RUF002", # ambiguous-unicode-character-docstring
|
|
127
|
+
"RUF003", # ambiguous-unicode-character-comment
|
|
128
|
+
"W191", # indentation contains tabs
|
|
129
|
+
"TID252", # relative-import
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
[tool.ruff.lint.isort]
|
|
133
|
+
length-sort = true
|
|
134
|
+
known-first-party = ["tests/*"]
|
|
135
|
+
extra-standard-library = ["typing_extensions"]
|
|
136
|
+
|
|
137
|
+
[tool.ruff.lint.pyupgrade]
|
|
138
|
+
keep-runtime-typing = true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from nonebot.plugin import PluginMetadata
|
|
2
|
+
from .config import Config
|
|
3
|
+
from nonebot import require
|
|
4
|
+
require("nonebot_plugin_localstore")
|
|
5
|
+
|
|
6
|
+
__plugin_meta__ = PluginMetadata(
|
|
7
|
+
name="VOICEVOX BRIDGE",
|
|
8
|
+
description="使用本地引擎或 Web API 的 VOICEVOX 语音合成插件",
|
|
9
|
+
usage=(
|
|
10
|
+
"命令列表:\n"
|
|
11
|
+
" speakers / 声源列表 — 查看可用声源\n"
|
|
12
|
+
" tts <id> <文本> / 语音合成 — 文字转语音\n"
|
|
13
|
+
" voicevox_status / voicevox状态 — 检查引擎状态\n"
|
|
14
|
+
" apilimit / voicevox api余量 — 检查api点数(仅限tts.quest)\n"
|
|
15
|
+
"\n"
|
|
16
|
+
"环境变量:\n"
|
|
17
|
+
" VOICEVOX_API_URL = http://127.0.0.1:50021 (本地引擎默认端口)\n"
|
|
18
|
+
" https://deprecatedapis.tts.quest/v2/voicevox (Web模式)\n"
|
|
19
|
+
" VOICEVOX_TIMEOUT = 30.0\n"
|
|
20
|
+
" VOICEVOX_API_KEY = 你的密钥 (仅Web模式需要)\n"
|
|
21
|
+
"\n"
|
|
22
|
+
"提示:URL 包含 tts.quest 时自动切换为 Web 模式,否则为本地引擎。注意默认的本地引擎需要自行部署并在本地运行。"
|
|
23
|
+
"\n"
|
|
24
|
+
"Web API说明:\n"
|
|
25
|
+
" • 本插件适配的API网站:https://voicevox.su-shiki.com/su-shikiapis/\n"
|
|
26
|
+
" • 密钥获取:https://su-shiki.com/api/\n"
|
|
27
|
+
" • 积分规则:1500 + 100 x (UTF-8文字数),每24h重置10,000,000积分\n"
|
|
28
|
+
" • 支持参数:speaker, pitch, intonationScale, speed\n"
|
|
29
|
+
"\n"
|
|
30
|
+
"本地引擎说明:\n"
|
|
31
|
+
" • 官方版本:https://github.com/VOICEVOX/voicevox_engine\n"
|
|
32
|
+
" • 支持完整的两步合成及高级参数(但本插件并不提供接口)\n"
|
|
33
|
+
),
|
|
34
|
+
type="application", # library
|
|
35
|
+
homepage="https://github.com/yiyuchichu/nonebot-plugin-voicevox-bridge",
|
|
36
|
+
config=Config,
|
|
37
|
+
supported_adapters={"~onebot.v11"}, # 仅 onebot
|
|
38
|
+
extra={"author": "yiyuchichu <your@mail.com>"},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from . import commands
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VoiceVoxError(Exception):
|
|
8
|
+
"""VOICEVOX Engine API error."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VoiceVoxClient:
|
|
12
|
+
"""Async HTTP client supporting both local VOICEVOX Engine and tts.quest Web API.
|
|
13
|
+
|
|
14
|
+
Default engine address: http://127.0.0.1:50021
|
|
15
|
+
tts.quest address: https://deprecatedapis.tts.quest/v2/voicevox
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, base_url: str = "http://127.0.0.1:50021", timeout: float = 30.0, api_key: str | None = None):
|
|
19
|
+
self.base_url = base_url.rstrip("/")
|
|
20
|
+
self.timeout = timeout
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
self.is_tts_quest = "tts.quest" in self.base_url
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------
|
|
25
|
+
# internal helpers
|
|
26
|
+
# ------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
async def _request(
|
|
29
|
+
self,
|
|
30
|
+
method: str,
|
|
31
|
+
path: str,
|
|
32
|
+
*,
|
|
33
|
+
params: dict[str, Any] | None = None,
|
|
34
|
+
json: dict[str, Any] | None = None,
|
|
35
|
+
headers: dict[str, str] | None = None,
|
|
36
|
+
) -> httpx.Response:
|
|
37
|
+
url = f"{self.base_url}{path}"
|
|
38
|
+
|
|
39
|
+
if self.is_tts_quest:
|
|
40
|
+
if params is None:
|
|
41
|
+
params = {}
|
|
42
|
+
if self.api_key:
|
|
43
|
+
params["key"] = self.api_key
|
|
44
|
+
|
|
45
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
46
|
+
resp = await client.request(
|
|
47
|
+
method, url, params=params, json=json, headers=headers
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if self.is_tts_quest:
|
|
51
|
+
if resp.status_code != 200:
|
|
52
|
+
raise VoiceVoxError(f"HTTP Error ({resp.status_code}): {resp.text}")
|
|
53
|
+
if resp.text in ["invalidApiKey", "failed", "notEnoughPoints"]:
|
|
54
|
+
raise VoiceVoxError(f"tts.quest API Error: {resp.text}")
|
|
55
|
+
return resp
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
resp.raise_for_status()
|
|
59
|
+
except httpx.HTTPStatusError as e:
|
|
60
|
+
detail = resp.text
|
|
61
|
+
raise VoiceVoxError(
|
|
62
|
+
f"VOICEVOX API error ({resp.status_code}): {detail}"
|
|
63
|
+
) from e
|
|
64
|
+
return resp
|
|
65
|
+
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
# core API – query creation
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
async def audio_query(
|
|
71
|
+
self, text: str, speaker: int, *, core_version: str | None = None
|
|
72
|
+
) -> dict[str, Any]:
|
|
73
|
+
"""Create a synthesis query from text."""
|
|
74
|
+
if self.is_tts_quest:
|
|
75
|
+
return {"text": text, "speaker": speaker, "_is_tts_quest": True}
|
|
76
|
+
|
|
77
|
+
params: dict[str, Any] = {"text": text, "speaker": speaker}
|
|
78
|
+
if core_version is not None:
|
|
79
|
+
params["core_version"] = core_version
|
|
80
|
+
resp = await self._request("POST", "/audio_query", params=params)
|
|
81
|
+
return resp.json()
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# core API – synthesis
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
async def synthesis(self, speaker: int, query: dict[str, Any]) -> bytes:
|
|
88
|
+
"""Synthesise WAV audio from a query JSON."""
|
|
89
|
+
if self.is_tts_quest and query.get("_is_tts_quest"):
|
|
90
|
+
return await self.tts(query.get("text", ""), speaker)
|
|
91
|
+
elif self.is_tts_quest:
|
|
92
|
+
raise VoiceVoxError("tts.quest Web API 无法直接处理本地化的 Query JSON 数据")
|
|
93
|
+
|
|
94
|
+
resp = await self._request(
|
|
95
|
+
"POST", "/synthesis",
|
|
96
|
+
params={"speaker": speaker},
|
|
97
|
+
json=query,
|
|
98
|
+
headers={"Accept": "audio/wav"},
|
|
99
|
+
)
|
|
100
|
+
return resp.content
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
# utility
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
async def get_version(self) -> str:
|
|
107
|
+
if self.is_tts_quest:
|
|
108
|
+
return "tts.quest v2 (Remote)"
|
|
109
|
+
|
|
110
|
+
resp = await self._request("GET", "/version")
|
|
111
|
+
return resp.json()
|
|
112
|
+
|
|
113
|
+
async def get_speakers(self) -> list[dict[str, Any]]:
|
|
114
|
+
path = "/speakers/" if self.is_tts_quest else "/speakers"
|
|
115
|
+
resp = await self._request("GET", path)
|
|
116
|
+
return resp.json()
|
|
117
|
+
|
|
118
|
+
async def is_ready(self) -> bool:
|
|
119
|
+
"""Return True if the engine is reachable."""
|
|
120
|
+
try:
|
|
121
|
+
if self.is_tts_quest:
|
|
122
|
+
await self.get_speakers()
|
|
123
|
+
else:
|
|
124
|
+
await self.get_version()
|
|
125
|
+
return True
|
|
126
|
+
except Exception:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
# high-level convenience
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
async def tts(self, text: str, speaker: int) -> bytes:
|
|
134
|
+
"""Full TTS pipeline: text -> audio_query -> synthesis -> WAV bytes."""
|
|
135
|
+
if self.is_tts_quest:
|
|
136
|
+
params = {
|
|
137
|
+
"text": text,
|
|
138
|
+
"speaker": speaker,
|
|
139
|
+
}
|
|
140
|
+
resp = await self._request("GET", "/audio/", params=params)
|
|
141
|
+
return resp.content
|
|
142
|
+
else:
|
|
143
|
+
query = await self.audio_query(text, speaker)
|
|
144
|
+
return await self.synthesis(speaker, query)
|
|
145
|
+
|
|
146
|
+
async def get_api_points(self) -> dict[str, Any]:
|
|
147
|
+
"""[tts.quest专用] 获取云端高效率 API 的剩余积分"""
|
|
148
|
+
if not self.is_tts_quest:
|
|
149
|
+
raise VoiceVoxError("积分查询功能仅在启用 tts.quest 模式时可用。")
|
|
150
|
+
|
|
151
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
152
|
+
url = "https://deprecatedapis.tts.quest/v2/api/"
|
|
153
|
+
params = {"key": self.api_key} if self.api_key else {}
|
|
154
|
+
resp = await client.request("GET", url, params=params)
|
|
155
|
+
|
|
156
|
+
if resp.status_code != 200:
|
|
157
|
+
raise VoiceVoxError(f"无法获取积分数据 ({resp.status_code}): {resp.text}")
|
|
158
|
+
return resp.json()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from nonebot import on_command, get_plugin_config
|
|
7
|
+
from nonebot.adapters.onebot.v11 import MessageSegment, Message
|
|
8
|
+
from nonebot.exception import MatcherException
|
|
9
|
+
from nonebot.log import logger
|
|
10
|
+
from nonebot.params import CommandArg
|
|
11
|
+
|
|
12
|
+
from .client import VoiceVoxClient, VoiceVoxError
|
|
13
|
+
from .config import Config, plugin_config
|
|
14
|
+
from .utils import save_and_send_audio
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ------------------------------------------------------------------
|
|
18
|
+
# configuration
|
|
19
|
+
# ------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
# 配置依然可以在这里获取,或者通过依赖注入
|
|
22
|
+
plugin_config = get_plugin_config(Config)
|
|
23
|
+
|
|
24
|
+
client = VoiceVoxClient(
|
|
25
|
+
base_url=plugin_config.voicevox_api_url,
|
|
26
|
+
timeout=plugin_config.voicevox_timeout,
|
|
27
|
+
api_key=plugin_config.voicevox_api_key,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# 1. /speakers — list available speakers
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
speakers_cmd = on_command("speakers", aliases={"声源列表", "voicevox_speakers"}, priority=5)
|
|
35
|
+
|
|
36
|
+
@speakers_cmd.handle()
|
|
37
|
+
async def handle_speakers():
|
|
38
|
+
try:
|
|
39
|
+
data = await client.get_speakers()
|
|
40
|
+
except VoiceVoxError as e:
|
|
41
|
+
await speakers_cmd.finish(f"获取声源失败: {e}")
|
|
42
|
+
except Exception:
|
|
43
|
+
logger.exception("Failed to reach VOICEVOX engine")
|
|
44
|
+
await speakers_cmd.finish(
|
|
45
|
+
f"无法连接到 VOICEVOX 引擎,请确认引擎已启动 "
|
|
46
|
+
f"({plugin_config.voicevox_api_url})"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if not data:
|
|
50
|
+
await speakers_cmd.finish("未找到可用声源")
|
|
51
|
+
|
|
52
|
+
lines = ["可用声源:"]
|
|
53
|
+
for spk in data:
|
|
54
|
+
name = spk.get("name", "unknown")
|
|
55
|
+
lines.append(f"\n【{name}】")
|
|
56
|
+
for style in spk.get("styles", []):
|
|
57
|
+
style_name = style.get("name", "unknown")
|
|
58
|
+
style_id = style.get("id", "?")
|
|
59
|
+
lines.append(f" {style_id} — {style_name}")
|
|
60
|
+
|
|
61
|
+
await speakers_cmd.finish("\n".join(lines))
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
# 2. /tts — text to speech
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
tts_cmd = on_command("tts", aliases={"语音合成"}, priority=5)
|
|
67
|
+
|
|
68
|
+
@tts_cmd.handle()
|
|
69
|
+
async def handle_tts(args: Message = CommandArg()):
|
|
70
|
+
text = args.extract_plain_text().strip()
|
|
71
|
+
if not text:
|
|
72
|
+
await tts_cmd.finish(
|
|
73
|
+
"用法: tts <speaker_id> <文本>\n"
|
|
74
|
+
"示例: tts 1 こんにちは\n"
|
|
75
|
+
"先用 /speakers 查看可用声源 ID"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
parts = text.split(maxsplit=1)
|
|
79
|
+
if len(parts) < 2:
|
|
80
|
+
await tts_cmd.finish(
|
|
81
|
+
"用法: tts <speaker_id> <文本>\n示例: tts 1 こんにちは"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
speaker_id = int(parts[0])
|
|
86
|
+
except ValueError:
|
|
87
|
+
await tts_cmd.finish("speaker_id 必须是数字,请用 /speakers 查看可用 ID")
|
|
88
|
+
|
|
89
|
+
text_to_speak = parts[1]
|
|
90
|
+
|
|
91
|
+
await tts_cmd.send(f"正在用声源 {speaker_id} 合成语音...")
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
wav = await client.tts(text_to_speak, speaker_id)
|
|
95
|
+
except VoiceVoxError as e:
|
|
96
|
+
await tts_cmd.finish(f"语音合成失败: {e}")
|
|
97
|
+
except Exception:
|
|
98
|
+
logger.exception("TTS failed")
|
|
99
|
+
await tts_cmd.finish(
|
|
100
|
+
f"语音合成失败,请确认 VOICEVOX 引擎已启动 "
|
|
101
|
+
f"({plugin_config.voicevox_api_url})"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if not wav:
|
|
105
|
+
await tts_cmd.finish("引擎返回了空的音频数据")
|
|
106
|
+
|
|
107
|
+
# 调用我们的工具函数来处理音频发送
|
|
108
|
+
await save_and_send_audio(tts_cmd, wav, speaker_id)
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# 3. /voicevox_status — check engine health
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
status_cmd = on_command("voicevox_status", aliases={"voicevox状态", "vvs"}, priority=5)
|
|
114
|
+
|
|
115
|
+
@status_cmd.handle()
|
|
116
|
+
async def handle_status():
|
|
117
|
+
try:
|
|
118
|
+
version = await client.get_version()
|
|
119
|
+
msg = f"VOICEVOX 引擎运行中\n版本: {version}\n地址: {plugin_config.voicevox_api_url}"
|
|
120
|
+
except VoiceVoxError as e:
|
|
121
|
+
msg = f"VOICEVOX 引擎返回错误: {e}"
|
|
122
|
+
except Exception:
|
|
123
|
+
msg = f"VOICEVOX 引擎未连接\n地址: {plugin_config.voicevox_api_url}\n请确认引擎已启动"
|
|
124
|
+
await status_cmd.finish(msg)
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
# 4. /voicevox_points — [tts.quest专用] 查看剩余API积分
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
points_cmd = on_command(
|
|
131
|
+
"voicevox_points",
|
|
132
|
+
aliases={"apilimit", "voicevox_points"},
|
|
133
|
+
priority=5
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@points_cmd.handle()
|
|
137
|
+
async def handle_points():
|
|
138
|
+
if not getattr(client, "is_tts_quest", False):
|
|
139
|
+
await points_cmd.finish("当前正在使用本地 VOICEVOX 引擎,无需消耗积分。")
|
|
140
|
+
|
|
141
|
+
msg = ""
|
|
142
|
+
try:
|
|
143
|
+
data = await client.get_api_points()
|
|
144
|
+
points = data.get("points", "未知")
|
|
145
|
+
await points_cmd.finish(
|
|
146
|
+
f"tts.quest 剩余积分:\n"
|
|
147
|
+
f"当前剩余 API 积分: {points} pt\n"
|
|
148
|
+
f"积分计算公式: 1500 + 100 x (UTF-8文字数)\n"
|
|
149
|
+
f"大约可合成 {points // 4500} 条语音(30字)"
|
|
150
|
+
)
|
|
151
|
+
except MatcherException:
|
|
152
|
+
raise # 让 NoneBot 正常结束事件
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(f"获取 tts.quest API 积分失败: {e}")
|
|
155
|
+
await points_cmd.finish(f"积分获取失败: {e}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from nonebot import get_driver, get_plugin_config
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Config(BaseModel):
|
|
6
|
+
voicevox_api_url: str = Field(
|
|
7
|
+
"https://deprecatedapis.tts.quest/v2/voicevox",
|
|
8
|
+
description="VOICEVOX Engine API base URL,需要本地引擎请使用 http://127.0.0.1:50021",
|
|
9
|
+
)
|
|
10
|
+
voicevox_timeout: float = Field(
|
|
11
|
+
30.0,
|
|
12
|
+
description="HTTP request timeout in seconds",
|
|
13
|
+
)
|
|
14
|
+
voicevox_api_key: str | None = Field(
|
|
15
|
+
None,
|
|
16
|
+
description="WEB API Key (Web API 模式需要,Local Engine 模式可留空)",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# 配置加载
|
|
21
|
+
plugin_config: Config = get_plugin_config(Config)
|
|
22
|
+
global_config = get_driver().config
|
|
23
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import aiofiles
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import nonebot_plugin_localstore as store
|
|
7
|
+
from nonebot.adapters.onebot.v11 import MessageSegment
|
|
8
|
+
from nonebot.log import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def save_and_send_audio(matcher, wav_data: bytes, speaker_id: int):
|
|
12
|
+
"""保存音频文件并尝试发送。"""
|
|
13
|
+
cache_file = store.get_plugin_cache_file(f"voicevox_tts_{speaker_id}.wav")
|
|
14
|
+
async with aiofiles.open(cache_file, "wb") as f:
|
|
15
|
+
await f.write(wav_data)
|
|
16
|
+
try:
|
|
17
|
+
await matcher.send(MessageSegment.record(file=cache_file.as_uri()))
|
|
18
|
+
except Exception:
|
|
19
|
+
logger.warning("通过 URI 发送语音失败,尝试 base64 降级...")
|
|
20
|
+
try:
|
|
21
|
+
b64 = base64.b64encode(wav_data).decode()
|
|
22
|
+
await matcher.send(MessageSegment.record(file=f"base64://{b64}"))
|
|
23
|
+
except Exception:
|
|
24
|
+
logger.warning("当前平台不支持 OneBot 语音段,降级为文本提示")
|
|
25
|
+
await matcher.send(
|
|
26
|
+
f"[本地测试提示] 语音合成成功!\n"
|
|
27
|
+
f"音频大小: {len(wav_data)/1024:.1f} KB\n"
|
|
28
|
+
f"暂存路径: {cache_file}"
|
|
29
|
+
)
|