Files
mission-control-v2/client/hermes-voice/audio_input.py
T
Hitonabi 037c4b11df 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>
2026-06-26 22:23:36 +02:00

92 lines
2.3 KiB
Python

"""
Audio-Input: Aufnahme + Whisper STT.
Kein Wake-Word-Loop — Aufnahme startet direkt auf Hotkey-Signal.
"""
import os
import tempfile
import threading
import numpy as np
import sounddevice as sd
import scipy.io.wavfile as wav
from faster_whisper import WhisperModel
from pathlib import Path
SAMPLE_RATE = 16000
CHUNK = 1024
ENERGY_THRESHOLD = 0.008
_model: WhisperModel | None = None
_model_size: str = ""
_recording = False
_frames: list[np.ndarray] = []
_stream: sd.InputStream | None = None
_lock = threading.Lock()
def load_model(model_size: str):
global _model, _model_size
if _model is None or _model_size != model_size:
model_dir = Path.home() / ".hermes-voice" / "whisper-models"
model_dir.mkdir(parents=True, exist_ok=True)
_model = WhisperModel(
model_size,
device="cpu",
compute_type="int8",
download_root=str(model_dir),
)
_model_size = model_size
def start_recording():
global _recording, _frames, _stream
with _lock:
_recording = True
_frames = []
def callback(indata, frames, time, status):
if _recording:
_frames.append(indata[:, 0].copy())
_stream = sd.InputStream(
samplerate=SAMPLE_RATE,
channels=1,
dtype="float32",
blocksize=CHUNK,
callback=callback,
)
_stream.start()
def stop_recording() -> np.ndarray:
global _recording, _stream
with _lock:
_recording = False
if _stream:
_stream.stop()
_stream.close()
_stream = None
if not _frames:
return np.array([], dtype=np.float32)
return np.concatenate(_frames)
def transcribe(audio: np.ndarray, model_size: str, language: str) -> str:
if len(audio) < SAMPLE_RATE * 0.3:
return ""
load_model(model_size)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
tmp = f.name
try:
wav.write(tmp, SAMPLE_RATE, (audio * 32767).astype(np.int16))
lang = language if language != "auto" else None
segments, _ = _model.transcribe(
tmp,
language=lang,
beam_size=3,
vad_filter=True,
)
return " ".join(s.text for s in segments).strip()
finally:
os.unlink(tmp)