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}")
98 self.log.debug(f
"{new_message}\n{transition.compare(value)=}, last value was {last_value}, {value != last_value=} {(not transition.compare(last_value))=}")
99 if transition.compare(value)
and (
102 (transition.op
is None and value != last_value)
or
104 (
not transition.compare(last_value))
108 sec_since_trigger = time.time() - last_transition_ts
109 debounce_expired = sec_since_trigger > transition.debounce_sec
110 self.log.debug(f
"Debounced {sec_since_trigger=}")
112 utterance = choice(utterance_choices)
113 self.log.debug(f
"Submitting speech request: {utterance}")
116 self.log.debug(f
"Would have talked, but it's only been {sec_since_trigger=}")
117 elif transition.compare(last_value)
and not transition.compare(value):
121 self.log.debug(f
"Got {new_message.device}.{new_message.name} but {transition=} did not match")
124 if isinstance(speech, Recording):
126 speech_text = speech.markup
127 substitutables = re.findall(
r"({[^}]+})", speech.markup)
128 for sub
in substitutables:
130 value = self.client[indi_id]
131 if hasattr(value,
'value'):
133 self.log.debug(f
"Replacing {repr(sub)} with {value=}")
134 if value
is not None:
137 value =
"{:.1f}".format(value)
138 except (TypeError, ValueError):
140 speech_text = speech_text.replace(sub, value)
141 return SSML(speech_text)
144 if not isinstance(new_message, messages.IndiNewMessage):
146 active_personality =
None
148 prop[elem] = constants.SwitchState.OFF
149 if elem
in new_message
and new_message[elem]
is constants.SwitchState.ON:
150 active_personality = elem
152 if active_personality
is not None:
153 self.log.info(f
"Switching to {active_personality=}")
156 self.update_property(prop)
160 if not isinstance(new_message, messages.IndiNewMessage):
162 for elem
in new_message:
163 if new_message[elem]
is constants.SwitchState.ON:
165 self.log.info(f
"Soundboard requested {srq}")
168 self.update_property(prop)
171 personality_file = os.path.join(HERE,
"personalities", f
"{personality_name}.xml")
172 for cb, device_name, property_name
in self.
_cb_handles_cb_handles:
174 self.client.unregister_callback(cb, device_name=device_name, property_name=property_name)
176 log.exception(f
"Tried to remove {cb=} {device_name=} {property_name=}")
181 self.log.info(f
"Loading personality from {personality_file}")
182 self.
personalitypersonality = Personality.from_path(personality_file)
186 rule=constants.SwitchRule.ONE_OF_MANY,
187 perm=constants.PropertyPerm.READ_WRITE,
189 for btn_name
in self.
personalitypersonality.soundboard:
190 self.
soundboard_sw_propsoundboard_sw_prop.add_element(DefSwitch(name=btn_name, _value=constants.SwitchState.OFF))
195 for reaction
in self.
personalitypersonality.reactions:
196 device_name, property_name, element_name = reaction.indi_id.split(
'.')
197 self.client.get_properties(reaction.indi_id)
198 for t
in reaction.transitions:
199 cb = partial(self.
reaction_handlerreaction_handler, element_name=element_name, transition=t, utterance_choices=reaction.transitions[t])
200 self.client.register_callback(
202 device_name=device_name,
203 property_name=property_name
205 self.
_cb_handles_cb_handles.add((cb, device_name, property_name))
206 self.log.debug(f
"Registered reaction handler on {device_name=} {property_name=} {element_name=} using transition {t}")
207 for idx, utterance
in enumerate(reaction.transitions[t]):
208 self.log.debug(f
"{reaction.indi_id}: {t}: {utterance}")
209 if isinstance(utterance, SSML):
212 self.log.debug(f
"Caching synthesis to {result}")
214 self.log.debug(f
"Cannot pre-cache because there are substitutions to be made")
216 self.telem(
"load_personality", {
'name': personality_name})
225 while self.client.status
is not constants.ConnectionStatus.CONNECTED:
226 self.log.info(
"Waiting for connection...")
228 self.log.info(
"Connected.")
229 self.log.debug(f
"Caching synthesis output to {self.config.cache.path}")
230 self.config.cache.ensure_exists()
233 sv = properties.SwitchVector(
235 rule=constants.SwitchRule.ONE_OF_MANY,
236 perm=constants.PropertyPerm.READ_WRITE,
238 sv.add_element(DefSwitch(name=f
"toggle", _value=constants.SwitchState.ON
if self.
mutemute
else constants.SwitchState.OFF))
241 sv = properties.SwitchVector(
243 rule=constants.SwitchRule.ONE_OF_MANY,
244 perm=constants.PropertyPerm.READ_WRITE,
246 for pers
in self.personalities:
248 sv.add_element(DefSwitch(name=pers, _value=constants.SwitchState.ON
if self.
active_personalityactive_personality == pers
else constants.SwitchState.OFF))
251 speech_text = properties.TextVector(name=
"speech_text", perm=constants.PropertyPerm.READ_WRITE)
252 speech_text.add_element(DefText(
256 speech_text.add_element(DefText(
262 speech_request = properties.SwitchVector(
264 rule=constants.SwitchRule.ANY_OF_MANY,
266 speech_request.add_element(DefSwitch(name=
"request", _value=constants.SwitchState.OFF))
269 reload_request = properties.SwitchVector(
270 name=
"reload_personality",
271 rule=constants.SwitchRule.ANY_OF_MANY,
273 reload_request.add_element(DefSwitch(name=
"request", _value=constants.SwitchState.OFF))
276 self.log.info(
"Set up complete")
282 self.log.debug(f
"Would have said: {repr(speech)}, but muted")
284 self.log.info(f
"Speaking: {repr(speech)}")
286 self.log.debug(
"Speech complete")
288 if time.time() - self.
last_utterance_tslast_utterance_ts > self.config.random_utterance_interval_sec
and len(self.
personalitypersonality.random_utterances):
289 next_utterance = choice(self.
personalitypersonality.random_utterances)
291 next_utterance = choice(self.
personalitypersonality.random_utterances)
295 self.log.debug(f
"Would have said: {repr(next_utterance)}, but muted")
297 self.log.info(f
"Randomly spouting off: {repr(next_utterance)}")
298 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)