From 52e855b130dc8bd1e23227cdc92b07b9106960ab Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 10 Dec 2020 08:59:50 +0530 Subject: [PATCH] Add slower and faster buttons to the TTS bar --- imgsrc/srv/faster.svg | 3 +++ imgsrc/srv/slower.svg | 3 +++ src/calibre/gui2/tts/linux.py | 19 +++++++++++++- src/calibre/gui2/tts/linux_config.py | 8 +++--- src/calibre/gui2/viewer/tts.py | 39 ++++++++++++++++++---------- src/pyj/read_book/read_aloud.pyj | 8 ++++++ 6 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 imgsrc/srv/faster.svg create mode 100644 imgsrc/srv/slower.svg 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')