Clean up and move code to utils

This commit is contained in:
Michael Hansen
2024-01-09 09:55:15 -06:00
parent 970742711d
commit da72689bed
6 changed files with 165 additions and 125 deletions
+1
View File
@@ -0,0 +1 @@
"""Voice satellite using the Wyoming protocol."""
+27 -101
View File
@@ -1,19 +1,15 @@
#!/usr/bin/env python3 """Main entry point for Wyoming satellite."""
import argparse import argparse
import asyncio import asyncio
import logging import logging
import shlex
import sys import sys
import time
import uuid
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
from typing import List, Optional
from wyoming.event import Event from wyoming.info import Attribution, Info, Satellite
from wyoming.info import Attribution, Describe, Info, Satellite from wyoming.server import AsyncServer, AsyncTcpServer
from wyoming.server import AsyncEventHandler, AsyncServer, AsyncTcpServer
from .event_handler import SatelliteEventHandler
from .satellite import ( from .satellite import (
AlwaysStreamingSatellite, AlwaysStreamingSatellite,
SatelliteBase, SatelliteBase,
@@ -28,7 +24,13 @@ from .settings import (
VadSettings, VadSettings,
WakeSettings, WakeSettings,
) )
from .utils import run_event_command from .utils import (
get_mac_address,
needs_silero,
needs_webrtc,
run_event_command,
split_command,
)
_LOGGER = logging.getLogger() _LOGGER = logging.getLogger()
_DIR = Path(__file__).parent _DIR = Path(__file__).parent
@@ -263,7 +265,7 @@ async def main() -> None:
settings = SatelliteSettings( settings = SatelliteSettings(
mic=MicSettings( mic=MicSettings(
uri=args.mic_uri, uri=args.mic_uri,
command=_split(args.mic_command), command=split_command(args.mic_command),
rate=args.mic_command_rate, rate=args.mic_command_rate,
width=args.mic_command_width, width=args.mic_command_width,
channels=args.mic_command_channels, channels=args.mic_command_channels,
@@ -281,12 +283,12 @@ async def main() -> None:
), ),
wake=WakeSettings( wake=WakeSettings(
uri=args.wake_uri, uri=args.wake_uri,
command=_split(args.wake_command), command=split_command(args.wake_command),
names=args.wake_word_name, names=args.wake_word_name,
), ),
snd=SndSettings( snd=SndSettings(
uri=args.snd_uri, uri=args.snd_uri,
command=_split(args.snd_command), command=split_command(args.snd_command),
rate=args.snd_command_rate, rate=args.snd_command_rate,
width=args.snd_command_width, width=args.snd_command_width,
channels=args.snd_command_channels, channels=args.snd_command_channels,
@@ -296,18 +298,18 @@ async def main() -> None:
), ),
event=EventSettings( event=EventSettings(
uri=args.event_uri, uri=args.event_uri,
startup=_split(args.startup_command), startup=split_command(args.startup_command),
streaming_start=_split(args.streaming_start_command), streaming_start=split_command(args.streaming_start_command),
streaming_stop=_split(args.streaming_stop_command), streaming_stop=split_command(args.streaming_stop_command),
detect=_split(args.detect_command), detect=split_command(args.detect_command),
detection=_split(args.detection_command), detection=split_command(args.detection_command),
transcript=_split(args.transcript_command), transcript=split_command(args.transcript_command),
stt_start=_split(args.stt_start_command), stt_start=split_command(args.stt_start_command),
stt_stop=_split(args.stt_stop_command), stt_stop=split_command(args.stt_stop_command),
synthesize=_split(args.synthesize_command), synthesize=split_command(args.synthesize_command),
tts_start=_split(args.tts_start_command), tts_start=split_command(args.tts_start_command),
tts_stop=_split(args.tts_stop_command), tts_stop=split_command(args.tts_stop_command),
error=_split(args.error_command), error=split_command(args.error_command),
), ),
) )
@@ -324,7 +326,7 @@ async def main() -> None:
satellite = AlwaysStreamingSatellite(settings) satellite = AlwaysStreamingSatellite(settings)
if args.startup_command: if args.startup_command:
await run_event_command(_split(args.startup_command)) await run_event_command(split_command(args.startup_command))
_LOGGER.info("Ready") _LOGGER.info("Ready")
@@ -363,82 +365,6 @@ async def main() -> None:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class SatelliteEventHandler(AsyncEventHandler):
"""Event handler for clients."""
def __init__(
self,
wyoming_info: Info,
satellite: SatelliteBase,
cli_args: argparse.Namespace,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.cli_args = cli_args
self.wyoming_info_event = wyoming_info.event()
self.client_id = str(time.monotonic_ns())
self.satellite = satellite
# -------------------------------------------------------------------------
async def handle_event(self, event: Event) -> bool:
"""Handle events from the server."""
if Describe.is_type(event.type):
await self.write_event(self.wyoming_info_event)
return True
if self.satellite.server_id is None:
# Take over after a problem occurred
self.satellite.set_server(self.client_id, self.writer)
elif self.satellite.server_id != self.client_id:
# New connection
_LOGGER.debug("Connection cancelled: %s", self.client_id)
return False
await self.satellite.event_from_server(event)
return True
async def disconnect(self) -> None:
"""Server disconnect."""
if self.satellite.server_id == self.client_id:
self.satellite.clear_server()
# -----------------------------------------------------------------------------
def get_mac_address() -> str:
"""Return MAC address formatted as hex with no colons."""
return "".join(
# pylint: disable=consider-using-f-string
["{:02x}".format((uuid.getnode() >> ele) & 0xFF) for ele in range(0, 8 * 6, 8)][
::-1
]
)
def needs_webrtc(args: argparse.Namespace) -> bool:
"""Return True if webrtc must be used."""
return (args.mic_noise_suppression > 0) or (args.mic_auto_gain > 0)
def needs_silero(args: argparse.Namespace) -> bool:
"""Return True if silero-vad must be used."""
return args.vad
def _split(command: Optional[str]) -> Optional[List[str]]:
if not command:
return None
return shlex.split(command)
# -----------------------------------------------------------------------------
def run(): def run():
try: try:
asyncio.run(main()) asyncio.run(main())
+56
View File
@@ -0,0 +1,56 @@
"""Wyoming event handler for satellites."""
import argparse
import logging
import time
from wyoming.event import Event
from wyoming.info import Describe, Info
from wyoming.server import AsyncEventHandler
from .satellite import SatelliteBase
_LOGGER = logging.getLogger()
class SatelliteEventHandler(AsyncEventHandler):
"""Event handler for clients."""
def __init__(
self,
wyoming_info: Info,
satellite: SatelliteBase,
cli_args: argparse.Namespace,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.cli_args = cli_args
self.wyoming_info_event = wyoming_info.event()
self.client_id = str(time.monotonic_ns())
self.satellite = satellite
# -------------------------------------------------------------------------
async def handle_event(self, event: Event) -> bool:
"""Handle events from the server."""
if Describe.is_type(event.type):
await self.write_event(self.wyoming_info_event)
return True
if self.satellite.server_id is None:
# Take over after a problem occurred
self.satellite.set_server(self.client_id, self.writer)
elif self.satellite.server_id != self.client_id:
# New connection
_LOGGER.debug("Connection cancelled: %s", self.client_id)
return False
await self.satellite.event_from_server(event)
return True
async def disconnect(self) -> None:
"""Server disconnect."""
if self.satellite.server_id == self.client_id:
self.satellite.clear_server()
+21
View File
@@ -0,0 +1,21 @@
"""Utility methods."""
from .audio import AudioBuffer, chunk_samples, multiply_volume, wav_to_events
from .misc import (
get_mac_address,
needs_silero,
needs_webrtc,
run_event_command,
split_command,
)
__all__ = [
"AudioBuffer",
"chunk_samples",
"get_mac_address",
"multiply_volume",
"needs_silero",
"needs_webrtc",
"run_event_command",
"split_command",
"wav_to_events",
]
@@ -1,10 +1,9 @@
"""Utilities for Wyoming satellite.""" """Audio utilities."""
import array import array
import asyncio
import logging import logging
import wave import wave
from pathlib import Path from pathlib import Path
from typing import Iterable, Iterator, List, Optional, Union from typing import Iterable, Iterator, Union
from wyoming.audio import AudioChunk, AudioStart, AudioStop from wyoming.audio import AudioChunk, AudioStart, AudioStop
from wyoming.event import Event from wyoming.event import Event
@@ -98,27 +97,6 @@ def chunk_samples(
leftover_chunk_buffer.append(rest_samples) leftover_chunk_buffer.append(rest_samples)
async def run_event_command(
command: Optional[List[str]], command_input: Optional[str] = None
) -> None:
"""Run a custom event command with optional input."""
if not command:
return
_LOGGER.debug("Running %s", command)
program, *program_args = command
proc = await asyncio.create_subprocess_exec(
program, *program_args, stdin=asyncio.subprocess.PIPE
)
assert proc.stdin is not None
if command_input:
await proc.communicate(input=command_input.encode("utf-8"))
else:
proc.stdin.close()
await proc.wait()
def wav_to_events( def wav_to_events(
wav_path: Union[str, Path], wav_path: Union[str, Path],
samples_per_chunk: int = 1024, samples_per_chunk: int = 1024,
+58
View File
@@ -0,0 +1,58 @@
"""Miscellaneous utilities."""
import argparse
import asyncio
import logging
import shlex
import uuid
from typing import List, Optional
_LOGGER = logging.getLogger()
async def run_event_command(
command: Optional[List[str]], command_input: Optional[str] = None
) -> None:
"""Run a custom event command with optional input."""
if not command:
return
_LOGGER.debug("Running %s", command)
program, *program_args = command
proc = await asyncio.create_subprocess_exec(
program, *program_args, stdin=asyncio.subprocess.PIPE
)
assert proc.stdin is not None
if command_input:
await proc.communicate(input=command_input.encode("utf-8"))
else:
proc.stdin.close()
await proc.wait()
def get_mac_address() -> str:
"""Return MAC address formatted as hex with no colons."""
return "".join(
# pylint: disable=consider-using-f-string
["{:02x}".format((uuid.getnode() >> ele) & 0xFF) for ele in range(0, 8 * 6, 8)][
::-1
]
)
def needs_webrtc(args: argparse.Namespace) -> bool:
"""Return True if webrtc must be used."""
return (args.mic_noise_suppression > 0) or (args.mic_auto_gain > 0)
def needs_silero(args: argparse.Namespace) -> bool:
"""Return True if silero-vad must be used."""
return args.vad
def split_command(command: Optional[str]) -> Optional[List[str]]:
"""Split command line program/args if not empty."""
if not command:
return None
return shlex.split(command)