import os import time import logging import requests import speech_recognition as sr import pygame import io from ctypes import * os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" # ALSA 에러 로그 숨기기 ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p) def py_error_handler(filename, line, function, err, fmt): pass c_error_handler = ERROR_HANDLER_FUNC(py_error_handler) try: asound = cdll.LoadLibrary('libasound.so.2') asound.snd_lib_error_set_handler(c_error_handler) except: pass logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') logger = logging.getLogger(__name__) # ─── 환경 설정 ─────────────────────────────────────────────── VM_IP = "192.168.0.149" OPENCLAW_PORT = 18789 # 오픈클로 Gateway RPC 엔드포인트 # chat.send: 텍스트 메시지 전송 CHAT_URL = f"http://{VM_IP}:{OPENCLAW_PORT}/api/v1/chat/completions" # 오디오 첨부 전송 (media.audio enabled 시 Gemini가 STT 처리) AUDIO_URL = f"http://{VM_IP}:{OPENCLAW_PORT}/v1/audio/transcriptions" # TTS: openedai-speech 또는 오픈클로 내장 TTS TTS_URL = f"http://{VM_IP}:{OPENCLAW_PORT}/v1/audio/speech" WAKE_WORD = "애미나이" MIC_INDEX = 3 # ───────────────────────────────────────────────────────────── pygame.mixer.init() def send_audio_to_openclaw(audio_data) -> str: """ WAV를 오픈클로에 전송. media.audio.enabled + google provider 설정 시 오픈클로가 Gemini로 STT 후 LLM 답변까지 처리. 반환값: 오픈클로 텍스트 응답 """ try: wav_bytes = audio_data.get_wav_data() files = { 'audio': ('cmd.wav', io.BytesIO(wav_bytes), 'audio/wav') } logger.info("오픈클로로 오디오 전송 중...") resp = requests.post(AUDIO_URL, files=files, timeout=30) resp.raise_for_status() logger.info(f"응답 코드: {resp.status_code}") # 오픈클로 응답이 JSON인 경우 try: data = resp.json() # 오픈클로 응답 구조에 따라 키 조정 return data.get("reply") or data.get("message") or data.get("text") or str(data) except Exception: # 텍스트 응답인 경우 return resp.text except requests.exceptions.ConnectionError: logger.error(f"연결 실패 - {VM_IP}:{OPENCLAW_PORT} 확인 필요") return "" except Exception as e: logger.error(f"오디오 전송 실패: {e}") return "" def send_text_to_openclaw(text: str) -> str: """텍스트를 오픈클로 chat API로 전송 (오디오 전송 안 될 때 폴백)""" try: payload = {"message": text} resp = requests.post(CHAT_URL, json=payload, timeout=30) resp.raise_for_status() data = resp.json() return data.get("reply") or data.get("message") or data.get("text") or str(data) except Exception as e: logger.error(f"텍스트 전송 실패: {e}") return "" def speak_edge_tts(text: str): """ 오픈클로 내장 TTS (Edge TTS, API 키 불필요) 사용. 안 되면 Google gTTS로 폴백. """ try: payload = { "model": "tts-1", "input": text, "voice": "alloy", "response_format": "mp3" } resp = requests.post(TTS_URL, json=payload, timeout=15) if resp.status_code == 200: audio_buf = io.BytesIO(resp.content) pygame.mixer.music.load(audio_buf, "mp3") pygame.mixer.music.play() while pygame.mixer.music.get_busy(): time.sleep(0.1) logger.info("TTS 재생 완료") return except Exception as e: logger.warning(f"오픈클로 TTS 실패, gTTS 시도: {e}") # gTTS 폴백 (로컬 설치: pip install gtts) try: from gtts import gTTS tts = gTTS(text=text, lang='ko') tts.save("/tmp/reply.mp3") pygame.mixer.music.load("/tmp/reply.mp3") pygame.mixer.music.play() while pygame.mixer.music.get_busy(): time.sleep(0.1) except Exception as e: logger.error(f"gTTS도 실패: {e}") def main(): recognizer = sr.Recognizer() recognizer.energy_threshold = 400 recognizer.pause_threshold = 1.0 # 마이크 목록 출력 mics = sr.Microphone.list_microphone_names() logger.info(f"사용 가능한 마이크 ({len(mics)}개):") for i, mic in enumerate(mics): logger.info(f" {i}: {mic}") # 안전하게 마이크 열기 mic = None for test_index in [MIC_INDEX, 0, 1, 2, 3]: # 여러 인덱스 시도 try: logger.info(f"마이크 {test_index} 테스트 중...") mic = sr.Microphone(device_index=test_index) with mic as source: test_audio = recognizer.listen(source, timeout=1, phrase_time_limit=1) logger.info(f"✅ 마이크 {test_index} 성공!") break except Exception as e: logger.warning(f"❌ 마이크 {test_index} 실패: {e}") continue if mic is None: logger.error("사용 가능한 마이크를 찾을 수 없습니다. 프로그램 종료.") return # 소음 보정 (실패해도 무시) try: with mic as source: recognizer.adjust_for_ambient_noise(source, duration=1) logger.info(f"energy_threshold: {recognizer.energy_threshold:.0f}") except Exception as e: logger.warning(f"소음 보정 실패 (무시): {e}") logger.info(f"==== '{WAKE_WORD}' 대기 중 ====") while True: try: with mic as source: logger.debug("마이크 듣는 중...") audio = recognizer.listen(source, timeout=None, phrase_time_limit=3) try: text = recognizer.recognize_google(audio, language='ko-KR') logger.info(f"🎤 인식: '{text}'") if WAKE_WORD in text: logger.info("🔔 호출어 감지!") speak_edge_tts("네, 말씀하세요") # 명령어 녹음 with mic as source: logger.info("명령 녹음 중...") cmd_audio = recognizer.listen(source, timeout=5, phrase_time_limit=10) # 오픈클로 전송 reply = send_audio_to_openclaw(cmd_audio) if reply: speak_edge_tts(reply) else: speak_edge_tts("응답을 받지 못했어요.") except sr.UnknownValueError: pass # 조용히 무시 except sr.RequestError as e: logger.error(f"Google STT 오류: {e}") except sr.WaitTimeoutError: pass # 타임아웃 무시 except Exception as e: logger.error(f"루프 오류: {e}") time.sleep(1) if __name__ == "__main__": main()