434 lines
14 KiB
Python
434 lines
14 KiB
Python
"""Main entry point for Wyoming satellite."""
|
|
import argparse
|
|
import asyncio
|
|
import logging
|
|
import sys
|
|
from functools import partial
|
|
from pathlib import Path
|
|
|
|
from wyoming.info import Attribution, Info, Satellite
|
|
from wyoming.server import AsyncServer, AsyncTcpServer
|
|
|
|
from . import __version__
|
|
from .event_handler import SatelliteEventHandler
|
|
from .satellite import (
|
|
AlwaysStreamingSatellite,
|
|
SatelliteBase,
|
|
VadStreamingSatellite,
|
|
WakeStreamingSatellite,
|
|
)
|
|
from .settings import (
|
|
EventSettings,
|
|
MicSettings,
|
|
SatelliteSettings,
|
|
SndSettings,
|
|
VadSettings,
|
|
WakeSettings,
|
|
WakeWordAndPipeline,
|
|
)
|
|
from .utils import (
|
|
get_mac_address,
|
|
needs_silero,
|
|
needs_webrtc,
|
|
run_event_command,
|
|
split_command,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger()
|
|
_DIR = Path(__file__).parent
|
|
|
|
|
|
async def main() -> None:
|
|
"""Main entry point."""
|
|
parser = argparse.ArgumentParser()
|
|
|
|
# Microphone input
|
|
parser.add_argument("--mic-uri", help="URI of Wyoming microphone service")
|
|
parser.add_argument("--mic-command", help="Program to run for microphone input")
|
|
parser.add_argument(
|
|
"--mic-command-rate",
|
|
type=int,
|
|
default=16000,
|
|
help="Sample rate of mic-command (hertz, default: 16000)",
|
|
)
|
|
parser.add_argument(
|
|
"--mic-command-width",
|
|
type=int,
|
|
default=2,
|
|
help="Sample width of mic-command (bytes, default: 2)",
|
|
)
|
|
parser.add_argument(
|
|
"--mic-command-channels",
|
|
type=int,
|
|
default=1,
|
|
help="Sample channels of mic-command (default: 1)",
|
|
)
|
|
parser.add_argument(
|
|
"--mic-command-samples-per-chunk",
|
|
type=int,
|
|
default=1024,
|
|
help="Sample per chunk for mic-command (default: 1024)",
|
|
)
|
|
parser.add_argument("--mic-volume-multiplier", type=float, default=1.0)
|
|
parser.add_argument(
|
|
"--mic-noise-suppression", type=int, default=0, choices=(0, 1, 2, 3, 4)
|
|
)
|
|
parser.add_argument("--mic-auto-gain", type=int, default=0, choices=list(range(32)))
|
|
parser.add_argument(
|
|
"--mic-seconds-to-mute-after-awake-wav",
|
|
type=float,
|
|
default=0.5,
|
|
help="Seconds to mute microphone after awake wav is finished playing (default: 0.5)",
|
|
)
|
|
parser.add_argument(
|
|
"--mic-no-mute-during-awake-wav",
|
|
action="store_true",
|
|
help="Don't mute the microphone while awake wav is being played",
|
|
)
|
|
|
|
# Sound output
|
|
parser.add_argument("--snd-uri", help="URI of Wyoming sound service")
|
|
parser.add_argument("--snd-command", help="Program to run for sound output")
|
|
parser.add_argument(
|
|
"--snd-command-rate",
|
|
type=int,
|
|
default=22050,
|
|
help="Sample rate of snd-command (hertz, default: 22050)",
|
|
)
|
|
parser.add_argument(
|
|
"--snd-command-width",
|
|
type=int,
|
|
default=2,
|
|
help="Sample width of snd-command (bytes, default: 2)",
|
|
)
|
|
parser.add_argument(
|
|
"--snd-command-channels",
|
|
type=int,
|
|
default=1,
|
|
help="Sample channels of snd-command (default: 1)",
|
|
)
|
|
parser.add_argument("--snd-volume-multiplier", type=float, default=1.0)
|
|
|
|
# Local wake word detection
|
|
parser.add_argument("--wake-uri", help="URI of Wyoming wake word detection service")
|
|
parser.add_argument(
|
|
"--wake-word-name",
|
|
action="append",
|
|
default=[],
|
|
nargs="+",
|
|
metavar=("name", "pipeline"),
|
|
help="Name of wake word to listen for and optional pipeline name to run (requires --wake-uri)",
|
|
)
|
|
parser.add_argument("--wake-command", help="Program to run for wake word detection")
|
|
parser.add_argument(
|
|
"--wake-command-rate",
|
|
type=int,
|
|
default=16000,
|
|
help="Sample rate of wake-command (hertz, default: 16000)",
|
|
)
|
|
parser.add_argument(
|
|
"--wake-command-width",
|
|
type=int,
|
|
default=2,
|
|
help="Sample width of wake-command (bytes, default: 2)",
|
|
)
|
|
parser.add_argument(
|
|
"--wake-command-channels",
|
|
type=int,
|
|
default=1,
|
|
help="Sample channels of wake-command (default: 1)",
|
|
)
|
|
parser.add_argument(
|
|
"--wake-refractory-seconds",
|
|
type=float,
|
|
default=5.0,
|
|
help="Seconds after a wake word detection before another detection is handled (default: 5)",
|
|
)
|
|
|
|
# Voice activity detector
|
|
parser.add_argument(
|
|
"--vad", action="store_true", help="Wait for speech before streaming audio"
|
|
)
|
|
parser.add_argument("--vad-threshold", type=float, default=0.5)
|
|
parser.add_argument("--vad-trigger-level", type=int, default=1)
|
|
parser.add_argument("--vad-buffer-seconds", type=float, default=2)
|
|
parser.add_argument(
|
|
"--vad-wake-word-timeout",
|
|
type=float,
|
|
default=5.0,
|
|
help="Seconds before going back to waiting for speech when wake word isn't detected",
|
|
)
|
|
|
|
# External event handlers
|
|
parser.add_argument(
|
|
"--event-uri", help="URI of Wyoming service to forward events to"
|
|
)
|
|
parser.add_argument(
|
|
"--startup-command", help="Command run when the satellite starts"
|
|
)
|
|
parser.add_argument(
|
|
"--detect-command", help="Command to run when wake word detection starts"
|
|
)
|
|
parser.add_argument(
|
|
"--detection-command", help="Command to run when wake word is detected"
|
|
)
|
|
parser.add_argument(
|
|
"--transcript-command",
|
|
help="Command to run when speech to text transcript is returned",
|
|
)
|
|
parser.add_argument(
|
|
"--stt-start-command",
|
|
help="Command to run when the user starts speaking",
|
|
)
|
|
parser.add_argument(
|
|
"--stt-stop-command",
|
|
help="Command to run when the user stops speaking",
|
|
)
|
|
parser.add_argument(
|
|
"--synthesize-command",
|
|
help="Command to run when text to speech text is returned",
|
|
)
|
|
parser.add_argument(
|
|
"--tts-start-command",
|
|
help="Command to run when text to speech response starts",
|
|
)
|
|
parser.add_argument(
|
|
"--tts-stop-command",
|
|
help="Command to run when text to speech response stops",
|
|
)
|
|
parser.add_argument(
|
|
"--tts-played-command",
|
|
help="Command to run when text-to-speech audio stopped playing",
|
|
)
|
|
parser.add_argument(
|
|
"--streaming-start-command",
|
|
help="Command to run when audio streaming starts",
|
|
)
|
|
parser.add_argument(
|
|
"--streaming-stop-command",
|
|
help="Command to run when audio streaming stops",
|
|
)
|
|
parser.add_argument(
|
|
"--error-command",
|
|
help="Command to run when an error occurs",
|
|
)
|
|
parser.add_argument(
|
|
"--connected-command",
|
|
help="Command to run when connected to the server",
|
|
)
|
|
parser.add_argument(
|
|
"--disconnected-command",
|
|
help="Command to run when disconnected from the server",
|
|
)
|
|
|
|
# Sounds
|
|
parser.add_argument(
|
|
"--awake-wav", help="WAV file to play when wake word is detected"
|
|
)
|
|
parser.add_argument(
|
|
"--done-wav", help="WAV file to play when voice command is done"
|
|
)
|
|
|
|
# Satellite details
|
|
parser.add_argument("--uri", required=True, help="unix:// or tcp://")
|
|
parser.add_argument(
|
|
"--name", default="Wyoming Satellite", help="Name of the satellite"
|
|
)
|
|
parser.add_argument("--area", help="Area name of the satellite")
|
|
|
|
# Zeroconf
|
|
parser.add_argument(
|
|
"--no-zeroconf", action="store_true", help="Disable discovery over zeroconf"
|
|
)
|
|
parser.add_argument(
|
|
"--zeroconf-name",
|
|
help="Name used for zeroconf discovery (default: MAC from uuid.getnode)",
|
|
)
|
|
parser.add_argument(
|
|
"--zeroconf-host",
|
|
help="Host address for zeroconf discovery (default: detect)",
|
|
)
|
|
#
|
|
parser.add_argument(
|
|
"--debug-recording-dir", help="Directory to store audio for debugging"
|
|
)
|
|
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
|
parser.add_argument(
|
|
"--log-format", default=logging.BASIC_FORMAT, help="Format for log messages"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
# Validate args
|
|
if (not args.mic_uri) and (not args.mic_command):
|
|
_LOGGER.fatal("Either --mic-uri or --mic-command is required")
|
|
sys.exit(1)
|
|
|
|
if needs_webrtc(args):
|
|
try:
|
|
import webrtc_noise_gain # noqa: F401
|
|
except ImportError:
|
|
_LOGGER.fatal("Install extras for webrtc")
|
|
sys.exit(1)
|
|
|
|
if needs_silero(args):
|
|
try:
|
|
import pysilero_vad # noqa: F401
|
|
except ImportError:
|
|
_LOGGER.fatal("Install extras for silerovad")
|
|
sys.exit(1)
|
|
|
|
if args.awake_wav and (not Path(args.awake_wav).is_file()):
|
|
_LOGGER.fatal("%s does not exist", args.awake_wav)
|
|
sys.exit(1)
|
|
|
|
if args.done_wav and (not Path(args.done_wav).is_file()):
|
|
_LOGGER.fatal("%s does not exist", args.done_wav)
|
|
sys.exit(1)
|
|
|
|
if args.vad and (args.wake_uri or args.wake_command):
|
|
_LOGGER.warning("VAD is not used with local wake word detection")
|
|
|
|
logging.basicConfig(
|
|
level=logging.DEBUG if args.debug else logging.INFO, format=args.log_format
|
|
)
|
|
_LOGGER.debug(args)
|
|
|
|
if args.debug_recording_dir:
|
|
args.debug_recording_dir = Path(args.debug_recording_dir)
|
|
_LOGGER.info("Recording audio to %s", args.debug_recording_dir)
|
|
|
|
wyoming_info = Info(
|
|
satellite=Satellite(
|
|
name=args.name,
|
|
area=args.area,
|
|
description=args.name,
|
|
attribution=Attribution(name="", url=""),
|
|
installed=True,
|
|
version=__version__,
|
|
)
|
|
)
|
|
|
|
settings = SatelliteSettings(
|
|
mic=MicSettings(
|
|
uri=args.mic_uri,
|
|
command=split_command(args.mic_command),
|
|
rate=args.mic_command_rate,
|
|
width=args.mic_command_width,
|
|
channels=args.mic_command_channels,
|
|
samples_per_chunk=args.mic_command_samples_per_chunk,
|
|
volume_multiplier=args.mic_volume_multiplier,
|
|
auto_gain=args.mic_auto_gain,
|
|
noise_suppression=args.mic_noise_suppression,
|
|
seconds_to_mute_after_awake_wav=args.mic_seconds_to_mute_after_awake_wav,
|
|
mute_during_awake_wav=(not args.mic_no_mute_during_awake_wav),
|
|
),
|
|
vad=VadSettings(
|
|
enabled=args.vad,
|
|
threshold=args.vad_threshold,
|
|
trigger_level=args.vad_trigger_level,
|
|
buffer_seconds=args.vad_buffer_seconds,
|
|
wake_word_timeout=args.vad_wake_word_timeout,
|
|
),
|
|
wake=WakeSettings(
|
|
uri=args.wake_uri,
|
|
command=split_command(args.wake_command),
|
|
names=[
|
|
WakeWordAndPipeline(*wake_name) for wake_name in args.wake_word_name
|
|
],
|
|
refractory_seconds=args.wake_refractory_seconds
|
|
if args.wake_refractory_seconds > 0
|
|
else None,
|
|
),
|
|
snd=SndSettings(
|
|
uri=args.snd_uri,
|
|
command=split_command(args.snd_command),
|
|
rate=args.snd_command_rate,
|
|
width=args.snd_command_width,
|
|
channels=args.snd_command_channels,
|
|
volume_multiplier=args.snd_volume_multiplier,
|
|
awake_wav=args.awake_wav,
|
|
done_wav=args.done_wav,
|
|
),
|
|
event=EventSettings(
|
|
uri=args.event_uri,
|
|
startup=split_command(args.startup_command),
|
|
streaming_start=split_command(args.streaming_start_command),
|
|
streaming_stop=split_command(args.streaming_stop_command),
|
|
detect=split_command(args.detect_command),
|
|
detection=split_command(args.detection_command),
|
|
played=split_command(args.tts_played_command),
|
|
transcript=split_command(args.transcript_command),
|
|
stt_start=split_command(args.stt_start_command),
|
|
stt_stop=split_command(args.stt_stop_command),
|
|
synthesize=split_command(args.synthesize_command),
|
|
tts_start=split_command(args.tts_start_command),
|
|
tts_stop=split_command(args.tts_stop_command),
|
|
error=split_command(args.error_command),
|
|
connected=split_command(args.connected_command),
|
|
disconnected=split_command(args.disconnected_command),
|
|
),
|
|
debug_recording_dir=args.debug_recording_dir,
|
|
)
|
|
|
|
satellite: SatelliteBase
|
|
|
|
if settings.wake.enabled:
|
|
# Local wake word detection
|
|
satellite = WakeStreamingSatellite(settings)
|
|
elif settings.vad.enabled:
|
|
# Stream after speech
|
|
satellite = VadStreamingSatellite(settings)
|
|
else:
|
|
# Stream all the time
|
|
satellite = AlwaysStreamingSatellite(settings)
|
|
|
|
if args.startup_command:
|
|
await run_event_command(split_command(args.startup_command))
|
|
|
|
_LOGGER.info("Ready")
|
|
|
|
# Start server
|
|
server = AsyncServer.from_uri(args.uri)
|
|
|
|
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,
|
|
port=tcp_server.port,
|
|
host=args.zeroconf_host,
|
|
)
|
|
_LOGGER.debug(
|
|
"Zeroconf discovery enabled (name=%s, host=%s)",
|
|
args.zeroconf_name,
|
|
args.zeroconf_host,
|
|
)
|
|
|
|
satellite_task = asyncio.create_task(satellite.run(), name="satellite run")
|
|
|
|
try:
|
|
await server.run(partial(SatelliteEventHandler, wyoming_info, satellite, args))
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
await satellite.stop()
|
|
await satellite_task
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
def run():
|
|
try:
|
|
asyncio.run(main())
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|