pySpeechModule 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ from .speechd_server import SpeechServer
2
+ from .speechd import SpeechDispatch
3
+ from .speechd_action_worker import ActionWorker
4
+ from .speechd_execution_worker import ExecutionWorker
5
+ from .speechd_utilities import hdlc_escape, parse_ssml, strip_ssml
6
+
7
+ __all__ = [
8
+ "SpeechServer",
9
+ "SpeechDispatch",
10
+ "ActionWorker",
11
+ "ExecutionWorker",
12
+ "hdlc_escape",
13
+ "parse_ssml",
14
+ "strip_ssml",
15
+ ]
@@ -0,0 +1,156 @@
1
+ import logging
2
+ import time
3
+ import soundfile as sf
4
+
5
+ class SpeechDispatch():
6
+ def __init__(self):
7
+ """
8
+ you should place the voices list here in __init__
9
+
10
+ .. code-block:: python
11
+
12
+ self.voices = [{"name": "Testing", "language": "en", "variant": "none"},
13
+ {"name": "Testing2", "language": "en", "variant": "none"}]
14
+
15
+ You should also set your max_ahead and min_ahead here.
16
+
17
+ .. code-block:: python
18
+
19
+ self.max_ahead = 60 # this sets the max number of second your generation will get ahead of played audio.
20
+ self.min_ahead = 30 # this sets how small the ahead number maybe before starting generation again.
21
+
22
+ You can also place model init or what ever else you would like to init here.
23
+ """
24
+ pass
25
+ def _set_init(self,queue, server):
26
+ """
27
+ Set the action_queue and server.
28
+
29
+ :meta private:
30
+ """
31
+
32
+ self.action_queue = queue
33
+ self._server = server
34
+ def configure(self, configure_file: str):
35
+ """
36
+ This method get passed in the file path to the modules dot conf file
37
+ If you need to use it you should open the file and parse here.
38
+ """
39
+ pass
40
+ def change_voice(self, voice: str):
41
+ """
42
+ This method gets passed the name of a voice when the user has requested to change the voice.
43
+
44
+ :param voice: String containing the voice name.
45
+ :type String: String
46
+ """
47
+ pass
48
+ def change_language(self, language: str):
49
+ """
50
+ This method gets passed a language when the user has requested to change the language.
51
+
52
+ :param voice: String containing the language.
53
+ :type String: String
54
+ """
55
+ pass
56
+ def change_speed(self, speed: str):
57
+ """
58
+ This method gets passed a speed when the user requests to change the speed.
59
+ The speed will be between -100 and 100
60
+
61
+ :param voice: String containing the new speed.
62
+ :type String: String
63
+ """
64
+ pass
65
+ def change_pitch(self, pitch: str):
66
+ """
67
+ This method gets passed a pitch when the user requests to change the pitch
68
+
69
+ :param voice: String containing the new pitch.
70
+ :type String: String
71
+ """
72
+ pass
73
+ def _parse_settings(self,options: dict):
74
+ """
75
+ When passed the settings it calls the convenience methods associated with that setting.
76
+
77
+ :meta private:
78
+ """
79
+ if 'synthesis_voice' in options:
80
+ self.change_voice(options['synthesis_voice'])
81
+ if 'language' in options:
82
+ self.change_language(options['language'])
83
+ if 'rate' in options:
84
+ self.change_speed(options['rate'])
85
+ if 'pitch' in options:
86
+ self.change_pitch(options['pitch'])
87
+ def _settings(self,options):
88
+ """
89
+ Passed the setting, sends it to the parsers.
90
+
91
+ :meta private:
92
+ """
93
+ self._parse_settings(options)
94
+ self.settings(options)
95
+
96
+ def settings(self,options: dict):
97
+ """
98
+ This gets passed the full settings options
99
+
100
+ :param options: Dict containing all the settings options that were sent.
101
+ :type Dictionary: Dictionary.
102
+ """
103
+ pass
104
+ def _speak(self,text: str):
105
+ """
106
+ This is called to generate speech
107
+ It then iterate's through speech chunks returned by the user.
108
+
109
+ :meta private:
110
+ """
111
+ self._server.stop_set(False)
112
+ self._ahead = 0
113
+ self.action_queue.put({"command": "begin"})
114
+ it = self.speak(text)
115
+ while (True):
116
+ try:
117
+ if (self._server.stop_get()):
118
+ break
119
+ start_time = time.time()
120
+ item = next(it) # get the next audio chunk
121
+ end_time = time.time()
122
+ total_time = end_time-start_time # total time it took to generate that audio chunk
123
+ produced_time = (len(item['audio']) / item['rate']) # amount of audio generate
124
+ logging.debug(f"Speak chunk took {total_time} to produce {produced_time} for mark {item['mark']}")
125
+ self._ahead = self._ahead + produced_time
126
+ self._ahead = self._ahead - total_time
127
+ logging.debug(f"Ahead: {self._ahead}")
128
+ self.action_queue.put({"command": "speak", "val": item})
129
+ # sleep if we have too much audio generated, only do this is max_ahead and min_ahead are set.
130
+ if (hasattr(self, "max_ahead") and hasattr(self, "min_ahead")):
131
+ if (self._ahead >= self.max_ahead):
132
+ logging.debug(f"Ahead by too much sleeping")
133
+ while (self._ahead >= self.min_ahead):
134
+ time.sleep(1)
135
+ self._ahead = self._ahead - 1
136
+ if (self._server.stop_get()):
137
+ break;
138
+ except StopIteration:
139
+ logging.debug("finished speak chunking")
140
+ break
141
+ self.action_queue.put({"command": "end"})
142
+ def speak(self,text: str):
143
+ """
144
+ This method get called to get the user to generate audio.
145
+ Data must be yield'ed in the format
146
+
147
+ .. code-block:: python
148
+
149
+ yield {"audio": <data>, "rate": <samplerate>, "mark": <mark>}
150
+
151
+ :param text: String containing ssml text.
152
+ :type String: String
153
+ :yields: Dict containing keys for "audio", "rate", "mark"
154
+ :ytype: Dict
155
+ """
156
+ pass
@@ -0,0 +1,41 @@
1
+ import logging
2
+ import threading
3
+ import queue
4
+
5
+ class ActionWorker(threading.Thread):
6
+ """
7
+ Worker for running speak commands on the server
8
+
9
+ :meta private:
10
+ """
11
+ def __init__(self, server):
12
+ super().__init__()
13
+ self.queue = queue.Queue()
14
+ self.server = server
15
+ self.daemon = True
16
+
17
+ def begin(self):
18
+ self.server._begin()
19
+
20
+ def end(self):
21
+ self.server._end()
22
+
23
+ def speak(self,val):
24
+ self.server._send_audio(val)
25
+
26
+ def stop(self):
27
+ """Sends the sentinel value to trigger a graceful shutdown."""
28
+ self.queue.put(None)
29
+
30
+ def run(self):
31
+ while True:
32
+ item = self.queue.get()
33
+ logging.debug(f"action worker: got item {item}")
34
+ if item is None:
35
+ break
36
+ if (item['command'] == 'begin'):
37
+ self.begin()
38
+ if (item['command'] == 'end'):
39
+ self.end()
40
+ if (item['command'] == 'speak'):
41
+ self.speak(item['val'])
@@ -0,0 +1,34 @@
1
+ import logging
2
+ import threading
3
+ import queue
4
+
5
+ class ExecutionWorker(threading.Thread):
6
+ """
7
+ Worker for running the execution callbacks.
8
+
9
+ :meta private:
10
+ """
11
+ def __init__(self):
12
+ super().__init__()
13
+ self.queue = queue.Queue()
14
+ self.daemon = True
15
+
16
+ def set_callback(self,callback):
17
+ self._callback = callback
18
+
19
+ def stop(self):
20
+ """Sends the sentinel value to trigger a graceful shutdown."""
21
+ self.queue.put(None)
22
+
23
+ def run(self):
24
+ while True:
25
+ item = self.queue.get()
26
+ logging.debug(f"execution worker: got item {item}")
27
+ if item is None:
28
+ break
29
+ elif (item['command'] == 'speak'):
30
+ self._callback._speak(item['args'])
31
+ elif (item['command'] == 'settings'):
32
+ self._callback._settings(item['args'])
33
+ elif (item['command'] == 'configure'):
34
+ self._callback.configure(item['args'])
@@ -0,0 +1,396 @@
1
+ import sys
2
+ import logging
3
+ import threading
4
+ import select
5
+ import soundfile as sf
6
+ import queue
7
+ import time
8
+
9
+ from .speechd_action_worker import ActionWorker
10
+ from .speechd_execution_worker import ExecutionWorker
11
+ from .speechd_utilities import hdlc_escape
12
+
13
+ stdout = sys.stdout
14
+ stdin = sys.stdin
15
+ sys.stdout = sys.stderr
16
+
17
+ logging.basicConfig(
18
+ stream=sys.stderr,
19
+ level=logging.DEBUG,
20
+ format="[%(asctime)s] %(levelname)s: %(message)s",
21
+ datefmt="%Y-%m-%d %H:%M:%S"
22
+ )
23
+
24
+ class SpeechServer():
25
+ """
26
+ This is the server class, it handles running the protocal.
27
+
28
+ .. note:: we set ``sys.stdout = sys.stderr``, this is to ensure that users don't accedentally break the protocal. Anything printed to stdout may break the server.
29
+ """
30
+ def __init__(self):
31
+ """
32
+ :meta private:
33
+ """
34
+
35
+ self.line_count = 0
36
+
37
+ self.execution_worker = ExecutionWorker()
38
+ self.action_worker = ActionWorker(server=self)
39
+
40
+ self.voices = []
41
+ self.server_lock = threading.RLock()
42
+ self.stop_lock = threading.RLock()
43
+ self.stop = False # make sure you use stop_lock when using this.
44
+
45
+ def _readline(self, block=True):
46
+ """
47
+ This is basically pythons readline but with the added ability to do it none blocking.
48
+
49
+ :meta private:
50
+ """
51
+
52
+ try:
53
+ if (not block):
54
+ timeout = 0.0
55
+ ready, _, _ = select.select([stdin], [], [], timeout)
56
+ if ready:
57
+ output = stdin.readline()
58
+ logging.debug(f"line {self.line_count}: {output[:-1]}")
59
+ self.line_count = self.line_count + 1
60
+ return output
61
+ return None
62
+ else:
63
+ output = stdin.readline()
64
+ logging.debug(f"line {self.line_count}: {output[:-1]}")
65
+ self.line_count = self.line_count + 1
66
+ return output
67
+ except Exception as e:
68
+ logging.error(f"_readline: {e}")
69
+
70
+
71
+ def _begin(self):
72
+ """
73
+ Send command telling speechd we are starting to send audio data.
74
+
75
+ :meta private:
76
+ """
77
+
78
+ with self.server_lock:
79
+ stdout.write("701 BEGIN\n")
80
+ stdout.flush()
81
+ def _end(self):
82
+ """
83
+ Tells speechd we are done sending audio data.
84
+
85
+ :meta private:
86
+ """
87
+
88
+ with self.server_lock:
89
+ stdout.write("702 END\n")
90
+ stdout.flush()
91
+ def _mark(self, mark):
92
+ """
93
+ Tells speechd which mark we are on.
94
+
95
+ :meta private:
96
+ """
97
+
98
+ if (mark==""):
99
+ return
100
+
101
+ with self.server_lock:
102
+ stdout.write(f"700-{mark}\n700 INDEX MARK\n")
103
+ stdout.flush()
104
+
105
+ def _send_audio(self, val):
106
+ """
107
+ Send audio data to speechd.
108
+
109
+ :meta private:
110
+ """
111
+
112
+ with self.server_lock:
113
+ logging.debug("server: _send_audio")
114
+ data = val['audio']
115
+ sample_rate = val['rate']
116
+ mark = val['mark']
117
+
118
+ chunk_size = 5000
119
+ for i in range(0, len(data), chunk_size):
120
+ start_time = time.time()
121
+ tmp_data = data[i : i + chunk_size]
122
+ num_samples = len(tmp_data)
123
+ tmp_data = tmp_data.tobytes()
124
+ stdout.write(f"705-bits=16\n")
125
+ stdout.flush()
126
+ stdout.write(f"705-num_channels=1\n")
127
+ stdout.flush()
128
+ stdout.write(f"705-sample_rate={sample_rate}\n")
129
+ stdout.flush()
130
+ stdout.write(f"705-num_samples={num_samples}\n")
131
+ stdout.flush()
132
+ stdout.write(f"705-big_endian=0\n")
133
+ stdout.flush()
134
+ stdout.write(f"705-AUDIO")
135
+ stdout.flush()
136
+ stdout.buffer.write(b'\x00')
137
+
138
+ tmp_data2 = hdlc_escape(tmp_data)
139
+ stdout.buffer.write(tmp_data2)
140
+ stdout.write("\n")
141
+ stdout.write("705 AUDIO\n")
142
+ stdout.flush()
143
+ end_time = time.time()
144
+ total_time = end_time-start_time
145
+ self._process(block=False)
146
+ self._mark(mark)
147
+
148
+ def _configure(self):
149
+ """
150
+ Get the config file path and send it to the client.
151
+
152
+ :meta private:
153
+ """
154
+
155
+ if len(sys.argv) > 1:
156
+ configfile = sys.argv[1]
157
+ self.execution_worker.queue.put({"command": "configure", "args": configfile})
158
+
159
+ def parse_set_params(self,ack: int, param_type: str):
160
+ """
161
+ Get a settings string.
162
+
163
+ :meta private:
164
+ """
165
+
166
+ with self.server_lock:
167
+ stdout.write(f"{ack} OK RECEIVING {param_type} SETTINGS\n")
168
+ stdout.flush()
169
+
170
+ output = {}
171
+
172
+ while (True):
173
+ line = self._readline()
174
+ if (line == ".\n"):
175
+ break
176
+ line_clean = line.rstrip("\n")
177
+ var, val = line_clean.split("=", 1)
178
+ output[var] = val
179
+
180
+ return output
181
+
182
+ def _audio(self):
183
+ """
184
+ :meta private:
185
+ """
186
+
187
+ tmp = self.parse_set_params(207, "AUDIO")
188
+ with self.server_lock:
189
+ stdout.write("203 OK AUDIO INITIALIZED\n")
190
+ stdout.flush()
191
+
192
+ def _loglevel(self):
193
+ """
194
+ Sets the logging level from a settings string.
195
+
196
+ The logging levels don't quit match up with speechd but we try and get them as close as possible.
197
+ see this link for the speechd logging levels.
198
+ https://htmlpreview.github.io/?https://github.com/brailcom/speechd/blob/master/doc/speech-dispatcher.html#Log-Levels
199
+
200
+ :meta private:
201
+ """
202
+
203
+ levels = {'5': logging.DEBUG, '4': logging.INFO, '3': logging.WARNING, '2': logging.ERROR, '1': logging.CRITICAL}
204
+
205
+ tmp = self.parse_set_params(207, "LOGLEVEL")
206
+ logging.critical(f"Changing log level to {tmp['log_level']}")
207
+ if (tmp['log_level'] == '0'):
208
+ logging.disable(logging.CRITICAL) # Disable all logging globally.
209
+ else:
210
+ logging.disable(logging.NOTSET) # Enable all logging globally.
211
+ logging.getLogger().setLevel(levels[tmp['log_level']]) # set the logging level.
212
+
213
+ with self.server_lock:
214
+ stdout.write("203 OK LOGLEVEL SET\n")
215
+ stdout.flush()
216
+
217
+ def _list_voices(self):
218
+ """
219
+ List the voices and send them back to speechd.
220
+
221
+ :meta private:
222
+ """
223
+ voices = self.voices
224
+ with self.server_lock:
225
+ for voice in voices:
226
+ name = voice['name'] if 'name' in voice else "none"
227
+ language = voice['language'] if 'language' in voice else "none"
228
+ variant = voice['variant'] if 'variant' in voice else "none"
229
+ stdout.write(f"200-{name}\t{language}\t{variant}\n")
230
+
231
+ stdout.write("200 OK VOICE LIST SENT\n")
232
+ stdout.flush()
233
+
234
+ def _setting(self):
235
+ """
236
+ Receive setting from speechd.
237
+
238
+ :meta private:
239
+ """
240
+ tmp = self.parse_set_params(203, "")
241
+ self.execution_worker.queue.put({"command": "settings", "args": tmp})
242
+ with self.server_lock:
243
+ stdout.write("203 OK SETTINGS RECEIVED\n")
244
+ stdout.flush()
245
+
246
+ # def _speak_test(self):
247
+ # data, samplerate = sf.read("deep_learning.wav", dtype='int16')
248
+ # val = {"audio": data, "rate": samplerate, "mark": ""}
249
+ # self._begin()
250
+ # self._send_audio(val)
251
+ # self._end()
252
+
253
+ def _speak(self):
254
+ """
255
+ Get a speak command from speechd.
256
+
257
+ :meta private:
258
+ """
259
+ with self.server_lock:
260
+ stdout.write("202 OK RECEIVING MESSAGE\n")
261
+ stdout.flush()
262
+
263
+ full_text = ""
264
+
265
+ while (True):
266
+ line = self._readline()
267
+ if (line == ".\n"):
268
+ break
269
+ else:
270
+ full_text = full_text + line[:-1] + " "
271
+
272
+ self.execution_worker.queue.put({"command": "speak", "args": full_text})
273
+
274
+ with self.server_lock:
275
+ stdout.write("200 OK SPEAKING\n")
276
+ stdout.flush()
277
+
278
+ def stop_get(self):
279
+ """
280
+ Get the value of the stop var.
281
+
282
+ :meta private:
283
+ """
284
+ with self.stop_lock:
285
+ return self.stop
286
+ def stop_set(self, val):
287
+ """
288
+ Set the value of the stop var.
289
+
290
+ :meta private:
291
+ """
292
+ with self.stop_lock:
293
+ self.stop = val
294
+ def _stop(self):
295
+ """
296
+ Stop the client.
297
+
298
+ :meta private:
299
+ """
300
+ self.stop_set(True)
301
+ with self.server_lock:
302
+ stdout.write("703 STOP\n")
303
+ stdout.flush()
304
+ def _pause(self):
305
+ """
306
+ Stop the client.
307
+
308
+ :meta private:
309
+ """
310
+
311
+ self.stop_set(True)
312
+ with self.server_lock:
313
+ stdout.write("704 PAUSE\n")
314
+ stdout.flush()
315
+
316
+ def _process(self, block=True):
317
+ """
318
+ Process commands from speechd.
319
+
320
+ :meta private:
321
+ """
322
+ while (True):
323
+ line = self._readline(block)
324
+ if (line == None):
325
+ break
326
+ if (line == ""):
327
+ break
328
+ if (line == "SPEAK\n"):
329
+ self._speak()
330
+ elif (line == "SOUND_ICON\n"):
331
+ self._speak()
332
+ elif (line == "CHAR\n"):
333
+ self._speak()
334
+ elif (line == "KEY\n"):
335
+ self._speak()
336
+ elif (line[:11] == "LIST VOICES"):
337
+ self._list_voices()
338
+ elif (line == "SET\n"):
339
+ self._setting()
340
+ elif (line == "AUDIO\n"):
341
+ self._audio()
342
+ elif (line == "LOGLEVEL\n"):
343
+ self._loglevel()
344
+ elif (line == "STOP\n"):
345
+ self._stop()
346
+ elif (line == "PAUSE\n"):
347
+ self._pause()
348
+ elif (line[:5] == "DEBUG"):
349
+ # TODO: not sure the diffrence between log level and debug
350
+ pass
351
+ elif (line == "QUIT\n"):
352
+ break
353
+ else:
354
+ logging.error("_process: unknown command")
355
+ logging.error(line)
356
+
357
+ def start(self, callback):
358
+ """
359
+ Starts the mainloop and sets the client callback.
360
+ """
361
+
362
+ self._callback = callback
363
+ # keep a local copy of the voices
364
+ # it should be set at the start and never changed.
365
+ if hasattr(callback, "voices"):
366
+ self.voices = callback.voices
367
+ else:
368
+ self.voices = [{"name": "GenericPythonModule", "language": "en", "variant": "none"}]
369
+
370
+ # Start the worker threads.
371
+ self.execution_worker.set_callback(callback)
372
+ self.execution_worker.start()
373
+ self.action_worker.start()
374
+ self._callback._set_init(self.action_worker.queue, self)
375
+
376
+ # run configure.
377
+ self._configure()
378
+
379
+ line = self._readline()
380
+ if (line != "INIT\n"):
381
+ logging.error("ERROR: Server did not start with INIT\n")
382
+
383
+ msg = "GOOD"
384
+ stdout.write(f"299-{msg}\n")
385
+ stdout.write("299 OK LOADED SUCCESSFULLY\n")
386
+ stdout.flush()
387
+
388
+ logging.info("server: Starting _process()")
389
+ self._process()
390
+
391
+ # let the workers stop before quitting
392
+ self.execution_worker.stop()
393
+ self.action_worker.stop()
394
+ self.execution_worker.join()
395
+ self.action_worker.join()
396
+ logging.debug("server: Quiting")
@@ -0,0 +1,93 @@
1
+ import xml.etree.ElementTree as ET
2
+ import logging
3
+
4
+ def hdlc_escape(data: bytes) -> bytes:
5
+ """
6
+ hdlc_escape is for excaping newline chars using hdlc encoding.
7
+ This function likely has little value to the users but is need internally.
8
+
9
+ :meta private:
10
+ """
11
+ ESCAPE = 0x7D
12
+ INVERT = 1 << 5
13
+ out = bytearray()
14
+
15
+ for byte in data:
16
+ if byte == ESCAPE or byte == ord("\n"):
17
+ out.append(ESCAPE)
18
+ out.append(byte ^ INVERT)
19
+ else:
20
+ out.append(byte)
21
+
22
+ return bytes(out)
23
+
24
+ def parse_ssml(data: bytes) -> list[dict]:
25
+ """
26
+ This is a convenience function for parsing SSML into a list of dicts in the format
27
+
28
+ .. code-block:: python
29
+
30
+ [{"mark": "<mark>", "text": "<text>"}]
31
+
32
+ :param data: The bytes string for your SSML text
33
+ :type bytes: bytes
34
+ :return: List of dicts each with mark and text
35
+ :rtype: list
36
+ """
37
+
38
+ output = []
39
+ buffer = []
40
+
41
+ # Parse the SSML XML data
42
+ try:
43
+ root = ET.fromstring(data)
44
+ except ET.ParseError:
45
+ logging.error("parse_ssml: Failed to parse XML")
46
+ return output
47
+
48
+ # Handle text directly inside <speak> before any child elements
49
+ if root.text:
50
+ buffer.append(root.text)
51
+
52
+ # Iterate through direct child elements of <speak>
53
+ for child in root:
54
+ # Check for <mark> element
55
+ if child.tag.endswith("mark"):
56
+ mark_name = child.attrib.get("name", "")
57
+
58
+ output.append(
59
+ {"mark": mark_name, "text": "".join(buffer)}
60
+ )
61
+
62
+ # Reset buffer after encountering a mark
63
+ buffer.clear()
64
+
65
+ # Append trailing text after a child node (XML tail text)
66
+ if child.tail:
67
+ buffer.append(child.tail)
68
+
69
+ # Add any remaining trailing text after the final mark
70
+ if buffer:
71
+ output.append({"mark": "", "text": "".join(buffer)})
72
+
73
+ return output
74
+
75
+ def strip_ssml(ssml_string: bytes):
76
+ """
77
+ This is a convenience function for stripping out all SSML tags leaving just the text.
78
+
79
+ :param data: The bytes string for your SSML text
80
+ :type bytes: bytes
81
+ :return: extracted text
82
+ :rtype: string
83
+ """
84
+
85
+ # Wrap in a root element if the string is an XML fragment
86
+ try:
87
+ root = ET.fromstring(ssml_string)
88
+ except ET.ParseError:
89
+ # Wrap fragments in a temporary root node to ensure valid parsing
90
+ root = ET.fromstring(f"<root>{ssml_string}</root>")
91
+
92
+ # itertext() yields all text within the element and its children
93
+ return "".join(root.itertext())
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: pySpeechModule
3
+ Version: 1.0.0
4
+ Summary: A framework for creating speechd modules
5
+ Author-email: John Settlemyer <author@example.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jsett/py-speech-module
8
+ Project-URL: Issues, https://github.com/jsett/py-speech-module/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: soundfile>=0.14.0
14
+ Dynamic: license-file
15
+
16
+ # pySpeechModule
17
+ A framework for writing python speechd modules.
18
+
19
+ documentation: https://pyspeechmodule.readthedocs.io/en/latest/index.html
@@ -0,0 +1,11 @@
1
+ pySpeechModule/__init__.py,sha256=HnUIZcruZdaU0_on1k4IaH-1Xi073xmIWqh3CCRQ-Xc,404
2
+ pySpeechModule/speechd.py,sha256=ZpGiRSaD9GjAByvhO266tPCj7dafgBItM07GTkIJoTE,5605
3
+ pySpeechModule/speechd_action_worker.py,sha256=BlDqY3unaKrJLvJ7sOMvIfxoYA1myBLsNpsAGfBjRFI,1028
4
+ pySpeechModule/speechd_execution_worker.py,sha256=NdivW52SChodixhEzHxSFrw8Hv2RxYeofOk7Nyrx-Hs,981
5
+ pySpeechModule/speechd_server.py,sha256=HXIibHkFvsWccg2JuGBdQ-_-qb3X7ppShlNXSFVehIY,11646
6
+ pySpeechModule/speechd_utilities.py,sha256=4EAAOxo_Q0Kkl3HhT1UO5BwMK0JtFFCk_RtICQ_OUU8,2567
7
+ pyspeechmodule-1.0.0.dist-info/licenses/LICENSE,sha256=OXwwpMpzCNpvI_zaXWLu9pjohqxzllI8ThD1U1UPGWQ,1072
8
+ pyspeechmodule-1.0.0.dist-info/METADATA,sha256=Y44RiKk_FT8DdSvz2uAd_J4lSzLTLjHL5B3yE5NwZJg,648
9
+ pyspeechmodule-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ pyspeechmodule-1.0.0.dist-info/top_level.txt,sha256=Z3uAt95enI7oQ1Fkf-jkTOuWHl-ad-5ls7gyZVYtu1k,15
11
+ pyspeechmodule-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Settlemyer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pySpeechModule