diff --git a/src/calibre/gui2/tts/develop.py b/src/calibre/gui2/tts/develop.py index 529fffcddf..d74fa5025c 100644 --- a/src/calibre/gui2/tts/develop.py +++ b/src/calibre/gui2/tts/develop.py @@ -43,13 +43,14 @@ def add_markup(text): class TTSWidget(QWidget): - events_available = pyqtSignal() + dispatch_on_main_thread_signal = pyqtSignal(object) mark_changed = pyqtSignal(object) def __init__(self, parent=None): QWidget.__init__(self, parent) 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.la = la = QLabel(self) la.setTextFormat(Qt.RichText) @@ -86,7 +87,6 @@ example, which of. l.addWidget(bb) b.clicked.connect(self.play_clicked) self.render_text() - self.events_available.connect(self.handle_events, type=Qt.QueuedConnection) def render_text(self): text = self.text @@ -103,16 +103,22 @@ example, which of. self.la.setText('\n'.join(lines)) 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): - for event in self.tts.get_events(): - if event.type is EventType.mark: - try: - mark = int(event.data) - except Exception: - return - self.mark_changed.emit(mark) + def dispatch_on_main_thread(self, func): + try: + func() + except Exception: + import traceback + traceback.print_exc() + + 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): self.current_mark = mark @@ -126,7 +132,7 @@ def main(): w.setCentralWidget(tts) w.show() app.exec_() - tts.events_available.disconnect() + tts.dispatch_on_main_thread_signal.disconnect() tts.mark_changed.disconnect() tts.tts.shutdown() diff --git a/src/calibre/gui2/tts/linux.py b/src/calibre/gui2/tts/linux.py index c861649a03..3af203ed27 100644 --- a/src/calibre/gui2/tts/linux.py +++ b/src/calibre/gui2/tts/linux.py @@ -2,6 +2,8 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2020, Kovid Goyal +from functools import partial + from calibre import prepare_string_for_xml from .common import Event, EventType @@ -16,10 +18,10 @@ class Client: def escape_marked_text(cls, text): return prepare_string_for_xml(text) - def __init__(self): + def __init__(self, dispatch_on_main_thread): self.create_ssip_client() - self.pending_events = [] self.status = {'synthesizing': False, 'paused': False} + self.dispatch_on_main_thread = dispatch_on_main_thread def create_ssip_client(self): from speechd.client import SpawnError, SSIPClient @@ -46,8 +48,11 @@ class Client: def speak_simple_text(self, text): 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): from speechd.client import CallbackType @@ -62,33 +67,31 @@ class Client: elif callback_type is CallbackType.RESUME: 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 + 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): self.update_status(callback_type, index_mark) - if callback_type is CallbackType.INDEX_MARK: - event = Event(EventType.mark, index_mark) - elif callback_type is CallbackType.BEGIN: - event = Event(EventType.begin) - elif callback_type is CallbackType.END: - event = Event(EventType.end) - 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() + event = self.msg_as_event(callback_type, index_mark) + if event is not None: + callback(event) + + def cw(callback_type, index_mark=None): + self.dispatch_on_main_thread(partial(callback_wrapper, callback_type, index_mark)) self.set_use_ssml(True) - self.pending_events = [] - self.ssip_client.speak(text, callback=callback_wrapper) - - def get_events(self): - events = self.pending_events - self.pending_events = [] - return events + self.ssip_client.speak(text, callback=cw) diff --git a/src/calibre/gui2/tts/macos.py b/src/calibre/gui2/tts/macos.py index c32cbd41a9..c637b1269f 100644 --- a/src/calibre/gui2/tts/macos.py +++ b/src/calibre/gui2/tts/macos.py @@ -13,11 +13,11 @@ class Client: def escape_marked_text(cls, text): return text.replace('[[', ' [ [ ').replace(']]', ' ] ] ') - def __init__(self): + def __init__(self, dispatch_on_main_thread): from calibre_extensions.cocoa import NSSpeechSynthesizer self.nsss = NSSpeechSynthesizer(self.handle_message) self.current_callback = None - self.pending_events = [] + self.dispatch_on_main_thread = dispatch_on_main_thread def __del__(self): self.nsss = None @@ -25,31 +25,23 @@ class Client: def handle_message(self, message_type, data): 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 message_type == MARK: - 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() + self.current_callback(event) def speak_simple_text(self, text): self.current_callback = None - self.pending_events = [] self.nsss.speak(self.escape_marked_text(text)) def speak_marked_text(self, text, callback): self.current_callback = callback - self.pending_events = [] self.nsss.speak(text) - def get_events(self): - events = self.pending_events - self.pending_events = [] - return events - @property def status(self): ans = self.nsss.status() diff --git a/src/calibre/gui2/tts/windows.py b/src/calibre/gui2/tts/windows.py index 5b4486befc..2d3f54d8f7 100644 --- a/src/calibre/gui2/tts/windows.py +++ b/src/calibre/gui2/tts/windows.py @@ -18,13 +18,14 @@ class Client: def escape_marked_text(cls, text): return prepare_string_for_xml(text) - def __init__(self): + def __init__(self, dispatch_on_main_thread): from calibre.utils.windows.winsapi import ISpVoice self.sp_voice = ISpVoice() self.events_thread = Thread(name='SAPIEvents', target=self.wait_for_events, daemon=True) self.events_thread.start() self.current_stream_number = None self.current_callback = None + self.dispatch_on_main_thread = dispatch_on_main_thread def __del__(self): if self.sp_voice is not None: @@ -37,27 +38,24 @@ class Client: while True: if self.sp_voice.wait_for_event() is False: break - c = self.current_callback - if c is not None: - c() + self.dispatch_on_main_thread(self.handle_events) - def get_events(self): + def handle_events(self): from calibre_extensions.winsapi import ( 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(): - 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 + 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 + if c is not None and stream_number == self.current_stream_number: + c(event) def speak_simple_text(self, text): from calibre_extensions.winsapi import ( diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index c41f67143b..408f02c25c 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -464,6 +464,7 @@ class WebView(RestartingWebEngineView): paged_mode_changed = pyqtSignal() standalone_misc_settings_changed = pyqtSignal(object) view_created = pyqtSignal(object) + dispatch_on_main_thread_signal = pyqtSignal(object) def __init__(self, parent=None): self._host_widget = None @@ -474,6 +475,7 @@ class WebView(RestartingWebEngineView): RestartingWebEngineView.__init__(self, parent) self.dead_renderer_error_shown = False 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() QApplication.instance().palette_changed.connect(self.palette_changed) self.show_home_page_on_ready = True @@ -534,11 +536,22 @@ class WebView(RestartingWebEngineView): def tts_client(self): if self._tts_client is None: 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 + def dispatch_on_main_thread(self, func): + try: + func() + except Exception: + import traceback + traceback.print_exc() + 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): if self._tts_client is not None: