-
Notifications
You must be signed in to change notification settings - Fork 181
/
Copy pathapi.py
275 lines (225 loc) · 9.63 KB
/
api.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
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
# Copyright 2015-2021 Mathieu Bernard
#
# This file is part of phonemizer: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Phonemizer is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with phonemizer. If not, see <http://www.gnu.org/licenses/>.
"""Low-level bindings to the espeak API"""
import atexit
import ctypes
import pathlib
import shutil
import sys
import tempfile
import weakref
from ctypes import CDLL
from pathlib import Path
from typing import Union
from phonemizer.backend.espeak.voice import EspeakVoice
if sys.platform != 'win32':
# cause a crash on Windows
import dlinfo
class EspeakAPI:
"""Exposes the espeak API to the EspeakWrapper
This class exposes only low-level bindings to the API and should not be
used directly.
"""
def __init__(self, library: Union[str, Path]):
# set to None to avoid an AttributeError in _delete if the __init__
# method raises, will be properly initialized below
self._library = None
# Because the library is not designed to be wrapped nor to be used in
# multithreaded/multiprocess contexts (massive use of global variables)
# we need a copy of the original library for each instance of the
# wrapper... (see "man dlopen" on Linux/MacOS: we cannot load two times
# the same library because a reference is then returned by dlopen). The
# tweak is therefore to make a copy of the original library in a
# different (temporary) directory.
try:
# load the original library in order to retrieve its full path?
# Forced as str as it is required on Windows.
espeak: CDLL = ctypes.cdll.LoadLibrary(str(library))
library_path = self._shared_library_path(espeak)
del espeak
except OSError as error:
raise RuntimeError(
f'failed to load espeak library: {str(error)}') from None
# will be automatically destroyed after use
self._tempdir = tempfile.mkdtemp()
# properly exit when the wrapper object is destroyed (see
# https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods).
# But... weakref implementation does not work on windows so we register
# the cleanup with atexit. This means that, on Windows, all the
# temporary directories created by EspeakAPI instances will remain on
# disk until the Python process exit.
if sys.platform == 'win32': # pragma: nocover
atexit.register(self._delete_win32)
else:
weakref.finalize(self, self._delete, self._library, self._tempdir)
espeak_copy = pathlib.Path(self._tempdir) / library_path.name
shutil.copy(library_path, espeak_copy, follow_symlinks=False)
# finally load the library copy and initialize it. 0x02 is
# AUDIO_OUTPUT_SYNCHRONOUS in the espeak API
self._library = ctypes.cdll.LoadLibrary(str(espeak_copy))
try:
if self._library.espeak_Initialize(0x02, 0, None, 0) <= 0:
raise RuntimeError( # pragma: nocover
'failed to initialize espeak shared library')
except AttributeError: # pragma: nocover
raise RuntimeError(
'failed to load espeak library') from None
# the path to the original one (the copy is considered an
# implementation detail and is not exposed)
self._library_path = library_path
def _delete_win32(self): # pragma: nocover
# Windows does not support static methods with ctypes libraries
# (library == None) so we use a proxy method...
self._delete(self._library, self._tempdir)
@staticmethod
def _delete(library, tempdir):
try:
# clean up the espeak library allocated memory
library.espeak_Terminate()
except AttributeError: # library not loaded
pass
# on Windows it is required to unload the library or the .dll file
# cannot be erased from the temporary directory
if sys.platform == 'win32': # pragma: nocover
# pylint: disable=import-outside-toplevel
# pylint: disable=protected-access
# pylint: disable=no-member
import _ctypes
_ctypes.FreeLibrary(library._handle)
# clean up the tempdir containing the copy of the library
shutil.rmtree(tempdir)
@property
def library_path(self):
"""Absolute path to the espeak library being in use"""
return self._library_path
@staticmethod
def _shared_library_path(library) -> Path:
"""Returns the absolute path to `library`
This function is cross-platform and works for Linux, MacOS and Windows.
Raises a RuntimeError if the library path cannot be retrieved
"""
# pylint: disable=protected-access
path = pathlib.Path(library._name).resolve()
if path.is_file():
return path
try:
# Linux or MacOS only, ImportError on Windows
return pathlib.Path(dlinfo.DLInfo(library).path).resolve()
except (Exception, ImportError): # pragma: nocover
raise RuntimeError(
f'failed to retrieve the path to {library} library') from None
def info(self):
"""Bindings to espeak_Info
Returns
-------
version, data_path: encoded strings containing the espeak version
number and data path respectively
"""
f_info = self._library.espeak_Info
f_info.restype = ctypes.c_char_p
data_path = ctypes.c_char_p()
version = f_info(ctypes.byref(data_path))
return version, data_path.value
def list_voices(self, name):
"""Bindings to espeak_ListVoices
Parameters
----------
name (str or None): if specified, a filter on voices to be listed
Returns
-------
voices: a pointer to EspeakVoice.Struct instances
"""
f_list_voices = self._library.espeak_ListVoices
f_list_voices.argtypes = [ctypes.POINTER(EspeakVoice.VoiceStruct)]
f_list_voices.restype = ctypes.POINTER(
ctypes.POINTER(EspeakVoice.VoiceStruct))
return f_list_voices(name)
def set_voice_by_name(self, name) -> int:
"""Bindings to espeak_SetVoiceByName
Parameters
----------
name (str) : the voice name to setup
Returns
-------
0 on success, non-zero integer on failure
"""
f_set_voice_by_name = self._library.espeak_SetVoiceByName
f_set_voice_by_name.argtypes = [ctypes.c_char_p]
return f_set_voice_by_name(name)
def get_current_voice(self):
"""Bindings to espeak_GetCurrentVoice
Returns
-------
a EspeakVoice.Struct instance or None if no voice has been setup
"""
f_get_current_voice = self._library.espeak_GetCurrentVoice
f_get_current_voice.restype = ctypes.POINTER(EspeakVoice.VoiceStruct)
return f_get_current_voice().contents
def text_to_phonemes(self, text_ptr, text_mode, phonemes_mode):
"""Bindings to espeak_TextToPhonemes
Parameters
----------
text_ptr (pointer): the text to be phonemized, as a pointer to a
pointer of chars
text_mode (bits field): see espeak sources for details
phonemes_mode (bits field): see espeak sources for details
Returns
-------
an encoded string containing the computed phonemes
"""
f_text_to_phonemes = self._library.espeak_TextToPhonemes
f_text_to_phonemes.restype = ctypes.c_char_p
f_text_to_phonemes.argtypes = [
ctypes.POINTER(ctypes.c_char_p),
ctypes.c_int,
ctypes.c_int]
return f_text_to_phonemes(text_ptr, text_mode, phonemes_mode)
def set_phoneme_trace(self, mode, file_pointer):
""""Bindings on espeak_SetPhonemeTrace
This method must be called before any call to synthetize()
Parameters
----------
mode (bits field): see espeak sources for details
file_pointer (FILE*): a pointer to an opened file in which to output
the phoneme trace
"""
f_set_phoneme_trace = self._library.espeak_SetPhonemeTrace
f_set_phoneme_trace.argtypes = [
ctypes.c_int,
ctypes.c_void_p]
f_set_phoneme_trace(mode, file_pointer)
def synthetize(self, text_ptr, size, mode):
"""Bindings on espeak_Synth
The output phonemes are sent to the file specified by a call to
set_phoneme_trace().
Parameters
----------
text (pointer) : a pointer to chars
size (int) : number of chars in `text`
mode (bits field) : see espeak sources for details
Returns
-------
0 on success, non-zero integer on failure
"""
f_synthetize = self._library.espeak_Synth
f_synthetize.argtypes = [
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_uint,
ctypes.c_int, # position_type
ctypes.c_uint,
ctypes.POINTER(ctypes.c_uint),
ctypes.c_void_p]
return f_synthetize(text_ptr, size, 0, 1, 0, mode, None, None)