mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Start work on configuring TTS backends
This commit is contained in:
parent
289a5977af
commit
380f49b8fc
@ -13,13 +13,13 @@ from .errors import TTSSystemUnavailable
|
||||
class Client:
|
||||
|
||||
mark_template = '<mark name="{}"/>'
|
||||
name = 'speechd'
|
||||
|
||||
@classmethod
|
||||
def escape_marked_text(cls, text):
|
||||
return prepare_string_for_xml(text)
|
||||
|
||||
def __init__(self, dispatch_on_main_thread):
|
||||
self.create_ssip_client()
|
||||
self.status = {'synthesizing': False, 'paused': False}
|
||||
self.dispatch_on_main_thread = dispatch_on_main_thread
|
||||
self.current_marked_text = None
|
||||
@ -27,6 +27,9 @@ class Client:
|
||||
self.next_cancel_is_for_pause = False
|
||||
self.next_begin_is_for_resume = False
|
||||
self.current_callback = None
|
||||
self.settings_applied = False
|
||||
self.ssip_client = None
|
||||
self.system_default_output_module = None
|
||||
|
||||
def create_ssip_client(self):
|
||||
from speechd.client import Priority, SpawnError, SSIPClient
|
||||
@ -37,12 +40,29 @@ class Client:
|
||||
self.ssip_client.set_priority(Priority.TEXT)
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, 'ssip_client'):
|
||||
if self.ssip_client is not None:
|
||||
self.ssip_client.cancel()
|
||||
self.ssip_client.close()
|
||||
del self.ssip_client
|
||||
self.ssip_client = None
|
||||
shutdown = __del__
|
||||
|
||||
def ensure_state(self, use_ssml=False):
|
||||
if self.ssip_client is None:
|
||||
self.create_ssip_client()
|
||||
if self.system_default_output_module is None:
|
||||
self.system_default_output_module = self.ssip_client.get_output_module()
|
||||
if not self.settings_applied:
|
||||
self.apply_settings()
|
||||
self.set_use_ssml(use_ssml)
|
||||
|
||||
def apply_settings(self, new_settings=None):
|
||||
if self.settings_applied:
|
||||
self.shutdown()
|
||||
self.settings_applied = False
|
||||
self.ensure_state()
|
||||
self.settings_applied = True
|
||||
# TODO: Implement this
|
||||
|
||||
def set_use_ssml(self, on):
|
||||
from speechd.client import DataMode, SSIPCommunicationError
|
||||
mode = DataMode.SSML if on else DataMode.TEXT
|
||||
@ -50,12 +70,12 @@ class Client:
|
||||
self.ssip_client.set_data_mode(mode)
|
||||
except SSIPCommunicationError:
|
||||
self.ssip_client.close()
|
||||
self.create_ssip_client()
|
||||
self.ssip_client.set_data_mode(mode)
|
||||
self.ssip_client = None
|
||||
self.ensure_state(on)
|
||||
|
||||
def speak_simple_text(self, text):
|
||||
self.stop()
|
||||
self.set_use_ssml(False)
|
||||
self.ensure_state(use_ssml=False)
|
||||
self.current_marked_text = self.last_mark = None
|
||||
|
||||
def callback(callback_type, index_mark=None):
|
||||
@ -104,7 +124,7 @@ class Client:
|
||||
self.dispatch_on_main_thread(partial(callback_wrapper, callback_type, index_mark))
|
||||
self.current_callback = cw
|
||||
|
||||
self.set_use_ssml(True)
|
||||
self.ensure_state(use_ssml=True)
|
||||
self.ssip_client.speak(text, callback=self.current_callback)
|
||||
|
||||
def pause(self):
|
||||
@ -131,4 +151,21 @@ class Client:
|
||||
self.current_callback = self.current_marked_text = self.last_mark = None
|
||||
self.next_cancel_is_for_pause = False
|
||||
self.next_begin_is_for_resume = False
|
||||
self.ssip_client.stop()
|
||||
if self.ssip_client is not None:
|
||||
self.ssip_client.stop()
|
||||
|
||||
def config_widget(self, backend_settings, parent):
|
||||
from calibre.gui2.tts.linux_config import Widget
|
||||
return Widget(self, backend_settings, parent)
|
||||
|
||||
def get_voice_data(self):
|
||||
ans = getattr(self, 'voice_data', None)
|
||||
if ans is None:
|
||||
self.ensure_state()
|
||||
ans = self.voice_data = {}
|
||||
output_module = self.ssip_client.get_output_module()
|
||||
for om in self.ssip_client.list_output_modules():
|
||||
self.ssip_client.set_output_module(om)
|
||||
ans[om] = tuple(self.ssip_client.list_synthesis_voices())
|
||||
self.ssip_client.set_output_module(output_module)
|
||||
return ans
|
||||
|
72
src/calibre/gui2/tts/linux_config.py
Normal file
72
src/calibre/gui2/tts/linux_config.py
Normal file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QAbstractTableModel, QComboBox, QFontMetrics, QFormLayout, Qt, QTableView,
|
||||
QWidget
|
||||
)
|
||||
|
||||
from calibre.gui2.preferences.look_feel import BusyCursor
|
||||
|
||||
|
||||
class VoicesModel(QAbstractTableModel):
|
||||
|
||||
def __init__(self, voice_data, default_output_module, parent=None):
|
||||
super().__init__(parent)
|
||||
self.voice_data = voice_data
|
||||
self.current_voices = voice_data[default_output_module]
|
||||
self.column_headers = (_('Name'), _('Language'), _('Variant'))
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return len(self.current_voices)
|
||||
|
||||
def columnCount(self, parent=None):
|
||||
return 3
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
|
||||
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
|
||||
return self.column_headers[section]
|
||||
return super().headerData(section, orientation, role)
|
||||
|
||||
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
row = index.row()
|
||||
try:
|
||||
data = self.current_voices[row]
|
||||
return data[index.column()]
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
def change_output_module(self, om):
|
||||
self.beginResetModel()
|
||||
try:
|
||||
self.current_voices = self.voice_data[om]
|
||||
finally:
|
||||
self.endResetModel()
|
||||
|
||||
|
||||
class Widget(QWidget):
|
||||
|
||||
def __init__(self, tts_client, initial_backend_settings, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.l = l = QFormLayout(self)
|
||||
self.tts_client = tts_client
|
||||
|
||||
self.output_modules = om = QComboBox(self)
|
||||
with BusyCursor():
|
||||
self.voice_data = self.tts_client.get_voice_data()
|
||||
self.system_default_output_module = self.tts_client.system_default_output_module
|
||||
om.addItem(_('System default'), self.system_default_output_module)
|
||||
l.addRow(_('Speech synthesizer:'), om)
|
||||
|
||||
self.voices = v = QTableView(self)
|
||||
self.voices_model = VoicesModel(self.voice_data, self.system_default_output_module, parent=v)
|
||||
v.setModel(self.voices_model)
|
||||
v.horizontalHeader().resizeSection(0, QFontMetrics(self.font()).averageCharWidth() * 30)
|
||||
l.addRow(v)
|
||||
|
||||
def sizeHint(self):
|
||||
ans = super().sizeHint()
|
||||
ans.setHeight(max(ans.height(), 600))
|
||||
return ans
|
@ -8,6 +8,7 @@ from .common import Event, EventType
|
||||
class Client:
|
||||
|
||||
mark_template = '[[sync 0x{:x}]]'
|
||||
name = 'nsss'
|
||||
|
||||
@classmethod
|
||||
def escape_marked_text(cls, text):
|
||||
|
@ -13,6 +13,7 @@ from .common import Event, EventType
|
||||
class Client:
|
||||
|
||||
mark_template = '<bookmark mark="{}"/>'
|
||||
name = 'sapi'
|
||||
|
||||
@classmethod
|
||||
def escape_marked_text(cls, text):
|
||||
|
@ -22,3 +22,8 @@ def get_session_pref(name, default=None, group='standalone_misc_settings'):
|
||||
sd = vprefs['session_data']
|
||||
g = sd.get(group, {}) if group else sd
|
||||
return g.get(name, default)
|
||||
|
||||
|
||||
def get_pref_group(name):
|
||||
sd = vprefs['session_data']
|
||||
return sd.get(name) or {}
|
||||
|
@ -2,9 +2,30 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from PyQt5.Qt import QObject, pyqtSignal
|
||||
from PyQt5.Qt import QObject, QVBoxLayout, pyqtSignal
|
||||
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.viewer.config import get_pref_group, vprefs
|
||||
from calibre.gui2.widgets2 import Dialog
|
||||
|
||||
|
||||
class Config(Dialog):
|
||||
|
||||
def __init__(self, tts_client, ui_settings, backend_settings, parent):
|
||||
self.tts_client = tts_client
|
||||
self.ui_settings = ui_settings
|
||||
self.backend_settings = backend_settings
|
||||
Dialog.__init__(self, _('Configure Read aloud'), 'read-aloud-config', parent, prefs=vprefs)
|
||||
|
||||
def setup_ui(self):
|
||||
self.l = l = QVBoxLayout(self)
|
||||
self.config_widget = self.tts_client.config_widget(self.backend_settings, self)
|
||||
l.addWidget(self.config_widget)
|
||||
l.addWidget(self.bb)
|
||||
|
||||
def accept(self):
|
||||
self.backend_settings = self.config_widget.backend_settings
|
||||
return super().accept()
|
||||
|
||||
|
||||
def add_markup(text_parts):
|
||||
@ -23,6 +44,7 @@ class TTS(QObject):
|
||||
|
||||
dispatch_on_main_thread_signal = pyqtSignal(object)
|
||||
event_received = pyqtSignal(object, object)
|
||||
settings_changed = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
@ -80,3 +102,11 @@ class TTS(QObject):
|
||||
|
||||
def stop(self, data):
|
||||
self.tts_client.stop()
|
||||
|
||||
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())
|
||||
if d.exec_() == d.DialogCode.Accepted:
|
||||
vprefs[key] = d.backend_settings
|
||||
self.settings_changed.emit(d.ui_settings)
|
||||
|
@ -351,8 +351,10 @@ class WebPage(QWebEnginePage):
|
||||
QApplication.instance().clipboard().setMimeData(md)
|
||||
|
||||
def javaScriptConsoleMessage(self, level, msg, linenumber, source_id):
|
||||
prefix = {QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel: 'INFO', QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel: 'WARNING'}.get(
|
||||
level, 'ERROR')
|
||||
prefix = {
|
||||
QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel: 'INFO',
|
||||
QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel: 'WARNING'
|
||||
}.get(level, 'ERROR')
|
||||
prints('%s: %s:%s: %s' % (prefix, source_id, linenumber, msg), file=sys.stderr)
|
||||
try:
|
||||
sys.stderr.flush()
|
||||
|
@ -117,6 +117,7 @@ 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('configure', 'cogs', _('Configure Read aloud')))
|
||||
bar.appendChild(cb('hide', 'close', _('Close Read aloud')))
|
||||
if self.state is not WAITING_FOR_PLAY_TO_START:
|
||||
notes_container = bar_container.lastChild
|
||||
@ -129,6 +130,10 @@ class ReadAloud:
|
||||
else:
|
||||
notes_container.textContent = _('Tap/click on a word to continue from there')
|
||||
|
||||
def configure(self):
|
||||
self.pause()
|
||||
ui_operations.tts('configure')
|
||||
|
||||
def play(self):
|
||||
if self.state is PAUSED:
|
||||
ui_operations.tts('resume')
|
||||
@ -194,8 +199,7 @@ class ReadAloud:
|
||||
self.state = PLAYING
|
||||
elif which is 'end':
|
||||
self.state = STOPPED
|
||||
if not self.view.show_next_spine_item():
|
||||
self.hide()
|
||||
self.view.show_next_spine_item()
|
||||
|
||||
def send_message(self, type, **kw):
|
||||
self.view.iframe_wrapper.send_message('tts', type=type, **kw)
|
||||
|
@ -69,6 +69,7 @@ defaults = {
|
||||
'selection_bar_actions': v"['copy', 'lookup', 'highlight', 'remove_highlight', 'search_net', 'clear']",
|
||||
'selection_bar_quick_highlights': v"[]",
|
||||
'skipped_dialogs': v'{}',
|
||||
'tts': v'{}',
|
||||
}
|
||||
|
||||
is_local_setting = {
|
||||
@ -98,6 +99,7 @@ is_local_setting = {
|
||||
'standalone_recently_opened': True,
|
||||
'user_stylesheet': True,
|
||||
'highlight_style': True,
|
||||
'tts': True,
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user