代码补充
This commit is contained in:
		
							parent
							
								
									cb2eb13b6e
								
							
						
					
					
						commit
						ab42cc6de5
					
				| @ -0,0 +1,101 @@ | |||||||
|  | from openai import OpenAI | ||||||
|  | import time | ||||||
|  | import sys | ||||||
|  | import queue  # 新增:用于缓存实时文本片段 | ||||||
|  | import threading  # 新增:用于并行处理语音播放 | ||||||
|  | # 原代码7. 火山方舟API调用完整逻辑 | ||||||
|  | class ArkAPIController: | ||||||
|  |     def __init__(self, ark_api_key, ark_model_id, tts_controller, feedback_text): | ||||||
|  |         # 接收调度脚本传入的TTS实例和反馈文本,保持原逻辑 | ||||||
|  |         self.ARK_API_KEY = ark_api_key | ||||||
|  |         self.ARK_MODEL_ID = ark_model_id | ||||||
|  |         self.tts_controller = tts_controller | ||||||
|  |         self.FEEDBACK_TEXT = feedback_text | ||||||
|  |         self.chat_context = []  # 聊天上下文由模块内部维护(与原逻辑一致) | ||||||
|  |         self.MAX_CONTEXT_LEN = 10 | ||||||
|  | 
 | ||||||
|  |          # 新增:实时语音播放队列与线程 | ||||||
|  |         self.speech_queue = queue.Queue()  # 缓存待播放的文本片段 | ||||||
|  |         self.speech_thread = threading.Thread(target=self._process_speech_queue, daemon=True) | ||||||
|  |         self.speech_thread.start()  # 启动语音播放线程 | ||||||
|  | 
 | ||||||
|  |      # 新增:处理语音队列的函数(循环从队列取片段并播放) | ||||||
|  |     def _process_speech_queue(self): | ||||||
|  |         """持续从队列中获取文本片段并调用TTS播放""" | ||||||
|  |         while True: | ||||||
|  |             text = self.speech_queue.get()  # 阻塞等待队列消息 | ||||||
|  |             if text is None:  # 退出信号 | ||||||
|  |                 break | ||||||
|  |             self.tts_controller.speak(text)  # 播放片段 | ||||||
|  |             self.speech_queue.task_done()  # 标记任务完成 | ||||||
|  |     def call_ark_api(self, content_type: str, content: dict): | ||||||
|  |         # 播放操作反馈(同步执行) | ||||||
|  |         self.tts_controller.speak(self.FEEDBACK_TEXT[content_type]) | ||||||
|  | 
 | ||||||
