-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspeak.py
More file actions
120 lines (96 loc) · 2.97 KB
/
speak.py
File metadata and controls
120 lines (96 loc) · 2.97 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
"""
TTS speaker: reads text from stdin, synthesizes and plays audio.
Protocol (stdin/stdout):
stdin: text to speak (one line = one utterance)
stdin: STOP — interrupt current utterance
stdout: "DONE\n" after each utterance finishes (or is stopped)
Usage:
echo "Hello world" | python speak.py
python speak.py # interactive, type lines
"""
import argparse
import os
import select
import signal
import subprocess
import sys
import tempfile
import wave
from pathlib import Path
from piper import PiperVoice
running = True
_sigcount = 0
def _handle_signal(*_):
global running, _sigcount
_sigcount += 1
running = False
if _sigcount >= 2:
os._exit(1)
signal.signal(signal.SIGTERM, _handle_signal)
signal.signal(signal.SIGINT, _handle_signal)
_DIR = Path(__file__).resolve().parent
def read_stdin():
"""Blocking read of one non-empty line from stdin. Returns None on EOF."""
while running:
ready, _, _ = select.select([sys.stdin], [], [], 0.5)
if not ready:
continue
line = sys.stdin.readline()
if not line:
return None
text = line.strip()
if text:
return text
return None
def drain_stop():
"""Non-blocking check if STOP is waiting on stdin."""
while True:
ready, _, _ = select.select([sys.stdin], [], [], 0)
if not ready:
return False
line = sys.stdin.readline()
if not line:
return False
if line.strip() == "STOP":
return True
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--model", default=str(_DIR / "models" / "en_GB-cori-high.onnx"))
args = parser.parse_args()
print(f"TTS: loading {Path(args.model).stem}...", file=sys.stderr, flush=True)
voice = PiperVoice.load(args.model)
print("TTS: ready", file=sys.stderr, flush=True)
while running:
text = read_stdin()
if text is None:
break
if text == "STOP":
continue
# Synthesize (blocking)
tmp = tempfile.mktemp(suffix=".wav")
with wave.open(tmp, "wb") as wav_file:
voice.synthesize_wav(text, wav_file)
# Check if STOP arrived during inference
if drain_stop():
Path(tmp).unlink(missing_ok=True)
print("DONE", flush=True)
continue
# Play
proc = subprocess.Popen(["afplay", tmp])
while proc.poll() is None:
if not running:
proc.terminate()
proc.wait()
break
# Check for STOP during playback
ready, _, _ = select.select([sys.stdin], [], [], 0.1)
if ready:
line = sys.stdin.readline()
if line.strip() == "STOP":
proc.terminate()
proc.wait()
break
Path(tmp).unlink(missing_ok=True)
print("DONE", flush=True)
if __name__ == "__main__":
main()