Clean up and move code to utils
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
"""Voice satellite using the Wyoming protocol."""
|
||||||
|
|||||||
+27
-101
@@ -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())
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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,
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user