mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
Refactor tts client design so all interaction with underlying API happens only on the main thread
This commit is contained in:
parent
3309f3728f
commit
ab377cfda5
@ -10,6 +10,7 @@ from PyQt5.Qt import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from calibre import prepare_string_for_xml
|
from calibre import prepare_string_for_xml
|
||||||
|
from calibre.constants import iswindows
|
||||||
from calibre.gui2 import Application
|
from calibre.gui2 import Application
|
||||||
|
|
||||||
from .common import EventType
|
from .common import EventType
|
||||||
@ -22,6 +23,10 @@ def add_markup(text):
|
|||||||
counter = count()
|
counter = count()
|
||||||
pos_map = {}
|
pos_map = {}
|
||||||
last = None
|
last = None
|
||||||
|
if iswindows:
|
||||||
|
bm = '<bookmark mark="{}"/>'
|
||||||
|
else:
|
||||||
|
bm = '<mark name="{}"/>'
|
||||||
for m in re.finditer(r'\w+', text):
|
for m in re.finditer(r'\w+', text):
|
||||||
start, end = m.start(), m.end()
|
start, end = m.start(), m.end()
|
||||||
if first:
|
if first:
|
||||||
@ -29,7 +34,7 @@ def add_markup(text):
|
|||||||
if start:
|
if start:
|
||||||
buf.append(prepare_string_for_xml(text[:start]))
|
buf.append(prepare_string_for_xml(text[:start]))
|
||||||
num = next(counter)
|
num = next(counter)
|
||||||
buf.append(f'<mark name="{num}"/>')
|
buf.append(bm.format(num))
|
||||||
pos_map[num] = start, end
|
pos_map[num] = start, end
|
||||||
buf.append(prepare_string_for_xml(m.group()))
|
buf.append(prepare_string_for_xml(m.group()))
|
||||||
last = end
|
last = end
|
||||||
@ -40,8 +45,9 @@ def add_markup(text):
|
|||||||
return ''.join(buf), pos_map
|
return ''.join(buf), pos_map
|
||||||
|
|
||||||
|
|
||||||
class TTS(QWidget):
|
class TTSWidget(QWidget):
|
||||||
|
|
||||||
|
events_available = pyqtSignal()
|
||||||
mark_changed = pyqtSignal(object)
|
mark_changed = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@ -84,6 +90,7 @@ example, which of.
|
|||||||
l.addWidget(bb)
|
l.addWidget(bb)
|
||||||
b.clicked.connect(self.play_clicked)
|
b.clicked.connect(self.play_clicked)
|
||||||
self.render_text()
|
self.render_text()
|
||||||
|
self.events_available.connect(self.handle_events, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
def render_text(self):
|
def render_text(self):
|
||||||
text = self.text
|
text = self.text
|
||||||
@ -100,9 +107,10 @@ example, which of.
|
|||||||
self.la.setText('\n'.join(lines))
|
self.la.setText('\n'.join(lines))
|
||||||
|
|
||||||
def play_clicked(self):
|
def play_clicked(self):
|
||||||
self.tts.speak_marked_text(self.ssml, self.handle_event)
|
self.tts.speak_marked_text(self.ssml, self.events_available.emit)
|
||||||
|
|
||||||
def handle_event(self, event):
|
def handle_events(self):
|
||||||
|
for event in self.tts.get_events():
|
||||||
if event.type is EventType.mark:
|
if event.type is EventType.mark:
|
||||||
try:
|
try:
|
||||||
mark = int(event.data)
|
mark = int(event.data)
|
||||||
@ -118,11 +126,13 @@ example, which of.
|
|||||||
def main():
|
def main():
|
||||||
app = Application([])
|
app = Application([])
|
||||||
w = QMainWindow()
|
w = QMainWindow()
|
||||||
tts = TTS(w)
|
tts = TTSWidget(w)
|
||||||
w.setCentralWidget(tts)
|
w.setCentralWidget(tts)
|
||||||
w.show()
|
w.show()
|
||||||
app.exec_()
|
app.exec_()
|
||||||
del tts.tts
|
tts.events_available.disconnect()
|
||||||
|
tts.mark_changed.disconnect()
|
||||||
|
tts.tts.shutdown()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -10,6 +10,7 @@ class Client:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.create_ssip_client()
|
self.create_ssip_client()
|
||||||
|
self.pending_events = []
|
||||||
|
|
||||||
def create_ssip_client(self):
|
def create_ssip_client(self):
|
||||||
from speechd.client import SpawnError, SSIPClient
|
from speechd.client import SpawnError, SSIPClient
|
||||||
@ -19,8 +20,10 @@ class Client:
|
|||||||
raise TTSSystemUnavailable(_('Could not find speech-dispatcher on your system. Please install it.'), str(err))
|
raise TTSSystemUnavailable(_('Could not find speech-dispatcher on your system. Please install it.'), str(err))
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
if hasattr(self, 'ssip_client'):
|
||||||
self.ssip_client.close()
|
self.ssip_client.close()
|
||||||
del self.ssip_client
|
del self.ssip_client
|
||||||
|
shutdown = __del__
|
||||||
|
|
||||||
def set_use_ssml(self, on):
|
def set_use_ssml(self, on):
|
||||||
from speechd.client import DataMode, SSIPCommunicationError
|
from speechd.client import DataMode, SSIPCommunicationError
|
||||||
@ -54,7 +57,13 @@ class Client:
|
|||||||
event = Event(EventType.resume)
|
event = Event(EventType.resume)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
callback(event)
|
self.pending_events.append(event)
|
||||||
|
callback()
|
||||||
|
|
||||||
self.set_use_ssml(True)
|
self.set_use_ssml(True)
|
||||||
self.ssip_client.speak(text, callback=callback_wrapper)
|
self.ssip_client.speak(text, callback=callback_wrapper)
|
||||||
|
|
||||||
|
def get_events(self):
|
||||||
|
events = self.pending_events
|
||||||
|
self.pending_events = []
|
||||||
|
return events
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
from .common import Event, EventType
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
@ -13,18 +14,44 @@ class Client:
|
|||||||
self.sp_voice = ISpVoice()
|
self.sp_voice = ISpVoice()
|
||||||
self.events_thread = Thread(name='SAPIEvents', target=self.wait_for_events, daemon=True)
|
self.events_thread = Thread(name='SAPIEvents', target=self.wait_for_events, daemon=True)
|
||||||
self.events_thread.start()
|
self.events_thread.start()
|
||||||
|
self.current_stream_number = None
|
||||||
|
self.current_callback = None
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.sp_voice.shutdown_event_loop()
|
self.sp_voice.shutdown_event_loop()
|
||||||
self.events_thread.join(5)
|
self.events_thread.join(5)
|
||||||
self.sp_voice = None
|
self.sp_voice = None
|
||||||
|
shutdown = __del__
|
||||||
|
|
||||||
def wait_for_events(self):
|
def wait_for_events(self):
|
||||||
self.sp_voice.run_event_loop(self.process_event)
|
while True:
|
||||||
|
if self.sp_voice.wait_for_event() is False:
|
||||||
|
break
|
||||||
|
if self.current_callback is not None:
|
||||||
|
self.current_callback()
|
||||||
|
|
||||||
def process_event(self, stream_number, event_type, event_data=None):
|
def get_events(self):
|
||||||
pass
|
from calibre_extensions.winsapi import SPEI_TTS_BOOKMARK, SPEI_START_INPUT_STREAM, SPEI_END_INPUT_STREAM
|
||||||
|
ans = []
|
||||||
|
for (stream_number, event_type, event_data) in self.sp_voice.get_events():
|
||||||
|
if stream_number == self.current_stream_number:
|
||||||
|
if event_type == SPEI_TTS_BOOKMARK:
|
||||||
|
event = Event(EventType.mark, event_data)
|
||||||
|
elif event_type == SPEI_START_INPUT_STREAM:
|
||||||
|
event = Event(EventType.begin)
|
||||||
|
elif event_type == SPEI_END_INPUT_STREAM:
|
||||||
|
event = Event(EventType.end)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
ans.append(event)
|
||||||
|
return ans
|
||||||
|
|
||||||
def speak_simple_text(self, text):
|
def speak_simple_text(self, text):
|
||||||
from calibre_extensions.winsapi import SPF_ASYNC, SPF_PURGEBEFORESPEAK, SPF_IS_NOT_XML
|
from calibre_extensions.winsapi import SPF_ASYNC, SPF_PURGEBEFORESPEAK, SPF_IS_NOT_XML
|
||||||
self.sp_voice.speak(text, SPF_ASYNC | SPF_PURGEBEFORESPEAK | SPF_IS_NOT_XML)
|
self.current_callback = None
|
||||||
|
self.current_stream_number = self.sp_voice.speak(text, SPF_ASYNC | SPF_PURGEBEFORESPEAK | SPF_IS_NOT_XML)
|
||||||
|
|
||||||
|
def speak_marked_text(self, text, callback):
|
||||||
|
from calibre_extensions.winsapi import SPF_ASYNC, SPF_PURGEBEFORESPEAK, SPF_IS_XML
|
||||||
|
self.current_callback = callback
|
||||||
|
self.current_stream_number = self.sp_voice.speak(text, SPF_ASYNC | SPF_PURGEBEFORESPEAK | SPF_IS_XML, True)
|
||||||
|
@ -541,6 +541,8 @@ class WebView(RestartingWebEngineView):
|
|||||||
self.tts_client.speak_simple_text(text)
|
self.tts_client.speak_simple_text(text)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
|
if self._tts_client is not None:
|
||||||
|
self._tts_client.shutdown()
|
||||||
self._tts_client = None
|
self._tts_client = None
|
||||||
|
|
||||||
def set_shortcut_map(self, smap):
|
def set_shortcut_map(self, smap):
|
||||||
|
@ -30,6 +30,8 @@ static PyTypeObject VoiceType = {
|
|||||||
PyVarObject_HEAD_INIT(NULL, 0)
|
PyVarObject_HEAD_INIT(NULL, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const ULONGLONG speak_events = SPFEI(SPEI_START_INPUT_STREAM) | SPFEI(SPEI_END_INPUT_STREAM) | SPFEI(SPEI_TTS_BOOKMARK);
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
Voice_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
|
Voice_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
|
||||||
HRESULT hr = CoInitialize(NULL);
|
HRESULT hr = CoInitialize(NULL);
|
||||||
@ -55,19 +57,12 @@ Voice_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
|
|||||||
PyErr_SetString(PyExc_OSError, "Failed to get events handle for ISpVoice");
|
PyErr_SetString(PyExc_OSError, "Failed to get events handle for ISpVoice");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
self->shutdown_events_thread = CreateEvent(NULL, true, false, NULL);
|
self->shutdown_events_thread = CreateEventW(NULL, true, false, NULL);
|
||||||
if (self->shutdown_events_thread == INVALID_HANDLE_VALUE) {
|
if (self->shutdown_events_thread == INVALID_HANDLE_VALUE) {
|
||||||
Py_CLEAR(self);
|
Py_CLEAR(self);
|
||||||
PyErr_SetFromWindowsErr(0);
|
PyErr_SetFromWindowsErr(0);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
ULONGLONG events = SPFEI(SPEI_START_INPUT_STREAM) | SPFEI(SPEI_END_INPUT_STREAM) | SPFEI(SPEI_TTS_BOOKMARK);
|
|
||||||
if (FAILED(hr = self->voice->SetInterest(events, events))) {
|
|
||||||
CloseHandle(self->shutdown_events_thread);
|
|
||||||
Py_CLEAR(self);
|
|
||||||
return error_from_hresult(hr, "Failed to register event interest");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return (PyObject*)self;
|
return (PyObject*)self;
|
||||||
}
|
}
|
||||||
@ -266,13 +261,18 @@ static PyObject*
|
|||||||
Voice_speak(Voice *self, PyObject *args) {
|
Voice_speak(Voice *self, PyObject *args) {
|
||||||
wchar_raii text_or_path;
|
wchar_raii text_or_path;
|
||||||
unsigned long flags = SPF_DEFAULT;
|
unsigned long flags = SPF_DEFAULT;
|
||||||
if (!PyArg_ParseTuple(args, "O&|k", py_to_wchar, &text_or_path, &flags)) return NULL;
|
int want_events = 0;
|
||||||
ULONG stream_number;
|
|
||||||
HRESULT hr = S_OK;
|
HRESULT hr = S_OK;
|
||||||
|
if (!PyArg_ParseTuple(args, "O&|kp", py_to_wchar, &text_or_path, &flags, &want_events)) return NULL;
|
||||||
|
ULONGLONG events = want_events ? speak_events : 0;
|
||||||
|
if (FAILED(hr = self->voice->SetInterest(events, events))) {
|
||||||
|
return error_from_hresult(hr, "Failed to ask for events");
|
||||||
|
}
|
||||||
|
ULONG stream_number;
|
||||||
Py_BEGIN_ALLOW_THREADS;
|
Py_BEGIN_ALLOW_THREADS;
|
||||||
hr = self->voice->Speak(text_or_path.ptr(), flags, &stream_number);
|
hr = self->voice->Speak(text_or_path.ptr(), flags, &stream_number);
|
||||||
Py_END_ALLOW_THREADS;
|
Py_END_ALLOW_THREADS;
|
||||||
if (FAILED(hr)) return error_from_hresult(hr, "Failed to speak", PyTuple_GET_ITEM(args, 0));
|
if (FAILED(hr)) return error_from_hresult(hr, "Failed to speak");
|
||||||
return PyLong_FromUnsignedLong(stream_number);
|
return PyLong_FromUnsignedLong(stream_number);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,8 +339,8 @@ Voice_shutdown_event_loop(Voice *self, PyObject *args) {
|
|||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static PyObject*
|
||||||
dispatch_events(Voice *self, PyObject *callback) {
|
get_events(Voice *self, PyObject *args) {
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
const ULONG asz = 32;
|
const ULONG asz = 32;
|
||||||
ULONG num_events;
|
ULONG num_events;
|
||||||
@ -348,6 +348,8 @@ dispatch_events(Voice *self, PyObject *callback) {
|
|||||||
PyObject *ret;
|
PyObject *ret;
|
||||||
long long val;
|
long long val;
|
||||||
int etype;
|
int etype;
|
||||||
|
PyObject *ans = PyList_New(0);
|
||||||
|
if (!ans) return NULL;
|
||||||
while (true) {
|
while (true) {
|
||||||
Py_BEGIN_ALLOW_THREADS;
|
Py_BEGIN_ALLOW_THREADS;
|
||||||
hr = self->voice->GetEvents(asz, events, &num_events);
|
hr = self->voice->GetEvents(asz, events, &num_events);
|
||||||
@ -356,38 +358,42 @@ dispatch_events(Voice *self, PyObject *callback) {
|
|||||||
if (num_events == 0) break;
|
if (num_events == 0) break;
|
||||||
for (ULONG i = 0; i < num_events; i++) {
|
for (ULONG i = 0; i < num_events; i++) {
|
||||||
etype = events[i].eEventId;
|
etype = events[i].eEventId;
|
||||||
#define CALL(fmt, ...) { ret = PyObject_CallFunction(callback, fmt, __VA_ARGS__); if (ret) Py_DECREF(ret); else PyErr_Print(); } break;
|
bool ok = false;
|
||||||
switch(etype) {
|
switch(etype) {
|
||||||
case SPEI_TTS_BOOKMARK:
|
case SPEI_TTS_BOOKMARK:
|
||||||
val = events[i].wParam;
|
val = events[i].wParam;
|
||||||
CALL("kiL", events[i].ulStreamNum, etype, val);
|
ok = true;
|
||||||
|
break;
|
||||||
case SPEI_START_INPUT_STREAM:
|
case SPEI_START_INPUT_STREAM:
|
||||||
case SPEI_END_INPUT_STREAM:
|
case SPEI_END_INPUT_STREAM:
|
||||||
CALL("ki", events[i].ulStreamNum, etype);
|
val = 0;
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
#undef CALL
|
if (ok) {
|
||||||
|
ret = Py_BuildValue("kiL", events[i].ulStreamNum, etype, val);
|
||||||
|
if (!ret) { Py_CLEAR(ans); return NULL; }
|
||||||
|
int x = PyList_Append(ans, ret);
|
||||||
|
Py_DECREF(ret);
|
||||||
|
if (x != 0) { Py_CLEAR(ans); return NULL; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return ans;
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
Voice_run_event_loop(Voice *self, PyObject *callback) {
|
Voice_wait_for_event(Voice *self, PyObject *callback) {
|
||||||
if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback object is not callable"); return NULL; }
|
if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback object is not callable"); return NULL; }
|
||||||
HANDLE handles[2] = {self->shutdown_events_thread, self->events_available};
|
const HANDLE handles[2] = {self->shutdown_events_thread, self->events_available};
|
||||||
bool keep_going = true;
|
|
||||||
DWORD ev;
|
|
||||||
while(keep_going) {
|
|
||||||
Py_BEGIN_ALLOW_THREADS;
|
Py_BEGIN_ALLOW_THREADS;
|
||||||
ev = WaitForMultipleObjects(2, handles, true, INFINITE);
|
ev = WaitForMultipleObjects(2, handles, true, INFINITE);
|
||||||
Py_END_ALLOW_THREADS;
|
Py_END_ALLOW_THREADS;
|
||||||
switch (ev) {
|
switch (ev) {
|
||||||
case WAIT_OBJECT_0:
|
case WAIT_OBJECT_0:
|
||||||
keep_going = false;
|
Py_RETURN_FALSE;
|
||||||
break;
|
|
||||||
case WAIT_OBJECT_0 + 1:
|
case WAIT_OBJECT_0 + 1:
|
||||||
dispatch_events(self, callback);
|
Py_RETURN_TRUE;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
@ -414,7 +420,8 @@ static PyMethodDef Voice_methods[] = {
|
|||||||
M(set_current_sound_output, METH_VARARGS),
|
M(set_current_sound_output, METH_VARARGS),
|
||||||
|
|
||||||
M(shutdown_event_loop, METH_NOARGS),
|
M(shutdown_event_loop, METH_NOARGS),
|
||||||
M(run_event_loop, METH_O),
|
M(wait_for_event, METH_O),
|
||||||
|
M(get_events, METH_NOARGS),
|
||||||
{NULL, NULL, 0, NULL}
|
{NULL, NULL, 0, NULL}
|
||||||
};
|
};
|
||||||
#undef M
|
#undef M
|
||||||
|
Loading…
x
Reference in New Issue
Block a user