diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f29ae6a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.13-slim-trixie + +ENV LANG=C.UTF-8 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install --yes --no-install-recommends \ + avahi-utils alsa-utils libportaudio2 portaudio19-dev \ + build-essential libmpv-dev pulseaudio + +WORKDIR /srv +COPY . ./ +RUN ./script/setup + +ENTRYPOINT ["./script/run"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..940d2a7 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,32 @@ +version: "3.8" +services: + linux-voice-assistant: + build: . + image: ohf/linux-voice-assistant:local + container_name: linux-voice-assistant + restart: unless-stopped + devices: + - "/dev/snd:/dev/snd" + group_add: + - audio + environment: + - FIXED_MAC_ADDRESS=device_mac_address + # - XDG_RUNTIME_DIR=/run/user/1000 + # - PULSE_SERVER=unix:/run/user/1000/pulse/native + - AUDIO_OUTPUT_DEVICE=default + volumes: + - ./config:/app/.config + - ./wakewords:/app/wakewords + - ./sounds:/app/sounds + # - /var/run/pulse/native:/var/run/pulse/native + # - /run/user/1000:/run/user/1000 + ports: + - "6053:6053" + tty: true + command: + - "--name" + - "voice-satellite" + - "--audio-input-device" + - "default" + - "--wake-model" + - "hey_jarvis" diff --git a/linux_voice_assistant/__main__.py b/linux_voice_assistant/__main__.py index 8ec3e72..eb7b1d3 100644 --- a/linux_voice_assistant/__main__.py +++ b/linux_voice_assistant/__main__.py @@ -80,6 +80,10 @@ async def main() -> None: parser.add_argument( "--timer-finished-sound", default=str(_SOUNDS_DIR / "timer_finished.flac") ) + parser.add_argument( + "--processing-sound", default=str(_SOUNDS_DIR / "processing.wav"), + help="Short sound to play while assistant is processing (thinking)" + ) # parser.add_argument("--preferences-file", default=_REPO_DIR / "preferences.json") # @@ -184,6 +188,7 @@ async def main() -> None: tts_player=MpvMediaPlayer(device=args.audio_output_device), wakeup_sound=args.wakeup_sound, timer_finished_sound=args.timer_finished_sound, + processing_sound=args.processing_sound, preferences=preferences, preferences_path=preferences_path, libtensorflowlite_c_path=libtensorflowlite_c_path, diff --git a/linux_voice_assistant/models.py b/linux_voice_assistant/models.py index 4ca4f9e..1542135 100644 --- a/linux_voice_assistant/models.py +++ b/linux_voice_assistant/models.py @@ -70,6 +70,7 @@ class ServerState: music_player: "MpvMediaPlayer" tts_player: "MpvMediaPlayer" wakeup_sound: str + processing_sound: str timer_finished_sound: str preferences: Preferences preferences_path: Path diff --git a/linux_voice_assistant/satellite.py b/linux_voice_assistant/satellite.py index 8b48e6d..4c47e9c 100644 --- a/linux_voice_assistant/satellite.py +++ b/linux_voice_assistant/satellite.py @@ -65,6 +65,7 @@ def __init__(self, state: ServerState) -> None: self._tts_played = False self._continue_conversation = False self._timer_finished = False + self._processing = False def handle_voice_event( self, event_type: VoiceAssistantEventType, data: Dict[str, str] @@ -75,6 +76,15 @@ def handle_voice_event( self._tts_url = data.get("url") self._tts_played = False self._continue_conversation = False + elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_START: + # Play short "thinking/processing" sound if configured + processing = getattr(self.state, "processing_sound", None) + if processing: + _LOGGER.debug("Playing processing sound: %s", processing) + self.state.stop_word.is_active = True + self._processing = True + self.duck() + self.state.tts_player.play(self.state.processing_sound) elif event_type in ( VoiceAssistantEventType.VOICE_ASSISTANT_STT_VAD_END, VoiceAssistantEventType.VOICE_ASSISTANT_STT_END, diff --git a/linux_voice_assistant/util.py b/linux_voice_assistant/util.py index 92726ef..8067687 100644 --- a/linux_voice_assistant/util.py +++ b/linux_voice_assistant/util.py @@ -4,9 +4,15 @@ import uuid from collections.abc import Callable from typing import Optional +import os def get_mac() -> str: + # Check for fixed MAC in environment variable + mac_env = os.getenv("FIXED_MAC_ADDRESS") + if mac_env: + return mac_env.lower() + # Fallback to uuid.getnode() mac = uuid.getnode() mac_str = ":".join(f"{(mac >> i) & 0xff:02x}" for i in range(40, -1, -8)) return mac_str diff --git a/sounds/processing.wav b/sounds/processing.wav new file mode 100644 index 0000000..d76ec5b Binary files /dev/null and b/sounds/processing.wav differ diff --git a/sounds/timer_finished_old.wav b/sounds/timer_finished_old.wav new file mode 100644 index 0000000..40a97bc Binary files /dev/null and b/sounds/timer_finished_old.wav differ diff --git a/sounds/wake_word_triggered_old.wav b/sounds/wake_word_triggered_old.wav new file mode 100644 index 0000000..8cb6535 Binary files /dev/null and b/sounds/wake_word_triggered_old.wav differ