-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtts.py
141 lines (102 loc) · 4.69 KB
/
tts.py
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
import re, os, subprocess, time
from dataclasses import dataclass, asdict, field
#basic text filtering
PAUSE_RE = re.compile(r'[\-]{2,}')
STRIPTAGS_RE = re.compile(r'<.+?>')
GRAMMAR_RE = re.compile(r'[^\w\n ,!?();\-\.]', re.I)
#main paths
CWD = os.getcwd()
TTSDIR = '\\'.join(__file__.split('\\')[:-1])
TTSSAVE = os.path.join(CWD, 'tts_wav')
if not os.path.isdir(TTSSAVE): os.mkdir(TTSSAVE) #create if necessary
@dataclass #base TTS object
class TTS:
voice :str = ''
pitch :int = 0
speed :int = 0
volume :int = 100
@property
def asdict(self) -> dict:
return asdict(self)
def __post_init__(self):
self._sp = None #subprocess
@property
def reading(self) -> bool:
return (self._sp and (self._sp.poll() is None))
def stop(self):
if self.reading: self._sp.kill()
def save(self, data):
self.say(data, True)
def say(self, data:str, towav:bool=False) -> None:
raise NotImplementedError('TTS.say: method must be overwritten in a subclass')
def _prepare(self, data:str, towav:bool=False) -> str:
self.stop()
self._isfile = os.path.isdir('\\'.join(data.split('\\')[:-1]))
if not self._isfile:
data = PAUSE_RE.sub('.', GRAMMAR_RE.sub('', STRIPTAGS_RE.sub('', data)))
towav = ('','-w')[towav]
path = '' if not towav else os.path.join(TTSSAVE, f'tts_{int(time.time())}.wav')
src = ('-t','-f')[self._isfile]
return data, towav, path, src
#ESPEAK
#espeak has no built-in method for pause/resume and making one isn't worth it
ESPEAKPATH = os.path.join(TTSDIR , 'espeak')
ESPEAKEXE = os.path.join(ESPEAKPATH, 'espeak')
ESPEAKVOICE = re.compile(r'!v\\([\w]+)')
@dataclass
class ESpeak(TTS):
gap :int = 0 #pause between words in 10ms units
@property #get available voices as a list by parsing stdout
def voices(self):
cmd = (ESPEAKEXE, '--voices=variant')
return sorted(m.group(1) for m in ESPEAKVOICE.finditer(subprocess.check_output(cmd).decode('utf8')))
def say(self, data:str, towav:bool=False):
data, towav, path, src = self._prepare(data, towav)
if self._isfile: data = f'{src}{data}'
data = data if not towav else towav, path, data
self._sp = subprocess.Popen(
(ESPEAKEXE, '-m', #-m is parse SSML or XML, mostly IGNORE HTML, plain text will still work
f'--path={ESPEAKPATH}', #parent of espeak-data folder
'-v', f'{self.voice}' , #set voice +m1-7 +f1-4
'-s', f'{self.speed}' , #set speed in words-per-minute
'-p', f'{self.pitch}' , #adjust pitch 0 to 99
'-a', f'{self.volume}', #set amplitude 0 to 200
'-g', f'{self.gap}' , #pause between words in units of 10ms
*data ) #set sources
)
#BALCON
#balcon can have more commands fed in after it has been opened
#pause, resume and stop are built in
BALCONPATH = os.path.join(TTSDIR , 'balcon')
BALCONEXE = os.path.join(BALCONPATH, 'balcon')
@dataclass
class Balcon(TTS):
sgap :int = 0 #length of pause after sentence (ms)
pgap :int = 0 #length of pause after paragraph (ms)
@property #get available voices as a list by parsing stdout
def voices(self) -> list[str]:
cmd = (BALCONEXE, '-l')
data = subprocess.check_output(cmd).decode('utf8').split(':')[1].strip()
return sorted(name.strip() for name in data.split('\n'))
#balcon pause/resume cmd
def toggle(self):
if self.reading:
subprocess.Popen((BALCONEXE, '-pr')) #pause/resume
#balcon stop cmd
def stop(self):
if self.reading:
subprocess.Popen((BALCONEXE, '-ka')) #kill active
# `data` can be text or a file path
def say(self, data:str, towav:bool=False):
data, towav, path, src = self._prepare(data, towav)
self._sp = subprocess.Popen(
(BALCONEXE,
src , data , #set source
'-n' , self.voice , #sets the voice
'-s' , f'{self.speed}' , #set speed -10 to 10
'-p' , f'{self.pitch}' , #adjust pitch -10 to 10
'-e' , f'{self.sgap}' , #pause between sentences in ms
'-a' , f'{self.pgap}' , #pause between paragraphs in ms
'-v' , f'{self.volume}', #volume 0 to 100
towav, path)
)