async def say( self, text: str, site_id="default", session_id="", request_id: typing.Optional[str] = None, block: bool = True, ) -> typing.AsyncIterable[ typing.Union[ TtsSay, HotwordToggleOn, HotwordToggleOff, AsrToggleOn, AsrToggleOff ] ]: """Send text to TTS system and wait for reply.""" finished_event = asyncio.Event() finished_id = request_id or str(uuid4()) self.message_events[TtsSayFinished][finished_id] = finished_event # Disable ASR/hotword at site yield HotwordToggleOff(site_id=site_id, reason=HotwordToggleReason.TTS_SAY) yield AsrToggleOff(site_id=site_id, reason=AsrToggleReason.TTS_SAY) # Wait for messages to be delivered await asyncio.sleep(self.toggle_delay) try: # Forward to TTS _LOGGER.debug("Say: %s", text) yield TtsSay( id=finished_id, site_id=site_id, session_id=session_id, text=text ) if block: # Wait for finished event say_finished_timeout = 10.0 if self.say_chars_per_second > 0: # Estimate timeout based on text length say_finished_timeout = max( say_finished_timeout, len(text) / self.say_chars_per_second ) _LOGGER.debug( "Waiting for sayFinished (id=%s, timeout=%s)", finished_id, say_finished_timeout, ) await asyncio.wait_for( finished_event.wait(), timeout=say_finished_timeout ) except asyncio.TimeoutError: _LOGGER.warning("Did not receive sayFinished before timeout") except Exception: _LOGGER.exception("say") finally: # Wait for audio to finish play await asyncio.sleep(self.toggle_delay) # Re-enable ASR/hotword at site yield HotwordToggleOn(site_id=site_id, reason=HotwordToggleReason.TTS_SAY) yield AsrToggleOn(site_id=site_id, reason=AsrToggleReason.TTS_SAY)
async def play_wav_data( self, wav_bytes: bytes, site_id: typing.Optional[str] = None ) -> AudioPlayFinished: """Play WAV data through speakers.""" if self.sound_system == "dummy": raise RuntimeError("No audio output system configured") site_id = site_id or self.site_id request_id = str(uuid4()) def handle_finished(): while True: _, message = yield if ( isinstance(message, AudioPlayFinished) and (message.id == request_id) ) or isinstance(message, AudioPlayError): return message def messages(): yield ( AudioPlayBytes(wav_bytes=wav_bytes), {"site_id": site_id, "request_id": request_id}, ) message_types: typing.List[typing.Type[Message]] = [ AudioPlayFinished, AudioPlayError, ] # Disable hotword/ASR self.publish( HotwordToggleOff(site_id=site_id, reason=HotwordToggleReason.PLAY_AUDIO) ) self.publish(AsrToggleOff(site_id=site_id, reason=AsrToggleReason.PLAY_AUDIO)) try: # Expecting only a single result result = None async for response in self.publish_wait( handle_finished(), messages(), message_types ): result = response if isinstance(result, AudioPlayError): _LOGGER.error(result) raise RuntimeError(result.error) assert isinstance(result, AudioPlayFinished) return result finally: # Enable hotword/ASR self.publish( HotwordToggleOn(site_id=site_id, reason=HotwordToggleReason.PLAY_AUDIO) ) self.publish( AsrToggleOn(site_id=site_id, reason=AsrToggleReason.PLAY_AUDIO) )
def on_connect(self, client, userdata, flags, rc): """Connected to MQTT broker.""" try: topics = [ AsrToggleOn.topic(), AsrToggleOff.topic(), AsrStartListening.topic(), AsrStopListening.topic(), ] if self.audioframe_topics: # Specific siteIds topics.extend(self.audioframe_topics) else: # All siteIds topics.append(AudioFrame.topic(siteId="+")) for topic in topics: self.client.subscribe(topic) _LOGGER.debug("Subscribed to %s", topic) except Exception: _LOGGER.exception("on_connect")
async def maybe_play_sound( self, sound_name: str, site_id: typing.Optional[str] = None, request_id: typing.Optional[str] = None, block: bool = True, ) -> typing.AsyncIterable[SoundsType]: """Play WAV sound through audio out if it exists.""" if site_id in self.no_sound: _LOGGER.debug("Sound is disabled for site %s", site_id) return site_id = site_id or self.site_id sound_path = self.sound_paths.get(sound_name) if sound_path: if sound_path.is_dir(): sound_file_paths = [ p for p in sound_path.rglob("*") if p.is_file() and (p.suffix in self.sound_suffixes) ] if not sound_file_paths: _LOGGER.debug("No sound files found in %s", str(sound_path)) return sound_path = random.choice(sound_file_paths) elif not sound_path.is_file(): _LOGGER.error("Sound does not exist: %s", str(sound_path)) return _LOGGER.debug("Playing sound %s", str(sound_path)) # Convert to WAV wav_bytes = DialogueHermesMqtt.convert_to_wav(sound_path) if (self.volume is not None) and (self.volume != 1.0): wav_bytes = DialogueHermesMqtt.change_volume(wav_bytes, self.volume) # Send messages request_id = request_id or str(uuid4()) finished_event = asyncio.Event() finished_id = request_id self.message_events[AudioPlayFinished][finished_id] = finished_event # Disable ASR/hotword at site yield HotwordToggleOff( site_id=site_id, reason=HotwordToggleReason.PLAY_AUDIO ) yield AsrToggleOff(site_id=site_id, reason=AsrToggleReason.PLAY_AUDIO) # Wait for messages to be delivered await asyncio.sleep(self.toggle_delay) try: yield ( AudioPlayBytes(wav_bytes=wav_bytes), {"site_id": site_id, "request_id": request_id}, ) # Wait for finished event or WAV duration if block: wav_duration = get_wav_duration(wav_bytes) wav_timeout = wav_duration + self.sound_timeout_extra _LOGGER.debug( "Waiting for playFinished (id=%s, timeout=%s)", finished_id, wav_timeout, ) await asyncio.wait_for(finished_event.wait(), timeout=wav_timeout) except asyncio.TimeoutError: _LOGGER.warning("Did not receive sayFinished before timeout") except Exception: _LOGGER.exception("maybe_play_sound") finally: # Wait for audio to finish playing await asyncio.sleep(self.toggle_delay) # Re-enable ASR/hotword at site yield HotwordToggleOn( site_id=site_id, reason=HotwordToggleReason.PLAY_AUDIO ) yield AsrToggleOn(site_id=site_id, reason=AsrToggleReason.PLAY_AUDIO)
def test_asr_toggle_off(): """Test AsrToggleOff.""" assert AsrToggleOff.topic() == "hermes/asr/toggleOff"
async def maybe_play_sound( self, sound_name: str, site_id: typing.Optional[str] = None, request_id: typing.Optional[str] = None, block: bool = True, ) -> typing.AsyncIterable[SoundsType]: """Play WAV sound through audio out if it exists.""" site_id = site_id or self.site_id wav_path = self.sound_paths.get(sound_name) if wav_path: if not wav_path.is_file(): _LOGGER.error("WAV does not exist: %s", str(wav_path)) return _LOGGER.debug("Playing WAV %s", str(wav_path)) wav_bytes = wav_path.read_bytes() request_id = request_id or str(uuid4()) finished_event = asyncio.Event() finished_id = request_id self.message_events[AudioPlayFinished][ finished_id] = finished_event # Disable ASR/hotword at site yield HotwordToggleOff(site_id=site_id, reason=HotwordToggleReason.PLAY_AUDIO) yield AsrToggleOff(site_id=site_id, reason=AsrToggleReason.PLAY_AUDIO) # Wait for messages to be delivered await asyncio.sleep(self.toggle_delay) try: yield ( AudioPlayBytes(wav_bytes=wav_bytes), { "site_id": site_id, "request_id": request_id }, ) # Wait for finished event or WAV duration if block: wav_duration = get_wav_duration(wav_bytes) wav_timeout = wav_duration + self.sound_timeout_extra _LOGGER.debug("Waiting for playFinished (timeout=%s)", wav_timeout) await asyncio.wait_for(finished_event.wait(), timeout=wav_timeout) except asyncio.TimeoutError: _LOGGER.warning("Did not receive sayFinished before timeout") except Exception: _LOGGER.exception("maybe_play_sound") finally: # Wait for audio to finish playing await asyncio.sleep(self.toggle_delay) # Re-enable ASR/hotword at site yield HotwordToggleOn(site_id=site_id, reason=HotwordToggleReason.PLAY_AUDIO) yield AsrToggleOn(site_id=site_id, reason=AsrToggleReason.PLAY_AUDIO)