311 lines
10 KiB
Python
311 lines
10 KiB
Python
#!/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()
|