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