Limit to 1 server connection
This commit is contained in:
@@ -130,6 +130,7 @@ You can also use `--mic-volume-multiplier X` to multiply all audio samples by `X
|
|||||||
|
|
||||||
Satellites can respond to events from the server by running commands:
|
Satellites can respond to events from the server by running commands:
|
||||||
|
|
||||||
|
* `--startup-command` - run when satellite starts
|
||||||
* `--detect-command` - wake word detection has started, but not detected yet (no stdin)
|
* `--detect-command` - wake word detection has started, but not detected yet (no stdin)
|
||||||
* `--streaming-start-command` - audio has started streaming to server
|
* `--streaming-start-command` - audio has started streaming to server
|
||||||
* `--detection-command` - wake word is detected (wake word name on stdin)
|
* `--detection-command` - wake word is detected (wake word name on stdin)
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ class SatelliteState(str, Enum):
|
|||||||
PLAYING_AUDIO = "playing-audio"
|
PLAYING_AUDIO = "playing-audio"
|
||||||
|
|
||||||
|
|
||||||
|
class ServerState:
|
||||||
|
current_client_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
"""Main entry point."""
|
"""Main entry point."""
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@@ -134,7 +138,13 @@ async def main() -> None:
|
|||||||
)
|
)
|
||||||
parser.add_argument("--vad-threshold", type=float, default=0.5)
|
parser.add_argument("--vad-threshold", type=float, default=0.5)
|
||||||
parser.add_argument("--vad-trigger-level", type=int, default=1)
|
parser.add_argument("--vad-trigger-level", type=int, default=1)
|
||||||
parser.add_argument("--vad-buffer-seconds", type=float, default=0.5)
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
# Audio enhancement (requires webrtc-noise-audio)
|
# Audio enhancement (requires webrtc-noise-audio)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -268,6 +278,7 @@ async def main() -> None:
|
|||||||
|
|
||||||
# Start server
|
# Start server
|
||||||
server = AsyncServer.from_uri(args.uri)
|
server = AsyncServer.from_uri(args.uri)
|
||||||
|
server_state = ServerState()
|
||||||
|
|
||||||
if (not args.no_zeroconf) and isinstance(server, AsyncTcpServer):
|
if (not args.no_zeroconf) and isinstance(server, AsyncTcpServer):
|
||||||
from wyoming.zeroconf import register_server
|
from wyoming.zeroconf import register_server
|
||||||
@@ -288,7 +299,9 @@ async def main() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await server.run(partial(SatelliteEventHandler, wyoming_info, args))
|
await server.run(
|
||||||
|
partial(SatelliteEventHandler, wyoming_info, server_state, args)
|
||||||
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -302,6 +315,7 @@ class SatelliteEventHandler(AsyncEventHandler):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
wyoming_info: Info,
|
wyoming_info: Info,
|
||||||
|
server_state: ServerState,
|
||||||
cli_args: argparse.Namespace,
|
cli_args: argparse.Namespace,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -349,8 +363,21 @@ class SatelliteEventHandler(AsyncEventHandler):
|
|||||||
|
|
||||||
self.events_client: Optional[AsyncClient] = None
|
self.events_client: Optional[AsyncClient] = None
|
||||||
|
|
||||||
|
self.server_state = server_state
|
||||||
|
self.server_state.current_client_id = self.client_id
|
||||||
_LOGGER.debug("Client connected: %s", self.client_id)
|
_LOGGER.debug("Client connected: %s", self.client_id)
|
||||||
|
|
||||||
|
self._stop_if_new_client_task = asyncio.create_task(self._stop_if_new_client())
|
||||||
|
|
||||||
|
async def _stop_if_new_client(self) -> None:
|
||||||
|
"""Stop this event handler if a new client connects."""
|
||||||
|
while self.is_running:
|
||||||
|
if self.server_state.current_client_id != self.client_id:
|
||||||
|
self.is_running = False
|
||||||
|
_LOGGER.debug("Detected new client. Stopping old client.")
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
async def handle_event(self, event: Event) -> bool:
|
async def handle_event(self, event: Event) -> bool:
|
||||||
@@ -457,6 +484,8 @@ class SatelliteEventHandler(AsyncEventHandler):
|
|||||||
|
|
||||||
async def run_satellite(self) -> None:
|
async def run_satellite(self) -> None:
|
||||||
"""Task to read mic input and do remote wake word detection."""
|
"""Task to read mic input and do remote wake word detection."""
|
||||||
|
vad_timeout_sec: Optional[float] = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mic_client = self._make_mic_client()
|
mic_client = self._make_mic_client()
|
||||||
async with mic_client:
|
async with mic_client:
|
||||||
@@ -496,7 +525,7 @@ class SatelliteEventHandler(AsyncEventHandler):
|
|||||||
chunk = AudioChunk.from_event(mic_event)
|
chunk = AudioChunk.from_event(mic_event)
|
||||||
if self.process_vad(chunk.audio):
|
if self.process_vad(chunk.audio):
|
||||||
# Ready to stream
|
# Ready to stream
|
||||||
self.state = SatelliteState.ASR
|
self.state = SatelliteState.WAKE
|
||||||
|
|
||||||
# Start pipeline
|
# Start pipeline
|
||||||
await self._run_pipeline(PipelineStage.WAKE)
|
await self._run_pipeline(PipelineStage.WAKE)
|
||||||
@@ -512,12 +541,43 @@ class SatelliteEventHandler(AsyncEventHandler):
|
|||||||
).event()
|
).event()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.cli_args.vad_wake_word_timeout is not None:
|
||||||
|
# Time in the future when timeout will occur
|
||||||
|
vad_timeout_sec = (
|
||||||
|
time.monotonic()
|
||||||
|
+ self.cli_args.vad_wake_word_timeout
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# No timeout
|
||||||
|
vad_timeout_sec = None
|
||||||
|
|
||||||
_LOGGER.info("Streaming audio")
|
_LOGGER.info("Streaming audio")
|
||||||
elif self.vad_buffer is not None:
|
elif self.vad_buffer is not None:
|
||||||
# Save audio right before speech
|
# Save audio right before speech
|
||||||
self.vad_buffer.put(chunk.audio)
|
self.vad_buffer.put(chunk.audio)
|
||||||
|
|
||||||
if self.state == SatelliteState.ASR:
|
if self.state == SatelliteState.WAKE:
|
||||||
|
if (vad_timeout_sec is not None) and (
|
||||||
|
time.monotonic() >= vad_timeout_sec
|
||||||
|
):
|
||||||
|
# Wake word was not detected within timeout
|
||||||
|
self.state = SatelliteState.VAD_RESET
|
||||||
|
|
||||||
|
# Cancel current pipeline with empty audio chunk
|
||||||
|
chunk = AudioChunk.from_event(mic_event)
|
||||||
|
await self.write_event(
|
||||||
|
AudioChunk(
|
||||||
|
rate=chunk.rate,
|
||||||
|
width=chunk.width,
|
||||||
|
channels=chunk.channels,
|
||||||
|
audio=bytes(),
|
||||||
|
).event()
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remote wake word detection
|
||||||
|
await self.write_event(mic_event)
|
||||||
|
elif self.state == SatelliteState.ASR:
|
||||||
# Forward all audio to server
|
# Forward all audio to server
|
||||||
await self.write_event(mic_event)
|
await self.write_event(mic_event)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user