audio_manager.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import asyncio
  2. import queue
  3. import random
  4. import signal
  5. import sys
  6. import threading
  7. import time
  8. import uuid
  9. import wave
  10. from dataclasses import dataclass
  11. from typing import Optional, Dict, Any
  12. import pyaudio
  13. import config
  14. from realtime_dialog_client import RealtimeDialogClient
  15. @dataclass
  16. class AudioConfig:
  17. """音频配置数据类"""
  18. format: str
  19. bit_size: int
  20. channels: int
  21. sample_rate: int
  22. chunk: int
  23. class AudioDeviceManager:
  24. """音频设备管理类,处理音频输入输出"""
  25. def __init__(self, input_config: AudioConfig, output_config: AudioConfig):
  26. self.input_config = input_config
  27. self.output_config = output_config
  28. self.pyaudio = pyaudio.PyAudio()
  29. self.input_stream: Optional[pyaudio.Stream] = None
  30. self.output_stream: Optional[pyaudio.Stream] = None
  31. def open_input_stream(self) -> pyaudio.Stream:
  32. """打开音频输入流"""
  33. # p = pyaudio.PyAudio()
  34. self.input_stream = self.pyaudio.open(
  35. format=self.input_config.bit_size,
  36. channels=self.input_config.channels,
  37. rate=self.input_config.sample_rate,
  38. input=True,
  39. frames_per_buffer=self.input_config.chunk
  40. )
  41. return self.input_stream
  42. def open_output_stream(self) -> pyaudio.Stream:
  43. """打开音频输出流"""
  44. self.output_stream = self.pyaudio.open(
  45. format=self.output_config.bit_size,
  46. channels=self.output_config.channels,
  47. rate=self.output_config.sample_rate,
  48. output=True,
  49. frames_per_buffer=self.output_config.chunk
  50. )
  51. return self.output_stream
  52. def cleanup(self) -> None:
  53. """清理音频设备资源"""
  54. for stream in [self.input_stream, self.output_stream]:
  55. if stream:
  56. stream.stop_stream()
  57. stream.close()
  58. self.pyaudio.terminate()
  59. class DialogSession:
  60. """对话会话管理类"""
  61. is_audio_file_input: bool
  62. mod: str
  63. def __init__(self, ws_config: Dict[str, Any], output_audio_format: str = "pcm", audio_file_path: str = "",
  64. mod: str = "audio", recv_timeout: int = 10):
  65. self.audio_file_path = audio_file_path
  66. self.recv_timeout = recv_timeout
  67. self.is_audio_file_input = self.audio_file_path != ""
  68. if self.is_audio_file_input:
  69. mod = 'audio_file'
  70. else:
  71. self.say_hello_over_event = asyncio.Event()
  72. self.mod = mod
  73. self.session_id = str(uuid.uuid4())
  74. self.client = RealtimeDialogClient(config=ws_config, session_id=self.session_id,
  75. output_audio_format=output_audio_format, mod=mod, recv_timeout=recv_timeout)
  76. if output_audio_format == "pcm_s16le":
  77. config.output_audio_config["format"] = "pcm_s16le"
  78. config.output_audio_config["bit_size"] = pyaudio.paInt16
  79. self.is_running = True
  80. self.is_session_finished = False
  81. self.is_user_querying = False
  82. self.is_sending_chat_tts_text = False
  83. self.audio_buffer = b''
  84. signal.signal(signal.SIGINT, self._keyboard_signal)
  85. self.audio_queue = queue.Queue()
  86. if not self.is_audio_file_input:
  87. self.audio_device = AudioDeviceManager(
  88. AudioConfig(**config.input_audio_config),
  89. AudioConfig(**config.output_audio_config)
  90. )
  91. # 初始化音频队列和输出流
  92. self.output_stream = self.audio_device.open_output_stream()
  93. # 启动播放线程
  94. self.is_recording = True
  95. self.is_playing = True
  96. self.player_thread = threading.Thread(target=self._audio_player_thread)
  97. self.player_thread.daemon = True
  98. self.player_thread.start()
  99. def _audio_player_thread(self):
  100. """音频播放线程"""
  101. while self.is_playing:
  102. try:
  103. # 从队列获取音频数据
  104. audio_data = self.audio_queue.get(timeout=1.0)
  105. if audio_data is not None:
  106. self.output_stream.write(audio_data)
  107. except queue.Empty:
  108. # 队列为空时等待一小段时间
  109. time.sleep(0.1)
  110. except Exception as e:
  111. print(f"音频播放错误: {e}")
  112. time.sleep(0.1)
  113. def handle_server_response(self, response: Dict[str, Any]) -> None:
  114. if response == {}:
  115. return
  116. """处理服务器响应"""
  117. if response['message_type'] == 'SERVER_ACK' and isinstance(response.get('payload_msg'), bytes):
  118. # print(f"\n接收到音频数据: {len(response['payload_msg'])} 字节")
  119. if self.is_sending_chat_tts_text:
  120. return
  121. audio_data = response['payload_msg']
  122. if not self.is_audio_file_input:
  123. self.audio_queue.put(audio_data)
  124. self.audio_buffer += audio_data
  125. elif response['message_type'] == 'SERVER_FULL_RESPONSE':
  126. print(f"服务器响应: {response}")
  127. event = response.get('event')
  128. payload_msg = response.get('payload_msg', {})
  129. if event == 450:
  130. print(f"清空缓存音频: {response['session_id']}")
  131. while not self.audio_queue.empty():
  132. try:
  133. self.audio_queue.get_nowait()
  134. except queue.Empty:
  135. continue
  136. self.is_user_querying = True
  137. if event == 350 and self.is_sending_chat_tts_text and payload_msg.get("tts_type") in ["chat_tts_text", "external_rag"]:
  138. while not self.audio_queue.empty():
  139. try:
  140. self.audio_queue.get_nowait()
  141. except queue.Empty:
  142. continue
  143. self.is_sending_chat_tts_text = False
  144. if event == 459:
  145. self.is_user_querying = False
  146. if random.randint(0, 100000)%1 == 0:
  147. self.is_sending_chat_tts_text = True
  148. asyncio.create_task(self.trigger_chat_tts_text())
  149. asyncio.create_task(self.trigger_chat_rag_text())
  150. elif response['message_type'] == 'SERVER_ERROR':
  151. print(f"服务器错误: {response['payload_msg']}")
  152. raise Exception("服务器错误")
  153. async def trigger_chat_tts_text(self):
  154. """概率触发发送ChatTTSText请求"""
  155. print("hit ChatTTSText event, start sending...")
  156. await self.client.chat_tts_text(
  157. is_user_querying=self.is_user_querying,
  158. start=True,
  159. end=False,
  160. content="这是查询到外部数据之前的安抚话术。",
  161. )
  162. await self.client.chat_tts_text(
  163. is_user_querying=self.is_user_querying,
  164. start=False,
  165. end=True,
  166. content="",
  167. )
  168. async def trigger_chat_rag_text(self):
  169. await asyncio.sleep(5) # 模拟查询外部RAG的耗时,这里为了不影响GTA安抚话术的播报,直接sleep 5秒
  170. print("hit ChatRAGText event, start sending...")
  171. await self.client.chat_rag_text(self.is_user_querying, external_rag='[{"title":"北京天气","content":"今天北京整体以晴到多云为主,但西部和北部地带可能会出现分散性雷阵雨,特别是午后至傍晚时段需注意突发降雨。\n💨 风况与湿度\n风力较弱,一般为 2–3 级南风或西南风\n白天湿度较高,早晚略凉爽"}]')
  172. def _keyboard_signal(self, sig, frame):
  173. print(f"receive keyboard Ctrl+C")
  174. self.stop()
  175. def stop(self):
  176. self.is_recording = False
  177. self.is_playing = False
  178. self.is_running = False
  179. async def receive_loop(self):
  180. try:
  181. while True:
  182. response = await self.client.receive_server_response()
  183. self.handle_server_response(response)
  184. if 'event' in response and (response['event'] == 152 or response['event'] == 153):
  185. print(f"receive session finished event: {response['event']}")
  186. self.is_session_finished = True
  187. break
  188. if 'event' in response and response['event'] == 359:
  189. if self.is_audio_file_input:
  190. print(f"receive tts ended event")
  191. self.is_session_finished = True
  192. break
  193. else:
  194. if not self.say_hello_over_event.is_set():
  195. print(f"receive tts sayhello ended event")
  196. self.say_hello_over_event.set()
  197. if self.mod == "text":
  198. print("请输入内容:")
  199. except asyncio.CancelledError:
  200. print("接收任务已取消")
  201. except Exception as e:
  202. print(f"接收消息错误: {e}")
  203. finally:
  204. self.stop()
  205. self.is_session_finished = True
  206. async def process_audio_file(self) -> None:
  207. await self.process_audio_file_input(self.audio_file_path)
  208. async def process_text_input(self) -> None:
  209. await self.client.say_hello()
  210. await self.say_hello_over_event.wait()
  211. """主逻辑:处理文本输入和WebSocket通信"""
  212. # 确保连接最终关闭
  213. try:
  214. # 启动输入监听线程
  215. input_queue = queue.Queue()
  216. input_thread = threading.Thread(target=self.input_listener, args=(input_queue,), daemon=True)
  217. input_thread.start()
  218. # 主循环:处理输入和上下文结束
  219. while self.is_running:
  220. try:
  221. # 检查是否有输入(非阻塞)
  222. input_str = input_queue.get_nowait()
  223. if input_str is None:
  224. # 输入流关闭
  225. print("Input channel closed")
  226. break
  227. if input_str:
  228. # 发送输入内容
  229. await self.client.chat_text_query(input_str)
  230. except queue.Empty:
  231. # 无输入时短暂休眠
  232. await asyncio.sleep(0.1)
  233. except Exception as e:
  234. print(f"Main loop error: {e}")
  235. break
  236. finally:
  237. print("exit text input")
  238. def input_listener(self, input_queue: queue.Queue) -> None:
  239. """在单独线程中监听标准输入"""
  240. print("Start listening for input")
  241. try:
  242. while True:
  243. # 读取标准输入(阻塞操作)
  244. line = sys.stdin.readline()
  245. if not line:
  246. # 输入流关闭
  247. input_queue.put(None)
  248. break
  249. input_str = line.strip()
  250. input_queue.put(input_str)
  251. except Exception as e:
  252. print(f"Input listener error: {e}")
  253. input_queue.put(None)
  254. async def process_audio_file_input(self, audio_file_path: str) -> None:
  255. # 读取WAV文件
  256. with wave.open(audio_file_path, 'rb') as wf:
  257. chunk_size = config.input_audio_config["chunk"]
  258. framerate = wf.getframerate() # 采样率(如16000Hz)
  259. # 时长 = chunkSize(帧数) ÷ 采样率(帧/秒)
  260. sleep_seconds = chunk_size / framerate
  261. print(f"开始处理音频文件: {audio_file_path}")
  262. # 分块读取并发送音频数据
  263. while True:
  264. audio_data = wf.readframes(chunk_size)
  265. if not audio_data:
  266. break # 文件读取完毕
  267. await self.client.task_request(audio_data)
  268. # sleep与chunk对应的音频时长一致,模拟实时输入
  269. await asyncio.sleep(sleep_seconds)
  270. print(f"音频文件处理完成,等待服务器响应...")
  271. async def process_silence_audio(self) -> None:
  272. """发送静音音频"""
  273. silence_data = b'\x00' * 320
  274. await self.client.task_request(silence_data)
  275. async def process_microphone_input(self) -> None:
  276. await self.client.say_hello()
  277. await self.say_hello_over_event.wait()
  278. await self.client.chat_text_query("你好,我也叫豆包")
  279. """处理麦克风输入"""
  280. stream = self.audio_device.open_input_stream()
  281. print("已打开麦克风,请讲话...")
  282. while self.is_recording:
  283. try:
  284. # 添加exception_on_overflow=False参数来忽略溢出错误
  285. audio_data = stream.read(config.input_audio_config["chunk"], exception_on_overflow=False)
  286. save_input_pcm_to_wav(audio_data, "input.pcm")
  287. await self.client.task_request(audio_data)
  288. await asyncio.sleep(0.01) # 避免CPU过度使用
  289. except Exception as e:
  290. print(f"读取麦克风数据出错: {e}")
  291. await asyncio.sleep(0.1) # 给系统一些恢复时间
  292. async def start(self) -> None:
  293. """启动对话会话"""
  294. try:
  295. await self.client.connect()
  296. if self.mod == "text":
  297. asyncio.create_task(self.process_text_input())
  298. asyncio.create_task(self.receive_loop())
  299. while self.is_running:
  300. await asyncio.sleep(0.1)
  301. else:
  302. if self.is_audio_file_input:
  303. asyncio.create_task(self.process_audio_file())
  304. await self.receive_loop()
  305. else:
  306. asyncio.create_task(self.process_microphone_input())
  307. asyncio.create_task(self.receive_loop())
  308. while self.is_running:
  309. await asyncio.sleep(0.1)
  310. await self.client.finish_session()
  311. while not self.is_session_finished:
  312. await asyncio.sleep(0.1)
  313. await self.client.finish_connection()
  314. await asyncio.sleep(0.1)
  315. await self.client.close()
  316. print(f"dialog request logid: {self.client.logid}, chat mod: {self.mod}")
  317. save_output_to_file(self.audio_buffer, "output.pcm")
  318. except Exception as e:
  319. print(f"会话错误: {e}")
  320. finally:
  321. if not self.is_audio_file_input:
  322. self.audio_device.cleanup()
  323. def save_input_pcm_to_wav(pcm_data: bytes, filename: str) -> None:
  324. """保存PCM数据为WAV文件"""
  325. with wave.open(filename, 'wb') as wf:
  326. wf.setnchannels(config.input_audio_config["channels"])
  327. wf.setsampwidth(2) # paInt16 = 2 bytes
  328. wf.setframerate(config.input_audio_config["sample_rate"])
  329. wf.writeframes(pcm_data)
  330. def save_output_to_file(audio_data: bytes, filename: str) -> None:
  331. """保存原始PCM音频数据到文件"""
  332. if not audio_data:
  333. print("No audio data to save.")
  334. return
  335. try:
  336. with open(filename, 'wb') as f:
  337. f.write(audio_data)
  338. except IOError as e:
  339. print(f"Failed to save pcm file: {e}")