Files
jarvis/archive/claw_mic.py
2026-09-01 01:25:09 +09:00

204 lines
7.3 KiB
Python

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