diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7d4344 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +.env +venv/ + diff --git a/__pycache__/config.cpython-311.pyc b/__pycache__/config.cpython-311.pyc index 65f72c1..766f5f6 100644 Binary files a/__pycache__/config.cpython-311.pyc and b/__pycache__/config.cpython-311.pyc differ diff --git a/voice_nora.py b/voice_nora.py new file mode 100644 index 0000000..fb63e17 --- /dev/null +++ b/voice_nora.py @@ -0,0 +1,132 @@ +import asyncio +import os +import numpy as np +import pyaudio +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") + + +class VoiceNora: + def __init__(self): + self.agent = Agent() + self.running = True + self.vad = webrtcvad.Vad(2) # Aggressivität 0-3 + + # Wake Word + self.ww_model = Model(wakeword="nora") + + # STT + self.stt_model = WhisperModel("tiny", device="cpu", compute_type="int8") + + print("\n🎤 Voice N.O.R.A gestartet") + print(" Wakeword: 'Nora'") + print(" Weibliche Stimme aktiviert\n") + + 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: + 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: + print("\n✅ 'Nora' erkannt!") + await self.process_voice_command() + except: + pass + + stream.stop_stream() + stream.close() + pa.terminate() + + async def process_voice_command(self): + print("🎤 Ich höre zu... (sprich natürlich)") + + audio_data = await self.record_with_vad(max_duration=12) + + if len(audio_data) < 8000: # zu kurz + print("❌ 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() + + if not text: + print("❌ Konnte nichts verstehen.") + return + + print(f"👤 Du: {text}") + + response = await self.agent.run(text) + print(f"🗣️ N.O.R.A: {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) + + frames = [] + silence_count = 0 + max_silence = 25 # ca. 1,5 Sekunden Stille + + for _ in range(int(16000 / 320 * max_duration)): + chunk = stream.read(320, 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 + + if silence_count > max_silence and len(frames) > 30: + break # aufhören wenn lange Stille + + stream.stop_stream() + stream.close() + pa.terminate() + + return b''.join(frames) + + async def speak(self, text: str): + """Edge TTS - weibliche Stimme""" + try: + communicate = Communicate(text, voice="de-DE-AmalaNeural") # Weiblich, natürlich + + await communicate.save("response.mp3") + os.system("mpg123 -q response.mp3") + + except Exception as e: + logger.error(f"TTS Fehler: {e}") + +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)) + +if __name__ == "__main__": + asyncio.run(main())