2 from typing
import Optional, Union
3 from random
import choice
5 from functools
import partial
13 from purepyindi2
import device, properties, constants, messages
14 from purepyindi2.messages
import DefNumber, DefSwitch, DefText
15 from magaox.indi.device
import XDevice, BaseConfig
17 from .personality
import Personality, Transition, Operation, SSML, Recording
18 from .opentts_bridge
import speak, ssml_to_wav
20 log = logging.getLogger(__name__)
21 HERE = os.path.dirname(__file__)
22 TAGS_RE = re.compile(
'<.*?>')
25 return TAGS_RE.sub(
'', raw_xml)
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")
30 cache : xconf.DirectoryConfig = xconf.field(default=xconf.DirectoryConfig(path=
"/tmp/audibleAlerts_cache"))
33 return '{' in text
or '}' in text
36 config : AudibleAlertsConfig
37 personality : Personality
39 _speech_requests : list[Union[SSML, Recording]]
40 soundboard_sw_prop : properties.SwitchVector =
None
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/"
46 latch_transitions : dict[Transition, constants.AnyIndiValue]
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']=}")
66 if new_message[
'request']
is constants.SwitchState.ON:
67 current_text = self.properties[
'speech_text'][
'current']
68 if current_text
is not None and len(current_text.strip()) != 0:
69 st =
'<speak>' + current_text +
'</speak>'
71 self.telem(
"speech_request", {
"text": current_text})
72 self.update_property(existing_property)
75 if new_message[
'request']
is constants.SwitchState.ON:
78 self.update_property(existing_property)
81 existing_property[
'toggle'] = new_message[
'toggle']
82 self.
mutemute = new_message[
'toggle']
is constants.SwitchState.ON
87 self.update_property(existing_property)
88 self.telem(
"mute_toggle", {
"mute": self.
mutemute})
91 if not isinstance(new_message, messages.IndiSetMessage):
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))=}")
100 if transition.compare(value)
and (
103 (transition.op
is None and value != last_value)
or
105 (
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
112 debounce_expired = sec_since_trigger > transition.debounce_sec
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=}")
120 elif transition.compare(last_value)
and not transition.compare(value):
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")
129 if isinstance(speech, Recording):
131 speech_text = speech.markup
132 substitutables = re.findall(
r"({[^}]+})", speech.markup)
133 for sub
in substitutables:
135 value = self.client[indi_id]
136 if hasattr(value,
'value'):
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):
145 speech_text = speech_text.replace(sub, value)
146 return SSML(speech_text)
149 if not isinstance(new_message, messages.IndiNewMessage):
151 active_personality =
None
153 prop[elem] = constants.SwitchState.OFF
154 if elem
in new_message
and new_message[elem]
is constants.SwitchState.ON:
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)
165 if not isinstance(new_message, messages.IndiNewMessage):
167 for elem
in new_message:
168 if new_message[elem]
is constants.SwitchState.ON:
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")
177 for cb, device_name, property_name, _
in self.
_cb_handles_cb_handles:
179 self.client.unregister_callback(cb, device_name=device_name, property_name=property_name)
181 log.exception(f
"Tried to remove {cb=} {device_name=} {property_name=}")
186 self.log.info(f
"Loading personality from {personality_file}")
187 self.
personalitypersonality = Personality.from_path(personality_file)
191 rule=constants.SwitchRule.ONE_OF_MANY,
192 perm=constants.PropertyPerm.READ_WRITE,
194 for btn_name
in self.
personalitypersonality.soundboard:
195 self.
soundboard_sw_propsoundboard_sw_prop.add_element(DefSwitch(name=btn_name, _value=constants.SwitchState.OFF))
200 for reaction
in self.
personalitypersonality.reactions:
201 device_name, property_name, element_name = reaction.indi_id.split(
'.')
202 self.client.get_properties(reaction.indi_id)
203 for t
in reaction.transitions:
204 cb = partial(self.
reaction_handlerreaction_handler, element_name=element_name, transition=t, utterance_choices=reaction.transitions[t])
205 self.client.register_callback(
207 device_name=device_name,
208 property_name=property_name
210 self.
_cb_handles_cb_handles.add((cb, device_name, property_name, t))
211 self.log.debug(f
"Registered reaction handler on {device_name=} {property_name=} {element_name=} using transition {t}")
212 for idx, utterance
in enumerate(reaction.transitions[t]):
213 self.log.debug(f
"{reaction.indi_id}: {t}: {utterance}")
214 if isinstance(utterance, SSML):
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()
231 while self.client.status
is not constants.ConnectionStatus.CONNECTED:
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}")
236 self.config.cache.ensure_exists()
239 sv = properties.SwitchVector(
241 rule=constants.SwitchRule.ONE_OF_MANY,
242 perm=constants.PropertyPerm.READ_WRITE,
244 sv.add_element(DefSwitch(name=f
"toggle", _value=constants.SwitchState.ON
if self.
mutemute
else constants.SwitchState.OFF))
247 sv = properties.SwitchVector(
249 rule=constants.SwitchRule.ONE_OF_MANY,
250 perm=constants.PropertyPerm.READ_WRITE,
252 for pers
in self.personalities:
254 sv.add_element(DefSwitch(name=pers, _value=constants.SwitchState.ON
if self.
active_personalityactive_personality == pers
else constants.SwitchState.OFF))
257 speech_text = properties.TextVector(name=
"speech_text", perm=constants.PropertyPerm.READ_WRITE)
258 speech_text.add_element(DefText(
262 speech_text.add_element(DefText(
268 speech_request = properties.SwitchVector(
270 rule=constants.SwitchRule.ANY_OF_MANY,
272 speech_request.add_element(DefSwitch(name=
"request", _value=constants.SwitchState.OFF))
275 reload_request = properties.SwitchVector(
276 name=
"reload_personality",
277 rule=constants.SwitchRule.ANY_OF_MANY,
279 reload_request.add_element(DefSwitch(name=
"request", _value=constants.SwitchState.OFF))
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")
294 if time.time() - self.
last_utterance_tslast_utterance_ts > self.config.random_utterance_interval_sec
and len(self.
personalitypersonality.random_utterances):
295 next_utterance = choice(self.
personalitypersonality.random_utterances)
297 next_utterance = choice(self.
personalitypersonality.random_utterances)
301 self.log.debug(f
"Would have said: {repr(next_utterance)}, but muted")
303 self.log.info(f
"Randomly spouting off: {repr(next_utterance)}")
304 speak(next_utterance, self.
default_voicedefault_voice, self.api_url, self.config.cache.path)
def reaction_handler(self, new_message, element_name, transition, utterance_choices)
def handle_personality_switch(self, properties.IndiProperty prop, new_message)
def handle_reload_request(self, existing_property, new_message)
def handle_speech_request(self, existing_property, new_message)
def enqueue_speech_request(self, sr)
def load_personality(self, personality_name)
def handle_speech_text(self, existing_property, new_message)
per_transition_cooldown_ts
def handle_soundboard_switch(self, properties.IndiProperty prop, new_message)
def preprocess(self, speech)
def handle_mute_toggle(self, existing_property, new_message)
def contains_substitutions(text)
def drop_xml_tags(raw_xml)
def speak(speech, default_voice, api_url, cache_dir)
def ssml_to_wav(ssml_text, default_voice, api_url, cache_dir)