2from typing
import Optional, Union
3from random
import choice
5from functools
import partial
15from purepyindi2
import device, properties, constants, messages
19from .personality
import Personality, Transition, Operation, SSML, Recording
20from .opentts_bridge
import speak, ssml_to_wav
31 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")
35 return '{' in text
or '}' in text
38 config : AudibleAlertsConfig
39 personality : Personality
41 _speech_requests : list[Union[SSML, Recording]]
43 default_voice : str =
"coqui-tts:en_ljspeech"
44 personalities : list[str] = [
'onsky',
'lab_mode',]
45 active_personality : str =
"lab_mode"
46 api_url : str =
"http://localhost:5500/"
49 per_transition_cooldown_ts : dict[Transition, float]
50 last_utterance_ts : float = 0
51 last_utterance_chosen : Optional[str] =
None
52 observers_device : str =
'observers'
53 last_walkup : Optional[dict[str,str]] =
None
54 last_walkup_ts : float = 0
55 walkup_double_trigger_timeout_sec : float = 30
59 log.warn(f
"Duplicated speech request while already enqueued: {sr}")
64 if 'target' in new_message
and new_message[
'target'] != existing_property[
'current']:
65 self.log.
debug(f
"Setting new speech text: {new_message['target']}")
66 existing_property[
'current'] = new_message[
'target']
67 existing_property[
'target'] = new_message[
'target']
68 self.update_property(existing_property)
71 self.log.
debug(f
"{new_message['request']=}")
73 current_text = self.properties[
'speech_text'][
'current']
75 st =
'<speak>' + current_text +
'</speak>'
77 self.telem(
"speech_request", {
"text": current_text})
78 self.update_property(existing_property)
84 self.update_property(existing_property)
87 existing_property[
'toggle'] = new_message[
'toggle']
90 self.log.info(
"Muted")
92 self.log.info(
"Unmuted")
93 self.update_property(existing_property)
94 self.telem(
"mute_toggle", {
"mute": self.
mutemute})
98 for element_name
in new_message:
99 value = new_message[element_name]
102 self.log.
debug(f
"Already did {self.last_walkup[which_updated]}")
106 self.log.info(f
"Queueing walk-up {utterance} for {element_name}")
114 if element_name
not in new_message:
116 value = new_message[element_name]
118 self.log.
debug(f
"Judging reaction for {element_name} change to {repr(value)} using {transition}")
119 self.log.
debug(f
"before check {self.latch_transitions=}")
122 self.log.
debug(f
"{new_message}\n{transition.compare(value)=}, last value was {last_value}, {value != last_value=} {(not transition.compare(last_value))=}")
132 self.log.
debug(f
"after update {self.latch_transitions=}")
133 self.log.
debug(f
"latched {transition=} with {value=}")
135 sec_since_trigger =
time.time() - last_transition_ts
137 self.log.
debug(f
"Checking for debounce: {sec_since_trigger=} {debounce_expired=}")
139 utterance = choice(utterance_choices)
140 self.log.
debug(f
"Submitting speech request: {utterance}")
143 self.log.
debug(f
"Would have spoken, but it's only been {sec_since_trigger=}")
146 self.log.
debug(f
"un-latch {transition}, so next time we change to a value that compares True we trigger again. ({last_value=} {value=})")
147 self.log.
debug(f
"before {self.latch_transitions=}")
150 self.log.
debug(f
"after {self.latch_transitions=}")
153 self.log.
debug(f
"Got {new_message.device}.{new_message.name} but {transition=} did not match")
160 for sub
in substitutables:
162 value = self.client[indi_id]
165 self.log.
debug(f
"Replacing {repr(sub)} with {value=}")
166 if value
is not None:
169 value =
"{:.1f}".format(value)
170 except (TypeError, ValueError):
173 return SSML(speech_text)
178 active_personality =
None
182 active_personality = elem
184 if active_personality
is not None:
185 self.log.info(f
"Switching to {active_personality=}")
188 self.update_property(prop)
194 for elem
in new_message:
197 self.log.info(f
"Soundboard requested {srq}")
200 self.update_property(prop)
203 personality_file =
os.path.join(HERE,
"personalities", f
"{personality_name}.xml")
208 log.exception(f
"Tried to remove {cb=} {device_name=} {property_name=}")
213 self.log.info(f
"Loading personality from {personality_file}")
234 device_name=device_name,
235 property_name=property_name
238 self.log.
debug(f
"Registered reaction handler on {device_name=} {property_name=} {element_name=} using transition {t}")
240 self.log.
debug(f
"{reaction.indi_id}: {t}: {utterance}")
244 self.log.
debug(f
"Caching synthesis to {result}")
246 self.log.
debug(f
"Cannot pre-cache because there are substitutions to be made")
252 self.telem(
"load_personality", {
'name': personality_name})
253 self.send_all_properties()
264 self.log.info(
"Waiting for connection...")
266 self.log.info(
"Connected.")
267 self.log.
debug(f
"Caching synthesis output to {self.config.cache}")
308 name=
"reload_personality",
314 self.log.info(
"Set up complete")
320 self.log.
debug(f
"Would have said: {repr(speech)}, but muted")
322 self.log.info(f
"Speaking: {repr(speech)}")
324 self.log.
debug(
"Speech complete")
333 self.log.
debug(f
"Would have said: {repr(next_utterance)}, but muted")
335 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)
per_transition_cooldown_ts
handle_reload_request(self, existing_property, new_message)
dict per_transition_cooldown_ts
walkup_handler(self, new_message)
Optional soundboard_sw_prop
contains_substitutions(text)