Files
AI-System/voice_nora.py(not_in_use)

238 lines
5.8 KiB
Plaintext

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
from agent import Agent
from util.logger import get_logger
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.running = True
# ---- VAD ----
self.vad = webrtcvad.Vad(VAD_MODE)
# ---- Wakeword Model ----
self.ww_model = self._load_wakeword_model()
# ---- 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}")
# =========================
# INIT HELPERS
# =========================
def _load_wakeword_model(self):
logger.info("Lade Wakeword-Modell...")
if not MODEL_PATH.exists():
logger.error(f"Model nicht gefunden: {MODEL_PATH}")
raise FileNotFoundError(MODEL_PATH)
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=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.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
text = await self.transcribe(audio)
if not text:
logger.warning("Keine Sprache erkannt")
return
logger.info(f"USER: {text}")
response = await self.agent.run(text)
logger.info(f"NORA: {response}")
await self.speak(response)
# =========================
# RECORDING (VAD)
# =========================
async def record_until_silence(self):
pa, stream = self._open_stream()
frames = []
silence = 0
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)
is_speech = self.vad.is_speech(chunk, SAMPLE_RATE)
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)
# =========================
# 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):
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():
nora = VoiceNora()
await nora.run()
if __name__ == "__main__":
asyncio.run(main())