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