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