From d89b0568860a87fa5c82efe43ee8c2f7db6e6671 Mon Sep 17 00:00:00 2001 From: chazzjimel <126439838+chazzjimel@users.noreply.github.com> Date: Sun, 3 Dec 2023 18:19:03 +0800 Subject: [PATCH] add ali voice output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加阿里云语音输出支持。 --- voice/ali/ali_api.py | 114 +++++++++++++++++++++++++++++++++ voice/ali/ali_voice.py | 74 +++++++++++++++++++++ voice/ali/config.json.template | 6 ++ voice/factory.py | 10 +-- 4 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 voice/ali/ali_api.py create mode 100644 voice/ali/ali_voice.py create mode 100644 voice/ali/config.json.template diff --git a/voice/ali/ali_api.py b/voice/ali/ali_api.py new file mode 100644 index 0000000..9d366a0 --- /dev/null +++ b/voice/ali/ali_api.py @@ -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 diff --git a/voice/ali/ali_voice.py b/voice/ali/ali_voice.py new file mode 100644 index 0000000..c9e90cc --- /dev/null +++ b/voice/ali/ali_voice.py @@ -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 \ No newline at end of file diff --git a/voice/ali/config.json.template b/voice/ali/config.json.template new file mode 100644 index 0000000..7f8d0e6 --- /dev/null +++ b/voice/ali/config.json.template @@ -0,0 +1,6 @@ +{ + "api_url": "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/tts", + "appkey": "", + "access_key_id": "", + "access_key_secret": "" +} \ No newline at end of file diff --git a/voice/factory.py b/voice/factory.py index 8725e29..01229eb 100644 --- a/voice/factory.py +++ b/voice/factory.py @@ -29,12 +29,8 @@ def create_voice(voice_type): from voice.azure.azure_voice import 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