| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- import asyncio
- import queue
- import random
- import signal
- import sys
- import threading
- import time
- import uuid
- import wave
- from dataclasses import dataclass
- from typing import Optional, Dict, Any
- import pyaudio
- import config
- from realtime_dialog_client import RealtimeDialogClient
- @dataclass
- class AudioConfig:
- """音频配置数据类"""
- format: str
- bit_size: int
- channels: int
- sample_rate: int
- chunk: int
- class AudioDeviceManager:
- """音频设备管理类,处理音频输入输出"""
- def __init__(self, input_config: AudioConfig, output_config: AudioConfig):
- self.input_config = input_config
- self.output_config = output_config
- self.pyaudio = pyaudio.PyAudio()
- self.input_stream: Optional[pyaudio.Stream] = None
- self.output_stream: Optional[pyaudio.Stream] = None
- def open_input_stream(self) -> pyaudio.Stream:
- """打开音频输入流"""
- # p = pyaudio.PyAudio()
- self.input_stream = self.pyaudio.open(
- format=self.input_config.bit_size,
- channels=self.input_config.channels,
- rate=self.input_config.sample_rate,
- input=True,
- frames_per_buffer=self.input_config.chunk
- )
- return self.input_stream
- def open_output_stream(self) -> pyaudio.Stream:
- """打开音频输出流"""
- self.output_stream = self.pyaudio.open(
- format=self.output_config.bit_size,
- channels=self.output_config.channels,
- rate=self.output_config.sample_rate,
- output=True,
- frames_per_buffer=self.output_config.chunk
- )
- return self.output_stream
- def cleanup(self) -> None:
- """清理音频设备资源"""
- for stream in [self.input_stream, self.output_stream]:
- if stream:
- stream.stop_stream()
- stream.close()
- self.pyaudio.terminate()
- class DialogSession:
- """对话会话管理类"""
- is_audio_file_input: bool
- mod: str
- def __init__(self, ws_config: Dict[str, Any], output_audio_format: str = "pcm", audio_file_path: str = "",
- mod: str = "audio", recv_timeout: int = 10):
- self.audio_file_path = audio_file_path
- self.recv_timeout = recv_timeout
- self.is_audio_file_input = self.audio_file_path != ""
- if self.is_audio_file_input:
- mod = 'audio_file'
- else:
- self.say_hello_over_event = asyncio.Event()
- self.mod = mod
- self.session_id = str(uuid.uuid4())
- self.client = RealtimeDialogClient(config=ws_config, session_id=self.session_id,
- output_audio_format=output_audio_format, mod=mod, recv_timeout=recv_timeout)
- if output_audio_format == "pcm_s16le":
- config.output_audio_config["format"] = "pcm_s16le"
- config.output_audio_config["bit_size"] = pyaudio.paInt16
- self.is_running = True
- self.is_session_finished = False
- self.is_user_querying = False
- self.is_sending_chat_tts_text = False
- self.audio_buffer = b''
- signal.signal(signal.SIGINT, self._keyboard_signal)
- self.audio_queue = queue.Queue()
- if not self.is_audio_file_input:
- self.audio_device = AudioDeviceManager(
- AudioConfig(**config.input_audio_config),
- AudioConfig(**config.output_audio_config)
- )
- # 初始化音频队列和输出流
- self.output_stream = self.audio_device.open_output_stream()
- # 启动播放线程
- self.is_recording = True
- self.is_playing = True
- self.player_thread = threading.Thread(target=self._audio_player_thread)
- self.player_thread.daemon = True
- self.player_thread.start()
- def _audio_player_thread(self):
- """音频播放线程"""
- while self.is_playing:
- try:
- # 从队列获取音频数据
- audio_data = self.audio_queue.get(timeout=1.0)
- if audio_data is not None:
- self.output_stream.write(audio_data)
- except queue.Empty:
- # 队列为空时等待一小段时间
- time.sleep(0.1)
- except Exception as e:
- print(f"音频播放错误: {e}")
- time.sleep(0.1)
- def handle_server_response(self, response: Dict[str, Any]) -> None:
- if response == {}:
- return
- """处理服务器响应"""
- if response['message_type'] == 'SERVER_ACK' and isinstance(response.get('payload_msg'), bytes):
- # print(f"\n接收到音频数据: {len(response['payload_msg'])} 字节")
- if self.is_sending_chat_tts_text:
- return
- audio_data = response['payload_msg']
- if not self.is_audio_file_input:
- self.audio_queue.put(audio_data)
- self.audio_buffer += audio_data
- elif response['message_type'] == 'SERVER_FULL_RESPONSE':
- print(f"服务器响应: {response}")
- event = response.get('event')
- payload_msg = response.get('payload_msg', {})
- if event == 450:
- print(f"清空缓存音频: {response['session_id']}")
- while not self.audio_queue.empty():
- try:
- self.audio_queue.get_nowait()
- except queue.Empty:
- continue
- self.is_user_querying = True
- if event == 350 and self.is_sending_chat_tts_text and payload_msg.get("tts_type") in ["chat_tts_text", "external_rag"]:
- while not self.audio_queue.empty():
- try:
- self.audio_queue.get_nowait()
- except queue.Empty:
- continue
- self.is_sending_chat_tts_text = False
- if event == 459:
- self.is_user_querying = False
- if random.randint(0, 100000)%1 == 0:
- self.is_sending_chat_tts_text = True
- asyncio.create_task(self.trigger_chat_tts_text())
- asyncio.create_task(self.trigger_chat_rag_text())
- elif response['message_type'] == 'SERVER_ERROR':
- print(f"服务器错误: {response['payload_msg']}")
- raise Exception("服务器错误")
- async def trigger_chat_tts_text(self):
- """概率触发发送ChatTTSText请求"""
- print("hit ChatTTSText event, start sending...")
- await self.client.chat_tts_text(
- is_user_querying=self.is_user_querying,
- start=True,
- end=False,
- content="这是查询到外部数据之前的安抚话术。",
- )
- await self.client.chat_tts_text(
- is_user_querying=self.is_user_querying,
- start=False,
- end=True,
- content="",
- )
- async def trigger_chat_rag_text(self):
- await asyncio.sleep(5) # 模拟查询外部RAG的耗时,这里为了不影响GTA安抚话术的播报,直接sleep 5秒
- print("hit ChatRAGText event, start sending...")
- await self.client.chat_rag_text(self.is_user_querying, external_rag='[{"title":"北京天气","content":"今天北京整体以晴到多云为主,但西部和北部地带可能会出现分散性雷阵雨,特别是午后至傍晚时段需注意突发降雨。\n💨 风况与湿度\n风力较弱,一般为 2–3 级南风或西南风\n白天湿度较高,早晚略凉爽"}]')
- def _keyboard_signal(self, sig, frame):
- print(f"receive keyboard Ctrl+C")
- self.stop()
- def stop(self):
- self.is_recording = False
- self.is_playing = False
- self.is_running = False
- async def receive_loop(self):
- try:
- while True:
- response = await self.client.receive_server_response()
- self.handle_server_response(response)
- if 'event' in response and (response['event'] == 152 or response['event'] == 153):
- print(f"receive session finished event: {response['event']}")
- self.is_session_finished = True
- break
- if 'event' in response and response['event'] == 359:
- if self.is_audio_file_input:
- print(f"receive tts ended event")
- self.is_session_finished = True
- break
- else:
- if not self.say_hello_over_event.is_set():
- print(f"receive tts sayhello ended event")
- self.say_hello_over_event.set()
- if self.mod == "text":
- print("请输入内容:")
- except asyncio.CancelledError:
- print("接收任务已取消")
- except Exception as e:
- print(f"接收消息错误: {e}")
- finally:
- self.stop()
- self.is_session_finished = True
- async def process_audio_file(self) -> None:
- await self.process_audio_file_input(self.audio_file_path)
- async def process_text_input(self) -> None:
- await self.client.say_hello()
- await self.say_hello_over_event.wait()
- """主逻辑:处理文本输入和WebSocket通信"""
- # 确保连接最终关闭
- try:
- # 启动输入监听线程
- input_queue = queue.Queue()
- input_thread = threading.Thread(target=self.input_listener, args=(input_queue,), daemon=True)
- input_thread.start()
- # 主循环:处理输入和上下文结束
- while self.is_running:
- try:
- # 检查是否有输入(非阻塞)
- input_str = input_queue.get_nowait()
- if input_str is None:
- # 输入流关闭
- print("Input channel closed")
- break
- if input_str:
- # 发送输入内容
- await self.client.chat_text_query(input_str)
- except queue.Empty:
- # 无输入时短暂休眠
- await asyncio.sleep(0.1)
- except Exception as e:
- print(f"Main loop error: {e}")
- break
- finally:
- print("exit text input")
- def input_listener(self, input_queue: queue.Queue) -> None:
- """在单独线程中监听标准输入"""
- print("Start listening for input")
- try:
- while True:
- # 读取标准输入(阻塞操作)
- line = sys.stdin.readline()
- if not line:
- # 输入流关闭
- input_queue.put(None)
- break
- input_str = line.strip()
- input_queue.put(input_str)
- except Exception as e:
- print(f"Input listener error: {e}")
- input_queue.put(None)
- async def process_audio_file_input(self, audio_file_path: str) -> None:
- # 读取WAV文件
- with wave.open(audio_file_path, 'rb') as wf:
- chunk_size = config.input_audio_config["chunk"]
- framerate = wf.getframerate() # 采样率(如16000Hz)
- # 时长 = chunkSize(帧数) ÷ 采样率(帧/秒)
- sleep_seconds = chunk_size / framerate
- print(f"开始处理音频文件: {audio_file_path}")
- # 分块读取并发送音频数据
- while True:
- audio_data = wf.readframes(chunk_size)
- if not audio_data:
- break # 文件读取完毕
- await self.client.task_request(audio_data)
- # sleep与chunk对应的音频时长一致,模拟实时输入
- await asyncio.sleep(sleep_seconds)
- print(f"音频文件处理完成,等待服务器响应...")
- async def process_silence_audio(self) -> None:
- """发送静音音频"""
- silence_data = b'\x00' * 320
- await self.client.task_request(silence_data)
- async def process_microphone_input(self) -> None:
- await self.client.say_hello()
- await self.say_hello_over_event.wait()
- await self.client.chat_text_query("你好,我也叫豆包")
- """处理麦克风输入"""
- stream = self.audio_device.open_input_stream()
- print("已打开麦克风,请讲话...")
- while self.is_recording:
- try:
- # 添加exception_on_overflow=False参数来忽略溢出错误
- audio_data = stream.read(config.input_audio_config["chunk"], exception_on_overflow=False)
- save_input_pcm_to_wav(audio_data, "input.pcm")
- await self.client.task_request(audio_data)
- await asyncio.sleep(0.01) # 避免CPU过度使用
- except Exception as e:
- print(f"读取麦克风数据出错: {e}")
- await asyncio.sleep(0.1) # 给系统一些恢复时间
- async def start(self) -> None:
- """启动对话会话"""
- try:
- await self.client.connect()
- if self.mod == "text":
- asyncio.create_task(self.process_text_input())
- asyncio.create_task(self.receive_loop())
- while self.is_running:
- await asyncio.sleep(0.1)
- else:
- if self.is_audio_file_input:
- asyncio.create_task(self.process_audio_file())
- await self.receive_loop()
- else:
- asyncio.create_task(self.process_microphone_input())
- asyncio.create_task(self.receive_loop())
- while self.is_running:
- await asyncio.sleep(0.1)
- await self.client.finish_session()
- while not self.is_session_finished:
- await asyncio.sleep(0.1)
- await self.client.finish_connection()
- await asyncio.sleep(0.1)
- await self.client.close()
- print(f"dialog request logid: {self.client.logid}, chat mod: {self.mod}")
- save_output_to_file(self.audio_buffer, "output.pcm")
- except Exception as e:
- print(f"会话错误: {e}")
- finally:
- if not self.is_audio_file_input:
- self.audio_device.cleanup()
- def save_input_pcm_to_wav(pcm_data: bytes, filename: str) -> None:
- """保存PCM数据为WAV文件"""
- with wave.open(filename, 'wb') as wf:
- wf.setnchannels(config.input_audio_config["channels"])
- wf.setsampwidth(2) # paInt16 = 2 bytes
- wf.setframerate(config.input_audio_config["sample_rate"])
- wf.writeframes(pcm_data)
- def save_output_to_file(audio_data: bytes, filename: str) -> None:
- """保存原始PCM音频数据到文件"""
- if not audio_data:
- print("No audio data to save.")
- return
- try:
- with open(filename, 'wb') as f:
- f.write(audio_data)
- except IOError as e:
- print(f"Failed to save pcm file: {e}")
|