diff --git a/imgsrc/srv/bullhorn.svg b/imgsrc/srv/bullhorn.svg new file mode 100644 index 0000000000..b5a4a0f133 --- /dev/null +++ b/imgsrc/srv/bullhorn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/calibre/utils/tts/__init__.py b/src/calibre/gui2/tts/__init__.py similarity index 100% rename from src/calibre/utils/tts/__init__.py rename to src/calibre/gui2/tts/__init__.py diff --git a/src/calibre/gui2/tts/errors.py b/src/calibre/gui2/tts/errors.py new file mode 100644 index 0000000000..1651001559 --- /dev/null +++ b/src/calibre/gui2/tts/errors.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2020, Kovid Goyal + + +class TTSSystemUnavailable(Exception): + def __init__(self, message, details): + Exception.__init__(self, message) + self.short_msg = message + self.details = details diff --git a/src/calibre/gui2/tts/implementation.py b/src/calibre/gui2/tts/implementation.py new file mode 100644 index 0000000000..48768d3d4a --- /dev/null +++ b/src/calibre/gui2/tts/implementation.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2020, Kovid Goyal + +from calibre.constants import iswindows, ismacos + +if iswindows: + pass +elif ismacos: + pass +else: + from .linux import speak_simple_text + +speak_simple_text diff --git a/src/calibre/gui2/tts/linux.py b/src/calibre/gui2/tts/linux.py new file mode 100644 index 0000000000..81d99d6846 --- /dev/null +++ b/src/calibre/gui2/tts/linux.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2020, Kovid Goyal + +import atexit +from .errors import TTSSystemUnavailable + + +def get_client(): + client = getattr(get_client, 'ans', None) + if client is not None: + return client + from speechd.client import SSIPClient, SpawnError + try: + client = get_client.ans = SSIPClient('calibre') + except SpawnError as err: + raise TTSSystemUnavailable(_('Could not find speech-dispatcher on your system. Please install it.'), str(err)) + atexit.register(client.close) + return client + + +def speak_simple_text(text): + client = get_client() + from speechd.client import SSIPCommunicationError + try: + client.speak(text) + except SSIPCommunicationError: + get_client.ans = None + client = get_client() + client.speak(text) diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index c1b987a70e..9ee6bc9d1b 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -268,6 +268,7 @@ class ViewerBridge(Bridge): close_prep_finished = from_js(object) highlights_changed = from_js(object) open_url = from_js(object) + speak_simple_text = from_js(object) create_view = to_js() start_book_load = to_js() @@ -514,6 +515,7 @@ class WebView(RestartingWebEngineView): self.bridge.close_prep_finished.connect(self.close_prep_finished) self.bridge.highlights_changed.connect(self.highlights_changed) self.bridge.open_url.connect(safe_open_url) + self.bridge.speak_simple_text.connect(self.speak_simple_text) self.bridge.export_shortcut_map.connect(self.set_shortcut_map) self.shortcut_map = {} self.bridge.report_cfi.connect(self.call_callback) @@ -527,6 +529,10 @@ class WebView(RestartingWebEngineView): self.inspector = Inspector(parent.inspector_dock.toggleViewAction(), self) parent.inspector_dock.setWidget(self.inspector) + def speak_simple_text(self, text): + from calibre.gui2.tts.implementation import speak_simple_text + speak_simple_text(text) + def set_shortcut_map(self, smap): self.shortcut_map = smap self.shortcuts_changed.emit(smap) diff --git a/src/pyj/read_book/selection_bar.pyj b/src/pyj/read_book/selection_bar.pyj index f363235e2c..1a0f4d521b 100644 --- a/src/pyj/read_book/selection_bar.pyj +++ b/src/pyj/read_book/selection_bar.pyj @@ -192,6 +192,7 @@ def all_actions(): 'search_net': a('global-search', _('Search for selection on the net'), 'internet_search'), 'remove_highlight': a('trash', _('Remove this highlight'), 'remove_highlight', True), 'clear': a('close', _('Clear selection'), 'clear_selection'), + 'speak': a('bullhorn', _('Speak aloud'), 'speak_aloud'), } qh = all_actions.ans.quick_highlight qh.icon_function = quick_highlight_icon.bind(None, qh.icon, qh.text) @@ -950,6 +951,11 @@ class SelectionBar: self.view.on_handle_shortcut({'name': 'clear_selection'}) self.hide() + def speak_aloud(self): + text = self.view.currently_showing.selection.text + if text: + ui_operations.speak_simple_text(text) + def create_highlight(self): cs = self.view.currently_showing.selection hs = self.current_highlight_style diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index add3a0eb07..8b0aafb2c7 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -398,6 +398,8 @@ if window is window.top: ui_operations.annots_changed = def(amap): if amap.highlight: to_python.highlights_changed(amap.highlight) + ui_operations.speak_simple_text = def(text): + to_python.speak_simple_text(text) document.body.appendChild(E.div(id='view')) window.onerror = onerror