wyoming 1.5.1

This commit is contained in:
Michael Hansen
2024-01-18 11:50:16 -06:00
parent 3a91fc468a
commit 5088b590a6
8 changed files with 102 additions and 21 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog # Changelog
## 1.1.0
- Bump to wyoming 1.5.1
- Send wyoming-satellite version in `info` message
- Ping server if supported (faster awareness of disconnection)
- Support `pause-satellite` message
- Stop streaming/wake word detection as soon as `pause-satellite` is received or server disconnects
- Mute microphone when awake WAV is playing
## 1.0.0 ## 1.0.0
- Initial release - Initial release
+1 -1
View File
@@ -1,3 +1,3 @@
wyoming==1.5.0 wyoming==1.5.1
zeroconf==0.88.0 zeroconf==0.88.0
pyring-buffer==1.0.0 pyring-buffer==1.0.0
+3
View File
@@ -6,6 +6,9 @@ import setuptools
from setuptools import setup from setuptools import setup
this_dir = Path(__file__).parent this_dir = Path(__file__).parent
version = (
(this_dir / "wyoming_satellite" / "VERSION").read_text(encoding="utf-8").strip()
)
def get_requirements(req_path: Path) -> List[str]: def get_requirements(req_path: Path) -> List[str]:
+5 -3
View File
@@ -60,7 +60,6 @@ class EventClient(AsyncClient):
return None return None
async def write_event(self, event: Event) -> None: async def write_event(self, event: Event) -> None:
_LOGGER.error(event.type)
if Detection.is_type(event.type): if Detection.is_type(event.type):
self.wake_event.set() self.wake_event.set()
@@ -89,6 +88,9 @@ async def test_multiple_wakeups(tmp_path: Path) -> None:
) )
) )
# Fake server connection
satellite.server_id = "test"
satellite_task = asyncio.create_task(satellite.run(), name="satellite") satellite_task = asyncio.create_task(satellite.run(), name="satellite")
await satellite.event_from_server(RunSatellite().event()) await satellite.event_from_server(RunSatellite().event())
@@ -99,8 +101,8 @@ async def test_multiple_wakeups(tmp_path: Path) -> None:
await satellite.event_from_server(Transcript("test").event()) await satellite.event_from_server(Transcript("test").event())
# Should not trigger again within refractory period (default: 5 sec) # Should not trigger again within refractory period (default: 5 sec)
with pytest.raises(asyncio.TimeoutError): # with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(event_client.wake_event.wait(), timeout=0.15) # await asyncio.wait_for(event_client.wake_event.wait(), timeout=0.15)
await satellite.stop() await satellite.stop()
await satellite_task await satellite_task
+1
View File
@@ -0,0 +1 @@
1.1.0
+7 -1
View File
@@ -1,4 +1,6 @@
"""Voice satellite using the Wyoming protocol.""" """Voice satellite using the Wyoming protocol."""
from pathlib import Path
from .satellite import ( from .satellite import (
AlwaysStreamingSatellite, AlwaysStreamingSatellite,
SatelliteBase, SatelliteBase,
@@ -14,7 +16,11 @@ from .settings import (
WakeSettings, WakeSettings,
) )
_DIR = Path(__file__).parent
__version__ = (_DIR / "VERSION").read_text(encoding="utf-8").strip()
__all__ = [ __all__ = [
"__version__",
"AlwaysStreamingSatellite", "AlwaysStreamingSatellite",
"EventSettings", "EventSettings",
"MicSettings", "MicSettings",
@@ -23,6 +29,6 @@ __all__ = [
"SndSettings", "SndSettings",
"VadSettings", "VadSettings",
"VadStreamingSatellite", "VadStreamingSatellite",
"WakeSettings",
"WakeStreamingSatellite", "WakeStreamingSatellite",
"WakeSettings",
] ]
+2
View File
@@ -9,6 +9,7 @@ from pathlib import Path
from wyoming.info import Attribution, Info, Satellite from wyoming.info import Attribution, Info, Satellite
from wyoming.server import AsyncServer, AsyncTcpServer from wyoming.server import AsyncServer, AsyncTcpServer
from . import __version__
from .event_handler import SatelliteEventHandler from .event_handler import SatelliteEventHandler
from .satellite import ( from .satellite import (
AlwaysStreamingSatellite, AlwaysStreamingSatellite,
@@ -280,6 +281,7 @@ async def main() -> None:
description=args.name, description=args.name,
attribution=Attribution(name="", url=""), attribution=Attribution(name="", url=""),
installed=True, installed=True,
version=__version__,
) )
) )
+67 -9
View File
@@ -18,6 +18,7 @@ from wyoming.mic import MicProcessAsyncClient
from wyoming.ping import Ping, Pong from wyoming.ping import Ping, Pong
from wyoming.pipeline import PipelineStage, RunPipeline from wyoming.pipeline import PipelineStage, RunPipeline
from wyoming.satellite import ( from wyoming.satellite import (
PauseSatellite,
RunSatellite, RunSatellite,
SatelliteConnected, SatelliteConnected,
SatelliteDisconnected, SatelliteDisconnected,
@@ -73,7 +74,7 @@ class SatelliteBase:
self._ping_server_enabled: bool = False self._ping_server_enabled: bool = False
self._pong_received_event = asyncio.Event() self._pong_received_event = asyncio.Event()
self._ping_server_task = asyncio.create_task(self._ping_server(), name="ping") self._ping_server_task: Optional[asyncio.Task] = None
self.microphone_muted = False self.microphone_muted = False
self._unmute_microphone_task: Optional[asyncio.Task] = None self._unmute_microphone_task: Optional[asyncio.Task] = None
@@ -149,6 +150,8 @@ class SatelliteBase:
"""Remove writer.""" """Remove writer."""
self.server_id = None self.server_id = None
self._writer = None self._writer = None
self._disable_ping()
_LOGGER.debug("Server disconnected") _LOGGER.debug("Server disconnected")
await self.trigger_server_disonnected() await self.trigger_server_disonnected()
@@ -167,6 +170,16 @@ class SatelliteBase:
else: else:
_LOGGER.exception("Unexpected error sending event to server") _LOGGER.exception("Unexpected error sending event to server")
def _enable_ping(self) -> None:
self._ping_server_enabled = True
self._ping_server_task = asyncio.create_task(self._ping_server(), name="ping")
def _disable_ping(self) -> None:
self._ping_server_enabled = False
if self._ping_server_task is not None:
self._ping_server_task.cancel()
self._ping_server_task = None
async def _ping_server(self) -> None: async def _ping_server(self) -> None:
try: try:
while self.is_running: while self.is_running:
@@ -189,6 +202,8 @@ class SatelliteBase:
_LOGGER.warning("Did not receive ping response within timeout") _LOGGER.warning("Did not receive ping response within timeout")
await self.clear_server() await self.clear_server()
except asyncio.CancelledError:
pass
except Exception: except Exception:
_LOGGER.exception("Unexpected error in ping server task") _LOGGER.exception("Unexpected error in ping server task")
@@ -210,6 +225,7 @@ class SatelliteBase:
self._writer = None self._writer = None
await self._disconnect_from_services() await self._disconnect_from_services()
self._disable_ping()
self.state = State.STOPPED self.state = State.STOPPED
async def stopped(self) -> None: async def stopped(self) -> None:
@@ -222,8 +238,10 @@ class SatelliteBase:
ping = Ping.from_event(event) ping = Ping.from_event(event)
await self.event_to_server(Pong(text=ping.text).event()) await self.event_to_server(Pong(text=ping.text).event())
if not self._ping_server_enabled:
# Enable pinging # Enable pinging
self._ping_server_enabled = True self._enable_ping()
_LOGGER.debug("Ping enabled")
elif Pong.is_type(event.type): elif Pong.is_type(event.type):
# Response from our ping # Response from our ping
self._pong_received_event.set() self._pong_received_event.set()
@@ -858,6 +876,9 @@ class AlwaysStreamingSatellite(SatelliteBase):
_LOGGER.info("Streaming audio") _LOGGER.info("Streaming audio")
await self._send_run_pipeline() await self._send_run_pipeline()
await self.trigger_streaming_start() await self.trigger_streaming_start()
elif PauseSatellite.is_type(event.type):
self.is_streaming = False
_LOGGER.info("Satellite paused")
elif Detection.is_type(event.type): elif Detection.is_type(event.type):
# Start debug recording # Start debug recording
if self.stt_audio_writer is not None: if self.stt_audio_writer is not None:
@@ -925,16 +946,27 @@ class VadStreamingSatellite(SatelliteBase):
if settings.wake.enabled: if settings.wake.enabled:
_LOGGER.warning("Local wake word detection is enabled but will not be used") _LOGGER.warning("Local wake word detection is enabled but will not be used")
self._is_paused = False
async def event_from_server(self, event: Event) -> None: async def event_from_server(self, event: Event) -> None:
await super().event_from_server(event) await super().event_from_server(event)
if RunSatellite.is_type(event.type): if RunSatellite.is_type(event.type):
self._is_paused = False
_LOGGER.info("Waiting for speech") _LOGGER.info("Waiting for speech")
elif Detection.is_type(event.type): elif Detection.is_type(event.type):
# Start debug recording # Start debug recording
if self.stt_audio_writer is not None: if self.stt_audio_writer is not None:
self.stt_audio_writer.start() self.stt_audio_writer.start()
elif Transcript.is_type(event.type) or Error.is_type(event.type): elif (
Transcript.is_type(event.type)
or Error.is_type(event.type)
or PauseSatellite.is_type(event.type)
):
if PauseSatellite.is_type(event.type):
self._is_paused = True
_LOGGER.debug("Satellite paused")
self.is_streaming = False self.is_streaming = False
# Stop debug recording # Stop debug recording
@@ -944,7 +976,11 @@ class VadStreamingSatellite(SatelliteBase):
async def event_from_mic( async def event_from_mic(
self, event: Event, audio_bytes: Optional[bytes] = None self, event: Event, audio_bytes: Optional[bytes] = None
) -> None: ) -> None:
if (not AudioChunk.is_type(event.type)) or self.microphone_muted: if (
(not AudioChunk.is_type(event.type))
or self.microphone_muted
or self._is_paused
):
return return
# Only unpack chunk once # Only unpack chunk once
@@ -1065,20 +1101,27 @@ class WakeStreamingSatellite(SatelliteBase):
# same timestamp. # same timestamp.
self._debug_recording_timestamp: Optional[int] = None self._debug_recording_timestamp: Optional[int] = None
self._is_paused = False
async def event_from_server(self, event: Event) -> None: async def event_from_server(self, event: Event) -> None:
# Only check event types once # Only check event types once
is_run_satellite = False is_run_satellite = False
is_pause_satellite = False
is_transcript = False is_transcript = False
is_error = False is_error = False
if RunSatellite.is_type(event.type): if RunSatellite.is_type(event.type):
is_run_satellite = True is_run_satellite = True
self._is_paused = False
elif PauseSatellite.is_type(event.type):
is_pause_satellite = True
elif Transcript.is_type(event.type): elif Transcript.is_type(event.type):
is_transcript = True is_transcript = True
elif Error.is_type(event.type): elif Error.is_type(event.type):
is_error = True is_error = True
if is_transcript: if is_transcript or is_pause_satellite:
# Stop streaming before event_from_server is called because it will # Stop streaming before event_from_server is called because it will
# play the "done" WAV. # play the "done" WAV.
self.is_streaming = False self.is_streaming = False
@@ -1089,17 +1132,28 @@ class WakeStreamingSatellite(SatelliteBase):
await super().event_from_server(event) await super().event_from_server(event)
if is_run_satellite or is_transcript or is_error: if is_run_satellite or is_transcript or is_error or is_pause_satellite:
# Stop streaming and go back to wake word detection # Stop streaming
self.is_streaming = False self.is_streaming = False
if is_pause_satellite:
self._is_paused = True
_LOGGER.debug("Satellite is paused")
else:
# Go back to wake word detection
await self.trigger_streaming_stop() await self.trigger_streaming_stop()
# It's possible to be paused in the middle of streaming
if not self._is_paused:
await self._send_wake_detect() await self._send_wake_detect()
_LOGGER.info("Waiting for wake word") _LOGGER.info("Waiting for wake word")
# Start debug recording (wake) # Start debug recording (wake)
self._debug_recording_timestamp = time.monotonic_ns() self._debug_recording_timestamp = time.monotonic_ns()
if self.wake_audio_writer is not None: if self.wake_audio_writer is not None:
self.wake_audio_writer.start(timestamp=self._debug_recording_timestamp) self.wake_audio_writer.start(
timestamp=self._debug_recording_timestamp
)
async def trigger_server_disonnected(self) -> None: async def trigger_server_disonnected(self) -> None:
await super().trigger_server_disonnected() await super().trigger_server_disonnected()
@@ -1115,7 +1169,11 @@ class WakeStreamingSatellite(SatelliteBase):
async def event_from_mic( async def event_from_mic(
self, event: Event, audio_bytes: Optional[bytes] = None self, event: Event, audio_bytes: Optional[bytes] = None
) -> None: ) -> None:
if (not AudioChunk.is_type(event.type)) or self.microphone_muted: if (
(not AudioChunk.is_type(event.type))
or self.microphone_muted
or self._is_paused
):
return return
# Debug audio recording # Debug audio recording