| import os |
| import sys |
| import time |
| import json |
| import base64 |
| import requests |
| import importlib |
| from datetime import datetime |
| from dotenv import load_dotenv |
|
|
| |
| try: |
| import speech_recognition as sr |
| except ImportError: |
| print("⚠️ 偵測到尚未安裝 `SpeechRecognition` 套件。") |
| print("請先執行以下指令安裝:") |
| print(" pip install SpeechRecognition") |
| sr = None |
|
|
| |
| 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_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() |
| |
| |
| result = call_whisper_audio(wav_bytes) |
| |
| if result: |
| record_dir = os.path.join(SCRIPT_DIR, "record") |
| os.makedirs(record_dir, exist_ok=True) |
| |
| |
| filename = os.path.join(record_dir, "latest_message.txt") |
| |
| |
| record_data = { |
| "timestamp": timestamp, |
| "duration_time": round(duration, 1), |
| "text": result, |
| "status": "not_processed" |
| } |
| |
| |
| 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") |
| |
| |
| with open(wav_filename, "wb") as f: |
| f.write(wav_bytes) |
| |
| converted = False |
| |
| 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 |
| |
| |
| 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: |
| |
| if os.path.exists(wav_filename): |
| os.remove(wav_filename) |
| print(f"✨ [聲音儲存成功] -> record/latest_voice.mp3") |
| else: |
| |
| 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_dir = os.path.join(SCRIPT_DIR, "record") |
| os.makedirs(record_dir, exist_ok=True) |
|
|
| recognizer = sr.Recognizer() |
| |
| |
| |
| recognizer.energy_threshold = 1200 |
| |
| |
| 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🎤 正在聆聽...") |
| |
| 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) |
| |
| |
| 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: |
| |
| 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() |
|
|