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 asyncio
import logging
import shlex
import sys
import time
import uuid
from functools import partial
from pathlib import Path
from typing import List, Optional
from wyoming.event import Event
from wyoming.info import Attribution, Describe, Info, Satellite
from wyoming.server import AsyncEventHandler, AsyncServer, AsyncTcpServer
from wyoming.info import Attribution, Info, Satellite
from wyoming.server import AsyncServer, AsyncTcpServer
from .event_handler import SatelliteEventHandler
from .satellite import (
AlwaysStreamingSatellite,
SatelliteBase,
@@ -28,7 +24,13 @@ from .settings import (
VadSettings,
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()
_DIR = Path(__file__).parent
@@ -263,7 +265,7 @@ async def main() -> None:
settings = SatelliteSettings(
mic=MicSettings(
uri=args.mic_uri,
command=_split(args.mic_command),
command=split_command(args.mic_command),
rate=args.mic_command_rate,
width=args.mic_command_width,
channels=args.mic_command_channels,
@@ -281,12 +283,12 @@ async def main() -> None:
),
wake=WakeSettings(
uri=args.wake_uri,
command=_split(args.wake_command),
command=split_command(args.wake_command),
names=args.wake_word_name,
),
snd=SndSettings(
uri=args.snd_uri,
command=_split(args.snd_command),
command=split_command(args.snd_command),
rate=args.snd_command_rate,
width=args.snd_command_width,
channels=args.snd_command_channels,
@@ -296,18 +298,18 @@ async def main() -> None:
),
event=EventSettings(
uri=args.event_uri,
startup=_split(args.startup_command),
streaming_start=_split(args.streaming_start_command),
streaming_stop=_split(args.streaming_stop_command),
detect=_split(args.detect_command),
detection=_split(args.detection_command),
transcript=_split(args.transcript_command),
stt_start=_split(args.stt_start_command),
stt_stop=_split(args.stt_stop_command),
synthesize=_split(args.synthesize_command),
tts_start=_split(args.tts_start_command),
tts_stop=_split(args.tts_stop_command),
error=_split(args.error_command),
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),
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),
),
)
@@ -324,7 +326,7 @@ async def main() -> None:
satellite = AlwaysStreamingSatellite(settings)
if args.startup_command:
await run_event_command(_split(args.startup_command))
await run_event_command(split_command(args.startup_command))
_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():
try:
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 asyncio
import logging
import wave
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.event import Event
@@ -98,27 +97,6 @@ def chunk_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(
wav_path: Union[str, Path],
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)