mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Refactor TTS clients to dispatch events on main thread
Gets rid of pending events and allows for state tracking even without callbacks
This commit is contained in:
parent
7ebfb0f248
commit
a8cf85ca9b
@ -43,13 +43,14 @@ def add_markup(text):
|
|||||||
|
|
||||||
class TTSWidget(QWidget):
|
class TTSWidget(QWidget):
|
||||||
|
|
||||||
events_available = pyqtSignal()
|
dispatch_on_main_thread_signal = pyqtSignal(object)
|
||||||
mark_changed = pyqtSignal(object)
|
mark_changed = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
self.mark_changed.connect(self.on_mark_change)
|
self.mark_changed.connect(self.on_mark_change)
|
||||||
self.tts = Client()
|
self.dispatch_on_main_thread_signal.connect(self.dispatch_on_main_thread, type=Qt.QueuedConnection)
|
||||||
|
self.tts = Client(self.dispatch_on_main_thread_signal.emit)
|
||||||
self.l = l = QVBoxLayout(self)
|
self.l = l = QVBoxLayout(self)
|
||||||
self.la = la = QLabel(self)
|
self.la = la = QLabel(self)
|
||||||
la.setTextFormat(Qt.RichText)
|
la.setTextFormat(Qt.RichText)
|
||||||
@ -86,7 +87,6 @@ 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
|
||||||
@ -103,16 +103,22 @@ 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.events_available.emit)
|
self.tts.speak_marked_text(self.ssml, self.handle_event)
|
||||||
|
|
||||||
def handle_events(self):
|
def dispatch_on_main_thread(self, func):
|
||||||
for event in self.tts.get_events():
|
try:
|
||||||
if event.type is EventType.mark:
|
func()
|
||||||
try:
|
except Exception:
|
||||||
mark = int(event.data)
|
import traceback
|
||||||
except Exception:
|
traceback.print_exc()
|
||||||
return
|
|
||||||
self.mark_changed.emit(mark)
|
def handle_event(self, event):
|
||||||
|
if event.type is EventType.mark:
|
||||||
|
try:
|
||||||
|
mark = int(event.data)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
self.mark_changed.emit(mark)
|
||||||
|
|
||||||
def on_mark_change(self, mark):
|
def on_mark_change(self, mark):
|
||||||
self.current_mark = mark
|
self.current_mark = mark
|
||||||
@ -126,7 +132,7 @@ def main():
|
|||||||
w.setCentralWidget(tts)
|
w.setCentralWidget(tts)
|
||||||
w.show()
|
w.show()
|
||||||
app.exec_()
|
app.exec_()
|
||||||
tts.events_available.disconnect()
|
tts.dispatch_on_main_thread_signal.disconnect()
|
||||||
tts.mark_changed.disconnect()
|
tts.mark_changed.disconnect()
|
||||||
tts.tts.shutdown()
|
tts.tts.shutdown()
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
# vim:fileencoding=utf-8
|
# vim:fileencoding=utf-8
|
||||||
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from calibre import prepare_string_for_xml
|
from calibre import prepare_string_for_xml
|
||||||
|
|
||||||
from .common import Event, EventType
|
from .common import Event, EventType
|
||||||
@ -16,10 +18,10 @@ class Client:
|
|||||||
def escape_marked_text(cls, text):
|
def escape_marked_text(cls, text):
|
||||||
return prepare_string_for_xml(text)
|
return prepare_string_for_xml(text)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, dispatch_on_main_thread):
|
||||||
self.create_ssip_client()
|
self.create_ssip_client()
|
||||||
self.pending_events = []
|
|
||||||
self.status = {'synthesizing': False, 'paused': False}
|
self.status = {'synthesizing': False, 'paused': False}
|
||||||
|
self.dispatch_on_main_thread = dispatch_on_main_thread
|
||||||
|
|
||||||
def create_ssip_client(self):
|
def create_ssip_client(self):
|
||||||
from speechd.client import SpawnError, SSIPClient
|
from speechd.client import SpawnError, SSIPClient
|
||||||
@ -46,8 +48,11 @@ class Client:
|
|||||||
|
|
||||||
def speak_simple_text(self, text):
|
def speak_simple_text(self, text):
|
||||||
self.set_use_ssml(False)
|
self.set_use_ssml(False)
|
||||||
self.pending_events = []
|
|
||||||
self.ssip_client.speak(text, self.update_status)
|
def callback(callback_type, index_mark=None):
|
||||||
|
self.dispatch_on_main_thread(partial(self.update_status, callback_type, index_mark))
|
||||||
|
|
||||||
|
self.ssip_client.speak(text, callback)
|
||||||
|
|
||||||
def update_status(self, callback_type, index_mark=None):
|
def update_status(self, callback_type, index_mark=None):
|
||||||
from speechd.client import CallbackType
|
from speechd.client import CallbackType
|
||||||
@ -62,33 +67,31 @@ class Client:
|
|||||||
elif callback_type is CallbackType.RESUME:
|
elif callback_type is CallbackType.RESUME:
|
||||||
self.status = {'synthesizing': True, 'paused': False}
|
self.status = {'synthesizing': True, 'paused': False}
|
||||||
|
|
||||||
def speak_marked_text(self, text, callback):
|
def msg_as_event(self, callback_type, index_mark=None):
|
||||||
from speechd.client import CallbackType
|
from speechd.client import CallbackType
|
||||||
|
if callback_type is CallbackType.INDEX_MARK:
|
||||||
|
return Event(EventType.mark, index_mark)
|
||||||
|
if callback_type is CallbackType.BEGIN:
|
||||||
|
return Event(EventType.begin)
|
||||||
|
if callback_type is CallbackType.END:
|
||||||
|
return Event(EventType.end)
|
||||||
|
if callback_type is CallbackType.CANCEL:
|
||||||
|
return Event(EventType.cancel)
|
||||||
|
if callback_type is CallbackType.PAUSE:
|
||||||
|
return Event(EventType.pause)
|
||||||
|
if callback_type is CallbackType.RESUME:
|
||||||
|
return Event(EventType.resume)
|
||||||
|
|
||||||
|
def speak_marked_text(self, text, callback):
|
||||||
|
|
||||||
def callback_wrapper(callback_type, index_mark=None):
|
def callback_wrapper(callback_type, index_mark=None):
|
||||||
self.update_status(callback_type, index_mark)
|
self.update_status(callback_type, index_mark)
|
||||||
if callback_type is CallbackType.INDEX_MARK:
|
event = self.msg_as_event(callback_type, index_mark)
|
||||||
event = Event(EventType.mark, index_mark)
|
if event is not None:
|
||||||
elif callback_type is CallbackType.BEGIN:
|
callback(event)
|
||||||
event = Event(EventType.begin)
|
|
||||||
elif callback_type is CallbackType.END:
|
def cw(callback_type, index_mark=None):
|
||||||
event = Event(EventType.end)
|
self.dispatch_on_main_thread(partial(callback_wrapper, callback_type, index_mark))
|
||||||
elif callback_type is CallbackType.CANCEL:
|
|
||||||
event = Event(EventType.cancel)
|
|
||||||
elif callback_type is CallbackType.PAUSE:
|
|
||||||
event = Event(EventType.pause)
|
|
||||||
elif callback_type is CallbackType.RESUME:
|
|
||||||
event = Event(EventType.resume)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
self.pending_events.append(event)
|
|
||||||
callback()
|
|
||||||
|
|
||||||
self.set_use_ssml(True)
|
self.set_use_ssml(True)
|
||||||
self.pending_events = []
|
self.ssip_client.speak(text, callback=cw)
|
||||||
self.ssip_client.speak(text, callback=callback_wrapper)
|
|
||||||
|
|
||||||
def get_events(self):
|
|
||||||
events = self.pending_events
|
|
||||||
self.pending_events = []
|
|
||||||
return events
|
|
||||||
|
@ -13,11 +13,11 @@ class Client:
|
|||||||
def escape_marked_text(cls, text):
|
def escape_marked_text(cls, text):
|
||||||
return text.replace('[[', ' [ [ ').replace(']]', ' ] ] ')
|
return text.replace('[[', ' [ [ ').replace(']]', ' ] ] ')
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, dispatch_on_main_thread):
|
||||||
from calibre_extensions.cocoa import NSSpeechSynthesizer
|
from calibre_extensions.cocoa import NSSpeechSynthesizer
|
||||||
self.nsss = NSSpeechSynthesizer(self.handle_message)
|
self.nsss = NSSpeechSynthesizer(self.handle_message)
|
||||||
self.current_callback = None
|
self.current_callback = None
|
||||||
self.pending_events = []
|
self.dispatch_on_main_thread = dispatch_on_main_thread
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.nsss = None
|
self.nsss = None
|
||||||
@ -25,31 +25,23 @@ class Client:
|
|||||||
|
|
||||||
def handle_message(self, message_type, data):
|
def handle_message(self, message_type, data):
|
||||||
from calibre_extensions.cocoa import MARK, END
|
from calibre_extensions.cocoa import MARK, END
|
||||||
|
if message_type == MARK:
|
||||||
|
event = Event(EventType.mark, data)
|
||||||
|
elif message_type == END:
|
||||||
|
event = Event(EventType.end if data else EventType.cancel)
|
||||||
|
else:
|
||||||
|
return
|
||||||
if self.current_callback is not None:
|
if self.current_callback is not None:
|
||||||
if message_type == MARK:
|
self.current_callback(event)
|
||||||
event = Event(EventType.mark, data)
|
|
||||||
elif message_type == END:
|
|
||||||
event = Event(EventType.end if data else EventType.cancel)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
self.pending_events.append(event)
|
|
||||||
self.current_callback()
|
|
||||||
|
|
||||||
def speak_simple_text(self, text):
|
def speak_simple_text(self, text):
|
||||||
self.current_callback = None
|
self.current_callback = None
|
||||||
self.pending_events = []
|
|
||||||
self.nsss.speak(self.escape_marked_text(text))
|
self.nsss.speak(self.escape_marked_text(text))
|
||||||
|
|
||||||
def speak_marked_text(self, text, callback):
|
def speak_marked_text(self, text, callback):
|
||||||
self.current_callback = callback
|
self.current_callback = callback
|
||||||
self.pending_events = []
|
|
||||||
self.nsss.speak(text)
|
self.nsss.speak(text)
|
||||||
|
|
||||||
def get_events(self):
|
|
||||||
events = self.pending_events
|
|
||||||
self.pending_events = []
|
|
||||||
return events
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
ans = self.nsss.status()
|
ans = self.nsss.status()
|
||||||
|
@ -18,13 +18,14 @@ class Client:
|
|||||||
def escape_marked_text(cls, text):
|
def escape_marked_text(cls, text):
|
||||||
return prepare_string_for_xml(text)
|
return prepare_string_for_xml(text)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, dispatch_on_main_thread):
|
||||||
from calibre.utils.windows.winsapi import ISpVoice
|
from calibre.utils.windows.winsapi import ISpVoice
|
||||||
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_stream_number = None
|
||||||
self.current_callback = None
|
self.current_callback = None
|
||||||
|
self.dispatch_on_main_thread = dispatch_on_main_thread
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
if self.sp_voice is not None:
|
if self.sp_voice is not None:
|
||||||
@ -37,27 +38,24 @@ class Client:
|
|||||||
while True:
|
while True:
|
||||||
if self.sp_voice.wait_for_event() is False:
|
if self.sp_voice.wait_for_event() is False:
|
||||||
break
|
break
|
||||||
c = self.current_callback
|
self.dispatch_on_main_thread(self.handle_events)
|
||||||
if c is not None:
|
|
||||||
c()
|
|
||||||
|
|
||||||
def get_events(self):
|
def handle_events(self):
|
||||||
from calibre_extensions.winsapi import (
|
from calibre_extensions.winsapi import (
|
||||||
SPEI_END_INPUT_STREAM, SPEI_START_INPUT_STREAM, SPEI_TTS_BOOKMARK
|
SPEI_END_INPUT_STREAM, SPEI_START_INPUT_STREAM, SPEI_TTS_BOOKMARK
|
||||||
)
|
)
|
||||||
ans = []
|
c = self.current_callback
|
||||||
for (stream_number, event_type, event_data) in self.sp_voice.get_events():
|
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:
|
||||||
if event_type == SPEI_TTS_BOOKMARK:
|
event = Event(EventType.mark, event_data)
|
||||||
event = Event(EventType.mark, event_data)
|
elif event_type == SPEI_START_INPUT_STREAM:
|
||||||
elif event_type == SPEI_START_INPUT_STREAM:
|
event = Event(EventType.begin)
|
||||||
event = Event(EventType.begin)
|
elif event_type == SPEI_END_INPUT_STREAM:
|
||||||
elif event_type == SPEI_END_INPUT_STREAM:
|
event = Event(EventType.end)
|
||||||
event = Event(EventType.end)
|
else:
|
||||||
else:
|
continue
|
||||||
continue
|
if c is not None and stream_number == self.current_stream_number:
|
||||||
ans.append(event)
|
c(event)
|
||||||
return ans
|
|
||||||
|
|
||||||
def speak_simple_text(self, text):
|
def speak_simple_text(self, text):
|
||||||
from calibre_extensions.winsapi import (
|
from calibre_extensions.winsapi import (
|
||||||
|
@ -464,6 +464,7 @@ class WebView(RestartingWebEngineView):
|
|||||||
paged_mode_changed = pyqtSignal()
|
paged_mode_changed = pyqtSignal()
|
||||||
standalone_misc_settings_changed = pyqtSignal(object)
|
standalone_misc_settings_changed = pyqtSignal(object)
|
||||||
view_created = pyqtSignal(object)
|
view_created = pyqtSignal(object)
|
||||||
|
dispatch_on_main_thread_signal = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
self._host_widget = None
|
self._host_widget = None
|
||||||
@ -474,6 +475,7 @@ class WebView(RestartingWebEngineView):
|
|||||||
RestartingWebEngineView.__init__(self, parent)
|
RestartingWebEngineView.__init__(self, parent)
|
||||||
self.dead_renderer_error_shown = False
|
self.dead_renderer_error_shown = False
|
||||||
self.render_process_failed.connect(self.render_process_died)
|
self.render_process_failed.connect(self.render_process_died)
|
||||||
|
self.dispatch_on_main_thread_signal.connect(self.dispatch_on_main_thread)
|
||||||
w = QApplication.instance().desktop().availableGeometry(self).width()
|
w = QApplication.instance().desktop().availableGeometry(self).width()
|
||||||
QApplication.instance().palette_changed.connect(self.palette_changed)
|
QApplication.instance().palette_changed.connect(self.palette_changed)
|
||||||
self.show_home_page_on_ready = True
|
self.show_home_page_on_ready = True
|
||||||
@ -534,11 +536,22 @@ class WebView(RestartingWebEngineView):
|
|||||||
def tts_client(self):
|
def tts_client(self):
|
||||||
if self._tts_client is None:
|
if self._tts_client is None:
|
||||||
from calibre.gui2.tts.implementation import Client
|
from calibre.gui2.tts.implementation import Client
|
||||||
self._tts_client = Client()
|
self._tts_client = Client(self.dispatch_on_main_thread_signal.emit)
|
||||||
return self._tts_client
|
return self._tts_client
|
||||||
|
|
||||||
|
def dispatch_on_main_thread(self, func):
|
||||||
|
try:
|
||||||
|
func()
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def speak_simple_text(self, text):
|
def speak_simple_text(self, text):
|
||||||
self.tts_client.speak_simple_text(text)
|
from calibre.gui2.tts.errors import TTSSystemUnavailable
|
||||||
|
try:
|
||||||
|
self.tts_client.speak_simple_text(text)
|
||||||
|
except TTSSystemUnavailable as err:
|
||||||
|
return error_dialog(self, _('Text-to-Speech unavailable'), str(err), show=True)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
if self._tts_client is not None:
|
if self._tts_client is not None:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user