ivan.lee
Initial commit with LFS tracking
a783ac1
Raw
History Blame Contribute Delete
10.7 kB
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()