prismatoid 0.4.5__cp313-cp313-manylinux_2_34_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prism/__init__.py +3 -0
- prism/_native/libprism.so +0 -0
- prism/core.py +374 -0
- prism/lib.py +230 -0
- prismatoid-0.4.5.dist-info/METADATA +53 -0
- prismatoid-0.4.5.dist-info/RECORD +22 -0
- prismatoid-0.4.5.dist-info/WHEEL +5 -0
- prismatoid-0.4.5.dist-info/licenses/LICENSE +356 -0
- prismatoid-0.4.5.dist-info/licenses/LICENSES/nvdaController/lgpl-2.1.txt +175 -0
- prismatoid-0.4.5.dist-info/licenses/LICENSES/nvgt/LICENSE.md +11 -0
- prismatoid-0.4.5.dist-info/licenses/LICENSES/prism/mpl-2.0.txt +144 -0
- prismatoid-0.4.5.dist-info/licenses/LICENSES/simdutf/apache-2.0.txt +73 -0
- prismatoid-0.4.5.dist-info/licenses/NOTICE +9 -0
- prismatoid-0.4.5.dist-info/sboms/auditwheel.cdx.json +1 -0
- prismatoid.libs/libblkid-78417c21.so.1.1.0 +0 -0
- prismatoid.libs/libgio-2-867cbb79.0.so.0.6800.4 +0 -0
- prismatoid.libs/libgmodule-2-75c2a560.0.so.0.6800.4 +0 -0
- prismatoid.libs/libltdl-ffb4ec7a.so.7 +0 -0
- prismatoid.libs/libmount-935e7fa5.so.1.1.0 +0 -0
- prismatoid.libs/libpcre2-8-b77f698a.so.0.11.0 +0 -0
- prismatoid.libs/libselinux-a1ff09bf.so.1 +0 -0
- prismatoid.libs/libspeechd-900e1c56.so.2.6.0 +0 -0
prism/__init__.py
ADDED
|
Binary file
|
prism/core.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
|
|
4
|
+
from .lib import ffi, lib
|
|
5
|
+
|
|
6
|
+
if sys.platform == "win32":
|
|
7
|
+
try:
|
|
8
|
+
from win32more.Windows.Win32.Foundation import HWND
|
|
9
|
+
except ImportError:
|
|
10
|
+
HWND = int
|
|
11
|
+
else:
|
|
12
|
+
HWND = int
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BackendId(IntEnum):
|
|
16
|
+
INVALID = 0
|
|
17
|
+
SAPI = 0x1D6DF72422CEEE66
|
|
18
|
+
AV_SPEECH = 0x28E3429577805C24
|
|
19
|
+
VOICE_OVER = 0xCB4897961A754BCB
|
|
20
|
+
SPEECH_DISPATCHER = 0xE3D6F895D949EBFE
|
|
21
|
+
NVDA = 0x89CC19C5C4AC1A56
|
|
22
|
+
JAWS = 0xAC3D60E9BD84B53E
|
|
23
|
+
ONE_CORE = 0x6797D32F0D994CB4
|
|
24
|
+
ORCA = 0x10AA1FC05A17F96C
|
|
25
|
+
ANDROID_SCREEN_READER = 0xD199C175AEEC494B
|
|
26
|
+
ANDROID_TTS = 0xBC175831BFE4E5CC
|
|
27
|
+
WEB_SPEECH = 0x3572538D44D44A8F
|
|
28
|
+
UIA = 0x6238F019DB678F8E
|
|
29
|
+
ZDSR = 0xAF63B44C8601A843
|
|
30
|
+
ZOOM_TEXT = 0xAE439D62DC7B1479
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PrismError(Exception):
|
|
34
|
+
"""Base class for all Prism-related errors."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, code: lib.PrismError, message: str | None = None) -> None:
|
|
37
|
+
self.code = code
|
|
38
|
+
self.message = message
|
|
39
|
+
super().__init__(message or f"Prism Error Code: {code}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PrismNotInitializedError(PrismError, RuntimeError):
|
|
43
|
+
"""PRISM_ERROR_NOT_INITIALIZED"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PrismAlreadyInitializedError(PrismError, RuntimeError):
|
|
47
|
+
"""PRISM_ERROR_ALREADY_INITIALIZED"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PrismInvalidOperationError(PrismError, RuntimeError):
|
|
51
|
+
"""PRISM_ERROR_INVALID_OPERATION"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PrismInternalError(PrismError, RuntimeError):
|
|
55
|
+
"""PRISM_ERROR_INTERNAL"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PrismBackendNotAvailableError(PrismError, RuntimeError):
|
|
59
|
+
"""PRISM_ERROR_BACKEND_NOT_AVAILABLE"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PrismNotImplementedError(PrismError, NotImplementedError):
|
|
63
|
+
"""PRISM_ERROR_NOT_IMPLEMENTED"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PrismInvalidParamError(PrismError, ValueError):
|
|
67
|
+
"""PRISM_ERROR_INVALID_PARAM"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PrismRangeError(PrismError, IndexError):
|
|
71
|
+
"""PRISM_ERROR_RANGE_OUT_OF_BOUNDS"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PrismInvalidUtf8Error(PrismError, UnicodeError):
|
|
75
|
+
"""PRISM_ERROR_INVALID_UTF8"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PrismNotSpeakingError(PrismError):
|
|
79
|
+
"""PRISM_ERROR_NOT_SPEAKING"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PrismNotPausedError(PrismError):
|
|
83
|
+
"""PRISM_ERROR_NOT_PAUSED"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class PrismAlreadyPausedError(PrismError):
|
|
87
|
+
"""PRISM_ERROR_ALREADY_PAUSED"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PrismSpeakError(PrismError, IOError):
|
|
91
|
+
"""PRISM_ERROR_SPEAK_FAILURE"""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PrismNoVoicesError(PrismError):
|
|
95
|
+
"""PRISM_ERROR_NO_VOICES"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class PrismVoiceNotFoundError(PrismError, LookupError):
|
|
99
|
+
"""PRISM_ERROR_VOICE_NOT_FOUND"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PrismMemoryError(PrismError, MemoryError):
|
|
103
|
+
"""PRISM_ERROR_MEMORY_FAILURE"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PrismUnknownError(PrismError):
|
|
107
|
+
"""PRISM_ERROR_UNKNOWN"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_ERROR_MAP = {
|
|
111
|
+
lib.PRISM_ERROR_NOT_INITIALIZED: PrismNotInitializedError,
|
|
112
|
+
lib.PRISM_ERROR_INVALID_PARAM: PrismInvalidParamError,
|
|
113
|
+
lib.PRISM_ERROR_NOT_IMPLEMENTED: PrismNotImplementedError,
|
|
114
|
+
lib.PRISM_ERROR_NO_VOICES: PrismNoVoicesError,
|
|
115
|
+
lib.PRISM_ERROR_VOICE_NOT_FOUND: PrismVoiceNotFoundError,
|
|
116
|
+
lib.PRISM_ERROR_SPEAK_FAILURE: PrismSpeakError,
|
|
117
|
+
lib.PRISM_ERROR_MEMORY_FAILURE: PrismMemoryError,
|
|
118
|
+
lib.PRISM_ERROR_RANGE_OUT_OF_BOUNDS: PrismRangeError,
|
|
119
|
+
lib.PRISM_ERROR_INTERNAL: PrismInternalError,
|
|
120
|
+
lib.PRISM_ERROR_NOT_SPEAKING: PrismNotSpeakingError,
|
|
121
|
+
lib.PRISM_ERROR_NOT_PAUSED: PrismNotPausedError,
|
|
122
|
+
lib.PRISM_ERROR_ALREADY_PAUSED: PrismAlreadyPausedError,
|
|
123
|
+
lib.PRISM_ERROR_INVALID_UTF8: PrismInvalidUtf8Error,
|
|
124
|
+
lib.PRISM_ERROR_INVALID_OPERATION: PrismInvalidOperationError,
|
|
125
|
+
lib.PRISM_ERROR_ALREADY_INITIALIZED: PrismAlreadyInitializedError,
|
|
126
|
+
lib.PRISM_ERROR_BACKEND_NOT_AVAILABLE: PrismBackendNotAvailableError,
|
|
127
|
+
lib.PRISM_ERROR_UNKNOWN: PrismUnknownError,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _check_error(error_code: int) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Checks the error code. If it is OK, returns None.
|
|
134
|
+
Otherwise, raises the appropriate PrismError.
|
|
135
|
+
"""
|
|
136
|
+
if error_code == 0:
|
|
137
|
+
return
|
|
138
|
+
exc_class = _ERROR_MAP.get(error_code, PrismUnknownError)
|
|
139
|
+
msg_ptr = lib.prism_error_string(error_code)
|
|
140
|
+
msg = ffi.string(msg_ptr).decode("utf-8")
|
|
141
|
+
raise exc_class(error_code, msg)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Backend:
|
|
145
|
+
_raw = None
|
|
146
|
+
|
|
147
|
+
def __init__(self, raw_ptr: lib.PrismBackend) -> None:
|
|
148
|
+
if raw_ptr == ffi.NULL:
|
|
149
|
+
raise RuntimeError("Backend raw pointer MUST NOT be NULL!")
|
|
150
|
+
self._raw = raw_ptr
|
|
151
|
+
res = lib.prism_backend_initialize(self._raw)
|
|
152
|
+
if res not in {lib.PRISM_OK, lib.PRISM_ERROR_ALREADY_INITIALIZED}:
|
|
153
|
+
_check_error(res)
|
|
154
|
+
|
|
155
|
+
def __del__(self) -> None:
|
|
156
|
+
lib.prism_backend_free(self._raw)
|
|
157
|
+
self._raw = None
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def name(self) -> str:
|
|
161
|
+
return ffi.string(lib.prism_backend_name(self._raw)).decode("utf-8")
|
|
162
|
+
|
|
163
|
+
def speak(self, text: str, interrupt: bool = False) -> None:
|
|
164
|
+
if len(text) == 0:
|
|
165
|
+
raise PrismInvalidParamError(
|
|
166
|
+
lib.PRISM_ERROR_INVALID_PARAM,
|
|
167
|
+
"Text MUST NOT be empty",
|
|
168
|
+
)
|
|
169
|
+
return _check_error(
|
|
170
|
+
lib.prism_backend_speak(self._raw, text.encode("utf-8"), interrupt),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def speak_to_memory(self, text: str, on_audio_data) -> None:
|
|
174
|
+
if len(text) == 0:
|
|
175
|
+
raise PrismInvalidParamError(
|
|
176
|
+
lib.PRISM_ERROR_INVALID_PARAM,
|
|
177
|
+
"Text MUST NOT be empty",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@ffi.callback("void(void *, const float *, size_t, size_t, size_t)")
|
|
181
|
+
def audio_callback_shim(
|
|
182
|
+
_userdata, samples_ptr: int, count, channels, rate
|
|
183
|
+
) -> None:
|
|
184
|
+
pcm_data = ffi.unpack(samples_ptr, count * channels)
|
|
185
|
+
on_audio_data(pcm_data, channels, rate)
|
|
186
|
+
|
|
187
|
+
self._active_callback = audio_callback_shim
|
|
188
|
+
return _check_error(
|
|
189
|
+
lib.prism_backend_speak_to_memory(
|
|
190
|
+
self._raw,
|
|
191
|
+
text.encode("utf-8"),
|
|
192
|
+
audio_callback_shim,
|
|
193
|
+
ffi.NULL,
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def braille(self, text: str) -> None:
|
|
198
|
+
if len(text) == 0:
|
|
199
|
+
raise PrismInvalidParamError(
|
|
200
|
+
lib.PRISM_ERROR_INVALID_PARAM,
|
|
201
|
+
"Text MUST NOT be empty",
|
|
202
|
+
)
|
|
203
|
+
return _check_error(lib.prism_backend_braille(self._raw, text.encode("utf-8")))
|
|
204
|
+
|
|
205
|
+
def output(self, text: str, interrupt: bool = False) -> None:
|
|
206
|
+
if len(text) == 0:
|
|
207
|
+
raise PrismInvalidParamError(
|
|
208
|
+
lib.PRISM_ERROR_INVALID_PARAM,
|
|
209
|
+
"Text MUST NOT be empty",
|
|
210
|
+
)
|
|
211
|
+
return _check_error(
|
|
212
|
+
lib.prism_backend_output(self._raw, text.encode("utf-8"), interrupt),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def stop(self) -> None:
|
|
216
|
+
return _check_error(lib.prism_backend_stop(self._raw))
|
|
217
|
+
|
|
218
|
+
def pause(self) -> None:
|
|
219
|
+
return _check_error(lib.prism_backend_pause(self._raw))
|
|
220
|
+
|
|
221
|
+
def resume(self) -> None:
|
|
222
|
+
return _check_error(lib.prism_backend_resume(self._raw))
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def speaking(self) -> bool:
|
|
226
|
+
p_speaking = ffi.new("bool*")
|
|
227
|
+
_check_error(lib.prism_backend_is_speaking(self._raw, p_speaking))
|
|
228
|
+
return p_speaking[0]
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def volume(self) -> float:
|
|
232
|
+
p_volume = ffi.new("float*")
|
|
233
|
+
_check_error(lib.prism_backend_get_volume(self._raw, p_volume))
|
|
234
|
+
return p_volume[0]
|
|
235
|
+
|
|
236
|
+
@volume.setter
|
|
237
|
+
def volume(self, volume: float) -> None:
|
|
238
|
+
return _check_error(lib.prism_backend_set_volume(self._raw, volume))
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def rate(self) -> float:
|
|
242
|
+
p_rate = ffi.new("float*")
|
|
243
|
+
_check_error(lib.prism_backend_get_rate(self._raw, p_rate))
|
|
244
|
+
return p_rate[0]
|
|
245
|
+
|
|
246
|
+
@rate.setter
|
|
247
|
+
def rate(self, rate: float) -> None:
|
|
248
|
+
return _check_error(lib.prism_backend_set_rate(self._raw, rate))
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def pitch(self) -> float:
|
|
252
|
+
p_pitch = ffi.new("float*")
|
|
253
|
+
_check_error(lib.prism_backend_get_pitch(self._raw, p_pitch))
|
|
254
|
+
return p_pitch[0]
|
|
255
|
+
|
|
256
|
+
@pitch.setter
|
|
257
|
+
def pitch(self, pitch: float) -> None:
|
|
258
|
+
return _check_error(lib.prism_backend_set_pitch(self._raw, pitch))
|
|
259
|
+
|
|
260
|
+
def refresh_voices(self) -> None:
|
|
261
|
+
return _check_error(lib.prism_backend_refresh_voices(self._raw))
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def voices_count(self) -> int:
|
|
265
|
+
out_count = ffi.new("size_t*")
|
|
266
|
+
_check_error(lib.prism_backend_count_voices(self._raw, out_count))
|
|
267
|
+
return out_count[0]
|
|
268
|
+
|
|
269
|
+
def get_voice_name(self, idx: int) -> str:
|
|
270
|
+
pp_name = ffi.new("char **")
|
|
271
|
+
_check_error(lib.prism_backend_get_voice_name(self._raw, idx, pp_name))
|
|
272
|
+
return ffi.string(pp_name[0]).decode("utf-8")
|
|
273
|
+
|
|
274
|
+
def get_voice_language(self, idx: int) -> str:
|
|
275
|
+
pp_lang = ffi.new("char **")
|
|
276
|
+
_check_error(lib.prism_backend_get_voice_language(self._raw, idx, pp_lang))
|
|
277
|
+
return ffi.string(pp_lang[0]).decode("utf-8")
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def voice(self) -> int:
|
|
281
|
+
out_voice_id = ffi.new("size_t*")
|
|
282
|
+
_check_error(lib.prism_backend_get_voice(self._raw, out_voice_id))
|
|
283
|
+
return out_voice_id[0]
|
|
284
|
+
|
|
285
|
+
@voice.setter
|
|
286
|
+
def voice(self, idx: int) -> None:
|
|
287
|
+
return _check_error(lib.prism_backend_set_voice(self._raw, idx))
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def channels(self) -> int:
|
|
291
|
+
out_channels = ffi.new("size_t*")
|
|
292
|
+
_check_error(lib.prism_backend_get_channels(self._raw, out_channels))
|
|
293
|
+
return out_channels[0]
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def sample_rate(self) -> int:
|
|
297
|
+
out_sample_rate = ffi.new("size_t*")
|
|
298
|
+
_check_error(lib.prism_backend_get_sample_rate(self._raw, out_sample_rate))
|
|
299
|
+
return out_sample_rate[0]
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def bit_depth(self) -> int:
|
|
303
|
+
out_bit_depth = ffi.new("size_t*")
|
|
304
|
+
_check_error(lib.prism_backend_get_bit_depth(self._raw, out_bit_depth))
|
|
305
|
+
return out_bit_depth[0]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class Context:
|
|
309
|
+
_ctx: lib.PrismContext = None
|
|
310
|
+
|
|
311
|
+
def __init__(self, hwnd: HWND | int | None = None) -> None:
|
|
312
|
+
config = lib.prism_config_init()
|
|
313
|
+
if hwnd is not None:
|
|
314
|
+
config.platform_data = ffi.cast("void*", int(hwnd))
|
|
315
|
+
self._ctx = lib.prism_init(ffi.new("PrismConfig *", config))
|
|
316
|
+
if self._ctx == ffi.NULL:
|
|
317
|
+
raise RuntimeError("Prism could not be initialized")
|
|
318
|
+
|
|
319
|
+
def __del__(self):
|
|
320
|
+
if hasattr(self, "_ctx") and self._ctx:
|
|
321
|
+
lib.prism_shutdown(self._ctx)
|
|
322
|
+
self._ctx = None
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def backends_count(self) -> int:
|
|
326
|
+
return lib.prism_registry_count(self._ctx)
|
|
327
|
+
|
|
328
|
+
def id_of(self, index_or_name: int | str) -> BackendId:
|
|
329
|
+
if isinstance(index_or_name, int):
|
|
330
|
+
res = lib.prism_registry_id_at(self._ctx, index_or_name)
|
|
331
|
+
elif isinstance(index_or_name, str):
|
|
332
|
+
res = lib.prism_registry_id(self._ctx, index_or_name.encode("utf-8"))
|
|
333
|
+
else:
|
|
334
|
+
raise TypeError("Expected int or string")
|
|
335
|
+
try:
|
|
336
|
+
return BackendId(res)
|
|
337
|
+
except ValueError as e:
|
|
338
|
+
raise ValueError(f"Prism returned unknown backend ID: {res:#x}") from e
|
|
339
|
+
|
|
340
|
+
def name_of(self, backend_id: BackendId) -> str:
|
|
341
|
+
c_ptr = lib.prism_registry_name(self._ctx, backend_id)
|
|
342
|
+
if c_ptr == ffi.NULL:
|
|
343
|
+
raise ValueError("Backend ID not found")
|
|
344
|
+
return ffi.string(c_ptr).decode("utf-8")
|
|
345
|
+
|
|
346
|
+
def priority_of(self, backend_id: BackendId) -> int:
|
|
347
|
+
return lib.prism_registry_priority(self._ctx, backend_id)
|
|
348
|
+
|
|
349
|
+
def exists(self, backend_id: BackendId) -> bool:
|
|
350
|
+
return bool(lib.prism_registry_exists(self._ctx, backend_id))
|
|
351
|
+
|
|
352
|
+
def create(self, backend_id: BackendId) -> Backend:
|
|
353
|
+
res = lib.prism_registry_create(self._ctx, int(backend_id))
|
|
354
|
+
if res == ffi.NULL:
|
|
355
|
+
raise ValueError("Invalid or unsupported backend")
|
|
356
|
+
return Backend(res)
|
|
357
|
+
|
|
358
|
+
def create_best(self) -> Backend:
|
|
359
|
+
res = lib.prism_registry_create_best(self._ctx)
|
|
360
|
+
if res == ffi.NULL:
|
|
361
|
+
raise ValueError("Invalid or unsupported backend")
|
|
362
|
+
return Backend(res)
|
|
363
|
+
|
|
364
|
+
def acquire(self, backend_id: BackendId) -> Backend:
|
|
365
|
+
res = lib.prism_registry_acquire(self._ctx, int(backend_id))
|
|
366
|
+
if res == ffi.NULL:
|
|
367
|
+
raise ValueError("Invalid or unsupported backend")
|
|
368
|
+
return Backend(res)
|
|
369
|
+
|
|
370
|
+
def acquire_best(self) -> Backend:
|
|
371
|
+
res = lib.prism_registry_acquire_best(self._ctx)
|
|
372
|
+
if res == ffi.NULL:
|
|
373
|
+
raise ValueError("Invalid or unsupported backend")
|
|
374
|
+
return Backend(res)
|
prism/lib.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from cffi import FFI
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _find_native_dir() -> str:
|
|
11
|
+
local_path = Path(__file__).parent / "_native"
|
|
12
|
+
if local_path.exists() and any(local_path.iterdir()):
|
|
13
|
+
return local_path
|
|
14
|
+
relative_path = Path("prism") / "_native"
|
|
15
|
+
for path in sys.path:
|
|
16
|
+
if not path:
|
|
17
|
+
continue
|
|
18
|
+
candidate = Path(path) / relative_path
|
|
19
|
+
if (candidate / "prism.dll").exists():
|
|
20
|
+
return candidate.resolve()
|
|
21
|
+
|
|
22
|
+
return local_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
dll_home = _find_native_dir()
|
|
26
|
+
with contextlib.suppress(AttributeError):
|
|
27
|
+
os.add_dll_directory(str(dll_home))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_android() -> bool:
|
|
31
|
+
with contextlib.suppress(ImportError):
|
|
32
|
+
if importlib.util.find_spec("", "android") is not None:
|
|
33
|
+
return True
|
|
34
|
+
if sys.platform == "linux":
|
|
35
|
+
with contextlib.suppress(FileNotFoundError), Path("/system/build.prop").open():
|
|
36
|
+
return True
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ffi = FFI()
|
|
41
|
+
if sys.platform == "win32" or _is_android():
|
|
42
|
+
ffi.cdef(r"""// SPDX-License-Identifier: MPL-2.0
|
|
43
|
+
|
|
44
|
+
typedef struct PrismContext PrismContext;
|
|
45
|
+
typedef struct PrismBackend PrismBackend;
|
|
46
|
+
typedef uint64_t PrismBackendId;
|
|
47
|
+
typedef struct {
|
|
48
|
+
uint8_t version;
|
|
49
|
+
void* platform_data;
|
|
50
|
+
} PrismConfig;
|
|
51
|
+
|
|
52
|
+
typedef enum PrismError {
|
|
53
|
+
PRISM_OK = 0,
|
|
54
|
+
PRISM_ERROR_NOT_INITIALIZED,
|
|
55
|
+
PRISM_ERROR_INVALID_PARAM,
|
|
56
|
+
PRISM_ERROR_NOT_IMPLEMENTED,
|
|
57
|
+
PRISM_ERROR_NO_VOICES,
|
|
58
|
+
PRISM_ERROR_VOICE_NOT_FOUND,
|
|
59
|
+
PRISM_ERROR_SPEAK_FAILURE,
|
|
60
|
+
PRISM_ERROR_MEMORY_FAILURE,
|
|
61
|
+
PRISM_ERROR_RANGE_OUT_OF_BOUNDS,
|
|
62
|
+
PRISM_ERROR_INTERNAL,
|
|
63
|
+
PRISM_ERROR_NOT_SPEAKING,
|
|
64
|
+
PRISM_ERROR_NOT_PAUSED,
|
|
65
|
+
PRISM_ERROR_ALREADY_PAUSED,
|
|
66
|
+
PRISM_ERROR_INVALID_UTF8,
|
|
67
|
+
PRISM_ERROR_INVALID_OPERATION,
|
|
68
|
+
PRISM_ERROR_ALREADY_INITIALIZED,
|
|
69
|
+
PRISM_ERROR_BACKEND_NOT_AVAILABLE,
|
|
70
|
+
PRISM_ERROR_UNKNOWN,
|
|
71
|
+
PRISM_ERROR_COUNT
|
|
72
|
+
} PrismError;
|
|
73
|
+
|
|
74
|
+
typedef void(PrismAudioCallback)(
|
|
75
|
+
void *userdata, const float *samples, size_t sample_count,
|
|
76
|
+
size_t channels, size_t sample_rate);
|
|
77
|
+
|
|
78
|
+
PrismConfig prism_config_init(void);
|
|
79
|
+
PrismContext *prism_init(PrismConfig* cfg);
|
|
80
|
+
void prism_shutdown(PrismContext *ctx);
|
|
81
|
+
size_t prism_registry_count(PrismContext *ctx);
|
|
82
|
+
PrismBackendId prism_registry_id_at(PrismContext *ctx, size_t index);
|
|
83
|
+
PrismBackendId prism_registry_id(PrismContext *ctx, const char *name);
|
|
84
|
+
const char *prism_registry_name(PrismContext *ctx, PrismBackendId id);
|
|
85
|
+
int prism_registry_priority(PrismContext *ctx, PrismBackendId id);
|
|
86
|
+
bool prism_registry_exists(PrismContext *ctx, PrismBackendId id);
|
|
87
|
+
PrismBackend *prism_registry_get(PrismContext *ctx, PrismBackendId id);
|
|
88
|
+
PrismBackend *prism_registry_create(PrismContext *ctx, PrismBackendId id);
|
|
89
|
+
PrismBackend *prism_registry_create_best(PrismContext *ctx);
|
|
90
|
+
PrismBackend *prism_registry_acquire(PrismContext *ctx, PrismBackendId id);
|
|
91
|
+
PrismBackend *prism_registry_acquire_best(PrismContext *ctx);
|
|
92
|
+
void prism_backend_free(PrismBackend *backend);
|
|
93
|
+
const char *prism_backend_name(PrismBackend *backend);
|
|
94
|
+
PrismError prism_backend_initialize(PrismBackend *backend);
|
|
95
|
+
PrismError prism_backend_speak(PrismBackend *backend, const char *text, bool interrupt);
|
|
96
|
+
PrismError prism_backend_speak_to_memory(
|
|
97
|
+
PrismBackend *backend,
|
|
98
|
+
const char *text,
|
|
99
|
+
PrismAudioCallback callback,
|
|
100
|
+
void *userdata
|
|
101
|
+
);
|
|
102
|
+
PrismError prism_backend_braille(PrismBackend *backend, const char *text);
|
|
103
|
+
PrismError prism_backend_output(PrismBackend *backend, const char *text, bool interrupt);
|
|
104
|
+
PrismError prism_backend_stop(PrismBackend *backend);
|
|
105
|
+
PrismError prism_backend_pause(PrismBackend *backend);
|
|
106
|
+
PrismError prism_backend_resume(PrismBackend *backend);
|
|
107
|
+
PrismError prism_backend_is_speaking(PrismBackend *backend, bool *out_speaking);
|
|
108
|
+
PrismError prism_backend_set_volume(PrismBackend *backend, float volume);
|
|
109
|
+
PrismError prism_backend_get_volume(PrismBackend *backend, float *out_volume);
|
|
110
|
+
PrismError prism_backend_set_rate(PrismBackend *backend, float rate);
|
|
111
|
+
PrismError prism_backend_get_rate(PrismBackend *backend, float *out_rate);
|
|
112
|
+
PrismError prism_backend_set_pitch(PrismBackend *backend, float pitch);
|
|
113
|
+
PrismError prism_backend_get_pitch(PrismBackend *backend, float *out_pitch);
|
|
114
|
+
PrismError prism_backend_refresh_voices(PrismBackend *backend);
|
|
115
|
+
PrismError prism_backend_count_voices(PrismBackend *backend, size_t *out_count);
|
|
116
|
+
PrismError prism_backend_get_voice_name(
|
|
117
|
+
PrismBackend *backend,
|
|
118
|
+
size_t voice_id,
|
|
119
|
+
const char **out_name
|
|
120
|
+
);
|
|
121
|
+
PrismError prism_backend_get_voice_language(
|
|
122
|
+
PrismBackend *backend,
|
|
123
|
+
size_t voice_id,
|
|
124
|
+
const char **out_language
|
|
125
|
+
);
|
|
126
|
+
PrismError prism_backend_set_voice(PrismBackend *backend, size_t voice_id);
|
|
127
|
+
PrismError prism_backend_get_voice(PrismBackend *backend, size_t *out_voice_id);
|
|
128
|
+
PrismError prism_backend_get_channels(PrismBackend *backend, size_t *out_channels);
|
|
129
|
+
PrismError prism_backend_get_sample_rate(PrismBackend *backend, size_t *out_sample_rate);
|
|
130
|
+
PrismError prism_backend_get_bit_depth(PrismBackend *backend, size_t *out_bit_depth);
|
|
131
|
+
const char *prism_error_string(PrismError error);
|
|
132
|
+
""")
|
|
133
|
+
else:
|
|
134
|
+
ffi.cdef(r"""// SPDX-License-Identifier: MPL-2.0
|
|
135
|
+
|
|
136
|
+
typedef struct PrismContext PrismContext;
|
|
137
|
+
typedef struct PrismBackend PrismBackend;
|
|
138
|
+
typedef uint64_t PrismBackendId;
|
|
139
|
+
typedef struct {
|
|
140
|
+
uint8_t version;
|
|
141
|
+
} PrismConfig;
|
|
142
|
+
|
|
143
|
+
typedef enum PrismError {
|
|
144
|
+
PRISM_OK = 0,
|
|
145
|
+
PRISM_ERROR_NOT_INITIALIZED,
|
|
146
|
+
PRISM_ERROR_INVALID_PARAM,
|
|
147
|
+
PRISM_ERROR_NOT_IMPLEMENTED,
|
|
148
|
+
PRISM_ERROR_NO_VOICES,
|
|
149
|
+
PRISM_ERROR_VOICE_NOT_FOUND,
|
|
150
|
+
PRISM_ERROR_SPEAK_FAILURE,
|
|
151
|
+
PRISM_ERROR_MEMORY_FAILURE,
|
|
152
|
+
PRISM_ERROR_RANGE_OUT_OF_BOUNDS,
|
|
153
|
+
PRISM_ERROR_INTERNAL,
|
|
154
|
+
PRISM_ERROR_NOT_SPEAKING,
|
|
155
|
+
PRISM_ERROR_NOT_PAUSED,
|
|
156
|
+
PRISM_ERROR_ALREADY_PAUSED,
|
|
157
|
+
PRISM_ERROR_INVALID_UTF8,
|
|
158
|
+
PRISM_ERROR_INVALID_OPERATION,
|
|
159
|
+
PRISM_ERROR_ALREADY_INITIALIZED,
|
|
160
|
+
PRISM_ERROR_BACKEND_NOT_AVAILABLE,
|
|
161
|
+
PRISM_ERROR_UNKNOWN,
|
|
162
|
+
PRISM_ERROR_COUNT
|
|
163
|
+
} PrismError;
|
|
164
|
+
|
|
165
|
+
typedef void(PrismAudioCallback)(
|
|
166
|
+
void *userdata, const float *samples, size_t sample_count,
|
|
167
|
+
size_t channels, size_t sample_rate);
|
|
168
|
+
|
|
169
|
+
PrismConfig prism_config_init(void);
|
|
170
|
+
PrismContext *prism_init(PrismConfig* cfg);
|
|
171
|
+
void prism_shutdown(PrismContext *ctx);
|
|
172
|
+
size_t prism_registry_count(PrismContext *ctx);
|
|
173
|
+
PrismBackendId prism_registry_id_at(PrismContext *ctx, size_t index);
|
|
174
|
+
PrismBackendId prism_registry_id(PrismContext *ctx, const char *name);
|
|
175
|
+
const char *prism_registry_name(PrismContext *ctx, PrismBackendId id);
|
|
176
|
+
int prism_registry_priority(PrismContext *ctx, PrismBackendId id);
|
|
177
|
+
bool prism_registry_exists(PrismContext *ctx, PrismBackendId id);
|
|
178
|
+
PrismBackend *prism_registry_get(PrismContext *ctx, PrismBackendId id);
|
|
179
|
+
PrismBackend *prism_registry_create(PrismContext *ctx, PrismBackendId id);
|
|
180
|
+
PrismBackend *prism_registry_create_best(PrismContext *ctx);
|
|
181
|
+
PrismBackend *prism_registry_acquire(PrismContext *ctx, PrismBackendId id);
|
|
182
|
+
PrismBackend *prism_registry_acquire_best(PrismContext *ctx);
|
|
183
|
+
void prism_backend_free(PrismBackend *backend);
|
|
184
|
+
const char *prism_backend_name(PrismBackend *backend);
|
|
185
|
+
PrismError prism_backend_initialize(PrismBackend *backend);
|
|
186
|
+
PrismError prism_backend_speak(PrismBackend *backend, const char *text, bool interrupt);
|
|
187
|
+
PrismError prism_backend_speak_to_memory(
|
|
188
|
+
PrismBackend *backend,
|
|
189
|
+
const char *text,
|
|
190
|
+
PrismAudioCallback callback,
|
|
191
|
+
void *userdata
|
|
192
|
+
);
|
|
193
|
+
PrismError prism_backend_braille(PrismBackend *backend, const char *text);
|
|
194
|
+
PrismError prism_backend_output(PrismBackend *backend, const char *text, bool interrupt);
|
|
195
|
+
PrismError prism_backend_stop(PrismBackend *backend);
|
|
196
|
+
PrismError prism_backend_pause(PrismBackend *backend);
|
|
197
|
+
PrismError prism_backend_resume(PrismBackend *backend);
|
|
198
|
+
PrismError prism_backend_is_speaking(PrismBackend *backend, bool *out_speaking);
|
|
199
|
+
PrismError prism_backend_set_volume(PrismBackend *backend, float volume);
|
|
200
|
+
PrismError prism_backend_get_volume(PrismBackend *backend, float *out_volume);
|
|
201
|
+
PrismError prism_backend_set_rate(PrismBackend *backend, float rate);
|
|
202
|
+
PrismError prism_backend_get_rate(PrismBackend *backend, float *out_rate);
|
|
203
|
+
PrismError prism_backend_set_pitch(PrismBackend *backend, float pitch);
|
|
204
|
+
PrismError prism_backend_get_pitch(PrismBackend *backend, float *out_pitch);
|
|
205
|
+
PrismError prism_backend_refresh_voices(PrismBackend *backend);
|
|
206
|
+
PrismError prism_backend_count_voices(PrismBackend *backend, size_t *out_count);
|
|
207
|
+
PrismError prism_backend_get_voice_name(
|
|
208
|
+
PrismBackend *backend,
|
|
209
|
+
size_t voice_id,
|
|
210
|
+
const char **out_name
|
|
211
|
+
);
|
|
212
|
+
PrismError prism_backend_get_voice_language(
|
|
213
|
+
PrismBackend *backend,
|
|
214
|
+
size_t voice_id,
|
|
215
|
+
const char **out_language
|
|
216
|
+
);
|
|
217
|
+
PrismError prism_backend_set_voice(PrismBackend *backend, size_t voice_id);
|
|
218
|
+
PrismError prism_backend_get_voice(PrismBackend *backend, size_t *out_voice_id);
|
|
219
|
+
PrismError prism_backend_get_channels(PrismBackend *backend, size_t *out_channels);
|
|
220
|
+
PrismError prism_backend_get_sample_rate(PrismBackend *backend, size_t *out_sample_rate);
|
|
221
|
+
PrismError prism_backend_get_bit_depth(PrismBackend *backend, size_t *out_bit_depth);
|
|
222
|
+
const char *prism_error_string(PrismError error);
|
|
223
|
+
""")
|
|
224
|
+
if sys.platform == "win32":
|
|
225
|
+
lib_path = (dll_home / "prism.dll").resolve()
|
|
226
|
+
elif sys.platform == "darwin":
|
|
227
|
+
lib_path = (dll_home / "libprism.dylib").resolve()
|
|
228
|
+
else:
|
|
229
|
+
lib_path = (dll_home / "libprism.so").resolve()
|
|
230
|
+
lib = ffi.dlopen(str(lib_path))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prismatoid
|
|
3
|
+
Version: 0.4.5
|
|
4
|
+
Summary: The Platform-Agnostic Reader Interface for Speech and Messages
|
|
5
|
+
Author-Email: Ethin Probst <ethindp@pm.me>
|
|
6
|
+
License-Expression: MPL-2.0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
License-File: LICENSES/nvdaController/lgpl-2.1.txt
|
|
9
|
+
License-File: LICENSES/nvgt/LICENSE.md
|
|
10
|
+
License-File: LICENSES/prism/mpl-2.0.txt
|
|
11
|
+
License-File: LICENSES/simdutf/apache-2.0.txt
|
|
12
|
+
License-File: NOTICE
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: cffi>=1.0.0
|
|
15
|
+
Requires-Dist: win32more>=0.8.0; sys_platform == "win32"
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# PRISM
|
|
19
|
+
|
|
20
|
+
Prism is the Platform-agnostic Reader Interface for Speech and Messages. Since that's a hell of a mouthful, we just call it Prism for short. The name comes from prisms in optics, which are transparent optical components with flat surfaces that refract light into many beams. Thus, the metaphor: refract your TTS strings to send them to many different backends, potentially simultaneously.
|
|
21
|
+
|
|
22
|
+
Prism aims to unify the various screen reader abstraction libraries like SpeechCore, UniversalSpeech, SRAL, Tolk, etc., into a single unified system with a single unified API. Of course, we also support traditional TTS engines. I have tried to develop Prism in such a way that compilation is trivial and requires no external dependencies. To that end, the CMake builder will download all needed dependencies. However, since it uses [cpm.cmake](https://github.com/cpm-cmake/CPM.cmake), vendoring of dependencies is very possible.
|
|
23
|
+
|
|
24
|
+
## Building
|
|
25
|
+
|
|
26
|
+
To build Prism, all you need do is create a build directory and run cmake as you ordinarily would. The only build option at this time is `PRISM_ENABLE_TESTS`, which will determine whether the test suite is built or not. In the 99 percent of cases, you should disable this option, since running the tests is only for validating that the library functions correctly. However, the tests have currently known problems and are still in a very early stage of development, so validation is platform-dependent and may fail in very strange ways on other platforms than Windows. (Specifically, failure is known to happen on at least MacOS, due to the header's various compiler attributes in use.)
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
Documentation uses [mdbook](https://github.com/rust-lang/mdBook). To view it offline, install mdbook and then run `mdbook serve` from the doc directory.
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
The API is fully documented in the documentation above. If the documentation and header do not align in guarantees or expectations, this is a bug and should be reported.
|
|
35
|
+
|
|
36
|
+
## Bindings
|
|
37
|
+
|
|
38
|
+
Currently bindings are an in-progress effort. We are working on bindings to at least Python and .NET, although both will take a bit of time to actually do. If you wish to contribute bindings, please don't hesitate to open a pull request. Your bindings must obey the restrictions in the documentation; specifically, where the documentation requires something to not occur, your bindings MUST prohibit that occurence before calling any Prism C API function. For example, calling `prism_backend_speak` with a `NULL` backend or text string MUST be prohibited by your bindings.
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
This project is licensed under the Mozilla Public License version 2.0. Full details are available in the LICENSE file.
|
|
43
|
+
|
|
44
|
+
This project uses code from other projects. Specifically:
|
|
45
|
+
|
|
46
|
+
* The SAPI bridge is credited to the [NVGT](https://github.com/samtupy/nvgt) project, as well as the functions `range_convert` and `range_convert_midpoint` in utils.h and utils.cpp. Similar attribution goes to NVGT for the Android screen reader backend.
|
|
47
|
+
* The `simdutf` library is licensed under the Apache-2.0 license.
|
|
48
|
+
* On Windows, Prism includes NVDA controller client RPC definitions under LGPL-2.1 (and generated RPC stubs from those inputs). Those portions remain under LGPL-2.1. See licenses/ and idl/ for the actual texts. On all non-Windows platforms, this does not apply, however the LGPL licenses are included for completeness in all SDKs.
|
|
49
|
+
|
|
50
|
+
## Contributing
|
|
51
|
+
|
|
52
|
+
Contributions are welcome. This includes, but is not limited to, documentation enhancements, new backends, bindings, build system improvements, etc. The project uses C++23 so please ensure that your compiler supports that standard.
|
|
53
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
prism/__init__.py,sha256=hvOVuGBvJt49J93-RVCaqhoVp2a8u-Kuta1V2j3sa1s,127
|
|
2
|
+
prism/core.py,sha256=C8IZwU_Q27ZGY6U3nXrnTRF4p_Jt-yKABGwIGVssrXw,12136
|
|
3
|
+
prism/lib.py,sha256=GkVMQrCrJ_0fXx28aulL5v6wMW1YZ0-itVOsqLumA8U,9162
|
|
4
|
+
prism/_native/libprism.so,sha256=eau5YBk-QljcwkW6LwSwmWqVCvIlkr8SztrgMPLTbgs,950241
|
|
5
|
+
prismatoid.libs/libblkid-78417c21.so.1.1.0,sha256=QqZ-GpXrrYD8Z-TLU44NwleXfE2ia_rcK1yLWFb7PnI,238513
|
|
6
|
+
prismatoid.libs/libgio-2-867cbb79.0.so.0.6800.4,sha256=2DOZ884qBG1HYMKg0a88c1oSLlSJFPc27SGln6hy_tU,2121369
|
|
7
|
+
prismatoid.libs/libgmodule-2-75c2a560.0.so.0.6800.4,sha256=8ZfC-XXe7zATWQ1CQz-7Gl2Y9LhPGQP5HO2H0bgf1yU,22505
|
|
8
|
+
prismatoid.libs/libltdl-ffb4ec7a.so.7,sha256=taLEZits6e1U8TMtnuz-OsGiis3gVT14hW0ZSZ0QuTc,41121
|
|
9
|
+
prismatoid.libs/libmount-935e7fa5.so.1.1.0,sha256=AbqJJuAlYyHYYihRNVFaEWLSMeug5EsfyZQo4b49H98,315697
|
|
10
|
+
prismatoid.libs/libpcre2-8-b77f698a.so.0.11.0,sha256=hQ2wPgk-6gm4ZYWGy6wPDCt46HtNTpYIRBUyH9Fq-9U,642033
|
|
11
|
+
prismatoid.libs/libselinux-a1ff09bf.so.1,sha256=7Y8jm3YrZ3BrrNxLFY1Bto0ik3_YTQYg8_rrT14cEiE,195361
|
|
12
|
+
prismatoid.libs/libspeechd-900e1c56.so.2.6.0,sha256=BF07fd9prhIVoWos6whZin2APQ4AVtHTOgvPd8Yr9wU,60001
|
|
13
|
+
prismatoid-0.4.5.dist-info/METADATA,sha256=6xAGZf4snAyJlm7AGci8ZfLA8Jz0OMR8-wfQ2mF6R6M,4227
|
|
14
|
+
prismatoid-0.4.5.dist-info/WHEEL,sha256=47S8GfAIuXQZhoya3X2DlgmMrDYgF2L6qDW1EsYZuBA,118
|
|
15
|
+
prismatoid-0.4.5.dist-info/RECORD,,
|
|
16
|
+
prismatoid-0.4.5.dist-info/licenses/LICENSE,sha256=bJjy9y1AlJwaGk73Q5A_oHcZ2pQdzPH7dHnRwV8Fq3o,15504
|
|
17
|
+
prismatoid-0.4.5.dist-info/licenses/NOTICE,sha256=v4o4GrCMWTkwIOfhO_SKDv44i_qA69vtbRf-z1xfn44,886
|
|
18
|
+
prismatoid-0.4.5.dist-info/licenses/LICENSES/nvdaController/lgpl-2.1.txt,sha256=2DCML-JO2IgZa8Uj30YhR1kAjYd-IlK0jM3ajiuO1Rs,25967
|
|
19
|
+
prismatoid-0.4.5.dist-info/licenses/LICENSES/nvgt/LICENSE.md,sha256=v_AyXYuvXEo6Ra4_2yZ5tOzk8TpmevvhIRozYbtjzQI,903
|
|
20
|
+
prismatoid-0.4.5.dist-info/licenses/LICENSES/prism/mpl-2.0.txt,sha256=ZsEFNaSV9M2BFWB-iQ-BFtZXBkuYVX9mDFHhI7Pz_uY,15190
|
|
21
|
+
prismatoid-0.4.5.dist-info/licenses/LICENSES/simdutf/apache-2.0.txt,sha256=wnT4A3LZDAEpNzcPDh8VCH0i4wjvmLJ86l3A0tCINmw,10279
|
|
22
|
+
prismatoid-0.4.5.dist-info/sboms/auditwheel.cdx.json,sha256=hu8LBuxzaChdW7GgFH3a1lsuPbvsMCP91CrhZKTGsDc,4107
|