037c4b11df
- 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>
92 lines
2.3 KiB
Python
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)
|