2from typing
import Optional, Union
3from random
import choice
5from functools
import partial
13from xconf.contrib
import DirectoryConfig
14from purepyindi2
import device, properties, constants, messages
15from purepyindi2.messages
import DefNumber, DefSwitch, DefText
16from magaox.indi.device
import XDevice, BaseConfig
18from .personality
import Personality, Transition, Operation, SSML, Recording
19from .opentts_bridge
import speak, ssml_to_wav
30 random_utterance_interval_sec : Union[float, int] =
xconf.field(default=15 * 60, help=
"Seconds since last (real or random) utterance before a random utterance should play")
31 cache : DirectoryConfig =
xconf.field(default=DirectoryConfig(path=
"/tmp/audibleAlerts_cache"))
34 return '{' in text
or '}' in text
37 config : AudibleAlertsConfig
38 personality : Personality
40 _speech_requests : list[Union[SSML, Recording]]
42 default_voice : str =
"coqui-tts:en_ljspeech"
43 personalities : list[str] = [
'default',
'lab_mode',]
44 active_personality : str =
"default"
45 api_url : str =
"http://localhost:5500/"
48 per_transition_cooldown_ts : dict[Transition, float]
49 last_utterance_ts : float = 0
50 last_utterance_chosen : Optional[str] =
None
51 observers_device : str =
'observers'
52 last_walkup : Optional[dict[str,str]] =
None
53 last_walkup_ts : float = 0
54 walkup_double_trigger_timeout_sec : float = 30
58 log.warn(f
"Duplicated speech request while already enqueued: {sr}")
63 if 'target' in new_message
and new_message[
'target'] != existing_property[
'current']:
64 self.log.
debug(f
"Setting new speech text: {new_message['target']}")
65 existing_property[
'current'] = new_message[
'target']
66 existing_property[
'target'] = new_message[
'target']
67 self.update_property(existing_property)
70 self.log.
debug(f
"{new_message['request']=}")
72 current_text = self.properties[
'speech_text'][
'current']
74 st =
'<speak>' + current_text +
'</speak>'
76 self.telem(
"speech_request", {
"text": current_text})
77 self.update_property(existing_property)
83 self.update_property(existing_property)
86 existing_property[
'toggle'] = new_message[
'toggle']
89 self.log.info(
"Muted")
91 self.log.info(
"Unmuted")
92 self.update_property(existing_property)
93 self.telem(
"mute_toggle", {
"mute": self.
mutemute})
97 for element_name
in new_message:
98 value = new_message[element_name]
101 self.log.
debug(f
"Already did {self.last_walkup[which_updated]}")
105 self.log.info(f
"Queueing walk-up {utterance} for {element_name}")
113 if element_name
not in new_message:
115 value = new_message[element_name]
117 self.log.
debug(f
"Judging reaction for {element_name} change to {repr(value)} using {transition}")
118 self.log.
debug(f
"before check {self.latch_transitions=}")
121 self.log.
debug(f
"{new_message}\n{transition.compare(value)=}, last value was {last_value}, {value != last_value=} {(not transition.compare(last_value))=}")
131 self.log.
debug(f
"after update {self.latch_transitions=}")
132 self.log.
debug(f
"latched {transition=} with {value=}")
134 sec_since_trigger =
time.time() - last_transition_ts
136 self.log.
debug(f
"Checking for debounce: {sec_since_trigger=} {debounce_expired=}")
138 utterance = choice(utterance_choices)
139 self.log.
debug(f
"Submitting speech request: {utterance}")
142 self.log.
debug(f
"Would have spoken, but it's only been {sec_since_trigger=}")
145 self.log.
debug(f
"un-latch {transition}, so next time we change to a value that compares True we trigger again. ({last_value=} {value=})")
146 self.log.
debug(f
"before {self.latch_transitions=}")
149 self.log.
debug(f
"after {self.latch_transitions=}")
152 self.log.
debug(f
"Got {new_message.device}.{new_message.name} but {transition=} did not match")
159 for sub
in substitutables:
161 value = self.client[indi_id]
164 self.log.
debug(f
"Replacing {repr(sub)} with {value=}")
165 if value
is not None:
168 value =
"{:.1f}".format(value)
169 except (TypeError, ValueError):
172 return SSML(speech_text)
177 active_personality =
None
181 active_personality = elem
183 if active_personality
is not None:
184 self.log.info(f
"Switching to {active_personality=}")
187 self.update_property(prop)
193 for elem
in new_message:
196 self.log.info(f
"Soundboard requested {srq}")
199 self.update_property(prop)
202 personality_file =
os.path.join(HERE,
"personalities", f
"{personality_name}.xml")
207 log.exception(f
"Tried to remove {cb=} {device_name=} {property_name=}")
212 self.log.info(f
"Loading personality from {personality_file}")
233 device_name=device_name,
234 property_name=property_name
237 self.log.
debug(f
"Registered reaction handler on {device_name=} {property_name=} {element_name=} using transition {t}")
239 self.log.
debug(f
"{reaction.indi_id}: {t}: {utterance}")
243 self.log.
debug(f
"Caching synthesis to {result}")
245 self.log.
debug(f
"Cannot pre-cache because there are substitutions to be made")
251 self.telem(
"load_personality", {
'name': personality_name})
252 self.send_all_properties()
263 self.log.info(
"Waiting for connection...")
265 self.log.info(
"Connected.")
266 self.log.
debug(f
"Caching synthesis output to {self.config.cache.path}")
307 name=
"reload_personality",
313 self.log.info(
"Set up complete")
319 self.log.
debug(f
"Would have said: {repr(speech)}, but muted")
321 self.log.info(f
"Speaking: {repr(speech)}")
323 self.log.
debug(
"Speech complete")
332 self.log.
debug(f
"Would have said: {repr(next_utterance)}, but muted")
334 self.log.info(f
"Randomly spouting off: {repr(next_utterance)}")
load_personality(self, personality_name)
handle_soundboard_switch(self, properties.IndiProperty prop, new_message)
reaction_handler(self, new_message, element_name, transition, utterance_choices)
Optional last_utterance_chosen
handle_speech_request(self, existing_property, new_message)
handle_mute_toggle(self, existing_property, new_message)
AudibleAlertsConfig config
enqueue_speech_request(self, sr)
handle_personality_switch(self, properties.IndiProperty prop, new_message)
handle_speech_text(self, existing_property, new_message)
properties soundboard_sw_prop
per_transition_cooldown_ts
handle_reload_request(self, existing_property, new_message)
dict per_transition_cooldown_ts
walkup_handler(self, new_message)
contains_substitutions(text)