voice_nora.py aktualisiert

This commit is contained in:
2026-07-06 13:32:25 +00:00
parent 8e2062c772
commit 7d33843b3f
+175 -79
View File
@@ -1,142 +1,238 @@
import asyncio
import os
import time
import traceback
from pathlib import Path
import numpy as np
import pyaudio
import webrtcvad
from openwakeword.model import Model
from faster_whisper import WhisperModel
from edge_tts import Communicate
import wave
import webrtcvad # Für bessere Sprach-Erkennung
from agent import Agent
from util.logger import get_logger
logger = get_logger("VOICE")
logger = get_logger("NORA")
# =========================
# CONFIG
# =========================
WAKEWORD = "nora"
WAKE_THRESHOLD = 0.60
SAMPLE_RATE = 16000
FRAME_SIZE = 1024
VAD_MODE = 3 # 0-3 (3 = aggressiv)
MAX_RECORD_SEC = 12
SILENCE_LIMIT = 25 # frames
TTS_VOICE = "de-DE-AmalaNeural"
MODEL_PATH = Path.home() / ".cache/openwakeword/models/hey_jarvis_v0.1.tflite"
# =========================
# VOICE ASSISTANT CORE
# =========================
class VoiceNora:
def __init__(self):
logger.info("Initialisiere N.O.R.A...")
self.agent = Agent()
self.vad = webrtcvad.Vad(3)
self.running = True
# === Wakeword Modell mit korrektem Pfad ===
model_dir = Path.home() / ".cache" / "openwakeword" / "models"
# ---- VAD ----
self.vad = webrtcvad.Vad(VAD_MODE)
self.ww_model = Model(
wakeword="nora",
model_paths={
"nora": str(model_dir / "hey_jarvis_v0.1.tflite")
# ---- Wakeword Model ----
self.ww_model = self._load_wakeword_model()
},
inference_framework="tflite"
)
# ---- Whisper STT ----
self.stt = WhisperModel(
"tiny",
device="cpu",
compute_type="int8"
)
logger.info("N.O.R.A bereit ✔")
logger.info(f"Wakeword: {WAKEWORD}")
logger.info(f"Model: {MODEL_PATH}")
# STT
self.stt_model = WhisperModel("tiny", device="cpu", compute_type="int8")
# =========================
# INIT HELPERS
# =========================
def _load_wakeword_model(self):
logger.info("Lade Wakeword-Modell...")
print("\n🎤 Voice N.O.R.A gestartet")
print(" Wakeword: 'Nora'")
print(" Weibliche Stimme aktiviert\n")
if not MODEL_PATH.exists():
logger.error(f"Model nicht gefunden: {MODEL_PATH}")
raise FileNotFoundError(MODEL_PATH)
async def listen_for_wakeword(self):
try:
model = Model(
wakeword_models=[str(MODEL_PATH)]
)
logger.info("Wakeword-Modell geladen ✔")
return model
except Exception as e:
logger.error("Fehler beim Laden des Wakeword-Modells")
logger.error(str(e))
raise
# =========================
# AUDIO STREAM
# =========================
def _open_stream(self):
pa = pyaudio.PyAudio()
stream = pa.open(format=pyaudio.paInt16, channels=1, rate=16000,
input=True, frames_per_buffer=1024)
stream = pa.open(
format=pyaudio.paInt16,
channels=1,
rate=SAMPLE_RATE,
input=True,
frames_per_buffer=FRAME_SIZE
)
return pa, stream
print("⏳ Lausche auf 'Nora'...")
# =========================
# MAIN LOOP
# =========================
async def run(self):
logger.info("Starte Audio-Loop...")
while self.running:
try:
audio_chunk = stream.read(1024, exception_on_overflow=False)
audio_np = np.frombuffer(audio_chunk, dtype=np.int16)
pa, stream = self._open_stream()
if self.ww_model.predict(audio_np).get("nora", 0) > 0.58:
print("\n'Nora' erkannt!")
await self.process_voice_command()
except:
pass
try:
while self.running:
audio = stream.read(FRAME_SIZE, exception_on_overflow=False)
audio_np = np.frombuffer(audio, dtype=np.int16)
stream.stop_stream()
stream.close()
pa.terminate()
try:
result = self.ww_model.predict(audio_np)
async def process_voice_command(self):
print("🎤 Ich höre zu... (sprich natürlich)")
score = max(result.values()) if result else 0.0
audio_data = await self.record_with_vad(max_duration=12)
if score > WAKE_THRESHOLD:
logger.info(f"Wakeword erkannt (score={score:.2f})")
await self.handle_command()
if len(audio_data) < 8000: # zu kurz
print("❌ Zu kurz.")
except Exception as e:
logger.error(f"Wakeword Fehler: {e}")
except KeyboardInterrupt:
logger.info("Beende N.O.R.A...")
finally:
stream.stop_stream()
stream.close()
pa.terminate()
# =========================
# COMMAND FLOW
# =========================
async def handle_command(self):
logger.info("Aufnahme startet...")
audio = await self.record_until_silence()
if len(audio) < 8000:
logger.warning("Audio zu kurz")
return
# STT
segments, _ = self.stt_model.transcribe(
audio_data, language="de", beam_size=5, vad_filter=True
)
text = " ".join(segment.text for segment in segments).strip()
text = await self.transcribe(audio)
if not text:
print("❌ Konnte nichts verstehen.")
logger.warning("Keine Sprache erkannt")
return
print(f"👤 Du: {text}")
logger.info(f"USER: {text}")
response = await self.agent.run(text)
print(f"🗣️ N.O.R.A: {response}")
logger.info(f"NORA: {response}")
await self.speak(response)
async def record_with_vad(self, max_duration=12):
"""Aufnahme mit Sprach-Erkennung (endet wenn du aufhörst zu sprechen)"""
pa = pyaudio.PyAudio()
stream = pa.open(format=pyaudio.paInt16, channels=1, rate=16000,
input=True, frames_per_buffer=320)
# =========================
# RECORDING (VAD)
# =========================
async def record_until_silence(self):
pa, stream = self._open_stream()
frames = []
silence_count = 0
max_silence = 25 # ca. 1,5 Sekunden Stille
silence = 0
for _ in range(int(16000 / 320 * max_duration)):
chunk = stream.read(320, exception_on_overflow=False)
max_frames = int(SAMPLE_RATE / FRAME_SIZE * MAX_RECORD_SEC)
for _ in range(max_frames):
chunk = stream.read(FRAME_SIZE, exception_on_overflow=False)
frames.append(chunk)
# VAD prüfen
is_speech = self.vad.is_speech(chunk, 16000)
if not is_speech:
silence_count += 1
else:
silence_count = 0
is_speech = self.vad.is_speech(chunk, SAMPLE_RATE)
if silence_count > max_silence and len(frames) > 30:
break # aufhören wenn lange Stille
if is_speech:
silence = 0
else:
silence += 1
if silence > SILENCE_LIMIT and len(frames) > 20:
break
stream.stop_stream()
stream.close()
pa.terminate()
return b''.join(frames)
return b"".join(frames)
async def speak(self, text: str):
"""Edge TTS - weibliche Stimme"""
# =========================
# STT
# =========================
async def transcribe(self, audio_bytes):
try:
communicate = Communicate(text, voice="de-DE-AmalaNeural") # Weiblich, natürlich
segments, _ = self.stt.transcribe(
audio_bytes,
language="de",
beam_size=5
)
await communicate.save("response.mp3")
os.system("mpg123 -q response.mp3")
text = " ".join([s.text for s in segments]).strip()
return text
except Exception as e:
logger.error(f"TTS Fehler: {e}")
logger.error("STT Fehler")
logger.error(traceback.format_exc())
return ""
# =========================
# TTS
# =========================
async def speak(self, text: str):
try:
filename = f"response_{int(time.time())}.mp3"
tts = Communicate(text, voice=TTS_VOICE)
await tts.save(filename)
os.system(f"mpg123 -q {filename}")
os.remove(filename)
except Exception as e:
logger.error("TTS Fehler")
logger.error(traceback.format_exc())
# =========================
# MAIN
# =========================
async def main():
voice = VoiceNora()
try:
await voice.listen_for_wakeword()
except KeyboardInterrupt:
print("\n\n👋 N.O.R.A wird beendet.")
except Exception as e:
logger.error(str(e))
nora = VoiceNora()
await nora.run()
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())