voice_nora.py aktualisiert

This commit is contained in:
2026-07-06 13:32:25 +00:00
parent 8e2062c772
commit 7d33843b3f
+174 -78
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): 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() pa = pyaudio.PyAudio()
stream = pa.open(format=pyaudio.paInt16, channels=1, rate=16000, stream = pa.open(
input=True, frames_per_buffer=1024) 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: pa, stream = self._open_stream()
try:
audio_chunk = stream.read(1024, exception_on_overflow=False)
audio_np = np.frombuffer(audio_chunk, dtype=np.int16)
if self.ww_model.predict(audio_np).get("nora", 0) > 0.58: try:
print("\n'Nora' erkannt!") while self.running:
await self.process_voice_command() audio = stream.read(FRAME_SIZE, exception_on_overflow=False)
except: audio_np = np.frombuffer(audio, dtype=np.int16)
pass
stream.stop_stream() try:
stream.close() result = self.ww_model.predict(audio_np)
pa.terminate()
async def process_voice_command(self): score = max(result.values()) if result else 0.0
print("🎤 Ich höre zu... (sprich natürlich)")
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 except Exception as e:
print("❌ Zu kurz.") 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 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)
async def speak(self, text: str): # =========================
"""Edge TTS - weibliche Stimme""" # STT
# =========================
async def transcribe(self, audio_bytes):
try: 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") text = " ".join([s.text for s in segments]).strip()
os.system("mpg123 -q response.mp3") return text
except Exception as e: 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(): 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())