File size: 10,668 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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import os
import sys
import time
import json
import base64
import requests
import importlib
from datetime import datetime
from dotenv import load_dotenv

# Try importing SpeechRecognition.
try:
    import speech_recognition as sr
except ImportError:
    print("⚠️ 偵測到尚未安裝 `SpeechRecognition` 套件。")
    print("請先執行以下指令安裝:")
    print("  pip install SpeechRecognition")
    sr = None

# Determine directories and load environment variables
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(SCRIPT_DIR, '.env'))
load_dotenv(os.path.join(SCRIPT_DIR, '../.env'))
load_dotenv(os.path.join(SCRIPT_DIR, '../sales-bot/.env'))

if SCRIPT_DIR not in sys.path:
    sys.path.insert(0, SCRIPT_DIR)

# Camp AI Control flag
CAMP_AI_RUNNING = False

def get_server_ip():
    ip = os.getenv("server_ip")
    if ip:
        return ip
    try:
        env_path = os.path.join(SCRIPT_DIR, '.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"

def call_whisper_audio(audio_bytes):
    """使用 requests 呼叫 Whisper ASR 服務"""
    server_ip = get_server_ip()
    url = f"http://{server_ip}:4002/v1/audio/transcriptions"
    
    import io
    try:
        files = {"file": ("audio.wav", io.BytesIO(audio_bytes), "audio/wav")}
        response = requests.post(url, files=files, timeout=30)
        response.raise_for_status()
        result = response.json()
        return result.get("text", "").strip()
    except Exception as e:
        print(f"\n❌ 呼叫 Whisper 失敗: {e}")
        return None

def process_and_save(audio_data, timestamp, duration):
    """處理音訊:上傳給 Whisper、儲存轉錄的文字為 JSON、轉換並保存語音檔案,並返回轉錄的文字結果"""
    try:
        wav_bytes = audio_data.get_wav_data()
        
        # 呼叫 Whisper 進行轉錄
        result = call_whisper_audio(wav_bytes)
        
        if result:
            record_dir = os.path.join(SCRIPT_DIR, "record")
            os.makedirs(record_dir, exist_ok=True)
            
            # 檔名固定為:latest_message.txt
            filename = os.path.join(record_dir, "latest_message.txt")
            
            # 建立要儲存的單筆 JSON 資料結構
            record_data = {
                "timestamp": timestamp,
                "duration_time": round(duration, 1),
                "text": result,
                "status": "not_processed"
            }
            
            # 寫入文字檔 (內容為 JSON)
            with open(filename, "w", encoding="utf-8") as f:
                json.dump(record_data, f, ensure_ascii=False, indent=4)
            
            print(f"\n✨ [轉錄完成並儲存] -> record/latest_message.txt")
            print(f"👉 \"{result}\"")
            
            # 保存音訊檔案
            import subprocess
            wav_filename = os.path.join(record_dir, "latest_voice.wav")
            mp3_filename = os.path.join(record_dir, "latest_voice.mp3")
            
            # 寫入暫存的 wav 檔
            with open(wav_filename, "wb") as f:
                f.write(wav_bytes)
                
            converted = False
            # 嘗試使用 pydub 轉檔為 MP3
            try:
                from pydub import AudioSegment
                import io
                audio_segment = AudioSegment.from_wav(io.BytesIO(wav_bytes))
                audio_segment.export(mp3_filename, format="mp3")
                converted = True
            except Exception:
                pass
                
            # 嘗試呼叫系統 ffmpeg 指令轉檔為 MP3
            if not converted:
                try:
                    subprocess.run(
                        ["ffmpeg", "-y", "-i", wav_filename, "-codec:a", "libmp3lame", "-qscale:a", "2", mp3_filename],
                        stdout=subprocess.DEVNULL,
                        stderr=subprocess.DEVNULL,
                        check=True
                    )
                    converted = True
                except Exception:
                    pass
            
            if converted:
                # 刪除暫存的 wav 檔
                if os.path.exists(wav_filename):
                    os.remove(wav_filename)
                print(f"✨ [聲音儲存成功] -> record/latest_voice.mp3")
            else:
                # 若無法轉成 mp3,保留原始 wav 檔案
                print(f"⚠️ [聲音轉換 MP3 失敗] 已保留原始格式為 record/latest_voice.wav")
                print("  提示:若要啟用 MP3 轉換,請安裝 ffmpeg (例如: brew install ffmpeg) 與 pydub (pip install pydub)")
            
            return result
        else:
            print(f"\n⚠️ [轉錄失敗] 時間: {timestamp} (時長: {duration:.1f} 秒) - Whisper 未能成功回傳文字")
            return None
            
    except Exception as e:
        print(f"\n❌ [處理錯誤] 在處理音訊時發生異常: {e}")
        return None

def run_command_script(script_to_run):
    """動態載入並執行指定的 Python 程式碼模組"""
    script_path = os.path.join(SCRIPT_DIR, script_to_run)
    if os.path.exists(script_path):
        print(f"\n🚀 啟動程式碼: {script_to_run} (直接載入執行)...", flush=True)
        try:
            module_name = script_to_run.replace(".py", "")
            if module_name in sys.modules:
                module = importlib.reload(sys.modules[module_name])
            else:
                module = importlib.import_module(module_name)
            
            if hasattr(module, "main"):
                module.main()
            else:
                print(f"⚠️ 模組 {module_name} 沒有 main() 函式,直接執行模組內頂層程式碼可能已在載入時完成。")
            print(f"✅ {script_to_run} 執行完畢。", flush=True)
        except Exception as e:
            import traceback
            print(f"❌ 執行 {script_to_run} 時出錯:", flush=True)
            traceback.print_exc()
    else:
        print(f"❌ 找不到對應的程式碼檔案: {script_to_run}", flush=True)

def main():
    if sr is None:
        print("❌ 請安裝 SpeechRecognition 後再重新執行此腳本。")
        return

    # 確保 record 資料夾存在
    record_dir = os.path.join(SCRIPT_DIR, "record")
    os.makedirs(record_dir, exist_ok=True)

    recognizer = sr.Recognizer()
    
    # === 語音辨識靈敏度與斷句設定 ===
    # 能量閥值:數字越大越不靈敏。預設 300,建議設 800 - 1500 避開小雜音與呼吸聲。
    recognizer.energy_threshold = 1200
    
    # 是否開啟動態自動調節:True 會自動調,但安靜房間常會降得太低變極度靈敏;推薦設為 False。
    recognizer.dynamic_energy_threshold = False
    
    # 靜音判定秒數:說完話後,停頓超過此秒數即判定這句話結束。
    recognizer.pause_threshold = 1.2 
    
    print("\n" + "="*60)
    print("🤖 Reachy Mini 語音控制與排程服務已啟動!")
    print("📂 所有轉錄與語音檔案將儲存在:record/ 目錄下")
    print("🎙️ 持續監聽語音中,若有觸發關鍵字將會同步執行對應的任務...")
    print("🛑 請按 Ctrl+C 可隨時終止程式")
    print("="*60)
    
    global CAMP_AI_RUNNING
    CAMP_AI_RUNNING = True
    
    try:
        with sr.Microphone() as source:
            print("⚡ 正在適應周圍環境噪音 (1秒)...請先保持安靜...")
            recognizer.adjust_for_ambient_noise(source, duration=1.0)
            print(f"🟢 系統準備就緒!(目前能量門檻設定為: {recognizer.energy_threshold})")
            print("🎤 隨時可以開始說話!")
    except Exception as e:
        print(f"❌ 麥克風初始化失敗: {e}")
        CAMP_AI_RUNNING = False
        return

    while CAMP_AI_RUNNING:
        try:
            with sr.Microphone() as source:
                print("\n🎤 正在聆聽...")
                # listen 加上 timeout=2.0,每 2 秒檢查一次是否已關閉服務
                audio_data = recognizer.listen(source, timeout=2.0, phrase_time_limit=30)
                
            # 計算錄音時間戳與時長
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            duration = len(audio_data.frame_data) / (audio_data.sample_rate * audio_data.sample_width)
            
            # 過濾低於 0.8 秒的短雜音
            if duration < 0.8:
                continue
                
            print(f"📝 偵測到語音,時長約 {duration:.1f} 秒,正在處理與上傳...")
            result = process_and_save(audio_data, timestamp, duration)
            
            if result:
                text_lower = result.lower()
                script_to_run = None
                
                # 關鍵字判定 (支援常見的語音辨識同音/近音字,不分大小寫)
                if "what is this" in text_lower or "what's this" in text_lower:
                    script_to_run = "look_that.py"
                elif "suggestion" in text_lower or "suggestions" in text_lower:
                    script_to_run = "ask.py"
                elif "guard mode" in text_lower or "god mode" in text_lower or "grad mode" in text_lower:
                    script_to_run = "guard.py"
                elif "location" in text_lower:
                    script_to_run = "location.py"
                
                if script_to_run:
                    run_command_script(script_to_run)
                else:
                    print("ℹ️ 語音內容未包含觸發指令的關鍵字。")
                    
        except sr.WaitTimeoutError:
            # 逾時未偵測到聲音,回到迴圈頂部以檢查 CAMP_AI_RUNNING 狀態
            continue
        except KeyboardInterrupt:
            print("\n👋 偵測到終止指令,正在關閉程式...")
            CAMP_AI_RUNNING = False
            break
        except Exception as e:
            print(f"\n❌ 監聽或處理過程發生異常: {e}")
            time.sleep(1)

if __name__ == "__main__":
    main()