voice_nora.py aktualisiert

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