#!/usr/bin/env python3 """자비스 원격 테스트 — 볼륨 키우고 스피커/마이크/제미나이 확인. 사용: source ~/jarvis_env/bin/activate python ~/test_jarvis.py # 전체 테스트 (큰 소리 재생) python ~/test_jarvis.py --volume 100 # 볼륨 % python ~/test_jarvis.py --speak-only # 스피커만 python ~/test_jarvis.py --mic-only # 마이크 레벨만 python ~/test_jarvis.py --gemini-only """ from __future__ import annotations import argparse import os import subprocess import sys import time from ctypes import CFUNCTYPE, c_char_p, c_int, cdll os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" 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 try: asound = cdll.LoadLibrary("libasound.so.2") asound.snd_lib_error_set_handler(ERROR_HANDLER_FUNC(_py_error_handler)) except Exception: pass def set_volume(percent: int) -> None: """시스템 스피커 볼륨을 percent%로 맞춤 (여러 방식 시도).""" percent = max(0, min(150, int(percent))) print(f"\n[볼륨] {percent}% 로 설정 시도...") cmds = [ ["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", f"{percent / 100:.2f}"], ["pactl", "set-sink-volume", "@DEFAULT_SINK@", f"{percent}%"], ["amixer", "-q", "sset", "Master", f"{min(percent, 100)}%"], ["amixer", "-q", "sset", "PCM", f"{min(percent, 100)}%"], ["amixer", "-c", "0", "-q", "sset", "Headphone", f"{min(percent, 100)}%"], ] ok = False for cmd in cmds: try: r = subprocess.run(cmd, capture_output=True, text=True, timeout=5) if r.returncode == 0: print(f" OK: {' '.join(cmd)}") ok = True except (FileNotFoundError, subprocess.TimeoutExpired): pass # 음소거 해제 for cmd in ( ["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "0"], ["pactl", "set-sink-mute", "@DEFAULT_SINK@", "0"], ["amixer", "-q", "sset", "Master", "unmute"], ): try: subprocess.run(cmd, capture_output=True, timeout=5) except (FileNotFoundError, subprocess.TimeoutExpired): pass if not ok: print(" 경고: 볼륨 명령을 적용하지 못했습니다. 재생은 시도합니다.") def show_volume() -> None: for cmd in ( ["wpctl", "get-volume", "@DEFAULT_AUDIO_SINK@"], ["pactl", "get-sink-volume", "@DEFAULT_SINK@"], ["amixer", "sget", "Master"], ): try: r = subprocess.run(cmd, capture_output=True, text=True, timeout=5) if r.returncode == 0 and r.stdout.strip(): print(f" 현재: {r.stdout.strip().splitlines()[0]}") return except (FileNotFoundError, subprocess.TimeoutExpired): pass def list_devices() -> None: import pyaudio pa = pyaudio.PyAudio() print("\n[오디오 장치]") for i in range(pa.get_device_count()): d = pa.get_device_info_by_index(i) if d["maxInputChannels"] > 0 or d["maxOutputChannels"] > 0: print( f" {i}: in={d['maxInputChannels']} out={d['maxOutputChannels']} " f"{d['name']}" ) pa.terminate() def load_api_key() -> str: key = os.environ.get("GOOGLE_API_KEY", "") if key and "여기에_" not in key: return key # jarvis.py 기본값에서 읽기 (이미 키가 들어있는 경우) jarvis = os.path.join(os.path.dirname(os.path.abspath(__file__)), "jarvis.py") if os.path.isfile(jarvis): text = open(jarvis, encoding="utf-8").read() import re m = re.search( r'GOOGLE_API_KEY = os\.environ\.get\(\s*"GOOGLE_API_KEY",\s*"([^"]+)"\s*\)', text, ) if not m: m = re.search( r'os\.environ\.get\(\s*"GOOGLE_API_KEY",\s*\n\s*"([^"]+)"', text, ) if m and "여기에_" not in m.group(1): return m.group(1) raise SystemExit("GOOGLE_API_KEY 를 찾을 수 없습니다. jarvis.py 에 키를 넣으세요.") def test_speak(volume_pygame: float = 1.0) -> None: from gtts import gTTS import pygame print("\n[스피커] TTS 재생 — 카메라로 들리는지 확인해 보세요.") msg = ( "안녕하세요. 자비스 스피커 테스트입니다. " "지금 소리가 크게 나고 있습니다. 하나, 둘, 셋." ) path = "/tmp/jarvis_test_reply.mp3" gTTS(text=msg, lang="ko").save(path) pygame.mixer.init() pygame.mixer.music.set_volume(max(0.0, min(1.0, volume_pygame))) pygame.mixer.music.load(path) pygame.mixer.music.play() while pygame.mixer.music.get_busy(): time.sleep(0.1) pygame.mixer.music.unload() print(" 재생 완료.") def test_beep() -> None: """네트워크 없이도 스피커 확인용 짧은 비프.""" print("\n[스피커] 비프음 재생...") # sox/aplay 또는 speaker-test for cmd in ( ["speaker-test", "-t", "sine", "-f", "880", "-l", "1", "-c", "2"], [ "bash", "-c", "ffmpeg -f lavfi -i 'sine=frequency=880:duration=1' -f wav - " "| aplay -q 2>/dev/null", ], ): try: r = subprocess.run(cmd, capture_output=True, timeout=8) if r.returncode == 0: print(" 비프 완료.") return except (FileNotFoundError, subprocess.TimeoutExpired): pass print(" 비프 도구 없음 — TTS만 사용합니다.") def test_mic(seconds: float = 3.0, mic_index: int | None = None) -> None: import numpy as np import pyaudio print(f"\n[마이크] {seconds:.0f}초간 입력 레벨 측정 (밖에 있어도 OK)...") pa = pyaudio.PyAudio() candidates: list[int | None] = [] if mic_index is not None: candidates.append(mic_index) else: for i in range(pa.get_device_count()): d = pa.get_device_info_by_index(i) if d["maxInputChannels"] <= 0: continue name = d["name"] if "pulse" in name or name == "default": candidates.append(i) elif "USB" in name: candidates.append(i) candidates.append(None) # 시스템 기본 stream = None rate = 16000 used_index: int | None = None for idx in candidates: for try_rate in (16000, 44100, 48000): kwargs = dict( format=pyaudio.paInt16, channels=1, rate=try_rate, input=True, frames_per_buffer=1024, ) if idx is not None: kwargs["input_device_index"] = idx try: stream = pa.open(**kwargs) rate = try_rate used_index = idx name = ( pa.get_device_info_by_index(idx)["name"] if idx is not None else "default" ) print(f" 열림: index={idx} ({name}) rate={rate}") break except OSError: continue if stream is not None: break if stream is None: pa.terminate() print(" → 마이크를 열 수 없습니다.") return peaks = [] n = max(1, int(rate / 1024 * seconds)) for _ in range(n): data = stream.read(1024, exception_on_overflow=False) arr = np.frombuffer(data, dtype=np.int16).astype(np.float32) peaks.append(float(np.max(np.abs(arr)))) stream.stop_stream() stream.close() pa.terminate() peak = max(peaks) if peaks else 0 avg = sum(peaks) / len(peaks) if peaks else 0 print(f" peak={peak:.0f} / 32767 avg={avg:.0f} (device={used_index})") if peak < 200: print(" → 거의 무음. 마이크 연결·권한·MIC_INDEX 확인 필요.") elif peak < 2000: print(" → 입력은 있으나 작음. 마이크 게인 올려보세요.") else: print(" → 마이크 입력 정상으로 보입니다.") def test_gemini() -> None: import google.generativeai as genai print("\n[제미나이] 짧은 텍스트 질의...") key = load_api_key() genai.configure(api_key=key) model_name = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash") model = genai.GenerativeModel(model_name) try: r = model.generate_content("한 문장으로만 답해: 너는 누구야?") print(f" 모델={model_name}") print(f" 답: {(r.text or '').strip()}") except Exception as e: print(f" 실패: {e}") print(" → 모델 이름을 gemini-1.5-flash 등으로 바꿔보세요.") def main() -> None: p = argparse.ArgumentParser(description="자비스 원격 테스트") p.add_argument("--volume", type=int, default=100, help="시스템 볼륨 %% (기본 100)") p.add_argument("--speak-only", action="store_true") p.add_argument("--mic-only", action="store_true") p.add_argument("--gemini-only", action="store_true") p.add_argument("--mic-index", type=int, default=None) p.add_argument("--no-beep", action="store_true") args = p.parse_args() print("=== 자비스 원격 테스트 ===") set_volume(args.volume) show_volume() only = args.speak_only or args.mic_only or args.gemini_only if not only: list_devices() if args.mic_only: test_mic(mic_index=args.mic_index) return if args.gemini_only: test_gemini() return if args.speak_only: if not args.no_beep: test_beep() test_speak(volume_pygame=1.0) return if not args.no_beep: test_beep() test_speak(volume_pygame=1.0) test_mic(mic_index=args.mic_index) test_gemini() print("\n=== 끝 ===") print("카메라로 TTS/비프가 들렸는지 확인해 주세요.") print("다시 크게: python ~/test_jarvis.py --speak-only --volume 100") if __name__ == "__main__": main()