-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.py
More file actions
317 lines (274 loc) · 8.01 KB
/
app.py
File metadata and controls
317 lines (274 loc) · 8.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import threading
import queue
import time
import logging
import signal
import argparse
from pathlib import Path
# NOTE: import pyttsx3 only inside TTS functions to avoid import-time
# errors in CI/tests
# Configuration defaults
DEFAULT_LINES = [
"Hello, this is a live speaking text printer.",
"This app prints and speaks text continuously.",
"You can modify the text list to include your own content.",
"Python makes it easy to combine speech and printing.",
"Thanks for using this demo!",
]
logging.basicConfig(
level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s"
)
def tts_worker(
msg_queue: queue.Queue,
stop_event: threading.Event,
rate: int = 150,
volume: float = 1.0,
):
"""TTS worker that initializes the pyttsx3 engine inside the
thread and speaks queued messages.
This avoids sharing the engine across threads and keeps
runAndWait blocking only inside this thread.
"""
try:
import pyttsx3
except Exception as exc: # pragma: no cover - optional dep
logging.exception("pyttsx3 is not available: %s", exc)
return
try:
engine = pyttsx3.init()
engine.setProperty("rate", rate)
engine.setProperty("volume", volume)
except Exception as exc:
logging.exception("Failed to initialize TTS engine: %s", exc)
return
logging.info("TTS worker started")
try:
while not stop_event.is_set():
try:
line = msg_queue.get(timeout=0.5)
except queue.Empty:
continue
if line is None:
# sentinel
break
try:
engine.say(line)
engine.runAndWait()
except Exception:
logging.exception("Error while speaking line: %r", line)
finally:
msg_queue.task_done()
finally:
try:
engine.stop()
except Exception:
pass
logging.info("TTS worker exiting")
def print_worker(
lines,
msg_queue: queue.Queue,
stop_event: threading.Event,
print_interval: float = 0.5,
enqueue_for_tts: bool = True,
run_once: bool = False,
):
"""Printer worker that prints lines and optionally enqueues them
for TTS.
Args:
lines: iterable of strings to print.
msg_queue: queue to put lines on for TTS.
stop_event: threading.Event to stop the worker.
print_interval: delay between lines.
enqueue_for_tts: whether to put printed lines on the msg_queue.
run_once: if True, run through the lines once and then exit.
"""
logging.info("Printer worker started")
try:
while not stop_event.is_set():
for line in lines:
if stop_event.is_set():
break
print(line)
if enqueue_for_tts and msg_queue is not None:
try:
msg_queue.put(line, timeout=0.5)
except queue.Full:
logging.warning("TTS queue full; skipping line")
if print_interval:
time.sleep(print_interval)
if run_once:
break
except Exception:
logging.exception("Printer worker error")
finally:
logging.info("Printer worker exiting")
def synthesize_to_file(
lines,
output_path: str,
rate: int = 150,
volume: float = 1.0,
):
"""Synthesize the provided lines to a file using
pyttsx3.save_to_file and runAndWait.
"""
try:
import pyttsx3
except Exception as exc: # pragma: no cover - optional dep
logging.exception("pyttsx3 is not available: %s", exc)
raise
engine = pyttsx3.init()
engine.setProperty("rate", rate)
engine.setProperty("volume", volume)
text = "\n".join(lines)
output_path = str(output_path)
logging.info("Saving synthesized audio to %s", output_path)
engine.save_to_file(text, output_path)
engine.runAndWait()
logging.info("Finished saving %s", output_path)
def run(lines=None,
*,
continuous=False,
rate=150,
volume=1.0,
print_interval=0.5):
"""Run the printer + TTS workers until interrupted.
Returns after graceful shutdown.
"""
if lines is None:
lines = DEFAULT_LINES
stop_event = threading.Event()
msg_queue = queue.Queue(maxsize=64)
def handle_signal(signum, frame):
logging.info("Signal %s received, shutting down", signum)
stop_event.set()
# Wake TTS worker if waiting
try:
msg_queue.put_nowait(None)
except Exception:
pass
signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGTERM, handle_signal)
tts_thread = threading.Thread(
target=tts_worker,
args=(msg_queue,
stop_event,
rate,
volume),
daemon=True
)
printer_thread = threading.Thread(
target=print_worker,
args=(
lines,
msg_queue,
stop_event,
print_interval,
True,
not continuous,
),
daemon=True,
)
tts_thread.start()
printer_thread.start()
try:
while (
(tts_thread.is_alive() or printer_thread.is_alive())
and not stop_event.is_set()
):
time.sleep(0.2)
except KeyboardInterrupt:
logging.info("KeyboardInterrupt, initiating shutdown")
stop_event.set()
try:
msg_queue.put_nowait(None)
except Exception:
pass
# Wait a short while for threads to finish
tts_thread.join(timeout=2.0)
printer_thread.join(timeout=2.0)
logging.info("Shutdown complete")
def _read_lines_from_file(path: str):
p = Path(path)
if not p.exists():
raise FileNotFoundError(path)
return [
line.rstrip("\n\r")
for line in p.read_text(encoding="utf-8").splitlines()
if line.strip()
]
def parse_args(argv=None):
parser = argparse.ArgumentParser(
description=(
"Voice stream demo: print text and speak it locally using pyttsx3."
)
)
parser.add_argument(
"--lines-file",
help=(
"Path to a text file with one line per utterance "
"(overrides built-in lines)"
),
)
parser.add_argument(
"--continuous",
action="store_true",
help="Loop continuously over the provided lines",
)
parser.add_argument(
"--rate",
type=int,
default=150,
help="Speech rate for TTS (words per minute)",
)
parser.add_argument(
"--volume",
type=float,
default=1.0,
help="TTS volume (0.0..1.0)"
)
parser.add_argument(
"--print-interval",
type=float,
default=0.5,
help="Seconds between printed lines",
)
parser.add_argument(
"--save",
metavar="OUTPUT",
help=(
"Synthesize lines to a file (e.g., output.mp3 or output.wav) "
"and exit"
),
)
parser.add_argument(
"--run-once",
dest="run_once",
action="store_true",
help=(
"Print and speak the lines once and exit (overrides --continuous)"
),
)
return parser.parse_args(argv)
def main(argv=None):
args = parse_args(argv)
if args.lines_file:
lines = _read_lines_from_file(args.lines_file)
else:
lines = DEFAULT_LINES
if args.save:
synthesize_to_file(lines,
args.save,
rate=args.rate,
volume=args.volume)
return
# run_once flag means not continuous
continuous = bool(args.continuous) and not args.run_once
run(
lines,
continuous=continuous,
rate=args.rate,
volume=args.volume,
print_interval=args.print_interval,
)
if __name__ == "__main__":
main()