Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,34 +40,34 @@ asr:
transformers_engine: "huggingface" # "huggingface" or "onnxruntime"
device: "cpu" # Pi5 : CPU (or "hailo")
compute_type: "int8"
download_root: null # Let Python code build the path dynamically
download_root: null # Let Python code build the path dynamically
skip_native_teardown: false # Avoid native teardown segfaults on some ARM/PortAudio stacks
store_audio: true
store_audio_path: ".tmp/asr.wav"


wake:
wake_word: "hey_jarvis"
model_name: "embedding_model"
wake_word: "hello_jarvis"
backend: "wakeword"
model_name: "hey_jarvis_v0.1"
model_path: null
inference_framework: "onnx"
threshold: 0.4
cooldown_seconds: 2.0
noise_suppression: true
download_root: null
vad_threshold: 0.6
download_root: ".cache/audio/models/wakeword"

audio:
input_sample_rate: 48000
input_chunk_ms: 30 # taille des chunks audio en ms
input_chunk_size: 500
input_chunk_ms: 300 # taille des chunks audio en ms
input_chunk_size: 1024
input_device_index: 0 # use system default when a mic is attached
input_device_name: 'USB ENC Audio Device' # Optional name of input device to select (overrides index if found)
volume: 0.5 # half as loud
output_device_index: 1 # default to USB PnP Audio Device: Audio (hw:3,0)
output_sample_rate: 44100
output_chunk_ms: 30 # taille des chunks audio en ms
output_chunk_size: 500
output_chunk_ms: 300 # taille des chunks audio en ms
output_chunk_size: 1024
output_device_name: 'USB PnP Audio Device' # Optional name of output device to select (overrides index if found)

# Platform-specific configuration
Expand Down
50 changes: 37 additions & 13 deletions examples/VAD/voice_agent_offline.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Minimal voice agent demonstrating ASR, TTS, and wake-word detection.

Features:
- Wake word detection (openWakeWord)
- Wake word detection (Direct ONNX Runtime, no openwakeword library package)
- Speech recognition (Whisper or Faster-Whisper)
- Text-to-speech synthesis (Piper)
- Simple intent-based response system
Expand All @@ -14,6 +14,7 @@
python examples/simple_voice_agent.py
"""

import datetime
import logging
import signal
import sys
Expand Down Expand Up @@ -112,8 +113,15 @@ def _on_wake_word_detected(self) -> None:
"""Callback when wake word is detected."""
logger.info("🟣 Wake word detected!")
self.wake_word_active = True

# Stop listening during TTS playback to avoid hearing ourselves
with self._listener_lock:
if self._wake_active:
self.wake_detector.stop()
self._wake_active = False

self.tts.speak("Yes? How can I help you?", blocking=True)
self._ensure_asr_mode()
self.tts.speak("Yes? How can I help you?")

def _on_transcript_received(self, transcript: str) -> None:
"""Callback when speech is transcribed."""
Expand All @@ -131,11 +139,25 @@ def _on_transcript_received(self, transcript: str) -> None:
# Process the command
response = self._generate_response(transcript)
logger.info("🤖 Response: '%s'", response)
self.tts.speak(response)

self.wake_word_active = False
logger.info("🟢 Returning to wake word mode")
self._ensure_wake_mode()
# Stop listening during TTS playback to avoid hearing ourselves
with self._listener_lock:
if self._asr_active:
self.asr.stop()
self._asr_active = False

self.tts.speak(response, blocking=True)

# Check if we should continue listening in ASR mode or return to wake word mode
should_continue = not any(exit_word in transcript.lower() for exit_word in ["stop", "exit", "quit"])

if should_continue:
logger.info("🟢 Continuing conversation, staying in ASR mode")
self._ensure_asr_mode()
else:
self.wake_word_active = False
logger.info("🟢 Returning to wake word mode")
self._ensure_wake_mode()

def _ensure_wake_mode(self) -> None:
"""Run wake-word listening without a parallel ASR capture stream."""
Expand Down Expand Up @@ -184,13 +206,15 @@ def _generate_response(self, user_input: str) -> str:

# Simple keyword matching
responses = {
"hello": "Hello! What can I do for you?",
"hi": "Hi there!",
"time": "I don't have real-time capabilities right now.",
"weather": "I can't check the weather, but I hope it's nice outside!",
"help": "I'm a voice agent. Try saying hello or ask me a question.",
"thanks": "You're welcome!",
"thank you": "Happy to help!",
"hello": f"Hello! I'm {self.config.wake.wake_word}, your AI assistant. How can I help you?",
"hi": f"Hi there! What can I do for you?",
'time': 'The current time is ' + datetime.datetime.now().strftime("%I:%M %p"), # Get the current time and format it
'date': 'Today, the date is: ' + datetime.datetime.now().strftime("%d %B %Y"), # Get the current date and format it
"lights": f"I would control your lights if I had smart home integration.",
"music": f"I would play music if I had access to your media system.",
"stop": f"Goodbye! Returning to wake word detection.",
"bye_bye": f"See you later! Going back to sleep mode.",
"help": f"I can respond to simple commands like hello, hi, time, date, lights, music, stop and bye-bye."
}

# Match keywords
Expand Down
2 changes: 1 addition & 1 deletion log.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"handlers": {
"stdout": {
"class": "logging.StreamHandler",
"level": "INFO",
"level": "WARNING",
"filters": ["max_warning"],
"formatter": "simple",
"stream": "ext://sys.stdout"
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ dependencies = [
"loguru>=0.7.0",
"numba>=0.57.0",
"numpy>=1.24.0",
"onnxruntime>=1.18.0",
"openai-whisper==20250625",
"onnxruntime>=1.16.0",
"openwakeword>=0.1.0",
"piper-tts>=1.4.1",
"psutil>=7.2.2",
Expand Down Expand Up @@ -128,7 +128,7 @@ test = [
]

[tool.ruff]
target-version = "py310"
target-version = "py311"
line-length = 120
output-format = "full"
src = ["src", "tests", "examples"]
Expand Down
9 changes: 8 additions & 1 deletion src/audio/asr.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Automatic Speech Recognition engine combining STT + VAD.

Architecture:
1. Microphone -> capture thread (PyAudio or sounddevice chunks)
1. Microphone -> capture thread sounddevice chunks
2. Silero VAD -> detect speech vs silence
3. Accumulate speech chunks
4. On silence detected -> STT (Whisper/Faster-Whisper) -> transcript
Expand Down Expand Up @@ -285,6 +285,13 @@ def start(self, callback: Callable[[str], None]) -> None:
self._transcript_callback = callback
self._running = True

# Clear the queue of any leftover items (including None sentinel)
while not self._audio_queue.empty():
try:
_ = self._audio_queue.get_nowait()
except queue.Empty:
break

if not self._open_input_stream():
self._running = False
return
Expand Down
13 changes: 5 additions & 8 deletions src/audio/audio_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,11 @@ class OpenedInputStream:

def get_audio_backend() -> str:
try:
import sounddevice as sd
return "sounddevice"
except ImportError:
raise ImportError(
"The 'sounddevice' library is required but not installed.\n \
Install it via: pip install sounddevice"
)

import sounddevice
_ = sounddevice
except ImportError as e:
raise ImportError("The 'sounddevice' library is required") from e
return "sounddevice"

def is_backend_available(backend: str) -> bool:
"""Check if a specific audio backend is available.
Expand Down
Loading
Loading