update:优化公共方法

This commit is contained in:
hrz
2025-05-26 11:57:55 +08:00
parent f78f6fc529
commit 6dda79ee10
12 changed files with 119 additions and 150 deletions
@@ -155,12 +155,6 @@ class ASRProvider(ASRProviderBase):
# f"剩余 {remaining:.2f}秒") # f"剩余 {remaining:.2f}秒")
return time.time() > self.expire_time 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}",
)
def _construct_request_url(self) -> str: def _construct_request_url(self) -> str:
"""构造请求URL,包含参数""" """构造请求URL,包含参数"""
request = f"{self.base_url}?appkey={self.app_key}" request = f"{self.base_url}?appkey={self.app_key}"
@@ -8,61 +8,74 @@ import requests
from datetime import datetime from datetime import datetime
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
import http.client
import urllib.parse
import time import time
import uuid import uuid
from urllib import parse from urllib import parse
class AccessToken: class AccessToken:
@staticmethod @staticmethod
def _encode_text(text): def _encode_text(text):
encoded_text = parse.quote_plus(text) encoded_text = parse.quote_plus(text)
return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') return encoded_text.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
@staticmethod @staticmethod
def _encode_dict(dic): def _encode_dict(dic):
keys = dic.keys() keys = dic.keys()
dic_sorted = [(key, dic[key]) for key in sorted(keys)] dic_sorted = [(key, dic[key]) for key in sorted(keys)]
encoded_text = parse.urlencode(dic_sorted) encoded_text = parse.urlencode(dic_sorted)
return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') return encoded_text.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
@staticmethod @staticmethod
def create_token(access_key_id, access_key_secret): def create_token(access_key_id, access_key_secret):
parameters = {'AccessKeyId': access_key_id, parameters = {
'Action': 'CreateToken', "AccessKeyId": access_key_id,
'Format': 'JSON', "Action": "CreateToken",
'RegionId': 'cn-shanghai', "Format": "JSON",
'SignatureMethod': 'HMAC-SHA1', "RegionId": "cn-shanghai",
'SignatureNonce': str(uuid.uuid1()), "SignatureMethod": "HMAC-SHA1",
'SignatureVersion': '1.0', "SignatureNonce": str(uuid.uuid1()),
'Timestamp': time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "SignatureVersion": "1.0",
'Version': '2019-02-28'} "Timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"Version": "2019-02-28",
}
# 构造规范化的请求字符串 # 构造规范化的请求字符串
query_string = AccessToken._encode_dict(parameters) 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) 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'), secreted_string = hmac.new(
bytes(string_to_sign, encoding='utf-8'), bytes(access_key_secret + "&", encoding="utf-8"),
hashlib.sha1).digest() bytes(string_to_sign, encoding="utf-8"),
hashlib.sha1,
).digest()
signature = base64.b64encode(secreted_string) signature = base64.b64encode(secreted_string)
# print('签名: %s' % signature) # print('签名: %s' % signature)
# 进行URL编码 # 进行URL编码
signature = AccessToken._encode_text(signature) 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) 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请求 # 提交HTTP GET请求
response = requests.get(full_url) response = requests.get(full_url)
if response.ok: if response.ok:
root_obj = response.json() root_obj = response.json()
key = 'Token' key = "Token"
if key in root_obj: if key in root_obj:
token = root_obj[key]['Id'] token = root_obj[key]["Id"]
expire_time = root_obj[key]['ExpireTime'] expire_time = root_obj[key]["ExpireTime"]
return token, expire_time return token, expire_time
# print(response.text) # print(response.text)
return None, None return None, None
@@ -70,10 +83,9 @@ class AccessToken:
class TTSProvider(TTSProviderBase): class TTSProvider(TTSProviderBase):
def __init__(self, config, delete_audio_file): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
# 新增空值判断逻辑 # 新增空值判断逻辑
self.access_key_id = config.get("access_key_id") self.access_key_id = config.get("access_key_id")
self.access_key_secret = config.get("access_key_secret") self.access_key_secret = config.get("access_key_secret")
@@ -87,9 +99,7 @@ class TTSProvider(TTSProviderBase):
self.pitch_rate = config.get("pitch_rate", 0) self.pitch_rate = config.get("pitch_rate", 0)
self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com") self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com")
self.api_url = f"https://{self.host}/stream/v1/tts" self.api_url = f"https://{self.host}/stream/v1/tts"
self.header = { self.header = {"Content-Type": "application/json"}
"Content-Type": "application/json"
}
if self.access_key_id and self.access_key_secret: if self.access_key_id and self.access_key_secret:
# 使用密钥对生成临时token # 使用密钥对生成临时token
@@ -99,35 +109,30 @@ class TTSProvider(TTSProviderBase):
self.token = config.get("token") self.token = config.get("token")
self.expire_time = None self.expire_time = None
def _refresh_token(self): def _refresh_token(self):
"""刷新Token并记录过期时间""" """刷新Token并记录过期时间"""
if self.access_key_id and self.access_key_secret: if self.access_key_id and self.access_key_secret:
self.token, expire_time_str = AccessToken.create_token( self.token, expire_time_str = AccessToken.create_token(
self.access_key_id, self.access_key_id, self.access_key_secret
self.access_key_secret
) )
if not expire_time_str: if not expire_time_str:
raise ValueError("无法获取有效的Token过期时间") raise ValueError("无法获取有效的Token过期时间")
try: try:
#统一转换为字符串处理 # 统一转换为字符串处理
expire_str = str(expire_time_str).strip() expire_str = str(expire_time_str).strip()
if expire_str.isdigit(): if expire_str.isdigit():
expire_time = datetime.fromtimestamp(int(expire_str)) expire_time = datetime.fromtimestamp(int(expire_str))
else: else:
expire_time = datetime.strptime( expire_time = datetime.strptime(expire_str, "%Y-%m-%dT%H:%M:%SZ")
expire_str,
"%Y-%m-%dT%H:%M:%SZ"
)
self.expire_time = expire_time.timestamp() - 60 self.expire_time = expire_time.timestamp() - 60
except Exception as e: except Exception as e:
raise ValueError(f"无效的过期时间格式: {expire_str}") from e raise ValueError(f"无效的过期时间格式: {expire_str}") from e
else: else:
self.expire_time = None self.expire_time = None
if not self.token: if not self.token:
raise ValueError("无法获取有效的访问Token") raise ValueError("无法获取有效的访问Token")
@@ -142,8 +147,6 @@ class TTSProvider(TTSProviderBase):
# f"过期时间 {datetime.fromtimestamp(self.expire_time)} | " # f"过期时间 {datetime.fromtimestamp(self.expire_time)} | "
# f"剩余 {remaining:.2f}秒") # f"剩余 {remaining:.2f}秒")
return time.time() > self.expire_time 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): async def text_to_speak(self, text, output_file):
if self._is_token_expired(): if self._is_token_expired():
@@ -158,21 +161,27 @@ class TTSProvider(TTSProviderBase):
"voice": self.voice, "voice": self.voice,
"volume": self.volume, "volume": self.volume,
"speech_rate": self.speech_rate, "speech_rate": self.speech_rate,
"pitch_rate": self.pitch_rate "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: try:
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header) resp = requests.post(
self.api_url, json.dumps(request_json), headers=self.header
)
if resp.status_code == 401: # Token过期特殊处理 if resp.status_code == 401: # Token过期特殊处理
self._refresh_token() self._refresh_token()
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header) resp = requests.post(
self.api_url, json.dumps(request_json), headers=self.header
)
# 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的 # 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的
if resp.headers['Content-Type'].startswith('audio/'): if resp.headers["Content-Type"].startswith("audio/"):
with open(output_file, 'wb') as f: with open(output_file, "wb") as f:
f.write(resp.content) f.write(resp.content)
return output_file return output_file
else: else:
raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}") raise Exception(
f"{__name__} status_code: {resp.status_code} response: {resp.content}"
)
except Exception as e: except Exception as e:
raise Exception(f"{__name__} error: {e}") raise Exception(f"{__name__} error: {e}")
@@ -3,6 +3,7 @@ from config.logger import setup_logging
import queue import queue
import os import os
import uuid import uuid
import datetime
import threading import threading
from core.utils import p3 from core.utils import p3
from core.utils import textUtils from core.utils import textUtils
@@ -57,9 +58,11 @@ class TTSProviderBase(ABC):
self.processed_chars = 0 self.processed_chars = 0
self.is_first_sentence = True self.is_first_sentence = True
@abstractmethod def generate_filename(self, extension=".wav"):
def generate_filename(self): return os.path.join(
pass self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
def to_tts(self, text): def to_tts(self, text):
tmp_file = self.generate_filename() tmp_file = self.generate_filename()
@@ -148,7 +151,7 @@ class TTSProviderBase(ABC):
self.tts_timeout = conn.config.get("tts_timeout", 10) self.tts_timeout = conn.config.get("tts_timeout", 10)
# tts 消化线程 # tts 消化线程
self.tts_priority_thread = threading.Thread( self.tts_priority_thread = threading.Thread(
target=self._tts_text_priority_thread, daemon=True target=self.tts_text_priority_thread, daemon=True
) )
self.tts_priority_thread.start() self.tts_priority_thread.start()
@@ -160,7 +163,7 @@ class TTSProviderBase(ABC):
# 这里默认是非流式的处理方式 # 这里默认是非流式的处理方式
# 流式处理方式请在子类中重写 # 流式处理方式请在子类中重写
def _tts_text_priority_thread(self): def tts_text_priority_thread(self):
while not self.conn.stop_event.is_set(): while not self.conn.stop_event.is_set():
try: try:
message = self.tts_text_queue.get(timeout=1) message = self.tts_text_queue.get(timeout=1)
@@ -219,9 +222,6 @@ class TTSProviderBase(ABC):
self.conn.loop, self.conn.loop,
) )
result = future.result() result = future.result()
logger.bind(tag=TAG).info(
f"audio_play_priority result: {text} {result}"
)
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error( logger.bind(tag=TAG).error(
f"audio_play_priority priority_thread: {text} {e}" f"audio_play_priority priority_thread: {text} {e}"
@@ -21,12 +21,6 @@ class TTSProvider(TTSProviderBase):
self.host = "api.coze.cn" self.host = "api.coze.cn"
self.api_url = f"https://{self.host}/v1/audio/speech" self.api_url = f"https://{self.host}/v1/audio/speech"
def generate_filename(self, extension=".wav"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
request_json = { request_json = {
"model": self.model, "model": self.model,
@@ -47,4 +41,4 @@ class TTSProvider(TTSProviderBase):
file_to_save = open(output_file, "wb") file_to_save = open(output_file, "wb")
file_to_save.write(data) file_to_save.write(data)
except Exception as e: except Exception as e:
raise Exception(f"{__name__} error: {e}") raise Exception(f"{__name__} error: {e}")
@@ -41,12 +41,6 @@ class TTSProvider(TTSProviderBase):
self.header = {"Authorization": f"{self.authorization}{self.access_token}"} self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
check_model_key("TTS", self.access_token) check_model_key("TTS", self.access_token)
def generate_filename(self, extension=".wav"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
request_json = { request_json = {
"app": { "app": {
@@ -133,12 +133,6 @@ class TTSProvider(TTSProviderBase):
self.seed = int(config.get("seed")) if config.get("seed") else None self.seed = int(config.get("seed")) if config.get("seed") else None
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts") self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
def generate_filename(self, extension=".wav"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
# Prepare reference data # Prepare reference data
byte_audios = [audio_to_bytes(ref_audio) for ref_audio in self.reference_audio] byte_audios = [audio_to_bytes(ref_audio) for ref_audio in self.reference_audio]
@@ -71,12 +71,6 @@ class TTSProvider(TTSProviderBase):
config.get("aux_ref_audio_paths") config.get("aux_ref_audio_paths")
) )
def generate_filename(self, extension=".wav"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
request_json = { request_json = {
"text": text, "text": text,
@@ -36,12 +36,6 @@ class TTSProvider(TTSProviderBase):
self.inp_refs = parse_string_to_list(config.get("inp_refs")) self.inp_refs = parse_string_to_list(config.get("inp_refs"))
self.if_sr = str(config.get("if_sr", False)).lower() in ("true", "1", "yes") self.if_sr = str(config.get("if_sr", False)).lower() in ("true", "1", "yes")
def generate_filename(self, extension=".wav"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
request_params = { request_params = {
"refer_wav_path": self.refer_wav_path, "refer_wav_path": self.refer_wav_path,
@@ -67,4 +61,3 @@ class TTSProvider(TTSProviderBase):
error_msg = f"GPT_SoVITS_V3 TTS请求失败: {resp.status_code} - {resp.text}" error_msg = f"GPT_SoVITS_V3 TTS请求失败: {resp.status_code} - {resp.text}"
logger.bind(tag=TAG).error(error_msg) logger.bind(tag=TAG).error(error_msg)
raise Exception(error_msg) raise Exception(error_msg)
@@ -8,6 +8,7 @@ import websockets
from datetime import datetime from datetime import datetime
from config.logger import setup_logging from config.logger import setup_logging
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, ContentType
from core.utils.util import pcm_to_data from core.utils.util import pcm_to_data
TAG = __name__ TAG = __name__
@@ -149,43 +150,6 @@ class TTSProvider(TTSProviderBase):
self.start_connection_flag = False self.start_connection_flag = False
self.tts_text = "" self.tts_text = ""
async def open_audio_channels(self, conn):
await super().open_audio_channels(conn)
ws_header = {
"X-Api-App-Key": self.appId,
"X-Api-Access-Key": self.access_token,
"X-Api-Resource-Id": self.resource_id,
"X-Api-Connect-Id": uuid.uuid4(),
}
self.ws = await websockets.connect(
self.ws_url, additional_headers=ws_header, max_size=1000000000
)
tts_priority = threading.Thread(
target=self._start_monitor_tts_response_thread, daemon=True
)
tts_priority.start()
def generate_filename(self, extension=".wav"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
def to_tts(self, text):
future = asyncio.run_coroutine_threadsafe(
self.start_session(self.conn.sentence_id), loop=self.conn.loop
)
future.result()
future = asyncio.run_coroutine_threadsafe(
self.text_to_speak(text, None), loop=self.conn.loop
)
future.result()
future = asyncio.run_coroutine_threadsafe(
self.finish_session(self.conn.sentence_id), loop=self.conn.loop
)
future.result()
return self.interface_type.value
async def send_event( async def send_event(
self, header: bytes, optional: bytes | None = None, payload: bytes = None self, header: bytes, optional: bytes | None = None, payload: bytes = None
): ):
@@ -360,14 +324,65 @@ class TTSProvider(TTSProviderBase):
self.start_connection_flag = False self.start_connection_flag = False
await self.start_connection() await self.start_connection()
self.start_connection_flag = True self.start_connection_flag = True
await super().reset()
async def close(self): async def close(self):
super().close()
"""资源清理方法""" """资源清理方法"""
await self.finish_connection() await self.finish_connection()
await self.ws.close() await self.ws.close()
###################################################################################
# 以下是火山双流式TTS重写父类的方法
###################################################################################
async def open_audio_channels(self, conn):
await super().open_audio_channels(conn)
ws_header = {
"X-Api-App-Key": self.appId,
"X-Api-Access-Key": self.access_token,
"X-Api-Resource-Id": self.resource_id,
"X-Api-Connect-Id": uuid.uuid4(),
}
self.ws = await websockets.connect(
self.ws_url, additional_headers=ws_header, max_size=1000000000
)
tts_priority = threading.Thread(
target=self._start_monitor_tts_response_thread, daemon=True
)
tts_priority.start()
def tts_text_priority_thread(self):
while not self.conn.stop_event.is_set():
try:
message = self.tts_text_queue.get(timeout=1)
if message.sentence_type == SentenceType.FIRST:
# 初始化参数
future = asyncio.run_coroutine_threadsafe(
self.start_session(self.conn.sentence_id), loop=self.conn.loop
)
future.result()
elif ContentType.TEXT == message.content_type:
if message.content_detail:
future = asyncio.run_coroutine_threadsafe(
self.text_to_speak(message.content_detail, None),
loop=self.conn.loop,
)
future.result()
elif ContentType.FILE == message.content_type:
pass
if message.sentence_type == SentenceType.LAST:
future = asyncio.run_coroutine_threadsafe(
self.finish_session(self.conn.sentence_id), loop=self.conn.loop
)
future.result()
except queue.Empty:
continue
except Exception as e:
logger.bind(tag=TAG).error(
f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}"
)
async def text_to_speak(self, text, _): async def text_to_speak(self, text, _):
# 发送文本 # 发送文本
await self.send_text(self.speaker, text, self.conn.sentence_id) await self.send_text(self.speaker, text, self.conn.sentence_id)
@@ -29,12 +29,6 @@ class TTSProvider(TTSProviderBase):
self.output_file = config.get("output_dir", "tmp/") self.output_file = config.get("output_dir", "tmp/")
check_model_key("TTS", self.api_key) check_model_key("TTS", self.api_key)
def generate_filename(self, extension=".wav"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
headers = { headers = {
"Authorization": f"Bearer {self.api_key}", "Authorization": f"Bearer {self.api_key}",
@@ -22,12 +22,6 @@ class TTSProvider(TTSProviderBase):
self.host = "api.siliconflow.cn" self.host = "api.siliconflow.cn"
self.api_url = f"https://{self.host}/v1/audio/speech" self.api_url = f"https://{self.host}/v1/audio/speech"
def generate_filename(self, extension=".wav"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
request_json = { request_json = {
"model": self.model, "model": self.model,
@@ -47,4 +41,4 @@ class TTSProvider(TTSProviderBase):
file_to_save = open(output_file, "wb") file_to_save = open(output_file, "wb")
file_to_save.write(data) file_to_save.write(data)
except Exception as e: except Exception as e:
raise Exception(f"{__name__} error: {e}") raise Exception(f"{__name__} error: {e}")
@@ -121,12 +121,6 @@ class TTSProvider(TTSProviderBase):
msg = msg.encode("utf-8") msg = msg.encode("utf-8")
return hmac.new(key, msg, hashlib.sha256).digest() return hmac.new(key, msg, hashlib.sha256).digest()
def generate_filename(self, extension=".wav"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
# 构建请求体 # 构建请求体
request_json = { request_json = {