diff --git a/src/calibre/gui2/tts/linux.py b/src/calibre/gui2/tts/linux.py index df0ef2813a..057fd303ee 100644 --- a/src/calibre/gui2/tts/linux.py +++ b/src/calibre/gui2/tts/linux.py @@ -19,8 +19,9 @@ class Client: def escape_marked_text(cls, text): return prepare_string_for_xml(text) - def __init__(self, dispatch_on_main_thread=lambda f: f()): + def __init__(self, settings, dispatch_on_main_thread=lambda f: f()): self.status = {'synthesizing': False, 'paused': False} + self.settings = settings self.dispatch_on_main_thread = dispatch_on_main_thread self.current_marked_text = None self.last_mark = None @@ -59,12 +60,22 @@ class Client: self.set_use_ssml(use_ssml) def apply_settings(self, new_settings=None): + if new_settings is not None: + self.settings = new_settings if self.settings_applied: self.shutdown() self.settings_applied = False self.ensure_state() self.settings_applied = True - # TODO: Implement this + om = self.settings.get('output_module') + if om: + self.ssip_client.set_output_module(om) + voice = self.settings.get('voice') + if voice: + self.ssip_client.set_synthesis_voice(voice[0]) + rate = self.settings.get('rate') + if rate: + self.ssip_client.set_rate(rate) def set_use_ssml(self, on): from speechd.client import DataMode, SSIPCommunicationError @@ -148,6 +159,7 @@ class Client: text = self.current_marked_text else: text = self.current_marked_text[idx:] + self.ensure_state(use_ssml=True) self.ssip_client.speak(text, callback=self.current_callback) def stop(self): diff --git a/src/calibre/gui2/tts/linux_config.py b/src/calibre/gui2/tts/linux_config.py index 14b32fb9a0..4437642a72 100644 --- a/src/calibre/gui2/tts/linux_config.py +++ b/src/calibre/gui2/tts/linux_config.py @@ -4,8 +4,8 @@ from contextlib import suppress from PyQt5.Qt import ( - QAbstractItemView, QAbstractTableModel, QComboBox, QFontMetrics, QFormLayout, Qt, - QTableView, QWidget, QSortFilterProxyModel, QItemSelectionModel + QAbstractItemView, QAbstractTableModel, QComboBox, QFontMetrics, QFormLayout, + QItemSelectionModel, QSortFilterProxyModel, QSlider, Qt, QTableView, QWidget ) from calibre.gui2.preferences.look_feel import BusyCursor @@ -57,14 +57,30 @@ class VoicesModel(QAbstractTableModel): finally: self.endResetModel() + def index_for_voice(self, v): + r = 0 + if v != self.system_default_voice: + try: + idx = self.current_voices.index(v) + except Exception: + return + r = idx + 1 + return self.index(r, 0) + class Widget(QWidget): - def __init__(self, tts_client, initial_backend_settings, parent=None): + def __init__(self, tts_client, initial_backend_settings=None, parent=None): QWidget.__init__(self, parent) self.l = l = QFormLayout(self) self.tts_client = tts_client + self.speed = s = QSlider(Qt.Orientation.Horizontal, self) + s.setMinimumWidth(200) + l.addRow(_('&Speed of speech:'), s) + s.setRange(-100, 100) + s.setSingleStep(5) + self.output_modules = om = QComboBox(self) with BusyCursor(): self.voice_data = self.tts_client.get_voice_data() @@ -72,7 +88,7 @@ class Widget(QWidget): om.addItem(_('System default'), self.system_default_output_module) for x in self.voice_data: om.addItem(x, x) - l.addRow(_('Speech synthesizer:'), om) + l.addRow(_('Speech s&ynthesizer:'), om) self.voices = v = QTableView(self) self.voices_model = VoicesModel(self.voice_data, self.system_default_output_module, parent=v) @@ -88,8 +104,12 @@ class Widget(QWidget): v.sortByColumn(0, Qt.SortOrder.AscendingOrder) om.currentIndexChanged.connect(self.output_module_changed) l.addRow(v) + self.backend_settings = initial_backend_settings or {} + def restore_to_defaults(self): + self.backend_settings = {} + def sizeHint(self): ans = super().sizeHint() ans.setHeight(max(ans.height(), 600)) @@ -127,6 +147,15 @@ class Widget(QWidget): om = self.selected_output_module self.voices_model.change_output_module(om) + @property + def rate(self): + return self.speed.value() + + @rate.setter + def rate(self, val): + val = int(val or 0) + self.speed.setValue(val) + @property def backend_settings(self): ans = {} @@ -136,6 +165,9 @@ class Widget(QWidget): voice = self.selected_voice if voice != VoicesModel.system_default_voice: ans['voice'] = voice + rate = self.rate + if rate: + ans['rate'] = rate return ans @backend_settings.setter @@ -144,6 +176,7 @@ class Widget(QWidget): self.selected_output_module = om voice = val.get('voice') or VoicesModel.system_default_voice self.selected_voice = voice + self.rate = val.get('rate') or 0 if __name__ == '__main__': @@ -154,3 +187,4 @@ if __name__ == '__main__': w = Widget(c, {}) w.show() app.exec_() + print(w.backend_settings) diff --git a/src/calibre/gui2/tts/macos.py b/src/calibre/gui2/tts/macos.py index 143ffdc59b..c57ec9cd5e 100644 --- a/src/calibre/gui2/tts/macos.py +++ b/src/calibre/gui2/tts/macos.py @@ -14,7 +14,7 @@ class Client: def escape_marked_text(cls, text): return text.replace('[[', ' [ [ ').replace(']]', ' ] ] ') - def __init__(self, dispatch_on_main_thread): + def __init__(self, settings, dispatch_on_main_thread): from calibre_extensions.cocoa import NSSpeechSynthesizer self.nsss = NSSpeechSynthesizer(self.handle_message) self.current_callback = None diff --git a/src/calibre/gui2/tts/windows.py b/src/calibre/gui2/tts/windows.py index 61b9fe6d2e..4b6a884a83 100644 --- a/src/calibre/gui2/tts/windows.py +++ b/src/calibre/gui2/tts/windows.py @@ -19,7 +19,7 @@ class Client: def escape_marked_text(cls, text): return prepare_string_for_xml(text) - def __init__(self, dispatch_on_main_thread): + def __init__(self, settings, 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) diff --git a/src/calibre/gui2/viewer/tts.py b/src/calibre/gui2/viewer/tts.py index 5c712e9a3a..dbbc0bb72e 100644 --- a/src/calibre/gui2/viewer/tts.py +++ b/src/calibre/gui2/viewer/tts.py @@ -2,7 +2,7 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2020, Kovid Goyal -from PyQt5.Qt import QObject, QVBoxLayout, pyqtSignal +from PyQt5.Qt import QDialogButtonBox, QObject, QVBoxLayout, pyqtSignal from calibre.gui2 import error_dialog from calibre.gui2.viewer.config import get_pref_group, vprefs @@ -22,6 +22,12 @@ class Config(Dialog): self.config_widget = self.tts_client.config_widget(self.backend_settings, self) l.addWidget(self.config_widget) l.addWidget(self.bb) + self.config_widget.restore_to_defaults + b = self.bb.addButton(QDialogButtonBox.StandardButton.RestoreDefaults) + b.clicked.connect(self.restore_to_defaults) + + def restore_to_defaults(self): + self.config_widget.restore_to_defaults() def accept(self): self.backend_settings = self.config_widget.backend_settings @@ -62,7 +68,7 @@ class TTS(QObject): def tts_client(self): if self._tts_client is None: from calibre.gui2.tts.implementation import Client - self._tts_client = Client(self.dispatch_on_main_thread_signal.emit) + self._tts_client = Client(self.backend_settings, self.dispatch_on_main_thread_signal.emit) return self._tts_client def shutdown(self): @@ -103,10 +109,23 @@ class TTS(QObject): def stop(self, data): self.tts_client.stop() + @property + def backend_settings(self): + from calibre.gui2.tts.implementation import Client + key = 'tts_' + Client.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) + def configure(self, data): ui_settings = get_pref_group('tts').copy() - key = 'tts_' + self.tts_client.name - d = Config(self.tts_client, ui_settings, vprefs.get(key) or {}, parent=self.parent()) + d = Config(self.tts_client, ui_settings, self.backend_settings, parent=self.parent()) if d.exec_() == d.DialogCode.Accepted: - vprefs[key] = d.backend_settings + self.backend_settings = d.backend_settings self.settings_changed.emit(d.ui_settings)