diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ea9f7d2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +* +!requirements.txt +!MANIFEST.in +!setup.py +!script/setup +!script/run +!wyoming_satellite/*.py +!docker/run diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..90b16dd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11-slim-bookworm + +ENV LANG C.UTF-8 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install --yes --no-install-recommends avahi-utils + +WORKDIR /app + +COPY script/setup ./script/ +COPY setup.py requirements.txt MANIFEST.in ./ +COPY wyoming_satellite/ ./wyoming_satellite/ + +RUN script/setup + +COPY script/run ./script/ +COPY docker/run ./ + +EXPOSE 10700 + +ENTRYPOINT ["/app/run"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..664d016 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: "3.8" +services: + microphone: + image: "rhasspy/wyoming-mic-external" + ports: + - "10600:10600" + devices: + - /dev/snd:/dev/snd + group_add: + - audio + command: + - "--device" + - "sysdefault" + - "--debug" + playback: + image: "rhasspy/wyoming-snd-external" + ports: + - "10601:10601" + devices: + - /dev/snd:/dev/snd + group_add: + - audio + command: + - "--device" + - "sysdefault" + - "--debug" + satellite: + image: "rhasspy/wyoming-satellite" + ports: + - "10700:10700" + command: + - "--name" + - "my satellite" + - "--mic-uri" + - "tcp://microphone:10600" + - "--snd-uri" + - "tcp://playback:10601" + - "--debug" diff --git a/docker/run b/docker/run new file mode 100755 index 0000000..2e6876d --- /dev/null +++ b/docker/run @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +/app/script/run \ + --uri tcp://0.0.0.0:10700 "$@" diff --git a/script/build_docker b/script/build_docker new file mode 100755 index 0000000..4b38066 --- /dev/null +++ b/script/build_docker @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +docker buildx build . \ + -t 'rhasspy/wyoming-satellite' "$@" diff --git a/script/run_docker b/script/run_docker new file mode 100755 index 0000000..09beb9f --- /dev/null +++ b/script/run_docker @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +docker run \ + -it \ + -p 10700:10700 \ + --device /dev/snd \ + 'rhasspy/wyoming-satellite' "$@" diff --git a/setup.py b/setup.py index d46657b..7a27c24 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ import setuptools from setuptools import setup this_dir = Path(__file__).parent -module_dir = this_dir / "wyoming_handle_external" requirements = [] requirements_path = this_dir / "requirements.txt" @@ -19,8 +18,8 @@ if requirements_path.is_file(): setup( name="wyoming_handle_external", version="1.0.0", - description="Wyoming server for external intent handling", - url="http://github.com/rhasspy/wyoming-handle-external", + description="Wyoming server for remote voice satellite", + url="http://github.com/rhasspy/wyoming-satellite", author="Michael Hansen", author_email="mike@rhasspy.org", packages=setuptools.find_packages(), @@ -35,5 +34,5 @@ setup( "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ], - keywords="rhasspy wyoming intent handle", + keywords="rhasspy wyoming satellite", ) diff --git a/wyoming_satellite/__main__.py b/wyoming_satellite/__main__.py index 425308c..f19406e 100644 --- a/wyoming_satellite/__main__.py +++ b/wyoming_satellite/__main__.py @@ -3,12 +3,13 @@ import argparse import asyncio import logging import time +import uuid from enum import Enum from functools import partial from pathlib import Path from typing import Optional -from wyoming.asr import Transcript +from wyoming.asr import Transcript, Transcribe from wyoming.audio import AudioChunk, AudioStart, AudioStop from wyoming.client import AsyncClient from wyoming.event import Event @@ -59,7 +60,7 @@ async def main() -> None: ) parser.add_argument( "--zeroconf-name", - help="Name used for zeroconf discovery (default: satellite name)", + help="Name used for zeroconf discovery (default: MAC from uuid.getnode)", ) # parser.add_argument("--debug", action="store_true", help="Log DEBUG messages") @@ -86,13 +87,16 @@ async def main() -> None: if (not args.no_zeroconf) and isinstance(server, AsyncTcpServer): from wyoming.zeroconf import register_server + if not args.zeroconf_name: + args.zeroconf_name = get_mac_address() + tcp_server: AsyncTcpServer = server await register_server( - name=args.zeroconf_name or args.name, + name=args.zeroconf_name, port=tcp_server.port, host=tcp_server.host, ) - _LOGGER.debug("Zeroconf discovery enabled") + _LOGGER.debug("Zeroconf discovery enabled (name=%s)", args.zeroconf_name) try: await server.run(partial(SatelliteEventHandler, wyoming_info, args)) @@ -195,6 +199,7 @@ class SatelliteEventHandler(AsyncEventHandler): elif ( Detect.is_type(event.type) or Detection.is_type(event.type) + or Transcribe.is_type(event.type) or VoiceStarted.is_type(event.type) or VoiceStopped.is_type(event.type) or Synthesize.is_type(event.type) @@ -202,6 +207,7 @@ class SatelliteEventHandler(AsyncEventHandler): # Other client events: # - Detect for start of wake word detection # - Detection for when wake word is detected + # - Transcribe for when STT starts # - VoiceStarted/VoiceStopped for when user starts/stops speaking # - Synthesize for TTS text await self.forward_event(event) @@ -322,6 +328,18 @@ class SatelliteEventHandler(AsyncEventHandler): self.is_running = False +# ----------------------------------------------------------------------------- + + +def get_mac_address() -> str: + """Return MAC address formatted as hex with no colons.""" + return "".join( + ["{:02x}".format((uuid.getnode() >> ele) & 0xFF) for ele in range(0, 8 * 6, 8)][ + ::-1 + ] + ) + + # ----------------------------------------------------------------------------- if __name__ == "__main__":