Feat: Hermes Voice Client — CustomTkinter GUI + Push-to-Talk + .exe Build

- main.py: CustomTkinter GUI (400x520) mit Status-Indikator, Conversation-Log,
  Screenshot-Toggle und Settings-Dialog. Thread-sicherer UI-Queue für Hotkey-Callbacks.
- hotkey_listener.py: pynput globaler Hotkey (press/release) + KeyCapturer für
  interaktive Hotkey-Konfiguration im Settings-Dialog.
- config_manager.py: JSON-Settings in ~/.hermes-voice/settings.json mit DEFAULTS,
  load() merged gespeicherte mit Default-Werten.
- audio_input.py: Vereinfacht auf start_recording/stop_recording/transcribe (kein
  Wake-Word-Loop mehr, direkt hotkey-gesteuert).
- ai_client.py: Settings-dict statt config.py-Imports, MC2-URL/Modelle konfigurierbar.
- requirements.txt: customtkinter>=5.2.0 + pynput>=1.7.6 hinzugefügt, pystray entfernt.
- build.bat: PyInstaller --onefile --windowed → dist/HermesVoice.exe.
- setup.bat: Veraltete Wake-Word-Hinweise entfernt, GUI-Check angepasst.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-26 22:23:36 +02:00
parent b51fc899ca
commit 037c4b11df
8 changed files with 555 additions and 239 deletions
+14 -12
View File
@@ -1,15 +1,20 @@
import httpx
from config import (
MC2_BASE_URL, CHAT_MODEL, VISION_MODEL,
MAX_RESPONSE_TOKENS, REQUEST_TIMEOUT, SYSTEM_PROMPT,
SCREENSHOT_ON_QUERY,
SYSTEM_PROMPT = (
"Du bist Hermes, ein hilfreicher KI-Assistent. "
"Antworte kurz und präzise, da deine Antwort vorgelesen wird. "
"Maximal 3-4 Sätze, kein Markdown."
)
REQUEST_TIMEOUT = 30
def ask(text: str, screenshot_b64: str | None = None) -> str:
def ask(text: str, screenshot_b64: str | None, settings: dict) -> str:
"""Schickt Text (+ optionalen Screenshot) an MC2 und gibt die Antwort zurück."""
use_vision = screenshot_b64 is not None and SCREENSHOT_ON_QUERY
model = VISION_MODEL if use_vision else CHAT_MODEL
use_vision = screenshot_b64 is not None
model = settings.get("vision_model", "Qwen3-VL-2B-Instruct") if use_vision \
else settings.get("chat_model", "Hermes-4-14B")
base_url = settings.get("mc2_url", "http://192.168.178.151:9001/v1")
max_tokens = int(settings.get("max_response_tokens", 200))
if use_vision:
user_content = [
@@ -27,16 +32,13 @@ def ask(text: str, screenshot_b64: str | None = None) -> str:
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_content},
],
"max_tokens": MAX_RESPONSE_TOKENS,
"max_tokens": max_tokens,
"stream": False,
}
try:
with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
response = client.post(
f"{MC2_BASE_URL}/chat/completions",
json=payload,
)
response = client.post(f"{base_url}/chat/completions", json=payload)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"].strip()
except httpx.TimeoutException: