import os
|
import uuid
|
import json
|
import queue
|
import asyncio
|
import traceback
|
import websockets
|
|
from typing import Callable, Any
|
from config.logger import setup_logging
|
from core.utils.util import check_model_key
|
from core.providers.tts.base import TTSProviderBase
|
from core.utils.tts import MarkdownCleaner, convert_percentage_to_range
|
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType
|
|
|
TAG = __name__
|
logger = setup_logging()
|
|
PROTOCOL_VERSION = 0b0001
|
DEFAULT_HEADER_SIZE = 0b0001
|
|
# Message Type:
|
FULL_CLIENT_REQUEST = 0b0001
|
AUDIO_ONLY_RESPONSE = 0b1011
|
FULL_SERVER_RESPONSE = 0b1001
|
ERROR_INFORMATION = 0b1111
|
|
# Message Type Specific Flags
|
MsgTypeFlagNoSeq = 0b0000 # Non-terminal packet with no sequence
|
MsgTypeFlagPositiveSeq = 0b1 # Non-terminal packet with sequence > 0
|
MsgTypeFlagLastNoSeq = 0b10 # last packet with no sequence
|
MsgTypeFlagNegativeSeq = 0b11 # Payload contains event number (int32)
|
MsgTypeFlagWithEvent = 0b100
|
# Message Serialization
|
NO_SERIALIZATION = 0b0000
|
JSON = 0b0001
|
# Message Compression
|
COMPRESSION_NO = 0b0000
|
COMPRESSION_GZIP = 0b0001
|
|
EVENT_NONE = 0
|
EVENT_Start_Connection = 1
|
|
EVENT_FinishConnection = 2
|
|
EVENT_ConnectionStarted = 50 # 成功建连
|
|
EVENT_ConnectionFailed = 51 # 建连失败(可能是无法通过权限认证)
|
|
EVENT_ConnectionFinished = 52 # 连接结束
|
|
# 上行Session事件
|
EVENT_StartSession = 100
|
EVENT_CancelSession = 101
|
EVENT_FinishSession = 102
|
# 下行Session事件
|
EVENT_SessionStarted = 150
|
EVENT_SessionCanceled = 151
|
EVENT_SessionFinished = 152
|
|
EVENT_SessionFailed = 153
|
|
# 上行通用事件
|
EVENT_TaskRequest = 200
|
|
# 下行TTS事件
|
EVENT_TTSSentenceStart = 350
|
|
EVENT_TTSSentenceEnd = 351
|
|
EVENT_TTSResponse = 352
|
|
|
class Header:
|
def __init__(
|
self,
|
protocol_version=PROTOCOL_VERSION,
|
header_size=DEFAULT_HEADER_SIZE,
|
message_type: int = 0,
|
message_type_specific_flags: int = 0,
|
serial_method: int = NO_SERIALIZATION,
|
compression_type: int = COMPRESSION_NO,
|
reserved_data=0,
|
):
|
self.header_size = header_size
|
self.protocol_version = protocol_version
|
self.message_type = message_type
|
self.message_type_specific_flags = message_type_specific_flags
|
self.serial_method = serial_method
|
self.compression_type = compression_type
|
self.reserved_data = reserved_data
|
|
def as_bytes(self) -> bytes:
|
return bytes(
|
[
|
(self.protocol_version << 4) | self.header_size,
|
(self.message_type << 4) | self.message_type_specific_flags,
|
(self.serial_method << 4) | self.compression_type,
|
self.reserved_data,
|
]
|
)
|
|
|
class Optional:
|
def __init__(
|
self, event: int = EVENT_NONE, sessionId: str = None, sequence: int = None
|
):
|
self.event = event
|
self.sessionId = sessionId
|
self.errorCode: int = 0
|
self.connectionId: str | None = None
|
self.response_meta_json: str | None = None
|
self.sequence = sequence
|
|
# 转成 byte 序列
|
def as_bytes(self) -> bytes:
|
option_bytes = bytearray()
|
if self.event != EVENT_NONE:
|
option_bytes.extend(self.event.to_bytes(4, "big", signed=True))
|
if self.sessionId is not None:
|
session_id_bytes = str.encode(self.sessionId)
|
size = len(session_id_bytes).to_bytes(4, "big", signed=True)
|
option_bytes.extend(size)
|
option_bytes.extend(session_id_bytes)
|
if self.sequence is not None:
|
option_bytes.extend(self.sequence.to_bytes(4, "big", signed=True))
|
return option_bytes
|
|
|
class Response:
|
def __init__(self, header: Header, optional: Optional):
|
self.optional = optional
|
self.header = header
|
self.payload: bytes | None = None
|
|
def __str__(self):
|
return super().__str__()
|
|
|
class TTSProvider(TTSProviderBase):
|
def __init__(self, config, delete_audio_file):
|
super().__init__(config, delete_audio_file)
|
self.ws = None
|
self.interface_type = InterfaceType.DUAL_STREAM
|
self._monitor_task = None # 监听任务引用
|
self.appId = config.get("appid")
|
self.access_token = config.get("access_token")
|
self.cluster = config.get("cluster")
|
self.resource_id = config.get("resource_id")
|
self.resource_type = True if self.resource_id == "seed-tts-2.0" else False
|
self.report_on_last = self.resource_type
|
self.activate_session = False
|
if config.get("private_voice"):
|
self.voice = config.get("private_voice")
|
else:
|
self.voice = config.get("speaker")
|
|
# 默认 audio_params 配置
|
default_audio_params = {
|
"speech_rate": 0,
|
"loudness_rate": 0
|
}
|
|
# 默认 additions 配置
|
default_additions = {
|
"aigc_metadata": {},
|
"cache_config": {},
|
"post_process": {
|
"pitch": 0
|
}
|
}
|
|
# 默认 mix_speaker 配置
|
default_mix_speaker = {}
|
|
# 合并用户配置
|
self.audio_params = {**default_audio_params, **config.get("audio_params", {})}
|
self.additions = {**default_additions, **config.get("additions", {})}
|
self.mix_speaker = {**default_mix_speaker, **config.get("mix_speaker", {})}
|
|
# 应用百分比调整(如果存在),否则使用公有化配置
|
if "ttsVolume" in config:
|
self.audio_params["loudness_rate"] = int(convert_percentage_to_range(
|
config["ttsVolume"], min_val=-50, max_val=100, base_val=0
|
))
|
|
if "ttsRate" in config:
|
self.audio_params["speech_rate"] = int(convert_percentage_to_range(
|
config["ttsRate"], min_val=-50, max_val=100, base_val=0
|
))
|
|
if "ttsPitch" in config:
|
self.additions["post_process"]["pitch"] = int(convert_percentage_to_range(
|
config["ttsPitch"], min_val=-12, max_val=12, base_val=0
|
))
|
|
self.ws_url = config.get("ws_url")
|
self.authorization = config.get("authorization")
|
self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
|
enable_ws_reuse_value = config.get("enable_ws_reuse", True)
|
self.enable_ws_reuse = False if str(enable_ws_reuse_value).lower() == 'false' else True
|
self.tts_text = ""
|
|
model_key_msg = check_model_key("TTS", self.access_token)
|
if model_key_msg:
|
logger.bind(tag=TAG).error(model_key_msg)
|
|
async def open_audio_channels(self, conn):
|
try:
|
await super().open_audio_channels(conn)
|
# 更新 audio_params 中的采样率为实际的 conn.sample_rate
|
self.audio_params["sample_rate"] = conn.sample_rate
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"Failed to open audio channels: {str(e)}")
|
self.ws = None
|
raise
|
|
async def _ensure_connection(self):
|
"""建立新的WebSocket连接,并启动监听任务(仅第一次)"""
|
try:
|
if self.ws:
|
if self.enable_ws_reuse:
|
logger.bind(tag=TAG).debug(f"使用已有链接...")
|
return self.ws
|
else:
|
try:
|
await self.finish_connection()
|
except:
|
pass
|
logger.bind(tag=TAG).debug("开始建立新连接...")
|
|
# 建立新连接前取消旧监听任务
|
await self._cancel_monitor_task()
|
|
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
|
)
|
logger.bind(tag=TAG).debug("WebSocket连接建立成功")
|
|
# 连接建立成功后,启动监听任务
|
if self._monitor_task is None or self._monitor_task.done():
|
logger.bind(tag=TAG).debug("启动监听任务...")
|
self._monitor_task = asyncio.create_task(self._start_monitor_tts_response())
|
|
return self.ws
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"建立连接失败: {str(e)}")
|
self.ws = None
|
raise
|
|
async def finish_connection(self):
|
"""发送 FinishConnection 事件,等待服务端返回 EVENT_ConnectionFinished"""
|
try:
|
if self.ws:
|
logger.bind(tag=TAG).debug("开始关闭连接...")
|
header = Header(
|
message_type=FULL_CLIENT_REQUEST,
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
serial_method=JSON,
|
).as_bytes()
|
optional = Optional(event=EVENT_FinishConnection).as_bytes()
|
payload = str.encode("{}")
|
await self.send_event(self.ws, header, optional, payload)
|
except:
|
pass
|
|
def tts_text_priority_thread(self):
|
"""火山引擎双流式TTS的文本处理线程"""
|
while not self.conn.stop_event.is_set():
|
try:
|
message = self.tts_text_queue.get(timeout=1)
|
|
if self.conn.client_abort:
|
try:
|
logger.bind(tag=TAG).info("收到打断信息,终止TTS文本处理线程")
|
if self.enable_ws_reuse:
|
asyncio.run_coroutine_threadsafe(
|
self.cancel_session(self.conn.sentence_id),
|
loop=self.conn.loop,
|
)
|
else:
|
asyncio.run_coroutine_threadsafe(
|
self.finish_connection(),
|
loop=self.conn.loop,
|
)
|
continue
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"取消TTS会话失败: {str(e)}")
|
continue
|
|
# 过滤旧消息:检查sentence_id是否匹配
|
if message.sentence_id != self.conn.sentence_id:
|
continue
|
|
logger.bind(tag=TAG).debug(
|
f"收到TTS任务|{message.sentence_type.name} | {message.content_type.name} | 会话ID: {message.sentence_id}"
|
)
|
|
if message.sentence_type == SentenceType.FIRST:
|
# 重置流式处理状态
|
self.reset_stream_state()
|
# 初始化参数
|
try:
|
if not getattr(self.conn, "sentence_id", None):
|
self.conn.sentence_id = uuid.uuid4().hex
|
logger.bind(tag=TAG).debug(f"自动生成新的 会话ID: {self.conn.sentence_id}")
|
|
logger.bind(tag=TAG).debug("开始启动TTS会话...")
|
future = asyncio.run_coroutine_threadsafe(
|
self.start_session(self.conn.sentence_id),
|
loop=self.conn.loop,
|
)
|
future.result(timeout=self.tts_timeout)
|
self.before_stop_play_files.clear()
|
logger.bind(tag=TAG).debug("TTS会话启动成功")
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"启动TTS会话失败: {str(e)}")
|
continue
|
|
elif ContentType.TEXT == message.content_type:
|
if message.content_detail:
|
try:
|
logger.bind(tag=TAG).debug(
|
f"开始发送TTS文本: {message.content_detail}"
|
)
|
future = asyncio.run_coroutine_threadsafe(
|
self.text_to_speak(message.content_detail, None),
|
loop=self.conn.loop,
|
)
|
future.result(timeout=self.tts_timeout)
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
|
continue
|
|
elif ContentType.FILE == message.content_type:
|
logger.bind(tag=TAG).info(
|
f"添加音频文件到待播放列表: {message.content_file}"
|
)
|
if message.content_file and os.path.exists(message.content_file):
|
# 先处理文件音频数据
|
self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
|
if message.sentence_type == SentenceType.LAST:
|
try:
|
logger.bind(tag=TAG).debug("开始结束TTS会话...")
|
future = asyncio.run_coroutine_threadsafe(
|
self.finish_session(self.conn.sentence_id),
|
loop=self.conn.loop,
|
)
|
future.result(timeout=self.tts_timeout)
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"结束TTS会话失败: {str(e)}")
|
continue
|
|
except queue.Empty:
|
continue
|
except Exception as e:
|
logger.bind(tag=TAG).error(
|
f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}"
|
)
|
continue
|
|
async def text_to_speak(self, text, _):
|
"""发送文本到TTS服务"""
|
try:
|
# 建立新连接
|
if self.ws is None:
|
logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本")
|
return
|
|
# 过滤Markdown
|
filtered_text = MarkdownCleaner.clean_markdown(text)
|
|
if filtered_text:
|
# 使用滑动窗口匹配处理跨分片的替换词
|
confirmed_texts, self._pending_prefix = self._match_stream_text(filtered_text)
|
|
# 发送每个确定的文本片段
|
for txt in confirmed_texts:
|
if txt and self.ws:
|
await self.send_text(self.voice, txt, self.conn.sentence_id)
|
|
return
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
|
if self.ws:
|
try:
|
await self.ws.close()
|
except:
|
pass
|
self.ws = None
|
raise
|
|
async def start_session(self, session_id):
|
logger.bind(tag=TAG).debug(f"开始会话~~{session_id}")
|
try:
|
# 上个会话处于激活状态时关闭上个连接新建链接
|
if self.activate_session:
|
await self.close()
|
|
# 设置会话激活标志
|
self.activate_session = True
|
|
# 确保连接建立
|
await self._ensure_connection()
|
|
header = Header(
|
message_type=FULL_CLIENT_REQUEST,
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
serial_method=JSON,
|
).as_bytes()
|
optional = Optional(
|
event=EVENT_StartSession, sessionId=session_id
|
).as_bytes()
|
payload = self.get_payload_bytes(
|
event=EVENT_StartSession, speaker=self.voice
|
)
|
await self.send_event(self.ws, header, optional, payload)
|
logger.bind(tag=TAG).debug("会话启动请求已发送")
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"启动会话失败: {str(e)}")
|
# 确保清理资源
|
await self.close()
|
raise
|
|
async def finish_session(self, session_id):
|
logger.bind(tag=TAG).debug(f"关闭会话~~{session_id}")
|
try:
|
if self.ws:
|
header = Header(
|
message_type=FULL_CLIENT_REQUEST,
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
serial_method=JSON,
|
).as_bytes()
|
optional = Optional(
|
event=EVENT_FinishSession, sessionId=session_id
|
).as_bytes()
|
payload = str.encode("{}")
|
await self.send_event(self.ws, header, optional, payload)
|
logger.bind(tag=TAG).debug("会话结束请求已发送")
|
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"关闭会话失败: {str(e)}")
|
# 确保清理资源
|
await self.close()
|
raise
|
|
async def cancel_session(self,session_id):
|
logger.bind(tag=TAG).debug(f"取消会话,释放服务端资源~~{session_id}")
|
try:
|
if self.ws:
|
header = Header(
|
message_type=FULL_CLIENT_REQUEST,
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
serial_method=JSON,
|
).as_bytes()
|
optional = Optional(
|
event=EVENT_CancelSession, sessionId=session_id
|
).as_bytes()
|
payload = str.encode("{}")
|
await self.send_event(self.ws, header, optional, payload)
|
logger.bind(tag=TAG).debug("会话取消请求已发送")
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"取消会话失败: {str(e)}")
|
# 确保清理资源
|
await self.close()
|
raise
|
|
async def close(self):
|
"""资源清理方法"""
|
await super().close()
|
self.activate_session = False
|
await self._cancel_monitor_task()
|
|
if self.ws:
|
try:
|
await self.ws.close()
|
except:
|
pass
|
self.ws = None
|
|
async def _start_monitor_tts_response(self):
|
"""监听TTS响应 - 长期运行"""
|
try:
|
while not self.conn.stop_event.is_set():
|
try:
|
# 确保 `recv()` 运行在同一个 event loop
|
msg = await self.ws.recv()
|
res = self.parser_response(msg)
|
self.print_response(res, "send_text res:")
|
|
# 优先处理连接级别事件
|
if res.optional.event == EVENT_ConnectionFinished:
|
logger.bind(tag=TAG).debug(f"链接关闭成功~~")
|
break
|
|
# 只处理当前活跃会话的响应
|
if res.optional.sessionId and self.conn.sentence_id != res.optional.sessionId:
|
# 如果是会话结束相关事件,即使会话ID不匹配也要重置状态
|
if res.optional.event in [EVENT_SessionCanceled, EVENT_SessionFailed, EVENT_SessionFinished]:
|
logger.bind(tag=TAG).debug(f"收到残余下行结束响应重置会话状态~~")
|
self.activate_session = False
|
continue
|
|
if res.optional.event == EVENT_SessionCanceled:
|
logger.bind(tag=TAG).debug(f"释放服务端资源成功~~")
|
self.activate_session = False
|
elif not self.resource_type and res.optional.event == EVENT_TTSSentenceStart:
|
json_data = json.loads(res.payload.decode("utf-8"))
|
self.tts_text = json_data.get("text", "")
|
logger.bind(tag=TAG).debug(f"句子语音生成开始: {self.tts_text}")
|
# 将TTS服务返回的替换后文本还原为原始文本,用于字幕显示
|
display_text = self._restore_original_text(self.tts_text)
|
self.tts_audio_queue.put((SentenceType.FIRST, [], display_text))
|
elif (
|
res.optional.event == EVENT_TTSResponse
|
and res.header.message_type == AUDIO_ONLY_RESPONSE
|
):
|
# 处理seed-tts-2.0文本字幕
|
if self.resource_type:
|
tts_text = self.get_tts_text(self.conn.sentence_id)
|
if tts_text:
|
logger.bind(tag=TAG).info(
|
f"句子语音生成成功: {tts_text}"
|
)
|
self.tts_audio_queue.put(
|
(SentenceType.FIRST, [], tts_text)
|
)
|
self.clear_tts_text(self.conn.sentence_id)
|
self.wav_to_opus_data_audio_raw_stream(res.payload, callback=self.handle_opus)
|
elif not self.resource_type and res.optional.event == EVENT_TTSSentenceEnd:
|
logger.bind(tag=TAG).info(f"句子语音生成成功:{self.tts_text}")
|
elif res.optional.event == EVENT_SessionFinished:
|
logger.bind(tag=TAG).debug(f"会话结束~~")
|
self.activate_session = False
|
self._process_before_stop_play_files()
|
# 非复用模式下,会话结束后发送 FinishConnection
|
if not self.enable_ws_reuse:
|
await self.finish_connection()
|
except websockets.ConnectionClosed:
|
logger.bind(tag=TAG).warning("WebSocket连接已关闭")
|
break
|
except Exception as e:
|
logger.bind(tag=TAG).error(
|
f"Error in _start_monitor_tts_response: {e}"
|
)
|
traceback.print_exc()
|
break
|
# 连接异常时关闭WebSocket
|
if self.ws:
|
try:
|
await self.ws.close()
|
except:
|
pass
|
self.ws = None
|
# 监听任务退出时清理引用
|
finally:
|
self.activate_session = False
|
self._monitor_task = None
|
|
async def send_event(
|
self,
|
ws: websockets.WebSocketClientProtocol,
|
header: bytes,
|
optional: bytes | None = None,
|
payload: bytes = None,
|
):
|
try:
|
full_client_request = bytearray(header)
|
if optional is not None:
|
full_client_request.extend(optional)
|
if payload is not None:
|
payload_size = len(payload).to_bytes(4, "big", signed=True)
|
full_client_request.extend(payload_size)
|
full_client_request.extend(payload)
|
await ws.send(full_client_request)
|
except websockets.ConnectionClosed:
|
logger.bind(tag=TAG).error(f"ConnectionClosed")
|
raise
|
|
async def send_text(self, speaker: str, text: str, session_id):
|
header = Header(
|
message_type=FULL_CLIENT_REQUEST,
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
serial_method=JSON,
|
).as_bytes()
|
optional = Optional(event=EVENT_TaskRequest, sessionId=session_id).as_bytes()
|
payload = self.get_payload_bytes(
|
event=EVENT_TaskRequest, text=text, speaker=speaker
|
)
|
return await self.send_event(self.ws, header, optional, payload)
|
|
# 读取 res 数组某段 字符串内容
|
def read_res_content(self, res: bytes, offset: int):
|
content_size = int.from_bytes(res[offset : offset + 4], "big", signed=True)
|
offset += 4
|
content = res[offset : offset + content_size].decode('utf-8')
|
offset += content_size
|
return content, offset
|
|
# 读取 payload
|
def read_res_payload(self, res: bytes, offset: int):
|
payload_size = int.from_bytes(res[offset : offset + 4], "big", signed=True)
|
offset += 4
|
payload = res[offset : offset + payload_size]
|
offset += payload_size
|
return payload, offset
|
|
def parser_response(self, res) -> Response:
|
if isinstance(res, str):
|
raise RuntimeError(res)
|
response = Response(Header(), Optional())
|
# 解析结果
|
# header
|
header = response.header
|
num = 0b00001111
|
header.protocol_version = res[0] >> 4 & num
|
header.header_size = res[0] & 0x0F
|
header.message_type = (res[1] >> 4) & num
|
header.message_type_specific_flags = res[1] & 0x0F
|
header.serialization_method = res[2] >> num
|
header.message_compression = res[2] & 0x0F
|
header.reserved = res[3]
|
#
|
offset = 4
|
optional = response.optional
|
if header.message_type == FULL_SERVER_RESPONSE or AUDIO_ONLY_RESPONSE:
|
# read event
|
if header.message_type_specific_flags == MsgTypeFlagWithEvent:
|
optional.event = int.from_bytes(res[offset:8], "big", signed=True)
|
offset += 4
|
if optional.event == EVENT_NONE:
|
return response
|
# read connectionId
|
elif optional.event == EVENT_ConnectionStarted:
|
optional.connectionId, offset = self.read_res_content(res, offset)
|
elif optional.event == EVENT_ConnectionFailed:
|
optional.response_meta_json, offset = self.read_res_content(
|
res, offset
|
)
|
elif (
|
optional.event == EVENT_SessionStarted
|
or optional.event == EVENT_SessionFailed
|
or optional.event == EVENT_SessionFinished
|
):
|
optional.sessionId, offset = self.read_res_content(res, offset)
|
optional.response_meta_json, offset = self.read_res_content(
|
res, offset
|
)
|
else:
|
optional.sessionId, offset = self.read_res_content(res, offset)
|
response.payload, offset = self.read_res_payload(res, offset)
|
|
elif header.message_type == ERROR_INFORMATION:
|
optional.errorCode = int.from_bytes(
|
res[offset : offset + 4], "big", signed=True
|
)
|
offset += 4
|
response.payload, offset = self.read_res_payload(res, offset)
|
return response
|
|
async def start_connection(self):
|
header = Header(
|
message_type=FULL_CLIENT_REQUEST,
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
).as_bytes()
|
optional = Optional(event=EVENT_Start_Connection).as_bytes()
|
payload = str.encode("{}")
|
return await self.send_event(self.ws, header, optional, payload)
|
|
def print_response(self, res, tag_msg: str):
|
logger.bind(tag=TAG).debug(f"===>{tag_msg} header:{res.header.__dict__}")
|
logger.bind(tag=TAG).debug(f"===>{tag_msg} optional:{res.optional.__dict__}")
|
|
def get_payload_bytes(
|
self,
|
uid="1234",
|
event=EVENT_NONE,
|
text="",
|
speaker="",
|
audio_format="pcm",
|
):
|
# 构建 req_params
|
req_params = {
|
"text": text,
|
"speaker": speaker,
|
"audio_params": {**self.audio_params, "format": audio_format},
|
"additions": json.dumps(self.additions)
|
}
|
|
# 如果有 mix_speaker 配置,添加到 req_params
|
if self.mix_speaker:
|
req_params["mix_speaker"] = self.mix_speaker
|
|
return str.encode(
|
json.dumps(
|
{
|
"user": {"uid": uid},
|
"event": event,
|
"namespace": "BidirectionalTTS",
|
"req_params": req_params
|
}
|
)
|
)
|
|
def audio_to_opus_data_stream(
|
self, audio_file_path, callback: Callable[[Any], Any] = None
|
):
|
"""重写父类方法:使用独立的临时编码器处理音频文件,避免与TTS流式编码器并发冲突。
|
双流式TTS中,monitor任务在event loop线程接收TTS音频并使用self.opus_encoder编码,
|
同时tts_text_priority_thread处理音乐文件也使用self.opus_encoder,
|
共享的encoder.buffer非线程安全,并发访问会导致SILK resampler断言失败。
|
"""
|
from core.utils.util import audio_to_data_stream
|
return audio_to_data_stream(
|
audio_file_path, is_opus=True, callback=callback,
|
sample_rate=self.conn.sample_rate, opus_encoder=None
|
)
|
|
def wav_to_opus_data_audio_raw_stream(self, raw_data_var, is_end=False, callback: Callable[[Any], Any]=None):
|
return self.opus_encoder.encode_pcm_to_opus_stream(raw_data_var, is_end, callback=callback)
|
|
async def _cancel_monitor_task(self):
|
"""取消监听任务"""
|
if self._monitor_task and not self._monitor_task.done():
|
self._monitor_task.cancel()
|
try:
|
await self._monitor_task
|
except asyncio.CancelledError:
|
pass
|
except Exception as e:
|
logger.bind(tag=TAG).warning(f"取消监听任务错误: {e}")
|
self._monitor_task = None
|
|
def to_tts(self, text: str) -> list:
|
"""非流式生成音频数据,用于生成音频及测试场景
|
Args:
|
text: 要转换的文本
|
Returns:
|
list: 音频数据列表
|
"""
|
try:
|
# 创建事件循环
|
loop = asyncio.new_event_loop()
|
asyncio.set_event_loop(loop)
|
|
# 生成会话ID
|
session_id = uuid.uuid4().__str__().replace("-", "")
|
|
# 存储音频数据
|
audio_data = []
|
|
async def _generate_audio():
|
# 创建新的WebSocket连接
|
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(),
|
}
|
ws = await websockets.connect(
|
self.ws_url, additional_headers=ws_header, max_size=1000000000
|
)
|
|
try:
|
# 启动会话
|
header = Header(
|
message_type=FULL_CLIENT_REQUEST,
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
serial_method=JSON,
|
).as_bytes()
|
optional = Optional(
|
event=EVENT_StartSession, sessionId=session_id
|
).as_bytes()
|
payload = self.get_payload_bytes(
|
event=EVENT_StartSession, speaker=self.voice
|
)
|
await self.send_event(ws, header, optional, payload)
|
|
# 发送文本
|
header = Header(
|
message_type=FULL_CLIENT_REQUEST,
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
serial_method=JSON,
|
).as_bytes()
|
optional = Optional(
|
event=EVENT_TaskRequest, sessionId=session_id
|
).as_bytes()
|
payload = self.get_payload_bytes(
|
event=EVENT_TaskRequest, text=text, speaker=self.voice
|
)
|
await self.send_event(ws, header, optional, payload)
|
|
# 发送结束会话请求
|
header = Header(
|
message_type=FULL_CLIENT_REQUEST,
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
serial_method=JSON,
|
).as_bytes()
|
optional = Optional(
|
event=EVENT_FinishSession, sessionId=session_id
|
).as_bytes()
|
payload = str.encode("{}")
|
await self.send_event(ws, header, optional, payload)
|
|
# 接收音频数据
|
while True:
|
msg = await ws.recv()
|
res = self.parser_response(msg)
|
|
if (
|
res.optional.event == EVENT_TTSResponse
|
and res.header.message_type == AUDIO_ONLY_RESPONSE
|
):
|
self.wav_to_opus_data_audio_raw_stream(res.payload, callback=lambda opus_frame: audio_data.append(opus_frame))
|
elif res.optional.event == EVENT_SessionFinished:
|
break
|
|
finally:
|
# 清理资源
|
try:
|
await ws.close()
|
except:
|
pass
|
|
# 运行异步任务
|
loop.run_until_complete(_generate_audio())
|
loop.close()
|
|
return audio_data
|
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"生成音频数据失败: {str(e)}")
|
return []
|