Feat: Hermes Voice Client (Wake Word + STT + Vision + TTS)
Windows-Desktop-Script: Energy VAD + Whisper Wake Detection, faster-whisper STT, mss Screen Capture, MC2 Vision/Chat API, Edge TTS Ausgabe, pystray System Tray. Kein Account nötig — Wake Word frei konfigurierbar via WAKE_WORDS in config.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Hermes Voice Client
|
||||
Wake Word → STT → Screenshot → Hermes (MC2) → TTS
|
||||
|
||||
Läuft als System-Tray-App im Hintergrund.
|
||||
Aktivierung: "Hey Jarvis" (konfigurierbar in config.py)
|
||||
"""
|
||||
|
||||
import threading
|
||||
import sys
|
||||
from PIL import Image, ImageDraw
|
||||
import pystray
|
||||
|
||||
from audio_input import wait_for_wake_word, record_speech, transcribe
|
||||
from audio_output import speak, play_ding, stop_speaking
|
||||
from screen_capture import capture_screen
|
||||
from ai_client import ask
|
||||
from config import SCREENSHOT_ON_QUERY
|
||||
|
||||
# ── Status ───────────────────────────────────────────────────────────────
|
||||
class State:
|
||||
IDLE = "idle" # wartet auf Wake Word
|
||||
LISTENING = "listen" # nimmt Sprache auf
|
||||
THINKING = "think" # wartet auf MC2-Antwort
|
||||
SPEAKING = "speak" # gibt Antwort aus
|
||||
|
||||
_state = State.IDLE
|
||||
_stop_event = threading.Event()
|
||||
_tray_icon: pystray.Icon | None = None
|
||||
|
||||
|
||||
# ── Tray Icon ────────────────────────────────────────────────────────────
|
||||
COLORS = {
|
||||
State.IDLE: "#22c55e", # grün
|
||||
State.LISTENING: "#eab308", # gelb
|
||||
State.THINKING: "#3b82f6", # blau
|
||||
State.SPEAKING: "#a855f7", # lila
|
||||
}
|
||||
|
||||
def _make_icon(color: str) -> Image.Image:
|
||||
img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.ellipse([4, 4, 60, 60], fill=color)
|
||||
return img
|
||||
|
||||
def _set_state(state: str):
|
||||
global _state
|
||||
_state = state
|
||||
if _tray_icon:
|
||||
_tray_icon.icon = _make_icon(COLORS[state])
|
||||
labels = {
|
||||
State.IDLE: "Hermes — bereit (Hey Jarvis)",
|
||||
State.LISTENING: "Hermes — hört zu…",
|
||||
State.THINKING: "Hermes — denkt…",
|
||||
State.SPEAKING: "Hermes — spricht",
|
||||
}
|
||||
_tray_icon.title = labels[state]
|
||||
|
||||
|
||||
# ── Hauptloop ────────────────────────────────────────────────────────────
|
||||
def _voice_loop():
|
||||
print("[Hermes] Bereit. Sage 'Hey Jarvis' zum Aktivieren.")
|
||||
while not _stop_event.is_set():
|
||||
_set_state(State.IDLE)
|
||||
|
||||
# 1. Auf Wake Word warten
|
||||
detected = wait_for_wake_word(_stop_event)
|
||||
if not detected:
|
||||
break
|
||||
|
||||
# 2. Aktivierungs-Ding + Aufnahme starten
|
||||
play_ding()
|
||||
_set_state(State.LISTENING)
|
||||
audio = record_speech()
|
||||
|
||||
# 3. Transkribieren
|
||||
text = transcribe(audio)
|
||||
if not text:
|
||||
continue
|
||||
print(f"[STT] {text!r}")
|
||||
|
||||
# 4. Screenshot machen
|
||||
screenshot = capture_screen() if SCREENSHOT_ON_QUERY else None
|
||||
|
||||
# 5. MC2 fragen
|
||||
_set_state(State.THINKING)
|
||||
response = ask(text, screenshot)
|
||||
print(f"[Hermes] {response!r}")
|
||||
|
||||
# 6. Antwort sprechen
|
||||
_set_state(State.SPEAKING)
|
||||
speak(response)
|
||||
|
||||
print("[Hermes] Beendet.")
|
||||
|
||||
|
||||
# ── System Tray ──────────────────────────────────────────────────────────
|
||||
def _on_quit(icon, item):
|
||||
_stop_event.set()
|
||||
stop_speaking()
|
||||
icon.stop()
|
||||
|
||||
def _on_mute(icon, item):
|
||||
stop_speaking()
|
||||
|
||||
def main():
|
||||
global _tray_icon
|
||||
|
||||
loop_thread = threading.Thread(target=_voice_loop, daemon=True)
|
||||
loop_thread.start()
|
||||
|
||||
menu = pystray.Menu(
|
||||
pystray.MenuItem("Hermes Voice", None, enabled=False),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("Sprechen stoppen", _on_mute),
|
||||
pystray.MenuItem("Beenden", _on_quit),
|
||||
)
|
||||
|
||||
_tray_icon = pystray.Icon(
|
||||
"hermes-voice",
|
||||
icon=_make_icon(COLORS[State.IDLE]),
|
||||
title="Hermes — bereit (Hey Jarvis)",
|
||||
menu=menu,
|
||||
)
|
||||
_tray_icon.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user