From 8bb596c74f2a1f0c38b7e94f4d43128cc9f8902e Mon Sep 17 00:00:00 2001 From: Ken Date: Wed, 26 Mar 2025 16:51:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0token=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E5=88=B7=E6=96=B0=EF=BC=8C=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/providers/tts/aliyun.py | 86 ++++++++++++++----- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/main/xiaozhi-server/core/providers/tts/aliyun.py b/main/xiaozhi-server/core/providers/tts/aliyun.py index ea8627be..61ab4364 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun.py @@ -39,22 +39,22 @@ class AccessToken: 'Version': '2019-02-28'} # 构造规范化的请求字符串 query_string = AccessToken._encode_dict(parameters) - print('规范化的请求字符串: %s' % query_string) + # print('规范化的请求字符串: %s' % query_string) # 构造待签名字符串 string_to_sign = 'GET' + '&' + AccessToken._encode_text('/') + '&' + AccessToken._encode_text(query_string) - print('待签名的字符串: %s' % string_to_sign) + # print('待签名的字符串: %s' % string_to_sign) # 计算签名 secreted_string = hmac.new(bytes(access_key_secret + '&', encoding='utf-8'), bytes(string_to_sign, encoding='utf-8'), hashlib.sha1).digest() signature = base64.b64encode(secreted_string) - print('签名: %s' % signature) + # print('签名: %s' % signature) # 进行URL编码 signature = AccessToken._encode_text(signature) - print('URL编码后的签名: %s' % signature) + # print('URL编码后的签名: %s' % signature) # 调用服务 full_url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s' % (signature, query_string) - print('url: %s' % full_url) + # print('url: %s' % full_url) # 提交HTTP GET请求 response = requests.get(full_url) if response.ok: @@ -64,7 +64,7 @@ class AccessToken: token = root_obj[key]['Id'] expire_time = root_obj[key]['ExpireTime'] return token, expire_time - print(response.text) + # print(response.text) return None, None @@ -75,37 +75,80 @@ class TTSProvider(TTSProviderBase): super().__init__(config, delete_audio_file) # 新增空值判断逻辑 - access_key_id = config.get("access_key_id") - access_key_secret = config.get("access_key_secret") - if access_key_id and access_key_secret: - # 使用密钥对生成临时token - token, expire_time = AccessToken.create_token(access_key_id, access_key_secret) - else: - # 直接使用预生成的长期token - token = config.get("token") - expire_time = None - - print('token: %s, expire time(s): %s' % (token, expire_time)) + self.access_key_id = config.get("access_key_id") + self.access_key_secret = config.get("access_key_secret") self.appkey = config.get("appkey") - self.token = token self.format = config.get("format", "wav") self.sample_rate = config.get("sample_rate", 16000) self.voice = config.get("voice", "xiaoyun") self.volume = config.get("volume", 50) self.speech_rate = config.get("speech_rate", 0) self.pitch_rate = config.get("pitch_rate", 0) - self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com") self.api_url = f"https://{self.host}/stream/v1/tts" self.header = { "Content-Type": "application/json" } + if self.access_key_id and self.access_key_secret: + # 使用密钥对生成临时token + self._refresh_token() + else: + # 直接使用预生成的长期token + self.token = config.get("token") + self.expire_time = None + + + def _refresh_token(self): + """刷新Token并记录过期时间""" + if self.access_key_id and self.access_key_secret: + self.token, expire_time_str = AccessToken.create_token( + self.access_key_id, + self.access_key_secret + ) + if not expire_time_str: + raise ValueError("无法获取有效的Token过期时间") + + try: + #统一转换为字符串处理 + expire_str = str(expire_time_str).strip() + + if expire_str.isdigit(): + expire_time = datetime.fromtimestamp(int(expire_str)) + else: + expire_time = datetime.strptime( + expire_str, + "%Y-%m-%dT%H:%M:%SZ" + ) + self.expire_time = expire_time.timestamp() - 60 + except Exception as e: + raise ValueError(f"无效的过期时间格式: {expire_str}") from e + + else: + self.expire_time = None + + if not self.token: + raise ValueError("无法获取有效的访问Token") + + def _is_token_expired(self): + """检查Token是否过期""" + if not self.expire_time: + return False # 长期Token不过期 + # 新增调试日志 + # current_time = time.time() + # remaining = self.expire_time - current_time + # print(f"Token过期检查: 当前时间 {datetime.fromtimestamp(current_time)} | " + # f"过期时间 {datetime.fromtimestamp(self.expire_time)} | " + # f"剩余 {remaining:.2f}秒") + return time.time() > self.expire_time def generate_filename(self, extension=".wav"): return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}") async def text_to_speak(self, text, output_file): + if self._is_token_expired(): + logger.warning("Token已过期,正在自动刷新...") + self._refresh_token() request_json = { "appkey": self.appkey, "token": self.token, @@ -118,9 +161,12 @@ class TTSProvider(TTSProviderBase): "pitch_rate": self.pitch_rate } - print(self.api_url, json.dumps(request_json, ensure_ascii=False)) + # print(self.api_url, json.dumps(request_json, ensure_ascii=False)) try: resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header) + if resp.status_code == 401: # Token过期特殊处理 + self._refresh_token() + resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header) # 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的 if resp.headers['Content-Type'].startswith('audio/'): with open(output_file, 'wb') as f: