diff --git a/imgsrc/srv/faster.svg b/imgsrc/srv/faster.svg
new file mode 100644
index 0000000000..6e25e3f177
--- /dev/null
+++ b/imgsrc/srv/faster.svg
@@ -0,0 +1,3 @@
+
diff --git a/imgsrc/srv/slower.svg b/imgsrc/srv/slower.svg
new file mode 100644
index 0000000000..5938c371ca
--- /dev/null
+++ b/imgsrc/srv/slower.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/calibre/gui2/tts/linux.py b/src/calibre/gui2/tts/linux.py
index 7cd0d38685..de91c3620e 100644
--- a/src/calibre/gui2/tts/linux.py
+++ b/src/calibre/gui2/tts/linux.py
@@ -19,6 +19,8 @@ class Client:
mark_template = ''
name = 'speechd'
+ min_rate = -100
+ max_rate = 100
@classmethod
def escape_marked_text(cls, text):
@@ -71,7 +73,6 @@ class Client:
self.shutdown()
self.settings_applied = False
self.ensure_state()
- self.settings_applied = True
om = self.settings.get('output_module')
if om:
self.ssip_client.set_output_module(om)
@@ -81,6 +82,7 @@ class Client:
rate = self.settings.get('rate')
if rate:
self.ssip_client.set_rate(rate)
+ self.settings_applied = True
def set_use_ssml(self, on):
from speechd.client import DataMode, SSIPCommunicationError
@@ -190,3 +192,18 @@ class Client:
ans[om] = tuple(self.ssip_client.list_synthesis_voices())
self.ssip_client.set_output_module(output_module)
return ans
+
+ def change_rate(self, steps=1):
+ rate = current_rate = self.settings.get('rate') or 0
+ step_size = (self.max_rate - self.min_rate) // 10
+ rate += steps * step_size
+ rate = max(self.min_rate, min(rate, self.max_rate))
+ if rate != current_rate:
+ self.settings['rate'] = rate
+ prev_state = self.status.copy()
+ self.apply_settings()
+ if prev_state['synthesizing'] and not prev_state['paused']:
+ self.status['synthesizing'] = True
+ self.status['paused'] = True
+ self.resume_after_configure()
+ return self.settings
diff --git a/src/calibre/gui2/tts/linux_config.py b/src/calibre/gui2/tts/linux_config.py
index 62ae89c136..5a532b88d9 100644
--- a/src/calibre/gui2/tts/linux_config.py
+++ b/src/calibre/gui2/tts/linux_config.py
@@ -76,10 +76,12 @@ class Widget(QWidget):
self.tts_client = tts_client
self.speed = s = QSlider(Qt.Orientation.Horizontal, self)
+ s.setTickPosition(QSlider.TickPosition.TicksAbove)
s.setMinimumWidth(200)
l.addRow(_('&Speed of speech:'), s)
- s.setRange(-100, 100)
- s.setSingleStep(5)
+ s.setRange(self.tts_client.min_rate, self.tts_client.max_rate)
+ s.setSingleStep(10)
+ s.setTickInterval((self.tts_client.max_rate - self.tts_client.min_rate) // 2)
self.output_modules = om = QComboBox(self)
with BusyCursor():
@@ -183,7 +185,7 @@ if __name__ == '__main__':
from calibre.gui2 import Application
from calibre.gui2.tts.implementation import Client
app = Application([])
- c = Client()
+ c = Client({})
w = Widget(c, {})
w.show()
app.exec_()
diff --git a/src/calibre/gui2/viewer/tts.py b/src/calibre/gui2/viewer/tts.py
index c5d078ee7c..0ff7461490 100644
--- a/src/calibre/gui2/viewer/tts.py
+++ b/src/calibre/gui2/viewer/tts.py
@@ -34,13 +34,12 @@ class Config(Dialog):
return super().accept()
-def add_markup(text_parts):
+def add_markup(text_parts, mark_template):
from calibre.gui2.tts.implementation import Client
buf = []
- bm = Client.mark_template
for x in text_parts:
if isinstance(x, int):
- buf.append(bm.format(x))
+ buf.append(mark_template.format(x))
else:
buf.append(Client.escape_marked_text(x))
return ''.join(buf)
@@ -64,11 +63,15 @@ class TTS(QObject):
import traceback
traceback.print_exc()
+ @property
+ def tts_client_class(self):
+ from calibre.gui2.tts.implementation import Client
+ return Client
+
@property
def tts_client(self):
if self._tts_client is None:
- from calibre.gui2.tts.implementation import Client
- self._tts_client = Client(self.backend_settings, self.dispatch_on_main_thread_signal.emit)
+ self._tts_client = self.tts_client_class(self.backend_settings, self.dispatch_on_main_thread_signal.emit)
return self._tts_client
def shutdown(self):
@@ -91,7 +94,7 @@ class TTS(QObject):
return error_dialog(self.parent(), _('Text-to-Speech unavailable'), str(err), show=True)
def play(self, data):
- marked_text = add_markup(data['marked_text'])
+ marked_text = add_markup(data['marked_text'], self.tts_client_class.mark_template)
self.tts_client.speak_marked_text(marked_text, self.callback)
def pause(self, data):
@@ -114,23 +117,31 @@ class TTS(QObject):
@property
def backend_settings(self):
- from calibre.gui2.tts.implementation import Client
- key = 'tts_' + Client.name
+ key = 'tts_' + self.tts_client_class.name
return vprefs.get(key) or {}
@backend_settings.setter
def backend_settings(self, val):
- from calibre.gui2.tts.implementation import Client
- key = 'tts_' + Client.name
- val = val or {}
- vprefs.set(key, val)
- self.tts_client.apply_settings(val)
+ key = 'tts_' + self.tts_client_class.name
+ vprefs.set(key, val or {})
def configure(self, data):
ui_settings = get_pref_group('tts').copy()
d = Config(self.tts_client, ui_settings, self.backend_settings, parent=self.parent())
if d.exec_() == QDialog.DialogCode.Accepted:
- self.backend_settings = d.backend_settings
+ s = d.backend_settings
+ self.backend_settings = s
+ self.tts_client.apply_settings(s)
self.settings_changed.emit(d.ui_settings)
else:
self.settings_changed.emit(None)
+
+ def slower(self, data):
+ settings = self.tts_client.change_rate(steps=-1)
+ if settings is not None:
+ self.backend_settings = settings
+
+ def faster(self, data):
+ settings = self.tts_client.change_rate(steps=1)
+ if settings is not None:
+ self.backend_settings = settings
diff --git a/src/pyj/read_book/read_aloud.pyj b/src/pyj/read_book/read_aloud.pyj
index bc263df46e..1add66ce30 100644
--- a/src/pyj/read_book/read_aloud.pyj
+++ b/src/pyj/read_book/read_aloud.pyj
@@ -117,6 +117,8 @@ class ReadAloud:
bar.appendChild(cb(None, 'hourglass', _('Pause reading')))
else:
bar.appendChild(cb('play', 'play', _('Start reading') if self.state is STOPPED else _('Resume reading')))
+ bar.appendChild(cb('slower', 'slower', _('Slow down speech')))
+ bar.appendChild(cb('faster', 'faster', _('Speed up speech')))
bar.appendChild(cb('configure', 'cogs', _('Configure Read aloud')))
bar.appendChild(cb('hide', 'close', _('Close Read aloud')))
if self.state is not WAITING_FOR_PLAY_TO_START:
@@ -135,6 +137,12 @@ class ReadAloud:
self.waiting_for_configure = True
ui_operations.tts('configure')
+ def slower(self):
+ ui_operations.tts('slower')
+
+ def faster(self):
+ ui_operations.tts('faster')
+
def play(self):
if self.state is PAUSED:
ui_operations.tts('resume_after_configure' if self.waiting_for_configure else 'resume')