| import time |
| import numpy as np |
| import requests |
| import os |
| from reachy_mini import ReachyMini |
| from reachy_mini.utils import create_head_pose |
|
|
| SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| RECORD_DIR = os.path.join(SCRIPT_DIR, "record") |
| os.makedirs(RECORD_DIR, exist_ok=True) |
|
|
| def get_server_ip(): |
| ip = os.getenv("server_ip") |
| if ip: |
| return ip |
| try: |
| env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env') |
| if os.path.exists(env_path): |
| with open(env_path, 'r', encoding='utf-8') as f: |
| for line in f: |
| line = line.strip() |
| if line and not line.startswith('#') and '=' in line: |
| parts = line.split('=', 1) |
| key = parts[0].strip() |
| val = parts[1].strip().strip('\'"') |
| if key == "server_ip": |
| return val |
| except Exception: |
| pass |
| return "10.112.5.79" |
|
|
|
|
| |
| from reachy_mini.media.media_manager import MediaManager |
| if not hasattr(MediaManager, "_is_patched"): |
| original_media_manager_init = MediaManager.__init__ |
| def mocked_media_manager_init(self, *args, **kwargs): |
| |
| if kwargs.get("signalling_host") not in (None, "localhost", "127.0.0.1"): |
| kwargs["signalling_host"] = "localhost" |
| original_media_manager_init(self, *args, **kwargs) |
| |
| MediaManager.__init__ = mocked_media_manager_init |
| MediaManager._is_patched = True |
| |
|
|
| def transcribe_audio(file_path=None): |
| if file_path is None: |
| file_path = os.path.join(RECORD_DIR, "ask.mp3") |
| server_ip = get_server_ip() |
| url = f"http://{server_ip}:4002/v1/audio/transcriptions" |
| try: |
| with open(file_path, "rb") as f: |
| files = {"file": f} |
| response = requests.post(url, files=files) |
| response.raise_for_status() |
| result = response.json() |
| return result.get("text", "") |
| except Exception as e: |
| print(f"Error during audio transcription: {e}") |
| return None |
|
|
| def text_to_speech(text, output_file=None): |
| if output_file is None: |
| output_file = os.path.join(RECORD_DIR, "tmp.wav") |
| url = "http://localhost:4003/v1/audio/speech" |
| headers = { |
| "Content-Type": "application/json" |
| } |
| payload = { |
| "text": text |
| } |
| print(f"Converting text to speech via {url}...") |
| try: |
| response = requests.post(url, headers=headers, json=payload, timeout=30) |
| response.raise_for_status() |
| with open(output_file, "wb") as f: |
| f.write(response.content) |
| print(f"Audio saved to {output_file}") |
| return True |
| except Exception as e: |
| print(f"Error in TTS request: {e}") |
| return False |
|
|
| def get_chat_completion(prompt): |
| server_ip = get_server_ip() |
| url = f"http://{server_ip}:4001/v1/chat/completions" |
| headers = { |
| "Content-Type": "application/json" |
| } |
| payload = { |
| "model": "openbmb/MiniCPM5-1B", |
| "messages": [{"role": "user", "content": prompt}], |
| "max_tokens": 128, |
| "temperature": 0.7 |
| } |
| try: |
| response = requests.post(url, headers=headers, json=payload) |
| response.raise_for_status() |
| result = response.json() |
| return result["choices"][0]["message"]["content"] |
| except Exception as e: |
| print(f"Error during chat completion: {e}") |
| return None |
|
|
| def ask_gesture(mini, count=3): |
| for _ in range(count): |
| mini.goto_target( |
| head=create_head_pose(y=0.0, roll=15, pitch=-20, mm=True), |
| antennas=np.deg2rad([-30, -30]), |
| body_yaw=np.deg2rad(0), |
| duration=1.0, |
| method="minjerk" |
| ) |
| time.sleep(0.1) |
|
|
| mini.goto_target( |
| head=create_head_pose(y=0.0, roll=-15, pitch=-20, mm=True), |
| antennas=np.deg2rad([30, 30]), |
| body_yaw=np.deg2rad(0), |
| duration=1.0, |
| method="minjerk" |
| ) |
| time.sleep(0.1) |
|
|
| def run_ask_workflow(mini): |
| print("Connected successfully!") |
| |
| |
| print("Enabling motors (Torque ON)...") |
| mini.enable_motors() |
| time.sleep(0.5) |
| |
| print("Moving robot up (goto_target)...") |
| mini.goto_target( |
| head=create_head_pose(z=10, mm=True), |
| antennas=np.deg2rad([0, 0]), |
| body_yaw=np.deg2rad(0), |
| duration=2.0, |
| method="minjerk" |
| ) |
| time.sleep(0.5) |
| |
|
|
| |
| transcribed_text = transcribe_audio(os.path.join(RECORD_DIR, "latest_voice.mp3")) |
| print("=== Transcription Result ===") |
| print(transcribed_text) |
| print("============================") |
| |
| if transcribed_text: |
| response_text = get_chat_completion(transcribed_text) |
| print("=== LLM Response ===") |
| print(response_text) |
| print("====================") |
| |
| |
| if response_text: |
| response_wav = os.path.join(RECORD_DIR, "ask_response.wav") |
| if text_to_speech(response_text, response_wav): |
| print('Playing...') |
| mini.media.play_sound(response_wav) |
| ask_gesture(mini, count=3) |
| mini.media.stop_playing() |
| else: |
| print("No response text returned from reasoning model, skipping text-to-speech.") |
|
|
| print("Moving robot down (goto_target)...") |
| mini.goto_target( |
| head=create_head_pose(z=-15, mm=True), |
| antennas=np.deg2rad([0, 0]), |
| body_yaw=np.deg2rad(0), |
| duration=2.0, |
| method="minjerk" |
| ) |
| print("Down movement completed!") |
| time.sleep(0.5) |
| |
| |
| print("Disabling motors (Torque OFF - making robot limp)...") |
| mini.disable_motors() |
| print("Done!") |
|
|
| def apply_safe_goto(mini): |
| original_goto = mini.goto_target |
| if not hasattr(original_goto, "_is_safe"): |
| def safe_goto(*args, **kwargs): |
| try: |
| return original_goto(*args, **kwargs) |
| except Exception as e: |
| if "timeout" in str(e).lower() or "TimeoutError" in type(e).__name__: |
| print(f"⚠️ [網路延遲] 動作發送超時但可能已在執行中,繼續下一個動作... ({e})") |
| else: |
| raise e |
| safe_goto._is_safe = True |
| mini.goto_target = safe_goto |
|
|
| def main(): |
| print("Connecting to Reachy Mini...") |
| shared_mini = None |
| try: |
| from app import robot_manager |
| if robot_manager and robot_manager.mini is not None: |
| shared_mini = robot_manager.mini |
| except Exception: |
| pass |
|
|
| try: |
| if shared_mini is not None: |
| print("Reusing existing ReachyMini connection from RobotManager.") |
| apply_safe_goto(shared_mini) |
| run_ask_workflow(shared_mini) |
| else: |
| |
| with ReachyMini(connection_mode="network", host="localhost") as mini: |
| apply_safe_goto(mini) |
| run_ask_workflow(mini) |
| finally: |
| |
| voice_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "record", "latest_voice.mp3") |
| if os.path.exists(voice_file): |
| try: |
| os.remove(voice_file) |
| print(f"🗑️ 已刪除暫存的語音檔: {voice_file}") |
| except Exception as e: |
| print(f"⚠️ 無法刪除暫存語音檔: {e}") |
|
|
| if __name__ == "__main__": |
| main() |
|
|