sendAudioHandle.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import json
  2. import time
  3. import asyncio
  4. from typing import TYPE_CHECKING
  5. if TYPE_CHECKING:
  6. from core.connection import ConnectionHandler
  7. from core.utils import textUtils
  8. from core.utils.util import audio_to_data
  9. from core.providers.tts.dto.dto import SentenceType
  10. from core.utils.audioRateController import AudioRateController
  11. TAG = __name__
  12. # 音频帧时长(毫秒)
  13. AUDIO_FRAME_DURATION = 60
  14. # 预缓冲包数量,直接发送以减少延迟
  15. PRE_BUFFER_COUNT = 5
  16. async def sendAudioMessage(conn: "ConnectionHandler", sentenceType, audios, text, sentence_id=None):
  17. # 跳过旧句子残留音频
  18. if sentence_id is not None and sentence_id != conn.sentence_id:
  19. return
  20. if conn.tts.tts_audio_first_sentence:
  21. conn.logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
  22. conn.tts.tts_audio_first_sentence = False
  23. if sentenceType == SentenceType.FIRST:
  24. # 同一句子的后续消息加入流控队列,其他情况立即发送
  25. if (
  26. hasattr(conn, "audio_rate_controller")
  27. and conn.audio_rate_controller
  28. and getattr(conn, "audio_flow_control", {}).get("sentence_id")
  29. == conn.sentence_id
  30. ):
  31. conn.audio_rate_controller.add_message(
  32. lambda: send_tts_message(conn, "sentence_start", text)
  33. )
  34. else:
  35. # 新句子或流控器未初始化,立即发送
  36. await send_tts_message(conn, "sentence_start", text)
  37. await sendAudio(conn, audios)
  38. # 发送句子开始消息
  39. if sentenceType is not SentenceType.MIDDLE:
  40. conn.logger.bind(tag=TAG).info(f"发送音频消息: {sentenceType}, {text}")
  41. # 发送结束消息(如果是最后一个文本)
  42. if sentenceType == SentenceType.LAST:
  43. await send_tts_message(conn, "stop", None)
  44. if conn.close_after_chat:
  45. await conn.close()
  46. async def _wait_for_audio_completion(conn: "ConnectionHandler"):
  47. """
  48. 等待音频队列清空并等待预缓冲包播放完成
  49. Args:
  50. conn: 连接对象
  51. """
  52. if hasattr(conn, "audio_rate_controller") and conn.audio_rate_controller:
  53. rate_controller = conn.audio_rate_controller
  54. conn.logger.bind(tag=TAG).debug(
  55. f"等待音频发送完成,队列中还有 {len(rate_controller.queue)} 个包"
  56. )
  57. await rate_controller.queue_empty_event.wait()
  58. # 等待预缓冲包播放完成
  59. # 前N个包直接发送,增加2个网络抖动包,需要额外等待它们在客户端播放完成
  60. frame_duration_ms = rate_controller.frame_duration
  61. pre_buffer_playback_time = (PRE_BUFFER_COUNT + 2) * frame_duration_ms / 1000.0
  62. await asyncio.sleep(pre_buffer_playback_time)
  63. conn.logger.bind(tag=TAG).debug("音频发送完成")
  64. async def _send_to_mqtt_gateway(
  65. conn: "ConnectionHandler", opus_packet, timestamp, sequence
  66. ):
  67. """
  68. 发送带16字节头部的opus数据包给mqtt_gateway
  69. Args:
  70. conn: 连接对象
  71. opus_packet: opus数据包
  72. timestamp: 时间戳
  73. sequence: 序列号
  74. """
  75. # 为opus数据包添加16字节头部
  76. header = bytearray(16)
  77. header[0] = 1 # type
  78. header[2:4] = len(opus_packet).to_bytes(2, "big") # payload length
  79. header[4:8] = sequence.to_bytes(4, "big") # sequence
  80. header[8:12] = timestamp.to_bytes(4, "big") # 时间戳
  81. header[12:16] = len(opus_packet).to_bytes(4, "big") # opus长度
  82. # 发送包含头部的完整数据包
  83. complete_packet = bytes(header) + opus_packet
  84. await conn.websocket.send(complete_packet)
  85. async def sendAudio(
  86. conn: "ConnectionHandler", audios, frame_duration=AUDIO_FRAME_DURATION
  87. ):
  88. """
  89. 发送音频包,使用 AudioRateController 进行精确的流量控制
  90. Args:
  91. conn: 连接对象
  92. audios: 单个opus包(bytes) 或 opus包列表
  93. frame_duration: 帧时长(毫秒),默认使用全局常量AUDIO_FRAME_DURATION
  94. """
  95. if audios is None or len(audios) == 0:
  96. return
  97. send_delay = conn.config.get("tts_audio_send_delay", -1) / 1000.0
  98. is_single_packet = isinstance(audios, bytes)
  99. # 初始化或获取 RateController
  100. rate_controller, flow_control = _get_or_create_rate_controller(
  101. conn, frame_duration, is_single_packet
  102. )
  103. # 统一转换为列表处理
  104. audio_list = [audios] if is_single_packet else audios
  105. # 发送音频包
  106. await _send_audio_with_rate_control(
  107. conn, audio_list, rate_controller, flow_control, send_delay
  108. )
  109. def _get_or_create_rate_controller(
  110. conn: "ConnectionHandler", frame_duration, is_single_packet
  111. ):
  112. """
  113. 获取或创建 RateController 和 flow_control
  114. Args:
  115. conn: 连接对象
  116. frame_duration: 帧时长
  117. is_single_packet: 是否单包模式(True: TTS流式单包, False: 批量包)
  118. Returns:
  119. (rate_controller, flow_control)
  120. """
  121. # 检查是否需要重置控制器
  122. need_reset = False
  123. if not hasattr(conn, "audio_rate_controller"):
  124. # 控制器不存在,需要创建
  125. need_reset = True
  126. else:
  127. rate_controller = conn.audio_rate_controller
  128. # 后台发送任务已停止, 则需要重置
  129. if (
  130. not rate_controller.pending_send_task
  131. or rate_controller.pending_send_task.done()
  132. ):
  133. need_reset = True
  134. # 当sentence_id 变化,需要重置
  135. elif (
  136. getattr(conn, "audio_flow_control", {}).get("sentence_id")
  137. != conn.sentence_id
  138. ):
  139. need_reset = True
  140. if need_reset:
  141. # 创建或获取 rate_controller
  142. if not hasattr(conn, "audio_rate_controller"):
  143. conn.audio_rate_controller = AudioRateController(frame_duration)
  144. else:
  145. conn.audio_rate_controller.reset()
  146. # 初始化 flow_control
  147. conn.audio_flow_control = {
  148. "packet_count": 0,
  149. "sequence": 0,
  150. "sentence_id": conn.sentence_id,
  151. }
  152. # 启动后台发送循环
  153. _start_background_sender(
  154. conn, conn.audio_rate_controller, conn.audio_flow_control
  155. )
  156. return conn.audio_rate_controller, conn.audio_flow_control
  157. def _start_background_sender(conn: "ConnectionHandler", rate_controller, flow_control):
  158. """
  159. 启动后台发送循环任务
  160. Args:
  161. conn: 连接对象
  162. rate_controller: 速率控制器
  163. flow_control: 流控状态
  164. """
  165. async def send_callback(packet):
  166. # 检查是否应该中止
  167. if conn.client_abort:
  168. raise asyncio.CancelledError("客户端已中止")
  169. conn.last_activity_time = time.time() * 1000
  170. await _do_send_audio(conn, packet, flow_control)
  171. # 使用 start_sending 启动后台循环
  172. rate_controller.start_sending(send_callback)
  173. async def _send_audio_with_rate_control(
  174. conn: "ConnectionHandler", audio_list, rate_controller, flow_control, send_delay
  175. ):
  176. """
  177. 使用 rate_controller 发送音频包
  178. Args:
  179. conn: 连接对象
  180. audio_list: 音频包列表
  181. rate_controller: 速率控制器
  182. flow_control: 流控状态
  183. send_delay: 固定延迟(秒),-1表示使用动态流控
  184. """
  185. for packet in audio_list:
  186. if conn.client_abort:
  187. return
  188. conn.last_activity_time = time.time() * 1000
  189. # 预缓冲:前N个包直接发送
  190. if flow_control["packet_count"] < PRE_BUFFER_COUNT:
  191. await _do_send_audio(conn, packet, flow_control)
  192. elif send_delay > 0:
  193. # 固定延迟模式
  194. await asyncio.sleep(send_delay)
  195. await _do_send_audio(conn, packet, flow_control)
  196. else:
  197. # 动态流控模式:仅添加到队列,由后台循环负责发送
  198. rate_controller.add_audio(packet)
  199. async def _do_send_audio(conn: "ConnectionHandler", opus_packet, flow_control):
  200. """
  201. 执行实际的音频发送
  202. """
  203. packet_index = flow_control.get("packet_count", 0)
  204. sequence = flow_control.get("sequence", 0)
  205. if conn.conn_from_mqtt_gateway:
  206. # 计算时间戳(基于播放位置)
  207. start_time = time.time()
  208. timestamp = int(start_time * 1000) % (2**32)
  209. await _send_to_mqtt_gateway(conn, opus_packet, timestamp, sequence)
  210. else:
  211. # 直接发送opus数据包
  212. await conn.websocket.send(opus_packet)
  213. # 更新流控状态
  214. flow_control["packet_count"] = packet_index + 1
  215. flow_control["sequence"] = sequence + 1
  216. async def send_tts_message(conn: "ConnectionHandler", state, text=None):
  217. """发送 TTS 状态消息"""
  218. if text is None and state == "sentence_start":
  219. return
  220. message = {"type": "tts", "state": state, "session_id": conn.session_id}
  221. if text is not None:
  222. message["text"] = textUtils.check_emoji(text)
  223. # TTS播放结束
  224. if state == "stop":
  225. # 保存当前的 sentence_id,用于后续判断是否是当前轮次
  226. current_sentence_id = conn.sentence_id
  227. # 播放提示音
  228. tts_notify = conn.config.get("enable_stop_tts_notify", False)
  229. if tts_notify:
  230. stop_tts_notify_voice = conn.config.get(
  231. "stop_tts_notify_voice", "config/assets/tts_notify.mp3"
  232. )
  233. audios = await audio_to_data(stop_tts_notify_voice, is_opus=True)
  234. await sendAudio(conn, audios)
  235. # 等待所有音频包发送完成
  236. await _wait_for_audio_completion(conn)
  237. # 检查是否是当前轮次
  238. if current_sentence_id != conn.sentence_id:
  239. return
  240. # 停止音频发送循环(仅在流控器已初始化时调用)
  241. if hasattr(conn, "audio_rate_controller") and conn.audio_rate_controller:
  242. conn.audio_rate_controller.stop_sending()
  243. conn.clearSpeakStatus()
  244. # 发送消息到客户端
  245. await conn.websocket.send(json.dumps(message))
  246. async def send_stt_message(conn: "ConnectionHandler", text):
  247. """发送 STT 状态消息"""
  248. end_prompt_str = conn.config.get("end_prompt", {}).get("prompt")
  249. if end_prompt_str and end_prompt_str == text:
  250. await send_tts_message(conn, "start")
  251. return
  252. # 解析JSON格式,提取实际的用户说话内容
  253. display_text = text
  254. try:
  255. # 尝试解析JSON格式
  256. if text.strip().startswith("{") and text.strip().endswith("}"):
  257. parsed_data = json.loads(text)
  258. if isinstance(parsed_data, dict) and "content" in parsed_data:
  259. # 如果是包含说话人信息的JSON格式,只显示content部分
  260. display_text = parsed_data["content"]
  261. # 保存说话人信息到conn对象
  262. if "speaker" in parsed_data:
  263. conn.current_speaker = parsed_data["speaker"]
  264. except (json.JSONDecodeError, TypeError):
  265. # 如果不是JSON格式,直接使用原始文本
  266. display_text = text
  267. stt_text = textUtils.get_string_no_punctuation_or_emoji(display_text)
  268. await conn.websocket.send(
  269. json.dumps({"type": "stt", "text": stt_text, "session_id": conn.session_id})
  270. )
  271. await send_tts_message(conn, "start")
  272. # 发送start消息后客户端状态会处于说话中状态,同步服务端状态
  273. conn.client_is_speaking = True
  274. async def send_display_message(conn: "ConnectionHandler", text):
  275. """发送纯显示消息"""
  276. message = {
  277. "type": "stt",
  278. "text": text,
  279. "session_id": conn.session_id
  280. }
  281. await conn.websocket.send(json.dumps(message))