voice_nora.py(not_in_use) aktualisiert
This commit is contained in:
@@ -0,0 +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
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user