@@ -0,0 +1,114 @@ | |||||
# coding=utf-8 | |||||
""" | |||||
Author: chazzjimel | |||||
Email: chazzjimel@gmail.com | |||||
wechat:cheung-z-x | |||||
Description: | |||||
""" | |||||
import json | |||||
import time | |||||
import requests | |||||
import datetime | |||||
import hashlib | |||||
import hmac | |||||
import base64 | |||||
import urllib.parse | |||||
import uuid | |||||
from common.log import logger | |||||
from common.tmp_dir import TmpDir | |||||
def text_to_speech_aliyun(url, text, appkey, token): | |||||
# 请求的headers | |||||
headers = { | |||||
"Content-Type": "application/json", | |||||
} | |||||
# 请求的payload | |||||
data = { | |||||
"text": text, | |||||
"appkey": appkey, | |||||
"token": token, | |||||
"format": "wav" | |||||
} | |||||
# 发送POST请求 | |||||
response = requests.post(url, headers=headers, data=json.dumps(data)) | |||||
# 检查响应状态码和内容类型 | |||||
if response.status_code == 200 and response.headers['Content-Type'] == 'audio/mpeg': | |||||
# 构造唯一的文件名 | |||||
output_file = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".wav" | |||||
# 将响应内容写入文件 | |||||
with open(output_file, 'wb') as file: | |||||
file.write(response.content) | |||||
logger.debug(f"音频文件保存成功,文件名:{output_file}") | |||||
else: | |||||
# 打印错误信息 | |||||
logger.debug("响应状态码: {}".format(response.status_code)) | |||||
logger.debug("响应内容: {}".format(response.text)) | |||||
output_file = None | |||||
return output_file | |||||
class AliyunTokenGenerator: | |||||
def __init__(self, access_key_id, access_key_secret): | |||||
self.access_key_id = access_key_id | |||||
self.access_key_secret = access_key_secret | |||||
def sign_request(self, parameters): | |||||
# 将参数排序 | |||||
sorted_params = sorted(parameters.items()) | |||||
# 构造待签名的字符串 | |||||
canonicalized_query_string = '' | |||||
for (k, v) in sorted_params: | |||||
canonicalized_query_string += '&' + self.percent_encode(k) + '=' + self.percent_encode(v) | |||||
string_to_sign = 'GET&%2F&' + self.percent_encode(canonicalized_query_string[1:]) # 使用GET方法 | |||||
# 计算签名 | |||||
h = hmac.new((self.access_key_secret + "&").encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha1) | |||||
signature = base64.encodebytes(h.digest()).strip() | |||||
return signature | |||||
def percent_encode(self, encode_str): | |||||
encode_str = str(encode_str) | |||||
res = urllib.parse.quote(encode_str, '') | |||||
res = res.replace('+', '%20') | |||||
res = res.replace('*', '%2A') | |||||
res = res.replace('%7E', '~') | |||||
return res | |||||
def get_token(self): | |||||
# 设置请求参数 | |||||
params = { | |||||
'Format': 'JSON', | |||||
'Version': '2019-02-28', | |||||
'AccessKeyId': self.access_key_id, | |||||
'SignatureMethod': 'HMAC-SHA1', | |||||
'Timestamp': datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), | |||||
'SignatureVersion': '1.0', | |||||
'SignatureNonce': str(uuid.uuid4()), # 使用uuid生成唯一的随机数 | |||||
'Action': 'CreateToken', | |||||
'RegionId': 'cn-shanghai' | |||||
} | |||||
# 计算签名 | |||||
signature = self.sign_request(params) | |||||
params['Signature'] = signature | |||||
# 构造请求URL | |||||
url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?' + urllib.parse.urlencode(params) | |||||
# 发送请求 | |||||
response = requests.get(url) | |||||
return response.text |
@@ -0,0 +1,74 @@ | |||||
# -*- coding: utf-8 -*- | |||||
""" | |||||
Author: chazzjimel | |||||
Email: chazzjimel@gmail.com | |||||
wechat:cheung-z-x | |||||
Description: | |||||
ali voice service | |||||
""" | |||||
import json | |||||
import os | |||||
import re | |||||
import time | |||||
from bridge.reply import Reply, ReplyType | |||||
from common.log import logger | |||||
from voice.voice import Voice | |||||
from voice.ali.ali_api import AliyunTokenGenerator | |||||
from voice.ali.ali_api import text_to_speech_aliyun | |||||
def textContainsEmoji(text): | |||||
# 此正则表达式匹配大多数表情符号和特殊字符 | |||||
pattern = re.compile( | |||||
'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FA6F\U0001FA70-\U0001FAFF\U00002702-\U000027B0\U00002600-\U000026FF]') | |||||
return bool(pattern.search(text)) | |||||
class AliVoice(Voice): | |||||
def __init__(self): | |||||
try: | |||||
curdir = os.path.dirname(__file__) | |||||
config_path = os.path.join(curdir, "config.json") | |||||
with open(config_path, "r") as fr: | |||||
config = json.load(fr) | |||||
self.token = None | |||||
self.token_expire_time = 0 | |||||
self.api_url = config.get("api_url") | |||||
self.appkey = config.get("appkey") | |||||
self.access_key_id = config.get("access_key_id") | |||||
self.access_key_secret = config.get("access_key_secret") | |||||
except Exception as e: | |||||
logger.warn("AliVoice init failed: %s, ignore " % e) | |||||
# def voiceToText(self, voice_file): | |||||
# pass | |||||
def textToVoice(self, text): | |||||
text = re.sub(r'[^\u4e00-\u9fa5\u3040-\u30FF\uAC00-\uD7AFa-zA-Z0-9' | |||||
r'äöüÄÖÜáéíóúÁÉÍÓÚàèìòùÀÈÌÒÙâêîôûÂÊÎÔÛçÇñÑ,。!?,.]', '', text) | |||||
# 提取 token_id 值 | |||||
token_id = self.get_valid_token() | |||||
fileName = text_to_speech_aliyun(self.api_url, text, self.appkey, token_id) | |||||
if fileName: | |||||
logger.info("[Ali] textToVoice text={} voice file name={}".format(text, fileName)) | |||||
reply = Reply(ReplyType.VOICE, fileName) | |||||
else: | |||||
reply = Reply(ReplyType.ERROR, "抱歉,语音合成失败") | |||||
return reply | |||||
def get_valid_token(self): | |||||
current_time = time.time() | |||||
if self.token is None or current_time >= self.token_expire_time: | |||||
get_token = AliyunTokenGenerator(self.access_key_id, self.access_key_secret) | |||||
token_str = get_token.get_token() | |||||
token_data = json.loads(token_str) | |||||
self.token = token_data["Token"]["Id"] | |||||
# 将过期时间减少一小段时间(例如5分钟),以避免在边界条件下的过期 | |||||
self.token_expire_time = token_data["Token"]["ExpireTime"] - 300 | |||||
logger.debug(f"新获取的阿里云token:{self.token}") | |||||
else: | |||||
logger.debug("使用缓存的token") | |||||
return self.token |
@@ -0,0 +1,6 @@ | |||||
{ | |||||
"api_url": "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/tts", | |||||
"appkey": "", | |||||
"access_key_id": "", | |||||
"access_key_secret": "" | |||||
} |
@@ -29,12 +29,8 @@ def create_voice(voice_type): | |||||
from voice.azure.azure_voice import AzureVoice | from voice.azure.azure_voice import AzureVoice | ||||
return AzureVoice() | return AzureVoice() | ||||
elif voice_type == "elevenlabs": | |||||
from voice.elevent.elevent_voice import ElevenLabsVoice | |||||
elif voice_type == "ali": | |||||
from voice.ali.ali_voice import AliVoice | |||||
return ElevenLabsVoice() | |||||
elif voice_type == "linkai": | |||||
from voice.linkai.linkai_voice import LinkAIVoice | |||||
return LinkAIVoice() | |||||
return AliVoice() | |||||
raise RuntimeError | raise RuntimeError |