1- import pyttsx3
21import threading
2+ import queue
33import time
4+ import logging
5+ import signal
6+ import sys
7+ import argparse
8+ from pathlib import Path
49
5- # Initialize the speech engine
6- engine = pyttsx3 .init ()
7-
8- # Text lines to print and speak
9- lines = [
10+ # NOTE: import pyttsx3 only inside TTS functions to avoid import-time errors in CI/tests
11+ # Configuration defaults
12+ DEFAULT_LINES = [
1013 "Hello, this is a live speaking text printer." ,
1114 "This app prints and speaks text continuously." ,
1215 "You can modify the text list to include your own content." ,
1316 "Python makes it easy to combine speech and printing." ,
1417 "Thanks for using this demo!"
1518]
1619
17- def speak_text ():
18- for line in lines :
19- engine .say (line )
20- engine .runAndWait ()
21- time .sleep (0.5 )
20+ logging .basicConfig (level = logging .INFO , format = "%(asctime)s [%(levelname)s] %(message)s" )
21+
22+ def tts_worker (msg_queue : queue .Queue , stop_event : threading .Event , rate : int = 150 , volume : float = 1.0 ):
23+ """TTS worker that initializes the pyttsx3 engine inside the thread and speaks queued messages.
24+
25+ This avoids sharing the engine across threads and keeps runAndWait blocking only inside this thread.
26+ """
27+ try :
28+ import pyttsx3
29+ except Exception as e :
30+ logging .exception ("pyttsx3 is not available: %s" , e )
31+ return
32+
33+ try :
34+ engine = pyttsx3 .init ()
35+ engine .setProperty ("rate" , rate )
36+ engine .setProperty ("volume" , volume )
37+ except Exception as e :
38+ logging .exception ("Failed to initialize TTS engine: %s" , e )
39+ return
40+
41+ logging .info ("TTS worker started" )
42+ try :
43+ while not stop_event .is_set ():
44+ try :
45+ line = msg_queue .get (timeout = 0.5 )
46+ except queue .Empty :
47+ continue
48+
49+ if line is None :
50+ # sentinel
51+ break
52+
53+ try :
54+ engine .say (line )
55+ engine .runAndWait ()
56+ except Exception :
57+ logging .exception ("Error while speaking line: %r" , line )
58+ finally :
59+ msg_queue .task_done ()
60+ finally :
61+ try :
62+ engine .stop ()
63+ except Exception :
64+ pass
65+ logging .info ("TTS worker exiting" )
66+
67+ 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 ):
68+ """Printer worker that prints lines and optionally enqueues them for TTS.
69+
70+ Args:
71+ lines: iterable of strings to print.
72+ msg_queue: queue to put lines on for TTS.
73+ stop_event: threading.Event to stop the worker.
74+ print_interval: delay between lines.
75+ enqueue_for_tts: whether to put printed lines on the msg_queue.
76+ run_once: if True, run through the lines once and then exit.
77+ """
78+ logging .info ("Printer worker started" )
79+ try :
80+ while not stop_event .is_set ():
81+ for line in lines :
82+ if stop_event .is_set ():
83+ break
84+ print (line )
85+ if enqueue_for_tts and msg_queue is not None :
86+ try :
87+ msg_queue .put (line , timeout = 0.5 )
88+ except queue .Full :
89+ logging .warning ("TTS queue full; skipping line" )
90+ if print_interval :
91+ time .sleep (print_interval )
92+ if run_once :
93+ break
94+ except Exception :
95+ logging .exception ("Printer worker error" )
96+ finally :
97+ logging .info ("Printer worker exiting" )
98+
99+ def synthesize_to_file (lines , output_path : str , rate : int = 150 , volume : float = 1.0 ):
100+ """Synthesize the provided lines to a file using pyttsx3.save_to_file and runAndWait.
101+ """
102+ try :
103+ import pyttsx3
104+ except Exception as e :
105+ logging .exception ("pyttsx3 is not available: %s" , e )
106+ raise
107+
108+ engine = pyttsx3 .init ()
109+ engine .setProperty ("rate" , rate )
110+ engine .setProperty ("volume" , volume )
111+
112+ text = "\n " .join (lines )
113+ output_path = str (output_path )
114+ logging .info ("Saving synthesized audio to %s" , output_path )
115+ engine .save_to_file (text , output_path )
116+ engine .runAndWait ()
117+ logging .info ("Finished saving %s" , output_path )
118+
119+ def run (lines = None , * , continuous = False , rate = 150 , volume = 1.0 , print_interval = 0.5 ):
120+ """Run the printer + TTS workers until interrupted.
121+
122+ Returns after graceful shutdown.
123+ """
124+ if lines is None :
125+ lines = DEFAULT_LINES
126+
127+ stop_event = threading .Event ()
128+ msg_queue = queue .Queue (maxsize = 64 )
129+
130+ def handle_signal (signum , frame ):
131+ logging .info ("Signal %s received, shutting down" , signum )
132+ stop_event .set ()
133+ # Wake TTS worker if waiting
134+ try :
135+ msg_queue .put_nowait (None )
136+ except Exception :
137+ pass
138+
139+ signal .signal (signal .SIGINT , handle_signal )
140+ signal .signal (signal .SIGTERM , handle_signal )
141+
142+ tts_thread = threading .Thread (target = tts_worker , args = (msg_queue , stop_event , rate , volume ), daemon = True )
143+ printer_thread = threading .Thread (
144+ target = print_worker , args = (lines , msg_queue , stop_event , print_interval , True , not continuous ), daemon = True
145+ )
146+
147+ tts_thread .start ()
148+ printer_thread .start ()
149+
150+ try :
151+ while (tts_thread .is_alive () or printer_thread .is_alive ()) and not stop_event .is_set ():
152+ time .sleep (0.2 )
153+ except KeyboardInterrupt :
154+ logging .info ("KeyboardInterrupt, initiating shutdown" )
155+ stop_event .set ()
156+ try :
157+ msg_queue .put_nowait (None )
158+ except Exception :
159+ pass
160+
161+ # Wait a short while for threads to finish
162+ tts_thread .join (timeout = 2.0 )
163+ printer_thread .join (timeout = 2.0 )
164+ logging .info ("Shutdown complete" )
165+
166+ def _read_lines_from_file (path : str ):
167+ p = Path (path )
168+ if not p .exists ():
169+ raise FileNotFoundError (path )
170+ return [l .rstrip ("\n \r " ) for l in p .read_text (encoding = "utf-8" ).splitlines () if l .strip ()]
171+
172+ def parse_args (argv = None ):
173+ parser = argparse .ArgumentParser (description = "Voice stream demo: print text and speak it locally using pyttsx3." )
174+ parser .add_argument ("--lines-file" , help = "Path to a text file with one line per utterance (overrides built-in lines)" )
175+ parser .add_argument ("--continuous" , action = "store_true" , help = "Loop continuously over the provided lines" )
176+ parser .add_argument ("--rate" , type = int , default = 150 , help = "Speech rate for TTS (words per minute)" )
177+ parser .add_argument ("--volume" , type = float , default = 1.0 , help = "TTS volume (0.0..1.0)" )
178+ parser .add_argument ("--print-interval" , type = float , default = 0.5 , help = "Seconds between printed lines" )
179+ parser .add_argument ("--save" , metavar = "OUTPUT" , help = "Synthesize lines to a file (e.g., output.mp3 or output.wav) and exit" )
180+ parser .add_argument ("--run-once" , dest = "run_once" , action = "store_true" , help = "Print and speak the lines once and exit (overrides --continuous)" )
181+ return parser .parse_args (argv )
182+
183+ def main (argv = None ):
184+ args = parse_args (argv )
185+
186+ if args .lines_file :
187+ lines = _read_lines_from_file (args .lines_file )
188+ else :
189+ lines = DEFAULT_LINES
22190
23- def print_text ():
24- for line in lines :
25- print (line )
26- time .sleep (0.5 )
191+ if args .save :
192+ synthesize_to_file (lines , args .save , rate = args .rate , volume = args .volume )
193+ return
27194
28- # Create threads
29- speak_thread = threading . Thread ( target = speak_text )
30- print_thread = threading . Thread ( target = print_text )
195+ # run_once flag means not continuous
196+ continuous = bool ( args . continuous ) and not args . run_once
197+ run ( lines , continuous = continuous , rate = args . rate , volume = args . volume , print_interval = args . print_interval )
31198
32- # Start threads
33- speak_thread .start ()
34- print_thread .start ()
35199
36- # Wait for both threads to complete
37- speak_thread .join ()
38- print_thread .join ()
200+ if __name__ == "__main__" :
201+ main ()
0 commit comments