2from typing
import Optional, Union
3from random
import choice
5from functools
import partial
13from purepyindi2
import device, properties, constants, messages
14from purepyindi2.messages
import DefNumber, DefSwitch, DefText
15from magaox.indi.device
import XDevice, BaseConfig
17from .personality
import Personality, Transition, Operation, SSML, Recording
18from .opentts_bridge
import speak, ssml_to_wav
29 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")
33 return '{' in text
or '}' in text
36 config : AudibleAlertsConfig
37 personality : Personality
39 _speech_requests : list[Union[SSML, Recording]]
41 default_voice : str =
"coqui-tts:en_ljspeech"
42 personalities : list[str] = [
'default',
'lab_mode',]
43 active_personality : str =
"default"
44 api_url : str =
"http://localhost:5500/"
47 per_transition_cooldown_ts : dict[Transition, float]
48 last_utterance_ts : float = 0
49 last_utterance_chosen : Optional[str] =
None
53 log.warn(f
"Duplicated speech request while already enqueued: {sr}")
58 if 'target' in new_message
and new_message[
'target'] != existing_property[
'current']:
59 self.log.
debug(f
"Setting new speech text: {new_message['target']}")
60 existing_property[
'current'] = new_message[
'target']
61 existing_property[
'target'] = new_message[
'target']
62 self.update_property(existing_property)
65 self.log.
debug(f
"{new_message['request']=}")
67 current_text = self.properties[
'speech_text'][
'current']
69 st =
'<speak>' + current_text +
'</speak>'
71 self.telem(
"speech_request", {
"text": current_text})
72 self.update_property(existing_property)
78 self.update_property(existing_property)
81 existing_property[
'toggle'] = new_message[
'toggle']
87 self.update_property(existing_property)
88 self.telem(
"mute_toggle", {
"mute": self.
mutemute})
93 if element_name
not in new_message:
95 value = new_message[element_name]
96 self.log.
debug(f
"Judging reaction for {element_name} change to {repr(value)} using {transition}")
97 self.log.
debug(f
"before check {self.latch_transitions=}")
99 self.log.
debug(f
"{new_message}\n{transition.compare(value)=}, last value was {last_value}, {value != last_value=} {(not transition.compare(last_value))=}")
108 self.log.
debug(f
"after update {self.latch_transitions=}")
109 self.log.
debug(f
"latched {transition=} with {value=}")
111 sec_since_trigger =
time.time() - last_transition_ts
113 self.log.
debug(f
"Checking for debounce: {sec_since_trigger=} {debounce_expired=}")
115 utterance = choice(utterance_choices)
116 self.log.
debug(f
"Submitting speech request: {utterance}")
119 self.log.
debug(f
"Would have spoken, but it's only been {sec_since_trigger=}")
121 self.log.
debug(f
"un-latch {transition}, so next time we change to a value that compares True we trigger again. ({last_value=} {value=})")
122 self.log.
debug(f
"before {self.latch_transitions=}")
124 self.log.
debug(f
"after {self.latch_transitions=}")
126 self.log.
debug(f
"Got {new_message.device}.{new_message.name} but {transition=} did not match")
133 for sub
in substitutables:
135 value = self.client[indi_id]
138 self.log.
debug(f
"Replacing {repr(sub)} with {value=}")
139 if value
is not None:
142 value =
"{:.1f}".format(value)
143 except (TypeError, ValueError):
146 return SSML(speech_text)
151 active_personality =
None
155 active_personality = elem
157 if active_personality
is not None:
158 self.log.info(f
"Switching to {active_personality=}")
161 self.update_property(prop)
167 for elem
in new_message:
170 self.log.info(f
"Soundboard requested {srq}")
173 self.update_property(prop)
176 personality_file =
os.path.join(HERE,
"personalities", f
"{personality_name}.xml")
181 log.exception(f
"Tried to remove {cb=} {device_name=} {property_name=}")
186 self.log.info(f
"Loading personality from {personality_file}")
207 device_name=device_name,
208 property_name=property_name
211 self.log.
debug(f
"Registered reaction handler on {device_name=} {property_name=} {element_name=} using transition {t}")
213 self.log.
debug(f
"{reaction.indi_id}: {t}: {utterance}")
217 self.log.
debug(f
"Caching synthesis to {result}")
219 self.log.
debug(f
"Cannot pre-cache because there are substitutions to be made")
221 self.telem(
"load_personality", {
'name': personality_name})
222 self.send_all_properties()
232 self.log.info(
"Waiting for connection...")
234 self.log.info(
"Connected.")
235 self.log.
debug(f
"Caching synthesis output to {self.config.cache.path}")
276 name=
"reload_personality",
282 self.log.info(
"Set up complete")
288 self.log.
debug(f
"Would have said: {repr(speech)}, but muted")
290 self.log.info(f
"Speaking: {repr(speech)}")
292 self.log.
debug(
"Speech complete")
301 self.log.
debug(f
"Would have said: {repr(next_utterance)}, but muted")
303 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
contains_substitutions(text)