mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-03-19 13:28:11 +08:00
add ali voice output
增加阿里云语音输出支持。
This commit is contained in:
114
voice/ali/ali_api.py
Normal file
114
voice/ali/ali_api.py
Normal file
@@ -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
|
||||||
74
voice/ali/ali_voice.py
Normal file
74
voice/ali/ali_voice.py
Normal file
@@ -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
|
||||||
6
voice/ali/config.json.template
Normal file
6
voice/ali/config.json.template
Normal file
@@ -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":
|
elif voice_type == "ali":
|
||||||
from voice.elevent.elevent_voice import ElevenLabsVoice
|
from voice.ali.ali_voice import AliVoice
|
||||||
|
|
||||||
return ElevenLabsVoice()
|
return AliVoice()
|
||||||
|
|
||||||
elif voice_type == "linkai":
|
|
||||||
from voice.linkai.linkai_voice import LinkAIVoice
|
|
||||||
return LinkAIVoice()
|
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
|||||||
Reference in New Issue
Block a user