|
|
@@ -59,6 +59,10 @@ class ASRNode(Node):
|
|
|
self.wakeup_pub = self.create_publisher(Bool, "wakeup", 5)
|
|
|
#创建发布录音状态发布者 / Create a publisher for recording status
|
|
|
self.record_status_pub=self.create_publisher(Bool, "record_status", 5)
|
|
|
+ # 创建 ASR 控制话题订阅者 / Create ASR control topic subscriber
|
|
|
+ self.asr_control_sub = self.create_subscription(
|
|
|
+ String, "/asr/control", self.asr_control_callback, 10
|
|
|
+ )
|
|
|
|
|
|
def init_param_config(self):
|
|
|
self.user_speechdir = os.path.join(
|
|
|
@@ -143,7 +147,7 @@ class ASRNode(Node):
|
|
|
self.current_thread.start()
|
|
|
rclpy.spin_once(self, timeout_sec=0.1)
|
|
|
|
|
|
- def kws_handler(self) -> None:
|
|
|
+ def kws_handler(self, play_error_response=True) -> None:
|
|
|
if self.stop_event.is_set():
|
|
|
return
|
|
|
|
|
|
@@ -164,9 +168,10 @@ class ASRNode(Node):
|
|
|
self.get_logger().warn(
|
|
|
"I still don't understand what you mean. Please try again"
|
|
|
)
|
|
|
- playsound(
|
|
|
- self.audio_dict[self.error_response]
|
|
|
- ) # 错误响应 / Error response
|
|
|
+ if play_error_response:
|
|
|
+ playsound(
|
|
|
+ self.audio_dict[self.error_response]
|
|
|
+ ) # 错误响应 / Error response
|
|
|
else:
|
|
|
self.get_logger().info(asr_text)
|
|
|
self.get_logger().info("😀okay, let me think for a moment...")
|
|
|
@@ -174,6 +179,67 @@ class ASRNode(Node):
|
|
|
else:
|
|
|
return
|
|
|
|
|
|
+ def asr_control_callback(self, msg):
|
|
|
+ """
|
|
|
+ 处理 /asr/control 控制指令
|
|
|
+ """
|
|
|
+ command = msg.data.strip()
|
|
|
+
|
|
|
+ if command in ["continue_listen", "start_listen", "listen_once"]:
|
|
|
+ self.get_logger().info(f"[多轮对话] 收到 ASR 控制指令: {command}")
|
|
|
+ # 停止旧线程
|
|
|
+ if self.current_thread and self.current_thread.is_alive():
|
|
|
+ self.stop_event.set()
|
|
|
+ time.sleep(0.05)
|
|
|
+ # 清空 buffer
|
|
|
+ while not self.audio_buffer.empty():
|
|
|
+ try:
|
|
|
+ self.audio_buffer.get_nowait()
|
|
|
+ except queue.Empty:
|
|
|
+ break
|
|
|
+ # 创建新线程直接开始录音(不播放唤醒音)
|
|
|
+ self.stop_event = threading.Event()
|
|
|
+ self.current_thread = threading.Thread(
|
|
|
+ target=self.kws_handler,
|
|
|
+ kwargs={"play_error_response": False}
|
|
|
+ )
|
|
|
+ self.current_thread.daemon = True
|
|
|
+ self.current_thread.start()
|
|
|
+ return
|
|
|
+
|
|
|
+ if command == "wake_listen":
|
|
|
+ self.get_logger().info("[ASR控制] 收到带唤醒音的监听指令")
|
|
|
+ # 停止旧线程
|
|
|
+ if self.current_thread and self.current_thread.is_alive():
|
|
|
+ self.stop_event.set()
|
|
|
+ time.sleep(0.05)
|
|
|
+ # 清空 buffer
|
|
|
+ while not self.audio_buffer.empty():
|
|
|
+ try:
|
|
|
+ self.audio_buffer.get_nowait()
|
|
|
+ except queue.Empty:
|
|
|
+ break
|
|
|
+ # 发布唤醒信号
|
|
|
+ self.wakeup_pub.publish(Bool(data=True))
|
|
|
+ self.get_logger().info("I'm here")
|
|
|
+ playsound(self.audio_dict[self.first_response])
|
|
|
+ # 创建新线程
|
|
|
+ self.stop_event = threading.Event()
|
|
|
+ self.current_thread = threading.Thread(
|
|
|
+ target=self.kws_handler,
|
|
|
+ kwargs={"play_error_response": True}
|
|
|
+ )
|
|
|
+ self.current_thread.daemon = True
|
|
|
+ self.current_thread.start()
|
|
|
+ return
|
|
|
+
|
|
|
+ if command in ["stop_listen", "cancel"]:
|
|
|
+ self.get_logger().info(f"[ASR控制] 收到停止监听指令: {command}")
|
|
|
+ self.stop_event.set()
|
|
|
+ return
|
|
|
+
|
|
|
+ self.get_logger().warn(f"[ASR控制] 未知指令: {command}")
|
|
|
+
|
|
|
def system_sound_init(
|
|
|
self,
|
|
|
): # 初始化系统声音相关的功能 / Initialize system sound functionality
|
|
|
@@ -270,7 +336,7 @@ class ASRNode(Node):
|
|
|
"input": True,
|
|
|
"frames_per_buffer": self.frame_bytes,
|
|
|
}
|
|
|
- if self.mic_index != 0:
|
|
|
+ if self.mic_index >= 0:
|
|
|
stream_kwargs["input_device_index"] = self.mic_index
|
|
|
|
|
|
stream = p.open(**stream_kwargs)
|
|
|
@@ -328,6 +394,8 @@ class ASRNode(Node):
|
|
|
frame_counter = 0 # 计数器 / Frame counter
|
|
|
empty_count = 0 # 连续空帧计数 / Consecutive empty frame count
|
|
|
MAX_EMPTY_FRAMES = 200 # 约6秒无音频则退出 / Exit after ~6s of no audio
|
|
|
+ WAIT_SPEECH_TIMEOUT_SEC = 8.0 # 等待用户开口超时 / Wait for user to start speaking timeout
|
|
|
+ wait_start_time = time.time()
|
|
|
|
|
|
# 通过蜂鸣器提示用户讲话 / Prompt the user to speak via the buzzer
|
|
|
self.pub_beep.publish(UInt16(data=1))
|
|
|
@@ -351,6 +419,12 @@ class ASRNode(Node):
|
|
|
|
|
|
is_speech = self.vad.is_speech(frame, self.sample_rate)
|
|
|
|
|
|
+ # 等待用户开口超时检测
|
|
|
+ if not speaking and time.time() - wait_start_time > WAIT_SPEECH_TIMEOUT_SEC:
|
|
|
+ self.get_logger().warn("No speech detected within timeout, exiting recording")
|
|
|
+ self.record_status_pub.publish(Bool(data=False))
|
|
|
+ return False
|
|
|
+
|
|
|
if is_speech:
|
|
|
speaking = True
|
|
|
audio_buffer.append(frame)
|
|
|
@@ -394,7 +468,7 @@ def main(args=None):
|
|
|
# 停止常驻音频读取线程 / Stop the persistent audio capture thread
|
|
|
sense_voice_node.audio_capture_running = False
|
|
|
if sense_voice_node.audio_capture_thread.is_alive():
|
|
|
- sense_voice_node.audio_capture_thread.join(timeout_sec=2)
|
|
|
+ sense_voice_node.audio_capture_thread.join(timeout=2)
|
|
|
sense_voice_node.destroy_node()
|
|
|
rclpy.shutdown()
|
|
|
|