|  |         client = OpenAI( | ||||||
|  |             base_url="https://ark.cn-beijing.volces.com/api/v3", | ||||||
|  |             api_key=self.ARK_API_KEY | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             messages = [] | ||||||
|  |             if content_type == "chat": | ||||||
|  |                 messages.extend(self.chat_context[-self.MAX_CONTEXT_LEN*2:]) | ||||||
|  |                 messages.append({"role": "user", "content": [{"type": "text", "text": content["prompt"]}]}) | ||||||
|  |             elif content_type == "image_recog": | ||||||
|  |                 messages.append({ | ||||||
|  |                     "role": "user", | ||||||
|  |                     "content": [ | ||||||
|  |                         {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{content['image_base64']}"}}, | ||||||
|  |                         {"type": "text", "text": content["prompt"]} | ||||||
|  |                     ] | ||||||
|  |                 }) | ||||||
|  | 
 | ||||||
|  |             response = client.chat.completions.create( | ||||||
|  |                 model=self.ARK_MODEL_ID, | ||||||
|  |                 messages=messages, | ||||||
|  |                 max_tokens=300, | ||||||
|  |                 temperature=0.7 if content_type == "chat" else 0.3, | ||||||
|  |                 stream=True | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             full_response = "" | ||||||
|  |             current_speech_chunk = ""  # 缓存当前待播放的片段 | ||||||
|  |             print("\n" + "="*50) | ||||||
|  |             print("🤖 回应:", end="", flush=True) | ||||||
|  |              | ||||||
|  |             for chunk in response: | ||||||
|  |                 if chunk.choices and chunk.choices[0].delta.content: | ||||||
|  |                     char = chunk.choices[0].delta.content | ||||||
|  |                     full_response += char | ||||||
|  |                     current_speech_chunk += char  # 累加片段 | ||||||
|  |                     print(char, end="", flush=True) | ||||||
|  |                     time.sleep(0.05) | ||||||
|  | 
 | ||||||
|  |                     # 关键逻辑:当片段包含标点或达到一定长度时,推送到语音队列 | ||||||
|  |                     if any(punct in current_speech_chunk for punct in [".", "。", "!", "!", "?", "?", ",", ",", ";", ";"]): | ||||||
|  |                         self.speech_queue.put(current_speech_chunk)  # 推送片段到队列 | ||||||
|  |                         current_speech_chunk = ""  # 重置片段缓存 | ||||||
|  | 
 | ||||||
|  |             # 处理最后剩余的片段(如果有) | ||||||
|  |             if current_speech_chunk: | ||||||
|  |                 self.speech_queue.put(current_speech_chunk) | ||||||
|  |              | ||||||
|  |             print("\n" + "="*50 + "\n") | ||||||
|  | 
 | ||||||
|  |             # 等待所有语音片段播放完成 | ||||||
|  |             self.speech_queue.join() | ||||||
|  | 
 | ||||||
|  |             # 维护聊天上下文(原有逻辑) | ||||||
|  |             if content_type == "chat" and full_response.strip(): | ||||||
|  |                 self.chat_context.append({"role": "user", "content": [{"type": "text", "text": content["prompt"]}]}) | ||||||
|  |                 self.chat_context.append({"role": "assistant", "content": [{"type": "text", "text": full_response}]}) | ||||||
|  | 
 | ||||||
|  |             return full_response | ||||||
|  |         except Exception as e: | ||||||
|  |             error_msg = f"❌ API调用失败:{str(e)}" | ||||||
|  |             print(f"\n" + "="*50) | ||||||
|  |             print(error_msg) | ||||||
|  |             print("="*50 + "\n") | ||||||
|  |             self.tts_controller.speak(self.FEEDBACK_TEXT["api_error"]) | ||||||
|  |             return error_msg | ||||||
| @ -0,0 +1,30 @@ | |||||||
|  | import io | ||||||
|  | import base64 | ||||||
|  | from PIL import Image | ||||||
|  | from picamera2 import Picamera2 | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | # 原代码6. 摄像头模块完整逻辑 | ||||||
|  | class CameraModule: | ||||||
|  |     def __init__(self): | ||||||
|  |         try: | ||||||
|  |             self.camera = Picamera2() | ||||||
|  |             cam_config = self.camera.create_still_configuration(main={"size": (320, 240)}) | ||||||
|  |             self.camera.configure(cam_config) | ||||||
|  |             self.camera.start() | ||||||
|  |             print("📷 摄像头模块初始化成功") | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 摄像头失败:{str(e)}") | ||||||
|  |             self.camera = None | ||||||
|  | 
 | ||||||
|  |     def capture_base64(self): | ||||||
|  |         if not self.camera: | ||||||
|  |             return None | ||||||
|  |         try: | ||||||
|  |             img_array = self.camera.capture_array() | ||||||
|  |             img_byte = io.BytesIO() | ||||||
|  |             Image.fromarray(img_array).save(img_byte, format="JPEG", quality=80) | ||||||
|  |             return base64.b64encode(img_byte.getvalue()).decode("utf-8") | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 拍摄失败:{str(e)}") | ||||||
|  |             return None | ||||||
| @ -0,0 +1,291 @@ | |||||||
|  | import signal | ||||||
|  | import sys | ||||||
|  | import time | ||||||
|  | import re | ||||||
|  | import subprocess | ||||||
|  | import queue  | ||||||
|  | # 导入所有模块 | ||||||
|  | from tts_module import BaiduOnlineTTS | ||||||
|  | from volume_module import VolumeController, detect_audio_control | ||||||
|  | from motion_module import RobotMotionController | ||||||
|  | from camera_module import CameraModule | ||||||
|  | from ark_api_module import ArkAPIController | ||||||
|  | from voice_recog_module import VoiceRecogController | ||||||
|  | 
 | ||||||
|  | # -------------------- 1. 基础配置(完全保留原代码1. 基础配置) -------------------- | ||||||
|  | # 1.1 项目路径与运动模型 | ||||||
|  | PROJECT_ROOT = "/home/duckpi/open_duck_mini_ws/OPEN_DUCK_MINI/Open_Duck_Mini_Runtime-2" | ||||||
|  | sys.path.append(PROJECT_ROOT) | ||||||
|  | ONNX_MODEL_PATH = "/home/duckpi/open_duck_mini_ws/OPEN_DUCK_MINI/Open_Duck_Mini-2/BEST_WALK_ONNX_2.onnx" | ||||||
|  | 
 | ||||||
|  | # 1.2 火山方舟API配置 | ||||||
|  | ARK_API_KEY = "390d517c-129a-41c1-bf3d-458048007b69" | ||||||
|  | ARK_MODEL_ID = "doubao-seed-1-6-250615" | ||||||
|  | 
 | ||||||
|  | # 1.3 语音识别与唤醒词配置 | ||||||
|  | APPID = "1ff50710" | ||||||
|  | ACCESS_KEY_ID = "a4f43e95ee0a9518d11befac8d31f1d4" | ||||||
|  | ACCESS_KEY_SECRET = "YzQ4NTRhZjc2ZTM4MDA1YjM2MmIyNDEy" | ||||||
|  | ACCESS_KEY = "e0EQQBoH0HIVU9KrXsmB7CMlVci+GAs2x0Ejtrdp8CTtZmf25rCLaQ==" | ||||||
|  | WAKEUP_WORD_PATH = "/home/duckpi/open_duck_mini_ws/OPEN_DUCK_MINI/resources/xiaohuangya_zh_raspberry-pi_v3_0_0.ppn" | ||||||
|  | MODEL_PATH = "/home/duckpi/open_duck_mini_ws/OPEN_DUCK_MINI/resources/porcupine_params_zh.pv" | ||||||
|  | 
 | ||||||
|  | # 1.4 百度在线TTS配置 | ||||||
|  | BAIDU_TTS_API_KEY = "TnwYZPPvElNushOzfL6vBlUI" | ||||||
|  | BAIDU_TTS_SECRET_KEY = "55HeI8VNUMNlkW3t2QRwVtrjumpxjfxk" | ||||||
|  | 
 | ||||||
|  | # 1.5 语音反馈文本配置 | ||||||
|  | FEEDBACK_TEXT = { | ||||||
|  |     "wakeup": "你好呀,有什么吩咐", | ||||||
|  |     "move_forward": "好的,我正在前进", | ||||||
|  |     "move_backward": "好的,我正在后退", | ||||||
|  |     "turn_left": "好的,我正在左转", | ||||||
|  |     "turn_right": "好的,我正在右转", | ||||||
|  |     "image_recog": "好的,我来识别一下", | ||||||
|  |     "chat": "好的,我来想想", | ||||||
|  |     "volume_increase": "音量已增大", | ||||||
|  |     "volume_decrease": "音量已减小", | ||||||
|  |     "volume_max": "已调至最大音量", | ||||||
|  |     "volume_min": "已调至最小音量", | ||||||
|  |     "unknown": "抱歉,没听懂,请再说一次", | ||||||
|  |     "api_error": "抱歉,处理请求时出错了" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | # 1.6 音频参数 | ||||||
|  | VOLUME_STEP = 10 | ||||||
|  | MIN_VOLUME = 0 | ||||||
|  | MAX_VOLUME = 100 | ||||||
|  | CURRENT_VOLUME = 40 | ||||||
|  | AUDIO_CONTROL_NAME = None | ||||||
|  | 
 | ||||||
|  | # 1.7 麦克风与扬声器参数(模块内部已定义,此处保留用于一致性) | ||||||
|  | SAMPLE_RATE = 16000 | ||||||
|  | CHANNELS = 1 | ||||||
|  | SAMPLE_FORMAT = "int16" | ||||||
|  | AUDIO_ENCODE = "pcm_s16le" | ||||||
|  | LANG = "autodialect" | ||||||
|  | INTERACTION_TIMEOUT = 30 | ||||||
|  | 
 | ||||||
|  | # -------------------- 2. 全局状态变量(完全保留原代码2. 全局状态变量,用列表传引用) -------------------- | ||||||
|  | audio_q = queue.Queue() | ||||||
|  | last_audio_time = [time.time()]  # 列表传引用,供模块修改 | ||||||
|  | current_text = [""]               # 列表传引用,供模块修改 | ||||||
|  | final_result = [""]              # 列表传引用,供模块修改 | ||||||
|  | is_processing = [False]          # 列表传引用,供模块修改 | ||||||
|  | last_command_time = [time.time()]# 列表传引用,供模块修改 | ||||||
|  | feedback_playing = False         # TTS模块使用的全局变量 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # -------------------- 8. 指令解析与执行(完全保留原代码8. 指令解析与执行) -------------------- | ||||||
|  | def parse_voice_command(command_text: str): | ||||||
|  |     command_text = command_text.strip().lower() | ||||||
|  |     if not command_text: | ||||||
|  |         return ("unknown", {}) | ||||||
|  | 
 | ||||||
|  |     # 运动指令 | ||||||
|  |     motion_rules = [ | ||||||
|  |         {"keywords": ["前进", "往前走", "向前走"], "action": "move_forward"}, | ||||||
|  |         {"keywords": ["后退", "往后走", "向后退"], "action": "move_backward"}, | ||||||
|  |         {"keywords": ["左转", "向左转", "往左转"], "action": "turn_left"}, | ||||||
|  |         {"keywords": ["右转", "向右转", "往右转"], "action": "turn_right"}, | ||||||
|  |     ] | ||||||
|  |     for rule in motion_rules: | ||||||
|  |         if any(keyword in command_text for keyword in rule["keywords"]): | ||||||
|  |             number_match = re.search(r"(\d{1,2})", command_text) | ||||||
|  |             seconds = int(number_match.group(1)) if number_match else 2 | ||||||
|  |             return ("motion", {"action": rule["action"], "seconds": seconds}) | ||||||
|  | 
 | ||||||
|  |     # 图像识别指令 | ||||||
|  |     image_keywords = ["是什么", "这是什么", "识别", "看这个", "这东西", "这物体", "辨认"] | ||||||
|  |     if any(keyword in command_text for keyword in image_keywords): | ||||||
|  |         prompt = f"请简洁描述图片中的物体,1-2句话说明:{command_text}" | ||||||
|  |         return ("image_recog", {"prompt": prompt}) | ||||||
|  | 
 | ||||||
|  |     # 闲聊指令 | ||||||
|  |     chat_keywords = [ | ||||||
|  |         "什么", "怎么", "为什么", "哪里", "多少", "如何", "吗", "呢", "吧", | ||||||
|  |         "你好", "哈喽", "嗨", "今天", "天气", "时间", "故事", "笑话", "知识" | ||||||
|  |     ] | ||||||
|  |     exclude_keywords = ["前进", "后退", "左转", "右转", "识别", "音量", "增大", "减小"] | ||||||
|  |     if len(command_text) >= 2 and any(k in command_text for k in chat_keywords) and not any(k in command_text for k in exclude_keywords): | ||||||
|  |         return ("chat", {"prompt": command_text}) | ||||||
|  | 
 | ||||||
|  |     # 音量控制指令 | ||||||
|  |     if any(keyword in command_text for keyword in ["增大音量", "声音大一点", "调大音量"]): | ||||||
|  |         return ("volume", {"action": "increase"}) | ||||||
|  |     elif any(keyword in command_text for keyword in ["减小音量", "声音小一点", "调小音量"]): | ||||||
|  |         return ("volume", {"action": "decrease"}) | ||||||
|  |     elif any(keyword in command_text for keyword in ["最大音量", "声音最大"]): | ||||||
|  |         return ("volume", {"action": "max"}) | ||||||
|  |     elif any(keyword in command_text for keyword in ["最小音量", "声音最小", "静音"]): | ||||||
|  |         return ("volume", {"action": "min"}) | ||||||
|  | 
 | ||||||
|  |     # 未知指令 | ||||||
|  |     return ("unknown", {}) | ||||||
|  | 
 | ||||||
|  | def execute_command(command_type: str, params: dict, motion_controller, ark_api_controller, volume_controller): | ||||||
|  |     global is_processing, feedback_playing | ||||||
|  |     if is_processing[0]: | ||||||
|  |         tts_controller.speak(FEEDBACK_TEXT["unknown"]) | ||||||
|  |         print("⚠️  已有指令处理中,请稍后再说") | ||||||
|  |         return | ||||||
|  |     is_processing[0] = True | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         if command_type == "motion": | ||||||
|  |             motion_controller.execute_motion(params["action"], params["seconds"]) | ||||||
|  |          | ||||||
|  |         elif command_type == "image_recog": | ||||||
|  |             print(f"\n🔍 触发图像识别,正在拍摄...") | ||||||
|  |             image_base64 = camera_module.capture_base64() | ||||||
|  |             if not image_base64: | ||||||
|  |                 tts_controller.speak(FEEDBACK_TEXT["unknown"]) | ||||||
|  |                 print("\n" + "="*50) | ||||||
|  |                 print("❌ 图像采集失败,无法识别") | ||||||
|  |                 print("="*50 + "\n") | ||||||
|  |                 return | ||||||
|  |             ark_api_controller.call_ark_api("image_recog", {"image_base64": image_base64, "prompt": params["prompt"]}) | ||||||
|  |          | ||||||
|  |         elif command_type == "chat": | ||||||
|  |             print(f"\n💬 触发闲聊,正在思考...") | ||||||
|  |             ark_api_controller.call_ark_api("chat", {"prompt": params["prompt"]}) | ||||||
|  |          | ||||||
|  |         elif command_type == "volume": | ||||||
|  |             volume_action = params["action"] | ||||||
|  |             if volume_action == "increase": | ||||||
|  |                 success = volume_controller.adjust_volume(is_increase=True) | ||||||
|  |                 if success: | ||||||
|  |                     tts_controller.speak(FEEDBACK_TEXT["volume_increase"]) | ||||||
|  |             elif volume_action == "decrease": | ||||||
|  |                 success = volume_controller.adjust_volume(is_increase=False) | ||||||
|  |                 if success: | ||||||
|  |                     tts_controller.speak(FEEDBACK_TEXT["volume_decrease"]) | ||||||
|  |             elif volume_action == "max": | ||||||
|  |                 success = volume_controller.set_system_volume(MAX_VOLUME) | ||||||
|  |                 if success: | ||||||
|  |                     tts_controller.speak(FEEDBACK_TEXT["volume_max"]) | ||||||
|  |             elif volume_action == "min": | ||||||
|  |                 success = volume_controller.set_system_volume(MIN_VOLUME) | ||||||
|  |                 if success: | ||||||
|  |                     tts_controller.speak(FEEDBACK_TEXT["volume_min"]) | ||||||
|  |          | ||||||
|  |         elif command_type == "unknown": | ||||||
|  |             tts_controller.speak(FEEDBACK_TEXT["unknown"]) | ||||||
|  |             print("\n" + "="*50) | ||||||
|  |             print(f"❌ 未识别到有效指令,支持:") | ||||||
|  |             print(f"  - 运动:前进3秒、左转2秒 |  - 图像识别:这是什么") | ||||||
|  |             print(f"  - 闲聊:今天天气怎么样 |  - 音量:增大音量、减小音量") | ||||||
|  |             print("="*50 + "\n") | ||||||
|  |      | ||||||
|  |     finally: | ||||||
|  |         is_processing[0] = False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # -------------------- 11. 主循环(完全保留原代码11. 主循环逻辑) -------------------- | ||||||
|  | def main(): | ||||||
|  |     global tts_controller, camera_module, AUDIO_CONTROL_NAME, feedback_playing | ||||||
|  | 
 | ||||||
|  |     # 初始化各模块(按原代码顺序) | ||||||
|  |     # 1. 初始化TTS | ||||||
|  |     try: | ||||||
|  |         tts_controller = BaiduOnlineTTS(BAIDU_TTS_API_KEY, BAIDU_TTS_SECRET_KEY) | ||||||
|  |     except Exception as e: | ||||||
|  |         print(f"❌ TTS初始化失败: {str(e)}") | ||||||
|  |         sys.exit(1) | ||||||
|  | 
 | ||||||
|  |     # 2. 初始化音量控制 | ||||||
|  |     AUDIO_CONTROL_NAME = detect_audio_control() | ||||||
|  |     volume_controller = VolumeController( | ||||||
|  |         audio_control_name=AUDIO_CONTROL_NAME, | ||||||
|  |         current_volume=CURRENT_VOLUME, | ||||||
|  |         volume_step=VOLUME_STEP, | ||||||
|  |         min_volume=MIN_VOLUME, | ||||||
|  |         max_volume=MAX_VOLUME | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # 3. 初始化运动控制 | ||||||
|  |     motion_controller = RobotMotionController( | ||||||
|  |         onnx_model_path=ONNX_MODEL_PATH, | ||||||
|  |         tts_controller=tts_controller, | ||||||
|  |         feedback_text=FEEDBACK_TEXT | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # 4. 初始化摄像头 | ||||||
|  |     camera_module = CameraModule() | ||||||
|  | 
 | ||||||
|  |     # 5. 初始化API控制器 | ||||||
|  |     ark_api_controller = ArkAPIController( | ||||||
|  |         ark_api_key=ARK_API_KEY, | ||||||
|  |         ark_model_id=ARK_MODEL_ID, | ||||||
|  |         tts_controller=tts_controller, | ||||||
|  |         feedback_text=FEEDBACK_TEXT | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # 6. 初始化语音识别 | ||||||
|  |     voice_recog_controller = VoiceRecogController( | ||||||
|  |         access_key=ACCESS_KEY, | ||||||
|  |         wakeup_word_path=WAKEUP_WORD_PATH, | ||||||
|  |         model_path=MODEL_PATH, | ||||||
|  |         appid=APPID, | ||||||
|  |         access_key_id=ACCESS_KEY_ID, | ||||||
|  |         access_key_secret=ACCESS_KEY_SECRET, | ||||||
|  |         tts_controller=tts_controller, | ||||||
|  |         feedback_text=FEEDBACK_TEXT | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # 中断处理(完全保留原逻辑) | ||||||
|  |     def handle_interrupt(signum, frame): | ||||||
|  |         print("\n🛑 收到退出信号,正在清理资源...") | ||||||
|  |         # 停止机器人运动 | ||||||
|  |         if 'motion_controller' in globals() and hasattr(motion_controller, 'rl_walk'): | ||||||
|  |             motion_controller.rl_walk.last_commands = [0.0, 0.0, 0.0] | ||||||
|  |         # 停止TTS播放 | ||||||
|  |         global feedback_playing | ||||||
|  |         feedback_playing = False | ||||||
|  |         # 停止摄像头与麦克风 | ||||||
|  |         if 'camera_module' in globals() and camera_module.camera: | ||||||
|  |             camera_module.camera.stop() | ||||||
|  |         if hasattr(voice_recog_controller, 'stream') and voice_recog_controller.stream and voice_recog_controller.stream.active: | ||||||
|  |             voice_recog_controller.stream.stop() | ||||||
|  |         # 关闭TTS资源 | ||||||
|  |         tts_controller.close() | ||||||
|  |         print("✅ 所有资源清理完成,程序退出") | ||||||
|  |         sys.exit(0) | ||||||
|  | 
 | ||||||
|  |     signal.signal(signal.SIGINT, handle_interrupt) | ||||||
|  | 
 | ||||||
|  |     # 强制测试一次语音输出(原逻辑) | ||||||
|  |     print("\n🔍 正在测试语音输出...") | ||||||
|  |     tts_controller.speak("系统初始化完成,等待语音唤醒") | ||||||
|  | 
 | ||||||
|  |     # 主循环(原逻辑) | ||||||
|  |     while True: | ||||||
|  |         if voice_recog_controller.wakeup_listener(): | ||||||
|  |             # 定义指令执行回调函数(关键修复) | ||||||
|  |             def execute_callback(command_text): | ||||||
|  |                 command_type, params = parse_voice_command(command_text) | ||||||
|  |                 execute_command(command_type, params, motion_controller, ark_api_controller, volume_controller) | ||||||
|  |              | ||||||
|  |             # 启动WebSocket时传入回调函数 | ||||||
|  |             voice_recog_controller.start_websocket( | ||||||
|  |                 current_text=current_text, | ||||||
|  |                 final_result=final_result, | ||||||
|  |                 last_audio_time=last_audio_time, | ||||||
|  |                 is_processing=is_processing, | ||||||
|  |                 last_command_time=last_command_time, | ||||||
|  |                 execute_callback=execute_callback  # 传入回调 | ||||||
|  |             ) | ||||||
|  |             # 重置状态 | ||||||
|  |             last_audio_time[0] = time.time() | ||||||
|  |             last_command_time[0] = time.time() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     # # 确保ffmpeg已安装(原逻辑) | ||||||
|  |     # try: | ||||||
|  |     #     subprocess.run(["ffmpeg", "--version"], capture_output=True, check=True) | ||||||
|  |     # except: | ||||||
|  |     #     print("⚠️  未检测到ffmpeg,正在尝试安装...") | ||||||
|  |     #     subprocess.run(["sudo", "apt-get", "install", "-y", "ffmpeg"], check=True) | ||||||
|  |      | ||||||
|  |     main() | ||||||
| @ -0,0 +1,59 @@ | |||||||
|  | import sys | ||||||
|  | import time | ||||||
|  | import threading | ||||||
|  | 
 | ||||||
|  | # 原代码5. 运动控制模块完整逻辑(保留原路径导入) | ||||||
|  | PROJECT_ROOT = "/home/duckpi/open_duck_mini_ws/OPEN_DUCK_MINI/Open_Duck_Mini_Runtime-2" | ||||||
|  | sys.path.append(PROJECT_ROOT) | ||||||
|  | from v2_rl_walk_mujoco import RLWalk | ||||||
|  | 
 | ||||||
|  | class RobotMotionController: | ||||||
|  |     def __init__(self, onnx_model_path, tts_controller, feedback_text): | ||||||
|  |         # 接收调度脚本传入的TTS实例和反馈文本,保持原逻辑调用 | ||||||
|  |         self.tts_controller = tts_controller | ||||||
|  |         self.FEEDBACK_TEXT = feedback_text | ||||||
|  |         try: | ||||||
|  |             self.rl_walk = RLWalk( | ||||||
|  |                 onnx_model_path=onnx_model_path, | ||||||
|  |                 cutoff_frequency=40, | ||||||
|  |                 pid=[30, 0, 0] | ||||||
|  |             ) | ||||||
|  |             self.walk_thread = threading.Thread(target=self.rl_walk.run, daemon=True) | ||||||
|  |             self.walk_thread.start() | ||||||
|  |             time.sleep(1) | ||||||
|  |             print("🤖 运动控制模块初始化成功") | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 运动控制失败:{str(e)}") | ||||||
|  |             sys.exit(1) | ||||||
|  | 
 | ||||||
|  |     def execute_motion(self, action_name: str, seconds: int): | ||||||
|  |         # 全局变量由调度脚本传入,保留原逻辑 | ||||||
|  |         global is_processing | ||||||
|  |         is_processing = True | ||||||
|  |         try: | ||||||
|  |             # 播放反馈(同步执行确保声音输出) | ||||||
|  |             self.tts_controller.speak(self.FEEDBACK_TEXT[action_name]) | ||||||
|  | 
 | ||||||
|  |             # 执行运动 | ||||||
|  |             seconds = max(2, min(seconds, 5)) | ||||||
|  |             if action_name == "move_forward": | ||||||
|  |                 print(f"\n🚶 前进{seconds}秒...") | ||||||
|  |                 self.rl_walk.last_commands[0] = 0.17 | ||||||
|  |             elif action_name == "move_backward": | ||||||
|  |                 print(f"\n🚶 后退{seconds}秒...") | ||||||
|  |                 self.rl_walk.last_commands[0] = -0.17 | ||||||
|  |             elif action_name == "turn_left": | ||||||
|  |                 print(f"\n🔄 左转{seconds}秒...") | ||||||
|  |                 self.rl_walk.last_commands[2] = 1.1 | ||||||
|  |             elif action_name == "turn_right": | ||||||
|  |                 print(f"\n🔄 右转{seconds}秒...") | ||||||
|  |                 self.rl_walk.last_commands[2] = -1.1 | ||||||
|  |              | ||||||
|  |             time.sleep(seconds) | ||||||
|  |             self.rl_walk.last_commands = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] | ||||||
|  |             print(f"✅ 运动完成") | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 运动执行失败:{str(e)}") | ||||||
|  |             self.rl_walk.last_commands = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] | ||||||
|  |         finally: | ||||||
|  |             is_processing = False | ||||||
							
								
								
									
										173
									
								
								tts_module.py
									
									
									
									
									
								
							
							
						
						
									
										173
									
								
								tts_module.py
									
									
									
									
									
								
							| @ -0,0 +1,173 @@ | |||||||
|  | import pyaudio | ||||||
|  | import wave | ||||||
|  | import tempfile | ||||||
|  | import os | ||||||
|  | import requests | ||||||
|  | import time | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | # 原代码3. 百度在线TTS模块完整逻辑 | ||||||
|  | class BaiduOnlineTTS: | ||||||
|  |     def __init__(self, api_key, secret_key): | ||||||
|  |         """初始化百度在线TTS""" | ||||||
|  |         self.api_key = api_key | ||||||
|  |         self.secret_key = secret_key | ||||||
|  |         self.access_token = None | ||||||
|  |         self.token_expires = 0 | ||||||
|  |          | ||||||
|  |         # 初始化音频播放器 | ||||||
|  |         self.audio_player = pyaudio.PyAudio() | ||||||
|  |          | ||||||
|  |         # TTS配置参数 | ||||||
|  |         self.default_options = { | ||||||
|  |             'vol': 5,    # 音量(0-15) | ||||||
|  |             'spd': 5,    # 语速(0-9) | ||||||
|  |             'pit': 5,    # 音调(0-9) | ||||||
|  |             'per': 0     # 发音人(0:女,1:男,3:情感女,4:情感男) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         # 获取初始访问令牌 | ||||||
|  |         if not self._get_access_token(): | ||||||
|  |             raise Exception("无法获取百度API访问令牌,请检查密钥是否正确") | ||||||
|  | 
 | ||||||
|  |     def _get_access_token(self): | ||||||
|  |         """获取百度API访问令牌""" | ||||||
|  |         # 检查令牌是否仍然有效 | ||||||
|  |         if self.access_token and time.time() < self.token_expires - 300: | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         try: | ||||||
|  |             url = f"https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id={self.api_key}&client_secret={self.secret_key}" | ||||||
|  |             response = requests.get(url) | ||||||
|  |             result = response.json() | ||||||
|  |              | ||||||
|  |             if "access_token" in result: | ||||||
|  |                 self.access_token = result["access_token"] | ||||||
|  |                 self.token_expires = time.time() + result["expires_in"] | ||||||
|  |                 print("✅ 成功获取百度API访问令牌") | ||||||
|  |                 return True | ||||||
|  |             else: | ||||||
|  |                 print(f"❌ 获取令牌失败: {result}") | ||||||
|  |                 return False | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 获取令牌时发生错误: {str(e)}") | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     def text_to_speech(self, text, options=None, save_path=None): | ||||||
|  |         """将文本转换为语音""" | ||||||
|  |         # 确保令牌有效 | ||||||
|  |         if not self._get_access_token(): | ||||||
|  |             return None | ||||||
|  |              | ||||||
|  |         # 合并配置参数 | ||||||
|  |         params = self.default_options.copy() | ||||||
|  |         if options: | ||||||
|  |             params.update(options) | ||||||
|  |              | ||||||
|  |         try: | ||||||
|  |             # 对文本进行URL编码 | ||||||
|  |             encoded_text = requests.utils.quote(text) | ||||||
|  |             url = f"https://tsn.baidu.com/text2audio?tex={encoded_text}&lan=zh&cuid=baidu-tts-python&ctp=1&tok={self.access_token}" | ||||||
|  |              | ||||||
|  |             # 添加合成参数 | ||||||
|  |             for key, value in params.items(): | ||||||
|  |                 url += f"&{key}={value}" | ||||||
|  |                  | ||||||
|  |             # 发送请求 | ||||||
|  |             response = requests.get(url) | ||||||
|  |              | ||||||
|  |             # 检查响应是否为音频数据 | ||||||
|  |             if response.headers.get("Content-Type", "").startswith("audio/"): | ||||||
|  |                 # 保存文件(如果需要) | ||||||
|  |                 if save_path: | ||||||
|  |                     with open(save_path, "wb") as f: | ||||||
|  |                         f.write(response.content) | ||||||
|  |                     print(f"✅ 音频已保存至: {save_path}") | ||||||
|  |                  | ||||||
|  |                 return response.content | ||||||
|  |             else: | ||||||
|  |                 # 解析错误信息 | ||||||
|  |                 try: | ||||||
|  |                     error = response.json() | ||||||
|  |                     print(f"❌ 语音合成失败: {error.get('err_msg', '未知错误')}") | ||||||
|  |                 except: | ||||||
|  |                     print(f"❌ 语音合成失败,响应内容: {response.text}") | ||||||
|  |                 return None | ||||||
|  |                  | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 语音合成时发生错误: {str(e)}") | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def speak(self, text, options=None): | ||||||
|  |         """直接播放文本转换的语音""" | ||||||
|  |         # 全局变量由调度脚本传入,此处保留原逻辑调用 | ||||||
|  |         from main_scheduler import feedback_playing | ||||||
|  |         if feedback_playing: | ||||||
|  |             return False | ||||||
|  |              | ||||||
|  |         feedback_playing = True | ||||||
|  |          | ||||||
|  |         # 限制文本长度(百度API有长度限制) | ||||||
|  |         if len(text) > 1024: | ||||||
|  |             print("⚠️ 文本过长,将截断为1024字符") | ||||||
|  |             text = text[:1024] | ||||||
|  |              | ||||||
|  |         # 获取音频数据 | ||||||
|  |         audio_data = self.text_to_speech(text, options) | ||||||
|  |         if not audio_data: | ||||||
|  |             feedback_playing = False | ||||||
|  |             return False | ||||||
|  |              | ||||||
|  |         try: | ||||||
|  |             # 创建临时MP3文件 | ||||||
|  |             with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_file: | ||||||
|  |                 temp_file.write(audio_data) | ||||||
|  |                 temp_filename = temp_file.name | ||||||
|  |              | ||||||
|  |             # 转换为WAV格式(适配pyaudio) | ||||||
|  |             from pydub import AudioSegment | ||||||
|  |             audio = AudioSegment.from_mp3(temp_filename) | ||||||
|  |             with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wav_file: | ||||||
|  |                 audio.export(wav_file.name, format="wav") | ||||||
|  |                 wav_filename = wav_file.name | ||||||
|  |              | ||||||
|  |             # 播放WAV文件 | ||||||
|  |             wf = wave.open(wav_filename, 'rb') | ||||||
|  |             stream = self.audio_player.open( | ||||||
|  |                 format=self.audio_player.get_format_from_width(wf.getsampwidth()), | ||||||
|  |                 channels=wf.getnchannels(), | ||||||
|  |                 rate=wf.getframerate(), | ||||||
|  |                 output=True | ||||||
|  |             ) | ||||||
|  |              | ||||||
|  |             # 播放音频 | ||||||
|  |             chunk = 1024 | ||||||
|  |             data = wf.readframes(chunk) | ||||||
|  |             while data and feedback_playing: | ||||||
|  |                 stream.write(data) | ||||||
|  |                 data = wf.readframes(chunk) | ||||||
|  |              | ||||||
|  |             # 清理资源 | ||||||
|  |             stream.stop_stream() | ||||||
|  |             stream.close() | ||||||
|  |             wf.close() | ||||||
|  |              | ||||||
|  |             print(f"✅ 语音播放完成: {text[:20]}...") | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 播放语音时发生错误: {str(e)}") | ||||||
|  |             return False | ||||||
|  |              | ||||||
|  |         finally: | ||||||
|  |             # 删除临时文件 | ||||||
|  |             if 'temp_filename' in locals() and os.path.exists(temp_filename): | ||||||
|  |                 os.remove(temp_filename) | ||||||
|  |             if 'wav_filename' in locals() and os.path.exists(wav_filename): | ||||||
|  |                 os.remove(wav_filename) | ||||||
|  |             feedback_playing = False | ||||||
|  | 
 | ||||||
|  |     def close(self): | ||||||
|  |         """释放资源""" | ||||||
|  |         self.audio_player.terminate() | ||||||
|  |         print("✅ TTS资源已释放") | ||||||
| @ -0,0 +1,226 @@ | |||||||
|  | import sounddevice as sd | ||||||
|  | import pvporcupine | ||||||
|  | import struct | ||||||
|  | import websocket | ||||||
|  | import threading | ||||||
|  | import hmac | ||||||
|  | import hashlib | ||||||
|  | import base64 | ||||||
|  | import json | ||||||
|  | import time | ||||||
|  | import urllib.parse | ||||||
|  | import uuid | ||||||
|  | import queue | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | # 原代码9. 音频采集与WebSocket + 10. 唤醒词监听完整逻辑 | ||||||
|  | class VoiceRecogController: | ||||||
|  |     def __init__(self, access_key, wakeup_word_path, model_path, appid, access_key_id, access_key_secret, tts_controller, feedback_text): | ||||||
|  |         # 接收调度脚本传入的参数,保持原逻辑 | ||||||
|  |         self.ACCESS_KEY = access_key | ||||||
|  |         self.WAKEUP_WORD_PATH = wakeup_word_path | ||||||
|  |         self.MODEL_PATH = model_path | ||||||
|  |         self.APPID = appid | ||||||
|  |         self.ACCESS_KEY_ID = access_key_id | ||||||
|  |         self.ACCESS_KEY_SECRET = access_key_secret | ||||||
|  |         self.tts_controller = tts_controller | ||||||
|  |         self.FEEDBACK_TEXT = feedback_text | ||||||
|  |         self.SAMPLE_RATE = 16000 | ||||||
|  |         self.CHANNELS = 1 | ||||||
|  |         self.SAMPLE_FORMAT = "int16" | ||||||
|  |         self.INTERACTION_TIMEOUT = 30 | ||||||
|  |         self.audio_q = queue.Queue() | ||||||
|  |         self.stream = None  # 麦克风流后续初始化 | ||||||
|  | 
 | ||||||
|  |     def wakeup_listener(self): | ||||||
|  |         """原代码10. 唤醒词监听""" | ||||||
|  |         try: | ||||||
|  |             porcupine = pvporcupine.create( | ||||||
|  |                 access_key=self.ACCESS_KEY, | ||||||
|  |                 keyword_paths=[self.WAKEUP_WORD_PATH], | ||||||
|  |                 model_path=self.MODEL_PATH | ||||||
|  |             ) | ||||||
|  |             print(f"\n🎯 唤醒词引擎就绪(采样率:{porcupine.sample_rate})") | ||||||
|  | 
 | ||||||
|  |             wakeup_mic = sd.RawInputStream( | ||||||
|  |                 samplerate=porcupine.sample_rate, | ||||||
|  |                 blocksize=porcupine.frame_length, | ||||||
|  |                 dtype="int16", | ||||||
|  |                 channels=1 | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             print("📢 等待唤醒词「小黄鸭」(按Ctrl+C退出)") | ||||||
|  |             with wakeup_mic: | ||||||
|  |                 while True: | ||||||
|  |                     pcm_data, _ = wakeup_mic.read(porcupine.frame_length) | ||||||
|  |                     pcm_unpacked = struct.unpack_from("h" * porcupine.frame_length, pcm_data) | ||||||
|  |                     if porcupine.process(pcm_unpacked) >= 0: | ||||||
|  |                         print("🚀 检测到唤醒词「小黄鸭」!") | ||||||
|  |                         # 播放唤醒反馈(同步执行) | ||||||
|  |                         self.tts_controller.speak(self.FEEDBACK_TEXT["wakeup"]) | ||||||
|  |                         porcupine.delete() | ||||||
|  |                         return True | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"\n❌ 唤醒词监听失败:{str(e)}") | ||||||
|  |             print("   排查:1. 唤醒词文件路径 2. 麦克风连接 3. PicoVoice Key有效性") | ||||||
|  |             sys.exit(1) | ||||||
|  | 
 | ||||||
|  |     def _audio_callback(self, indata, frames, t, status): | ||||||
|  |         """原代码9. 音频采集回调""" | ||||||
|  |         if status: | ||||||
|  |             print(f"⚠️  音频异常:{status}") | ||||||
|  |         self.audio_q.put(bytes(indata)) | ||||||
|  | 
 | ||||||
|  |     def _create_ws_url(self): | ||||||
|  |         """原代码9. 创建WebSocket URL""" | ||||||
|  |         try: | ||||||
|  |             host = "office-api-ast-dx.iflyaisol.com" | ||||||
|  |             path = "/ast/communicate/v1" | ||||||
|  |             utc = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) + "+0000" | ||||||
|  |             session_uuid = str(uuid.uuid4()) | ||||||
|  | 
 | ||||||
|  |             params = { | ||||||
|  |                 "accessKeyId": self.ACCESS_KEY_ID, | ||||||
|  |                 "appId": self.APPID, | ||||||
|  |                 "samplerate": self.SAMPLE_RATE, | ||||||
|  |                 "audio_encode": "pcm_s16le", | ||||||
|  |                 "lang": "autodialect", | ||||||
|  |                 "uuid": session_uuid, | ||||||
|  |                 "utc": utc, | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             sorted_params = sorted(params.items(), key=lambda x: x[0]) | ||||||
|  |             base_string = "&".join( | ||||||
|  |                 f"{urllib.parse.quote_plus(str(k))}={urllib.parse.quote_plus(str(v))}" | ||||||
|  |                 for k, v in sorted_params | ||||||
|  |             ) | ||||||
|  |             signature = hmac.new( | ||||||
|  |                 self.ACCESS_KEY_SECRET.encode("utf-8"), | ||||||
|  |                 base_string.encode("utf-8"), | ||||||
|  |                 hashlib.sha1 | ||||||
|  |             ).digest() | ||||||
|  |             signature = base64.b64encode(signature).decode("utf-8") | ||||||
|  | 
 | ||||||
|  |             query = base_string + "&signature=" + urllib.parse.quote_plus(signature) | ||||||
|  |             return f"wss://{host}{path}?{query}", session_uuid | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ WebSocket URL生成失败:{str(e)}") | ||||||
|  |             return None, None | ||||||
|  | 
 | ||||||
|  |     def _on_message(self, ws, message, current_text, last_audio_time): | ||||||
|  |         """原代码9. WebSocket消息处理(接收全局变量引用)""" | ||||||
|  |         try: | ||||||
|  |             data = json.loads(message) | ||||||
|  |             if data.get("msg_type") == "result" and "cn" in data.get("data", {}): | ||||||
|  |                 words = [ | ||||||
|  |                     cw.get("w", "")  | ||||||
|  |                     for rt in data["data"]["cn"].get("st", {}).get("rt", []) | ||||||
|  |                     for ws_item in rt.get("ws", []) | ||||||
|  |                     for cw in ws_item.get("cw", []) | ||||||
|  |                 ] | ||||||
|  |                 if words: | ||||||
|  |                     current_text[0] = "".join(words)  # 用列表传引用,修改全局变量 | ||||||
|  |                     last_audio_time[0] = time.time() | ||||||
|  |                     print(f"🎧 识别中:{current_text[0]}", end="\r") | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"\n❌ 语音识别消息处理错误:{str(e)}") | ||||||
|  | 
 | ||||||
|  |     def _on_error(self, ws, error, is_processing): | ||||||
|  |         """原代码9. WebSocket错误处理""" | ||||||
|  |         if not is_processing[0]: | ||||||
|  |             print(f"\n❌ WebSocket连接错误:{str(error)}") | ||||||
|  | 
 | ||||||
|  |     def _on_close(self, ws, close_status_code, close_msg, current_text, final_result, stream): | ||||||
|  |         """原代码9. WebSocket关闭处理""" | ||||||
|  |         print(f"\n🔌 WebSocket连接关闭 | 状态码:{close_status_code}") | ||||||
|  |         if stream and stream.active: | ||||||
|  |             stream.stop() | ||||||
|  |         current_text[0] = "" | ||||||
|  |         final_result[0] = "" | ||||||
|  | 
 | ||||||
|  |     def _on_open(self, ws, stream, current_text, final_result, last_audio_time, is_processing, last_command_time, execute_callback): | ||||||
|  |         """新增 execute_callback 参数,用于接收指令执行函数""" | ||||||
|  |         def send_audio_and_handle(): | ||||||
|  |             print("\n🎤 指令已就绪!支持:") | ||||||
|  |             print("  - 运动:前进3秒、左转2秒 |  - 图像识别:这是什么") | ||||||
|  |             print("  - 闲聊:今天天气怎么样 |  - 音量:增大音量、减小音量\n") | ||||||
|  |             stream.start() | ||||||
|  |             current_text[0] = "" | ||||||
|  |             final_result[0] = "" | ||||||
|  |             last_command_time[0] = time.time() | ||||||
|  | 
 | ||||||
|  |             while True: | ||||||
|  |                 try: | ||||||
|  |                     # 1. 处理音频队列(避免堆积) | ||||||
|  |                     while self.audio_q.qsize() > 5: | ||||||
|  |                         self.audio_q.get_nowait() | ||||||
|  |                     # 2. 发送音频数据(若队列有数据) | ||||||
|  |                     audio_data = self.audio_q.get(timeout=0.5) | ||||||
|  |                     ws.send(audio_data, websocket.ABNF.OPCODE_BINARY) | ||||||
|  | 
 | ||||||
|  |                     # 3. 指令识别与执行:有文本且2秒内无新音频时执行 | ||||||
|  |                     if current_text[0] and (time.time() - last_audio_time[0]) > 2: | ||||||
|  |                         final_result[0] = current_text[0].strip() | ||||||
|  |                         if len(final_result[0]) > 0:  # 确保指令有效 | ||||||
|  |                             print(f"\n⏹ 最终指令:{final_result[0]}") | ||||||
|  |                             # 调用回调函数执行指令(关键修复:直接在这里执行) | ||||||
|  |                             execute_callback(final_result[0]) | ||||||
|  |                             last_command_time[0] = time.time()  # 更新最后操作时间 | ||||||
|  |                         current_text[0] = ""  # 执行后清空,避免重复识别 | ||||||
|  |                         final_result[0] = "" | ||||||
|  |                         time.sleep(1)  # 等待指令执行完成 | ||||||
|  | 
 | ||||||
|  |                     # 4. 超时检测:30秒无操作则关闭连接 | ||||||
|  |                     if time.time() - last_command_time[0] > self.INTERACTION_TIMEOUT: | ||||||
|  |                         print(f"\n⌛ {self.INTERACTION_TIMEOUT}秒无操作,关闭连接") | ||||||
|  |                         self.tts_controller.speak(self.FEEDBACK_TEXT.get("wakeup_timeout", "长时间没操作,我先休息啦")) | ||||||
|  |                         time.sleep(1) | ||||||
|  |                         ws.send("close", websocket.ABNF.OPCODE_TEXT) | ||||||
|  |                         break | ||||||
|  | 
 | ||||||
|  |                 except queue.Empty: | ||||||
|  |                     # 队列为空时,检测超时 | ||||||
|  |                     if time.time() - last_command_time[0] > self.INTERACTION_TIMEOUT: | ||||||
|  |                         print(f"\n⌛ {self.INTERACTION_TIMEOUT}秒无操作,关闭连接") | ||||||
|  |                         self.tts_controller.speak(self.FEEDBACK_TEXT.get("wakeup_timeout", "长时间没操作,我先休息啦")) | ||||||
|  |                         time.sleep(1) | ||||||
|  |                         ws.send("close", websocket.ABNF.OPCODE_TEXT) | ||||||
|  |                         break | ||||||
|  |                     continue  # 继续循环等待音频 | ||||||
|  |                 except Exception as e: | ||||||
|  |                     print(f"\n❌ 音频发送错误:{str(e)}") | ||||||
|  |                     break | ||||||
|  | 
 | ||||||
|  |         audio_thread = threading.Thread(target=send_audio_and_handle, daemon=True) | ||||||
|  |         audio_thread.start() | ||||||
|  | 
 | ||||||
|  |     def start_websocket(self, current_text, final_result, last_audio_time, is_processing, last_command_time, execute_callback): | ||||||
|  |         """新增 execute_callback 参数,用于传递指令执行函数""" | ||||||
|  |         self.stream = sd.RawInputStream( | ||||||
|  |             samplerate=self.SAMPLE_RATE, | ||||||
|  |             channels=self.CHANNELS, | ||||||
|  |             dtype=self.SAMPLE_FORMAT, | ||||||
|  |             callback=self._audio_callback, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         ws_url, session_id = self._create_ws_url() | ||||||
|  |         if not ws_url: | ||||||
|  |             print("⚠️  无法生成语音识别连接,3秒后重新监听...") | ||||||
|  |             time.sleep(3) | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             print(f"🔄 连接语音识别服务(会话ID:{session_id[:8]}...)") | ||||||
|  |             # 绑定WebSocket回调时传入 execute_callback | ||||||
|  |             ws = websocket.WebSocketApp( | ||||||
|  |                 ws_url, | ||||||
|  |                 on_open=lambda ws: self._on_open(ws, self.stream, current_text, final_result, last_audio_time, is_processing, last_command_time, execute_callback), | ||||||
|  |                 on_message=lambda ws, msg: self._on_message(ws, msg, current_text, last_audio_time), | ||||||
|  |                 on_error=lambda ws, err: self._on_error(ws, err, is_processing), | ||||||
|  |                 on_close=lambda ws, status, msg: self._on_close(ws, status, msg, current_text, final_result, self.stream) | ||||||
|  |             ) | ||||||
|  |             ws.run_forever(ping_interval=10, ping_timeout=5) | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 语音识别连接失败:{str(e)}") | ||||||
|  |             print("⚠️  3秒后重新监听唤醒词...") | ||||||
|  |             time.sleep(3) | ||||||
| @ -0,0 +1,93 @@ | |||||||
|  | import subprocess | ||||||
|  | import re | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | # 原代码4. 音频控制项自动检测与音量控制完整逻辑 | ||||||
|  | def detect_audio_control(): | ||||||
|  |     """自动检测可用的音频播放控制项""" | ||||||
|  |     try: | ||||||
|  |         result = subprocess.run(["amixer", "controls"], capture_output=True, text=True) | ||||||
|  |         playback_controls = [] | ||||||
|  |          | ||||||
|  |         # 优先查找常见的音频控制项 | ||||||
|  |         priority_names = ["Master", "Speaker", "Headphone", "Audio", "Sound"] | ||||||
|  |          | ||||||
|  |         for name in priority_names: | ||||||
|  |             if name in result.stdout: | ||||||
|  |                 print(f"✅ 自动检测到音频控制项:{name}") | ||||||
|  |                 return name | ||||||
|  |          | ||||||
|  |         # 如果没有找到优先项,从所有控制项中提取 | ||||||
|  |         for line in result.stdout.splitlines(): | ||||||
|  |             if "Playback" in line: | ||||||
|  |                 match = re.search(r"'([^']+)'", line) | ||||||
|  |                 if match: | ||||||
|  |                     playback_controls.append(match.group(1)) | ||||||
|  |          | ||||||
|  |         if playback_controls: | ||||||
|  |             print(f"✅ 找到音频控制项:{playback_controls[0]}") | ||||||
|  |             return playback_controls[0] | ||||||
|  |         else: | ||||||
|  |             print("⚠️  未检测到音频控制项,将尝试不指定控制项调节音量") | ||||||
|  |             return None | ||||||
|  |     except Exception as e: | ||||||
|  |         print(f"❌ 音频控制检测失败:{str(e)}") | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  | class VolumeController: | ||||||
|  |     def __init__(self, audio_control_name, current_volume, volume_step, min_volume, max_volume): | ||||||
|  |         # 接收调度脚本传入的全局参数,保持原逻辑 | ||||||
|  |         self.available = True | ||||||
|  |         self.AUDIO_CONTROL_NAME = audio_control_name | ||||||
|  |         self.CURRENT_VOLUME = current_volume | ||||||
|  |         self.VOLUME_STEP = volume_step | ||||||
|  |         self.MIN_VOLUME = min_volume | ||||||
|  |         self.MAX_VOLUME = max_volume | ||||||
|  |          | ||||||
|  |         try: | ||||||
|  |             # 强制取消静音并设置初始音量 | ||||||
|  |             self.set_system_volume(self.CURRENT_VOLUME) | ||||||
|  |             current_volume = self.get_system_volume() | ||||||
|  |             if current_volume is not None: | ||||||
|  |                 self.CURRENT_VOLUME = current_volume | ||||||
|  |                 print(f"🔊 音量控制初始化成功(当前音量:{self.CURRENT_VOLUME}%)") | ||||||
|  |             else: | ||||||
|  |                 print(f"⚠️  无法获取当前音量,但已尝试设置为{self.CURRENT_VOLUME}%") | ||||||
|  |         except Exception as e: | ||||||
|  |             self.available = False | ||||||
|  |             print(f"❌ 音量控制失败:{str(e)}") | ||||||
|  | 
 | ||||||
|  |     def get_system_volume(self): | ||||||
|  |         try: | ||||||
|  |             # 根据检测到的控制项获取音量 | ||||||
|  |             cmd = ["amixer", "get"] | ||||||
|  |             if self.AUDIO_CONTROL_NAME: | ||||||
|  |                 cmd.append(self.AUDIO_CONTROL_NAME) | ||||||
|  |              | ||||||
|  |             result = subprocess.run(cmd, capture_output=True, text=True) | ||||||
|  |             volume_match = re.search(r"(\d+)%", result.stdout) | ||||||
|  |             return int(volume_match.group(1)) if volume_match else None | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 获取音量失败:{str(e)}") | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def set_system_volume(self, target_volume: int): | ||||||
|  |         target_volume = max(self.MIN_VOLUME, min(self.MAX_VOLUME, target_volume)) | ||||||
|  |         try: | ||||||
|  |             # 根据检测到的控制项设置音量 | ||||||
|  |             cmd = ["amixer", "set"] | ||||||
|  |             if self.AUDIO_CONTROL_NAME: | ||||||
|  |                 cmd.append(self.AUDIO_CONTROL_NAME) | ||||||
|  |             cmd.extend([f"{target_volume}%", "unmute"])  # 强制取消静音 | ||||||
|  |              | ||||||
|  |             subprocess.run(cmd, capture_output=True) | ||||||
|  |             self.CURRENT_VOLUME = target_volume | ||||||
|  |             print(f"🔊 音量已调整至:{self.CURRENT_VOLUME}%") | ||||||
|  |             return True | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"❌ 调整音量失败:{str(e)}") | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     def adjust_volume(self, is_increase: bool): | ||||||
|  |         target_volume = self.CURRENT_VOLUME + self.VOLUME_STEP if is_increase else self.CURRENT_VOLUME - self.VOLUME_STEP | ||||||
|  |         return self.set_system_volume(target_volume) | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user