lintonxue00 commited on
Commit
b2b3dca
1 Parent(s): 866fead

Upload ext_VOICEVOX.py

Browse files
Files changed (1) hide show
  1. 不知道/回收站/1/ext_VOICEVOX.py +319 -0
不知道/回收站/1/ext_VOICEVOX.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ '''
2
+ 启用tecent翻译 可以在YAML 中填入下面的参数
3
+ ng_voice_translate_on : True
4
+ tencentcloud_common_region : "ap-shanghai"
5
+ tencentcloud_common_secretid : "xxxxx"
6
+ tencentcloud_common_secretkey : "xxxxx"
7
+ ng_voice_tar : 'ja'
8
+ '''
9
+
10
+ from .Extension import Extension
11
+ import urllib
12
+ import requests
13
+ import uuid
14
+ import os
15
+ import base64
16
+ from aiohttp import request
17
+ from binascii import b2a_base64
18
+ from hashlib import sha1
19
+ from urllib.parse import urlencode
20
+ from hmac import new
21
+ import random
22
+ from sys import maxsize, version_info
23
+ from time import time
24
+ from nonebot import get_driver
25
+ from aiohttp import request
26
+ from loguru import logger
27
+ from nonebot.exception import ActionFailed
28
+ import asyncio
29
+
30
+
31
+ try:
32
+ from ujson import loads as loadJsonS
33
+ except:
34
+ from json import loads as loadJsonS
35
+
36
+
37
+ # 扩展的配置信息,用于ai理解扩展的功能 *必填*
38
+ ext_config: dict = {
39
+ "name": "voice", # 扩展名称,用于标识扩展
40
+ "arguments": {
41
+ 'sentence': 'str', # 需要转换的文本
42
+ 'emotion': 'str', # 情感
43
+ },
44
+ # 扩展的描述信息,用于提示ai理解扩展的功能 *必填* 尽量简短 使用英文更节省token
45
+ "description": "Send a voice sentence. The emotional parameter must be one of \"normal,sweet,tsundere,sexy,whisper,murmur\" (usage in response: /#voice&hello&sweet#/) ",
46
+ # 参考词,用于上下文参考使用,为空则每次都会被参考(消耗token)
47
+ "refer_word": [],
48
+ # 作者信息
49
+ "author": "恋如雨止",
50
+ # 版本
51
+ "version": "0.0.2",
52
+ # 扩展简介
53
+ "intro": "发送语音消息(支持翻译)",
54
+ }
55
+
56
+ # 情感参数表
57
+ emotion_rate_dict = {
58
+ 'normal': {
59
+ 'custom_attributes': {
60
+ "speed_scale": 1,
61
+ "volume_scale": 1,
62
+ "intonation_scale": 1,
63
+ "pre_phoneme_length": 0.1,
64
+ "post_phoneme_length": 0.1,
65
+ },
66
+ 'name': 'ノーマル',
67
+ },
68
+ 'sweet': {
69
+ 'custom_attributes': {
70
+ "speed_scale": 1.1,
71
+ "volume_scale": 0.9,
72
+ "intonation_scale": 1.3,
73
+ "pre_phoneme_length": 0.2,
74
+ "post_phoneme_length": 0.2,
75
+ },
76
+ 'name': 'あまあま',
77
+ },
78
+ 'tsundere': {
79
+ 'custom_attributes': {
80
+ "speed_scale": 1.0,
81
+ "volume_scale": 1.1,
82
+ "intonation_scale": 1.2,
83
+ "pre_phoneme_length": 0.3,
84
+ "post_phoneme_length": 0.3,
85
+ },
86
+ 'name': 'ツンツン',
87
+ },
88
+ 'sexy': {
89
+ 'custom_attributes': {
90
+ "speed_scale": 0.9,
91
+ "volume_scale": 1.2,
92
+ "intonation_scale": 1.1,
93
+ "pre_phoneme_length": 0.4,
94
+ "post_phoneme_length": 0.4,
95
+ },
96
+ 'name': 'セクシー',
97
+ },
98
+ 'whisper': {
99
+ 'custom_attributes': {
100
+ "speed_scale": 0.8,
101
+ "volume_scale": 1.3,
102
+ "intonation_scale": 1.0,
103
+ "pre_phoneme_length": 0.5,
104
+ "post_phoneme_length": 0.5,
105
+ },
106
+ 'name': 'ささやき',
107
+ },
108
+ 'murmur': {
109
+ 'custom_attributes': {
110
+ "speed_scale": 0.7,
111
+ "volume_scale": 1.4,
112
+ "intonation_scale": 0.9,
113
+ "pre_phoneme_length": 0.6,
114
+ "post_phoneme_length": 0.6,
115
+ },
116
+ 'name': 'ヒソヒソ',
117
+ },
118
+ }
119
+
120
+ # 情感翻译映射表
121
+ emotion_translate_jp2en = {
122
+ 'ノーマル': 'normal',
123
+ 'あまあま': 'sweet',
124
+ 'ツンツン': 'tsundere',
125
+ 'セクシー': 'sexy',
126
+ 'ささやき': 'whisper',
127
+ 'ヒソヒソ': 'murmur',
128
+ }
129
+ emotion_translate_en2jp = {f: t for t, f in emotion_translate_jp2en.items()}
130
+
131
+ class CustomExtension(Extension):
132
+ async def call(self, arg_dict: dict, ctx_data: dict) -> dict:
133
+ """ 当扩展被调用时执行的函数 *由扩展自行实现*
134
+
135
+ 参数:
136
+ arg_dict: dict, 由ai解析的参数字典 {参数名: 参数值}
137
+ """
138
+ custom_config: dict = self.get_custom_config() # 获取yaml中的配置信息
139
+
140
+ ng_voice_translate_on = custom_config.get(
141
+ 'ng_voice_translate_on', False) # 是否启用翻译
142
+ tencentcloud_common_region = custom_config.get(
143
+ 'tencentcloud_common_region', "ap-shanghai") # 腾讯翻译-地区
144
+ tencentcloud_common_secretid = custom_config.get(
145
+ 'tencentcloud_common_secretid', "xxxxx") # 腾讯翻译-密钥id
146
+ tencentcloud_common_secretkey = custom_config.get(
147
+ 'tencentcloud_common_secretkey', "xxxxx") # 腾讯翻译-密钥
148
+ ng_voice_tar = custom_config.get('g_voice_tar', 'ja') # 翻译目标语言
149
+ is_base64 = custom_config.get('is_base64', False) # 是否使用base64编码
150
+
151
+ character = custom_config.get('character', 'もち子さん') # 人物
152
+ url = custom_config.get('api_url', '127.0.0.1:50021')
153
+
154
+ if not url: # 如果没有配置语音服务器url则返回错误信息
155
+ return {'text': f"[ext_VOICEVOX] 未配置语音服务器url"}
156
+ if not url.startswith('http'): # 如果不是http开头则添加
157
+ url = f'http://{url}'
158
+ if not url.endswith('/'): # 如果不是/结尾则添加
159
+ url = f'{url}/'
160
+
161
+ # 音频缓存文件夹
162
+ voice_path = 'voice_cache/'
163
+ if not os.path.exists(voice_path):
164
+ os.mkdir(voice_path)
165
+
166
+ # 获取参数
167
+ raw_text = arg_dict.get('sentence', None)
168
+ emotion_key = arg_dict.get('emotion', 'normal')
169
+ # 判断情感索引是否存在 如果不存在则使用默认情感
170
+ if emotion_key not in self.character_emotion_dict[character]:
171
+ emotion_key = 'normal'
172
+
173
+ """ 腾讯翻译 """
174
+ # 腾讯翻译-签名
175
+ config = get_driver().config
176
+
177
+ async def getReqSign(params: dict) -> str:
178
+ common = {
179
+ "Action": "TextTranslate",
180
+ "Region": f"{tencentcloud_common_region}",
181
+ "Timestamp": int(time()),
182
+ "Nonce": random.randint(1, maxsize),
183
+ "SecretId": f"{tencentcloud_common_secretid}",
184
+ "Version": "2018-03-21",
185
+ }
186
+ params.update(common)
187
+ sign_str = "POSTtmt.tencentcloudapi.com/?"
188
+ sign_str += "&".join("%s=%s" %
189
+ (k, params[k]) for k in sorted(params))
190
+ secret_key = tencentcloud_common_secretkey
191
+ if version_info[0] > 2:
192
+ sign_str = bytes(sign_str, "utf-8")
193
+ secret_key = bytes(secret_key, "utf-8")
194
+ hashed = new(secret_key, sign_str, sha1)
195
+ signature = b2a_base64(hashed.digest())[:-1]
196
+ if version_info[0] > 2:
197
+ signature = signature.decode()
198
+ return signature
199
+
200
+ async def q_translate(message) -> str:
201
+ _source_text = message
202
+ _source = "auto"
203
+ _target = ng_voice_tar
204
+ try:
205
+ endpoint = "https://tmt.tencentcloudapi.com"
206
+ params = {
207
+ "Source": _source,
208
+ "SourceText": _source_text,
209
+ "Target": _target,
210
+ "ProjectId": 0,
211
+ }
212
+ params["Signature"] = await getReqSign(params)
213
+ # 加上超时参数
214
+ async with request("POST", endpoint, data=params) as resp:
215
+ data = loadJsonS(await asyncio.wait_for(resp.read(), timeout=30))["Response"]
216
+ message = data["TargetText"]
217
+ except ActionFailed as e:
218
+ logger.warning(
219
+ f"ActionFailed {e.info['retcode']} {e.info['msg'].lower()} {e.info['wording']}"
220
+ )
221
+ except TimeoutError as e:
222
+ logger.warning(
223
+ f"TimeoutError {e}"
224
+ )
225
+ return message
226
+
227
+ """ 腾讯翻译结束 """
228
+
229
+ if ng_voice_translate_on == True:
230
+ t_result = await q_translate(raw_text)
231
+ else:
232
+ t_result = raw_text
233
+ text = t_result + '~' # 加上一个字符,避免合成语音丢失结尾
234
+
235
+ # 从self.character_emotion_dict中获取角色,如果emotion_key不存在则使用第一个
236
+ speaker = self.character_emotion_dict[character][emotion_translate_en2jp[emotion_key]]['speaker'] if emotion_translate_en2jp[
237
+ emotion_key] in self.character_emotion_dict[character] else self.character_emotion_dict[character][0]['speaker']
238
+ # 根据emotion_key获取从emotion_rate_dict获取自定义属性
239
+ custom_attributes = emotion_rate_dict[emotion_key]['custom_attributes']
240
+
241
+ # 发送查询请求并保存结果
242
+ params = {
243
+ "text": text,
244
+ "speaker": speaker,
245
+ }
246
+ params_encoded = urlencode(params)
247
+ res = requests.post(url + "audio_query?" + params_encoded)
248
+ query_json = res.json()
249
+
250
+ # 更新voicevox_query属性
251
+ query_json['speedScale'] = custom_attributes["speed_scale"]
252
+ query_json['volumeScale'] = custom_attributes["volume_scale"]
253
+ query_json['intonationScale'] = custom_attributes["intonation_scale"]
254
+ query_json['prePhonemeLength'] = custom_attributes["pre_phoneme_length"]
255
+ query_json['postPhonemeLength'] = custom_attributes["post_phoneme_length"]
256
+
257
+ # 发送语音合成请求并保存结果
258
+ synthesis_params = {
259
+ "speaker": speaker
260
+ }
261
+ params_encoded = urlencode(synthesis_params)
262
+ res = requests.post(f"{url}synthesis?{params_encoded}", json=query_json, timeout=120)
263
+ audio_data = res.content
264
+
265
+ file_name = f"{voice_path}{uuid.uuid1()}.wav"
266
+
267
+ if is_base64:
268
+ audio_data = base64.b64decode(audio_data)
269
+
270
+ with open(file_name, "wb") as f:
271
+ f.write(audio_data)
272
+
273
+ local_url = f"file:///{os.path.abspath(file_name)}"
274
+
275
+ if text is not None:
276
+ return {
277
+ 'voice': local_url, # 语音url
278
+ 'text': f"[语音] {raw_text}", # 文本
279
+ }
280
+ return {}
281
+
282
+ def __init__(self, custom_config: dict):
283
+ super().__init__(ext_config.copy(), custom_config)
284
+
285
+ url = custom_config.get('api_url', '127.0.0.1:50021')
286
+
287
+ if not url: # 如果没有配置语音服务器url则返回错误信息
288
+ raise Exception("未配置语音服务器url")
289
+ if not url.startswith('http'): # 如果不是http开头则添加
290
+ url = f'http://{url}'
291
+ if not url.endswith('/'): # 如果不是/结尾则添加
292
+ url = f'{url}/'
293
+
294
+ # 从api获取可用角色json
295
+ for _ in range(3):
296
+ try:
297
+ res = requests.get(url + "speakers", timeout=10)
298
+ break
299
+ except requests.exceptions.RequestException as e:
300
+ continue
301
+ else:
302
+ raise Exception("获取语音服务器角色列表失败")
303
+ speaker_json = res.json()
304
+
305
+ self.character_emotion_dict = {}
306
+
307
+ # 遍历角色json,获取角色列表,保存到 character_emotion_dict 中
308
+ for character in speaker_json:
309
+ character_name = character["name"]
310
+ styles = character["styles"]
311
+ em_dict = {}
312
+ for style in styles:
313
+ em_dict[style["name"]] = {
314
+ "speaker": style["id"],
315
+ "name": style["name"],
316
+ }
317
+ self.character_emotion_dict[character_name] = em_dict
318
+
319
+ print(f"[ext_VOICEVOX] 共加载了 {len(self.character_emotion_dict)} 个角色")