File size: 7,848 Bytes
a783ac1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import os
import urllib.request
import json
import ssl
import time
import numpy as np
import requests
import subprocess
import platform
from reachy_mini import ReachyMini
from reachy_mini.utils import create_head_pose
from get_location import run_shortcuts_cli, get_mac_gps

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
RECORD_DIR = os.path.join(SCRIPT_DIR, "record")
os.makedirs(RECORD_DIR, exist_ok=True)

# ----------------- 加入 Monkey Patch 解決遠端連線問題 -----------------
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):
        # 如果傳入的信令伺服器不是 localhost,強制改為 localhost 以便走 SSH Tunnel
        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
# ---------------------------------------------------------------------

# 1. 讀取 .env 檔案中的金鑰
def load_env_variables():
    # 優先嘗試使用 python-dotenv
    try:
        from dotenv import load_dotenv
        load_dotenv('.env')
    except ImportError:
        pass
    
    # 手動備用解析 .env,確保在任何環境下都能正常讀取
    env_path = '.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('\'"')
                    os.environ[key] = val

load_env_variables()
def get_chat_completion(prompt: str) -> str:
    """使用 requests 呼叫 MiniCPM5-1B API 進行內容生成"""
    server_ip = os.getenv("server_ip", "10.112.5.79")
    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": 256,
        "temperature": 0.7
    }
    try:
        response = requests.post(url, headers=headers, json=payload, timeout=30)
        response.raise_for_status()
        result = response.json()
        return result["choices"][0]["message"]["content"]
    except Exception as e:
        return f"❌ 呼叫 MiniCPM 失敗:{e}"

def text_to_speech(text, output_file=None):
    if output_file is None:
        output_file = os.path.join(RECORD_DIR, "test.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 wiggle_antennas(mini, count=3):
    for _ in range(count):
        mini.goto_target(
            antennas=np.deg2rad([10, 10]),
            duration=3.0,
            method="minjerk"
        )
        mini.goto_target(
            antennas=np.deg2rad([-10, -10]),
            duration=3.0,
            method="minjerk"
        )

def run_location_workflow(mini):
    print("Connected successfully!")
    
    # 啟用馬達 (Torque ON)
    print("Enabling motors (Torque ON)...")
    mini.enable_motors()
    time.sleep(0.5)

    print("=" * 60)
    print("📍 正在獲取您的電腦 GPS 位置資訊...")
    print("=" * 60)
    
    # 取得位置
    raw_location = run_shortcuts_cli()
    gps_coords = get_mac_gps()
    
    if not raw_location and not gps_coords:
        location_info = "(無法取得定位,請確認定位服務與 'GetMyLocation' 捷徑是否已設定)"
    else:
        location_info = f"原始定位輸出:\n{raw_location}\n\n經緯度資料:\n{gps_coords}"
        
    print("\n" + "-" * 60)
    user_question = "請問我的位置在哪裡"
    print(f"💬 使用者提問:'{user_question}'")
    print("-" * 60)
    
    # 構建給 LLM 的 prompt,把位置資訊塞進 prompt 中
    prompt = f"""
使用者提問:'{user_question}'。

以下是系統為您偵測到的目前電腦位置與 GPS 資訊:
{location_info}

請整合上述的位置資訊,用自然流暢且親切的中文回覆使用者,請用4句簡短回覆。
"""
    
    print("⏳ 正在將位置資訊傳送給 MiniCPM 進行整合分析...")
    reply = get_chat_completion(prompt)
    
    print("\n🤖 MiniCPM 回覆:")
    print(reply)
    print("=" * 60)

    # 將回覆轉換成語音並播放
    if reply:
        location_wav = os.path.join(RECORD_DIR, "location.wav")
        if text_to_speech(reply, location_wav):
            print('Playing...')
            mini.media.play_sound(location_wav)
            # wiggle_antennas(mini, count=3)

            mini.goto_target(
                head=create_head_pose(y=10.0, mm=True),    # Up 5mm
                antennas=np.deg2rad([0, 0]),           # Antennas out
                body_yaw=np.deg2rad(0),                 # Turn body 30 degrees
                duration=3,                            # Slow movement
                method="minjerk"                         # Smooth movement
            )
            time.sleep(0.1)

            mini.goto_target(
                head=create_head_pose(y=-5.0, mm=True),    # Up 5mm
                antennas=np.deg2rad([0, 0]),           # Antennas out
                body_yaw=np.deg2rad(0),                 # Turn body 30 degrees
                duration=3,                            # Slow movement
                method="minjerk"                         # Smooth movement
            )
            time.sleep(0.1)

            mini.media.stop_playing()
    else:
        print("No text result returned from MiniCPM, skipping text-to-speech.")

    # 關閉馬達 (Torque OFF)
    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

    if shared_mini is not None:
        print("Reusing existing ReachyMini connection from RobotManager.")
        apply_safe_goto(shared_mini)
        run_location_workflow(shared_mini)
    else:
        # Standard standalone execution
        with ReachyMini(connection_mode="network", host="localhost") as mini:
            apply_safe_goto(mini)
            run_location_workflow(mini)

if __name__ == "__main__":
    main()