mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement persistence for engine settings
This commit is contained in:
parent
16f7ddb416
commit
bf47feab54
@ -53,11 +53,11 @@ class QtTTSBackend(QObject):
|
||||
saying = pyqtSignal(int, int)
|
||||
state_changed = pyqtSignal(QTextToSpeech.State)
|
||||
|
||||
def __init__(self, engine_name: str = '', settings: EngineSpecificSettings = EngineSpecificSettings(), parent: QObject|None = None):
|
||||
def __init__(self, engine_name: str = '', parent: QObject|None = None):
|
||||
super().__init__(parent)
|
||||
self.tracker = Tracker()
|
||||
self._voices = None
|
||||
self.apply_settings(engine_name, settings)
|
||||
self._create_engine(engine_name)
|
||||
|
||||
@property
|
||||
def available_voices(self) -> dict[str, tuple[Voice, ...]]:
|
||||
@ -65,32 +65,14 @@ class QtTTSBackend(QObject):
|
||||
self._voices = tuple(map(qvoice_to_voice, self.tts.availableVoices()))
|
||||
return {'': self._voices}
|
||||
|
||||
def apply_settings(self, engine_name: str, settings: EngineSpecificSettings) -> None:
|
||||
s = {}
|
||||
if settings.audio_device_id:
|
||||
for x in QMediaDevices.audioOutputs():
|
||||
if bytes(x.id) == settings.audio_device_id.id:
|
||||
s['audioDevice'] = x
|
||||
break
|
||||
self.tts = QTextToSpeech(engine_name, s, self)
|
||||
self.tts.setRate(max(-1, min(float(settings.rate), 1)))
|
||||
self.tts.setPitch(max(-1, min(float(settings.pitch), 1)))
|
||||
if settings.volume is not None:
|
||||
self.tts.setVolume(max(0, min(float(settings.volume), 1)))
|
||||
if settings.voice_name:
|
||||
for v in self.availableVoices():
|
||||
if v.name() == settings.voice_name:
|
||||
self.setVoice(v)
|
||||
break
|
||||
self.tts.sayingWord.connect(self._saying_word)
|
||||
self.tts.stateChanged.connect(self.state_changed.emit)
|
||||
|
||||
def change_rate(self, steps: int = 1) -> bool:
|
||||
current = self.tts.rate()
|
||||
new_rate = max(-1, min(current + 0.2 * steps, 1))
|
||||
if current == new_rate:
|
||||
return False
|
||||
self.tts.setRate(new_rate)
|
||||
self._current_settings = self._current_settings._replace(rate=new_rate)
|
||||
self._current_settings.save_to_config()
|
||||
return True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
@ -117,6 +99,40 @@ class QtTTSBackend(QObject):
|
||||
def error_message(self) -> str:
|
||||
return self.tts.errorString()
|
||||
|
||||
def _create_engine(self, engine_name: str) -> None:
|
||||
s = {}
|
||||
if engine_name:
|
||||
settings = EngineSpecificSettings.create_from_config(engine_name)
|
||||
if settings.audio_device_id:
|
||||
for x in QMediaDevices.audioOutputs():
|
||||
if bytes(x.id) == settings.audio_device_id.id:
|
||||
s['audioDevice'] = x
|
||||
break
|
||||
self.tts = QTextToSpeech(engine_name, s, self)
|
||||
else:
|
||||
self.tts = QTextToSpeech(self)
|
||||
engine_name = self.tts.engine()
|
||||
settings = EngineSpecificSettings.create_from_config(engine_name)
|
||||
if settings.audio_device_id:
|
||||
for x in QMediaDevices.audioOutputs():
|
||||
if bytes(x.id) == settings.audio_device_id.id:
|
||||
s['audioDevice'] = x
|
||||
self.tts = QTextToSpeech(engine_name, s, self)
|
||||
break
|
||||
|
||||
self.tts.setRate(max(-1, min(float(settings.rate), 1)))
|
||||
self.tts.setPitch(max(-1, min(float(settings.pitch), 1)))
|
||||
if settings.volume is not None:
|
||||
self.tts.setVolume(max(0, min(float(settings.volume), 1)))
|
||||
if settings.voice_name:
|
||||
for v in self.availableVoices():
|
||||
if v.name() == settings.voice_name:
|
||||
self.setVoice(v)
|
||||
break
|
||||
self.tts.sayingWord.connect(self._saying_word)
|
||||
self.tts.stateChanged.connect(self.state_changed.emit)
|
||||
self._current_settings = settings
|
||||
|
||||
def _saying_word(self, word: str, utterance_id: int, start: int, length: int) -> None:
|
||||
x = self.tracker.mark_word(start, length)
|
||||
if x is not None:
|
||||
|
@ -43,7 +43,7 @@ class SpeechdTTSBackend(QObject):
|
||||
|
||||
_event_signal = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, engine_name: str = '', settings: EngineSpecificSettings = EngineSpecificSettings(), parent: QObject|None = None):
|
||||
def __init__(self, engine_name: str = '', parent: QObject|None = None):
|
||||
super().__init__(parent)
|
||||
self._last_error = ''
|
||||
self._state = QTextToSpeech.State.Ready
|
||||
@ -55,7 +55,7 @@ class SpeechdTTSBackend(QObject):
|
||||
self._ssip_client: SSIPClient | None = None
|
||||
self._event_signal.connect(self._update_status, type=Qt.ConnectionType.QueuedConnection)
|
||||
self._current_marked_text = self._last_mark = None
|
||||
self.apply_settings(engine_name, settings)
|
||||
self._apply_settings(EngineSpecificSettings.create_from_config(engine_name))
|
||||
|
||||
@property
|
||||
def available_voices(self) -> dict[str, tuple[Voice, ...]]:
|
||||
@ -66,12 +66,6 @@ class SpeechdTTSBackend(QObject):
|
||||
self._set_error(str(e))
|
||||
return self._voices or {}
|
||||
|
||||
def apply_settings(self, engine_name: str, settings: EngineSpecificSettings) -> None:
|
||||
try:
|
||||
self._apply_settings(settings)
|
||||
except Exception as err:
|
||||
self._set_error(str(err))
|
||||
|
||||
def change_rate(self, steps: int = 1) -> bool:
|
||||
current = self._current_settings.rate
|
||||
new_rate = max(-1, min(current + 0.2 * steps, 1))
|
||||
@ -83,6 +77,7 @@ class SpeechdTTSBackend(QObject):
|
||||
self._set_error(str(e))
|
||||
return False
|
||||
self._current_settings = self._current_settings._replace(rate=new_rate)
|
||||
self._current_settings.save_to_config()
|
||||
return True
|
||||
|
||||
def stop(self) -> None:
|
||||
@ -168,16 +163,20 @@ class SpeechdTTSBackend(QObject):
|
||||
def _apply_settings(self, settings: EngineSpecificSettings) -> bool:
|
||||
if not self._ensure_state():
|
||||
return False
|
||||
self._ssip_client.set_pitch_range(int(max(-1, min(settings.pitch, 1)) * 100))
|
||||
self._ssip_client.set_rate(int(max(-1, min(settings.rate, 1)) * 100))
|
||||
if settings.volume is not None:
|
||||
self._ssip_client.set_volume(-100 + int(max(0, min(settings.volume, 1)) * 200))
|
||||
om = settings.output_module or self._system_default_output_module
|
||||
self._ssip_client.set_output_module(om)
|
||||
if settings.voice_name:
|
||||
self._ssip_client.set_synthesis_voice(settings.voice_name)
|
||||
self._current_settings = settings
|
||||
return True
|
||||
try:
|
||||
self._ssip_client.set_pitch_range(int(max(-1, min(settings.pitch, 1)) * 100))
|
||||
self._ssip_client.set_rate(int(max(-1, min(settings.rate, 1)) * 100))
|
||||
if settings.volume is not None:
|
||||
self._ssip_client.set_volume(-100 + int(max(0, min(settings.volume, 1)) * 200))
|
||||
om = settings.output_module or self._system_default_output_module
|
||||
self._ssip_client.set_output_module(om)
|
||||
if settings.voice_name:
|
||||
self._ssip_client.set_synthesis_voice(settings.voice_name)
|
||||
self._current_settings = settings
|
||||
return True
|
||||
except Exception as e:
|
||||
self._set_error(str(e))
|
||||
return False
|
||||
|
||||
def _get_all_voices_for_all_output_modules(self) -> dict[str, Voice]:
|
||||
ans = {}
|
||||
|
@ -2,6 +2,7 @@
|
||||
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
from contextlib import suppress
|
||||
from enum import Enum, auto
|
||||
from functools import lru_cache
|
||||
from typing import Literal, NamedTuple
|
||||
@ -9,9 +10,16 @@ from typing import Literal, NamedTuple
|
||||
from qt.core import QLocale, QObject, QTextToSpeech, QVoice
|
||||
|
||||
from calibre.constants import islinux, iswindows
|
||||
from calibre.utils.config import JSONConfig
|
||||
from calibre.utils.config_base import tweaks
|
||||
from calibre.utils.localization import canonicalize_lang
|
||||
|
||||
CONFIG_NAME = 'tts'
|
||||
|
||||
@lru_cache(2)
|
||||
def load_config():
|
||||
return JSONConfig(CONFIG_NAME)
|
||||
|
||||
|
||||
class TrackingCapability(Enum):
|
||||
NoTracking: int = auto()
|
||||
@ -65,7 +73,62 @@ class EngineSpecificSettings(NamedTuple):
|
||||
pitch: float = 0 # -1 to 1 0 is normal speech
|
||||
volume: float | None = None # 0 to 1, None is platform default volume
|
||||
output_module: str = ''
|
||||
engine_name: str = ''
|
||||
|
||||
@classmethod
|
||||
def create_from_prefs(cls, engine_name: str, prefs: dict[str, object]) -> 'EngineSpecificSettings':
|
||||
adev = prefs.get('audio_device_id')
|
||||
audio_device_id = None
|
||||
if adev:
|
||||
with suppress(Exception):
|
||||
aid = bytes.fromhex(adev['id'])
|
||||
description = adev['description']
|
||||
audio_device_id = AudioDeviceId(aid, description)
|
||||
rate = 0
|
||||
with suppress(Exception):
|
||||
rate = max(-1, min(float(prefs.get('rate')), 1))
|
||||
pitch = 0
|
||||
with suppress(Exception):
|
||||
pitch = max(-1, min(float(prefs.get('pitch')), 1))
|
||||
volume = None
|
||||
with suppress(Exception):
|
||||
volume = max(0, min(float(prefs.get('volume')), 1))
|
||||
return EngineSpecificSettings(
|
||||
voice_name=str(prefs.get('voice_name', '')),
|
||||
output_module=str(prefs.get('output_module', '')),
|
||||
audio_device_id=audio_device_id, rate=rate, pitch=pitch, volume=volume, engine_name=engine_name)
|
||||
|
||||
@classmethod
|
||||
def create_from_config(cls, engine_name: str) -> 'EngineSpecificSettings':
|
||||
prefs = load_config().get('engines', {}).get(engine_name, {})
|
||||
return cls.create_from_prefs(engine_name, prefs)
|
||||
|
||||
@property
|
||||
def as_dict(self) -> dict[str, object]:
|
||||
ans = {}
|
||||
if self.audio_device_id:
|
||||
ans['audio_device_id'] = {'id': self.audio_device_id.id.hex(), 'description': self.audio_device_id.description}
|
||||
if self.voice_name:
|
||||
ans['voice_name'] = self.voice_name
|
||||
if self.rate:
|
||||
ans['rate'] = self.rate
|
||||
if self.pitch:
|
||||
ans['pitch'] = self.pitch
|
||||
if self.volume is not None:
|
||||
ans['volume'] = self.volume
|
||||
if self.output_module:
|
||||
ans['output_module'] = self.output_module
|
||||
return ans
|
||||
|
||||
def save_to_config(self):
|
||||
prefs = load_config()
|
||||
val = self.as_dict
|
||||
engines = prefs.get('engines', {})
|
||||
if not val:
|
||||
engines.pop(self.engine_name, None)
|
||||
else:
|
||||
engines[self.engine_name] = val
|
||||
prefs['engines'] = engines
|
||||
|
||||
|
||||
@lru_cache(2)
|
||||
@ -102,7 +165,7 @@ def available_engines() -> dict[str, EngineMetadata]:
|
||||
return ans
|
||||
|
||||
|
||||
def create_tts_backend(engine_name: str = '', settings: EngineSpecificSettings = EngineSpecificSettings(), parent: QObject|None = None):
|
||||
def create_tts_backend(engine_name: str = '', parent: QObject|None = None):
|
||||
if engine_name == '':
|
||||
if iswindows and tweaks.get('prefer_winsapi'):
|
||||
engine_name = 'sapi'
|
||||
@ -113,10 +176,10 @@ def create_tts_backend(engine_name: str = '', settings: EngineSpecificSettings =
|
||||
|
||||
if engine_name == 'speechd':
|
||||
from calibre.gui2.tts2.speechd import SpeechdTTSBackend
|
||||
ans = SpeechdTTSBackend(engine_name, settings, parent)
|
||||
ans = SpeechdTTSBackend(engine_name, parent)
|
||||
else:
|
||||
from calibre.gui2.tts2.qt import QtTTSBackend
|
||||
ans = QtTTSBackend(engine_name, settings, parent)
|
||||
ans = QtTTSBackend(engine_name, parent)
|
||||
return ans
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user