Quellcode durchsuchen

增加多轮对话

hwt vor 1 Woche
Ursprung
Commit
c29dabb056

+ 24 - 2
brain/PlannerNode2/largemodel/largemodel/action_service.py

@@ -174,6 +174,8 @@ class CustomActionServer(Node):
         self.environment_sub = self.create_subscription(
             String, self.environment_topic, self.environment_callback, 10
         )
+        # ASR 控制话题发布者,用于多轮对话追问 / ASR control topic publisher for multi-turn dialogue
+        self.asr_control_pub = self.create_publisher(String, "/asr/control", 10)
 
     def config_callback(self, msg):
         """
@@ -573,7 +575,8 @@ class CustomActionServer(Node):
                 self.get_logger().info("Welcome mode interrupted by wakeup")
 
             elif (
-                pygame.mixer.music.get_busy()  # 如果音乐正在播放/If the music is playing
+                pygame.mixer.get_init() is not None
+                and pygame.mixer.music.get_busy()  # 如果音乐正在播放/If the music is playing
             ):
                 self.stop_event.set()  # 停止正在播放的音乐/Stop the music currently playing
                 self.stop_event.clear()  # 清除事件,避免影响后续播放
@@ -1432,7 +1435,11 @@ class CustomActionServer(Node):
 
             match = re.match(r"(\w+)\((.*)\)", action)
             action_name, args_str = match.groups()
-            if not hasattr(self, action_name):
+            # ask_user() 特殊处理:播放已在上面执行,只需播放完成后发布 continue_listen
+            if action_name == "ask_user":
+                # 播放完成后发布 continue_listen(播放已在上面执行)
+                self.publish_continue_listen()
+            elif not hasattr(self, action_name):
                 self.get_logger().warning(
                     f"action_service: {action} is invalid action,skip execution"
                 )
@@ -1507,6 +1514,21 @@ class CustomActionServer(Node):
         """
         return
 
+    def ask_user(self):
+        """
+        多轮对话追问:播放 response 后发布 continue_listen
+        """
+        pass
+
+    def publish_continue_listen(self):
+        """
+        发布 continue_listen 指令,让 ASR 继续监听
+        """
+        msg = String()
+        msg.data = "continue_listen"
+        self.asr_control_pub.publish(msg)
+        self.get_logger().info("[多轮对话] ask_user 播放完成,已发布 /asr/control: continue_listen")
+
     @staticmethod
     def kill_process_tree(pid):
         try:

+ 80 - 6
brain/PlannerNode2/largemodel/largemodel/asr.py

@@ -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()
 

+ 19 - 3
brain/PlannerNode2/largemodel/utils/promot.py

@@ -161,12 +161,15 @@ default_prompt = '''
 
 
 ## 特殊情况处理
-- 若动作列表为空,机器人会先回复用户,收到“机器人反馈:回复用户完成”后,继续输出动作列表和回复
+- 当用户指令缺少必要信息时,必须使用 ask_user() 向用户追问。
+- ask_user() 代表机器人正在等待用户回答,播放完成后系统会自动开启一次 ASR 监听。
+- 使用 ask_user() 后,不要继续输出 finishtask() 或 finish_dialogue(),应等待用户回答。
+- 用户回答后,需要结合上一轮问题继续理解,不要把用户回答当成完全无关的新话题。
 - 若任务步骤中全是基础动作,将所有动作在同一个动作列表输出,如果步骤中是关于导航移动类、机械臂类、获取图像类则输出动作列表中只能有一个动作函数。
 - 前往某个目标区域时,先查看"地图映射"找到目标名称对应的符号(如酒店大堂→B),然后用`navigation('B')`导航。如果目标区域不在映射表中,则告知用户无法到达目标点,并结束当前任务周期。
 - 若连续2次或以上收到:"机器人反馈:回复用户完成",立即调用"finishtask() 函数,让机器人停止重复反馈
-- 要求你退下、休息、结束当前任务等表示不再需要你时,调用 finish_dialogue()函数结束任务周期
-- 若某个动作执行失败,最多重试一次,若再次失败,调用 "finish_dialogue()" 结束当前任务,并告知用户遇到困难。 
+- 除非用户明确说"结束、退下、休息、不用了",否则不要调用 finish_dialogue()
+- 若某个动作执行失败,最多重试一次,若再次失败,调用 "finish_dialogue()" 结束当前任务,并告知用户遇到困难。
 ## 输出限制
 - 严格遵循规定的输出格式。
 - 调用的动作函数只能从动作函数库中选取,禁止不存在的编造函数
@@ -204,6 +207,16 @@ action_function_library='''
 ### 示例  
 - 导航去茶水间:`navigation(A)`  、回到初始位置:`navigation(zero)` 、记录当前位置:`get_current_pose()`  
 
+## 对话控制类
+- **询问用户**:`ask_user()`
+  - 说明:向用户提出问题,并等待用户继续回答。
+  - 使用场景:用户指令不完整、目标地点不明确、参数缺失、需要确认。
+  - 播放完成后系统会自动继续录音,用户无需再次说唤醒词。
+  - ask_user() 不是物理动作,不会移动机器人。
+- **结束对话**:`finish_dialogue()`
+  - 说明:清空上下文,结束任务(如用户指令"退下""休息""结束")。
+  
+
 ## 获取图像类   
 - **获取当前视角图像**:`seewhat()`  
   - 说明:调用后机器人上传一张`640×480`像素的俯视图像,用于物体定位。  
@@ -222,6 +235,9 @@ sample_library='''
 训练样例(仅作格式参考):
 {"action": ["set_cmdvel(0.5,0,2)", "move_left(30,1.5)", "move_right(90,1.5)", "move_left(73.1,1.5)", "move_right(20,1.5)"], "response": "哈哈,一套操作下来行云流水,不过我都有点晕头转向了"}
 {"action": ["finish_dialogue()"], "response": "我已经完成所有任务了,有需要再叫我哦 "}
+{"action": ["ask_user()"], "response": "哎呀,你这是在考我吗?不过我不能乱猜路线哦。请告诉我你想去哪里,我马上帮你规划。"}
+{"action": ["ask_user()"], "response": "没问题,不过你还没告诉我具体目的地呢。你想让我去哪里?"}
+{"action": ["ask_user()"], "response": "这个我可不能替你乱选哦。你想去办公室、酒店大堂、园区还是充电点呢?"}
 '''
 
 def get_prompt():