mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Workaround for QTextToSpeech crashes if more than one instance is created/destroyed
This commit is contained in:
parent
0fb5249ff0
commit
75d3714b41
@ -125,7 +125,7 @@ class Voices(QTreeWidget):
|
|||||||
self.system_default_voice = Voice()
|
self.system_default_voice = Voice()
|
||||||
|
|
||||||
def sizeHint(self) -> QSize:
|
def sizeHint(self) -> QSize:
|
||||||
return QSize(400, 600)
|
return QSize(400, 500)
|
||||||
|
|
||||||
def set_voices(self, all_voices: tuple[Voice, ...], current_voice: str, engine_metadata: EngineMetadata) -> None:
|
def set_voices(self, all_voices: tuple[Voice, ...], current_voice: str, engine_metadata: EngineMetadata) -> None:
|
||||||
self.clear()
|
self.clear()
|
||||||
@ -176,7 +176,7 @@ class EngineSpecificConfig(QWidget):
|
|||||||
l.addRow(_('&Output module:'), om)
|
l.addRow(_('&Output module:'), om)
|
||||||
self.engine_name = ''
|
self.engine_name = ''
|
||||||
om.currentIndexChanged.connect(self.rebuild_voices)
|
om.currentIndexChanged.connect(self.rebuild_voices)
|
||||||
self.engine_instances = {}
|
self.default_output_modules = {}
|
||||||
self.voice_data = {}
|
self.voice_data = {}
|
||||||
self.engine_specific_settings = {}
|
self.engine_specific_settings = {}
|
||||||
self.rate = r = FloatSlider(parent=self)
|
self.rate = r = FloatSlider(parent=self)
|
||||||
@ -199,12 +199,11 @@ class EngineSpecificConfig(QWidget):
|
|||||||
self.engine_specific_settings[self.engine_name] = self.as_settings()
|
self.engine_specific_settings[self.engine_name] = self.as_settings()
|
||||||
self.engine_name = engine_name
|
self.engine_name = engine_name
|
||||||
metadata = available_engines()[engine_name]
|
metadata = available_engines()[engine_name]
|
||||||
if engine_name not in self.engine_instances:
|
tts = create_tts_backend(force_engine=engine_name)
|
||||||
self.engine_instances[engine_name] = tts = create_tts_backend(force_engine=engine_name)
|
if engine_name not in self.voice_data:
|
||||||
self.voice_data[engine_name] = tts.available_voices
|
self.voice_data[engine_name] = tts.available_voices
|
||||||
self.engine_specific_settings[engine_name] = EngineSpecificSettings.create_from_config(engine_name)
|
self.engine_specific_settings[engine_name] = EngineSpecificSettings.create_from_config(engine_name)
|
||||||
else:
|
self.default_output_modules[engine_name] = tts.default_output_module
|
||||||
tts = self.engine_instances[engine_name]
|
|
||||||
self.output_module.blockSignals(True)
|
self.output_module.blockSignals(True)
|
||||||
self.output_module.clear()
|
self.output_module.clear()
|
||||||
if metadata.has_multiple_output_modules:
|
if metadata.has_multiple_output_modules:
|
||||||
@ -256,7 +255,7 @@ class EngineSpecificConfig(QWidget):
|
|||||||
metadata = available_engines()[self.engine_name]
|
metadata = available_engines()[self.engine_name]
|
||||||
output_module = self.output_module.currentData() or ''
|
output_module = self.output_module.currentData() or ''
|
||||||
if metadata.has_multiple_output_modules:
|
if metadata.has_multiple_output_modules:
|
||||||
output_module = output_module or self.engine_instances[self.engine_name].default_output_module
|
output_module = output_module or self.default_output_modules[self.engine_name].default_output_module
|
||||||
all_voices = self.voice_data[self.engine_name][output_module]
|
all_voices = self.voice_data[self.engine_name][output_module]
|
||||||
self.voices.set_voices(all_voices, s.voice_name, metadata)
|
self.voices.set_voices(all_voices, s.voice_name, metadata)
|
||||||
|
|
||||||
@ -289,7 +288,12 @@ class ConfigDialog(Dialog):
|
|||||||
l.addWidget(ec)
|
l.addWidget(ec)
|
||||||
l.addWidget(esc)
|
l.addWidget(esc)
|
||||||
l.addWidget(self.bb)
|
l.addWidget(self.bb)
|
||||||
esc.set_engine(ec.value)
|
self.initial_engine_choice = ec.value
|
||||||
|
esc.set_engine(self.initial_engine_choice)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def engine_changed(self) -> bool:
|
||||||
|
return self.engine_choice.value != self.initial_engine_choice
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
s = self.engine_specific_config.as_settings()
|
s = self.engine_specific_config.as_settings()
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
from qt.core import QAction, QKeySequence, QPlainTextEdit, Qt, QTextCursor, QTextToSpeech, QToolBar
|
from qt.core import QAction, QKeySequence, QPlainTextEdit, QSize, Qt, QTextCursor, QTextToSpeech, QToolBar
|
||||||
|
|
||||||
from calibre.gui2 import Application
|
from calibre.gui2 import Application
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
@ -78,7 +78,7 @@ class MainWindow(MainWindow):
|
|||||||
self.display.setTextCursor(c)
|
self.display.setTextCursor(c)
|
||||||
else:
|
else:
|
||||||
self.play_action.setChecked(True)
|
self.play_action.setChecked(True)
|
||||||
self.stop_action.setEnabled(state in (QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing))
|
self.stop_action.setEnabled(state in (QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused))
|
||||||
|
|
||||||
def toggled(self):
|
def toggled(self):
|
||||||
if self.play_action.isChecked():
|
if self.play_action.isChecked():
|
||||||
@ -100,6 +100,9 @@ class MainWindow(MainWindow):
|
|||||||
c.movePosition(QTextCursor.MoveOperation.WordRight, QTextCursor.MoveMode.KeepAnchor)
|
c.movePosition(QTextCursor.MoveOperation.WordRight, QTextCursor.MoveMode.KeepAnchor)
|
||||||
self.display.setTextCursor(c)
|
self.display.setTextCursor(c)
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return QSize(500, 400)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = Application([])
|
app = Application([])
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import NamedTuple, TYPE_CHECKING
|
from typing import TYPE_CHECKING, NamedTuple
|
||||||
|
|
||||||
from qt.core import QApplication, QDialog, QObject, QTextToSpeech, QWidget, pyqtSignal
|
from qt.core import QApplication, QDialog, QObject, QTextToSpeech, QWidget, pyqtSignal
|
||||||
|
|
||||||
@ -104,6 +104,11 @@ class Tracker:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ResumeData:
|
||||||
|
is_speaking: bool = True
|
||||||
|
needs_full_resume: bool = False
|
||||||
|
|
||||||
|
|
||||||
class TTSManager(QObject):
|
class TTSManager(QObject):
|
||||||
|
|
||||||
state_changed = pyqtSignal(QTextToSpeech.State)
|
state_changed = pyqtSignal(QTextToSpeech.State)
|
||||||
@ -120,7 +125,7 @@ class TTSManager(QObject):
|
|||||||
if self._tts is None:
|
if self._tts is None:
|
||||||
with BusyCursor():
|
with BusyCursor():
|
||||||
from calibre.gui2.tts2.types import create_tts_backend
|
from calibre.gui2.tts2.types import create_tts_backend
|
||||||
self._tts = create_tts_backend(parent=self)
|
self._tts = create_tts_backend()
|
||||||
self._tts.state_changed.connect(self._state_changed)
|
self._tts.state_changed.connect(self._state_changed)
|
||||||
self._tts.saying.connect(self._saying)
|
self._tts.saying.connect(self._saying)
|
||||||
return self._tts
|
return self._tts
|
||||||
@ -144,12 +149,13 @@ class TTSManager(QObject):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def resume_after(self):
|
def resume_after(self):
|
||||||
is_speaking = self._tts is not None and self.state in (QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused)
|
rd = ResumeData()
|
||||||
|
rd.is_speaking = self._tts is not None and self.state in (QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused)
|
||||||
if self.state is not QTextToSpeech.State.Paused:
|
if self.state is not QTextToSpeech.State.Paused:
|
||||||
self.tts.pause()
|
self.tts.pause()
|
||||||
yield is_speaking
|
yield rd
|
||||||
if is_speaking:
|
if rd.is_speaking:
|
||||||
if self._tts is None:
|
if rd.needs_full_resume:
|
||||||
self.tts.say(self.tracker.resume())
|
self.tts.say(self.tracker.resume())
|
||||||
else:
|
else:
|
||||||
self.tts.resume()
|
self.tts.resume()
|
||||||
@ -162,11 +168,10 @@ class TTSManager(QObject):
|
|||||||
if new_rate != s.rate:
|
if new_rate != s.rate:
|
||||||
s = s._replace(rate=new_rate)
|
s = s._replace(rate=new_rate)
|
||||||
s.save_to_config()
|
s.save_to_config()
|
||||||
with self.resume_after() as is_speaking:
|
with self.resume_after() as rd:
|
||||||
if self._tts is not None:
|
if self._tts is not None:
|
||||||
if is_speaking:
|
rd.needs_full_resume = True
|
||||||
self.tts.stop()
|
self.tts.reload_after_configure()
|
||||||
self._tts = None
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -183,13 +188,16 @@ class TTSManager(QObject):
|
|||||||
p = self
|
p = self
|
||||||
while p is not None and not isinstance(p, QWidget):
|
while p is not None and not isinstance(p, QWidget):
|
||||||
p = p.parent()
|
p = p.parent()
|
||||||
with self.resume_after() as is_speaking:
|
with self.resume_after() as rd:
|
||||||
d = ConfigDialog(parent=p)
|
d = ConfigDialog(parent=p)
|
||||||
if d.exec() == QDialog.DialogCode.Accepted:
|
if d.exec() == QDialog.DialogCode.Accepted and self._tts is not None:
|
||||||
if self._tts is not None:
|
rd.needs_full_resume = True
|
||||||
if is_speaking:
|
if d.engine_changed:
|
||||||
|
if rd.is_speaking:
|
||||||
self.tts.stop()
|
self.tts.stop()
|
||||||
self._tts = None
|
self._tts = None
|
||||||
|
else:
|
||||||
|
self.tts.reload_after_configure()
|
||||||
|
|
||||||
def _state_changed(self, state: QTextToSpeech.State) -> None:
|
def _state_changed(self, state: QTextToSpeech.State) -> None:
|
||||||
self.state = state
|
self.state = state
|
||||||
|
@ -11,8 +11,7 @@ class QtTTSBackend(TTSBackend):
|
|||||||
|
|
||||||
def __init__(self, engine_name: str = '', parent: QObject|None = None):
|
def __init__(self, engine_name: str = '', parent: QObject|None = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._voices = None
|
self._qt_reload_after_configure(engine_name)
|
||||||
self._create_engine(engine_name)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available_voices(self) -> dict[str, tuple[Voice, ...]]:
|
def available_voices(self) -> dict[str, tuple[Voice, ...]]:
|
||||||
@ -43,8 +42,14 @@ class QtTTSBackend(TTSBackend):
|
|||||||
def error_message(self) -> str:
|
def error_message(self) -> str:
|
||||||
return self.tts.errorString()
|
return self.tts.errorString()
|
||||||
|
|
||||||
def _create_engine(self, engine_name: str) -> None:
|
def reload_after_configure(self) -> None:
|
||||||
|
self._qt_reload_after_configure(self.engine_name)
|
||||||
|
|
||||||
|
def _qt_reload_after_configure(self, engine_name: str) -> None:
|
||||||
|
# Bad things happen with more than one QTextToSpeech instance
|
||||||
s = {}
|
s = {}
|
||||||
|
self._voices = None
|
||||||
|
new_backend = not hasattr(self, 'tts')
|
||||||
if engine_name:
|
if engine_name:
|
||||||
settings = EngineSpecificSettings.create_from_config(engine_name)
|
settings = EngineSpecificSettings.create_from_config(engine_name)
|
||||||
if settings.audio_device_id:
|
if settings.audio_device_id:
|
||||||
@ -52,9 +57,15 @@ class QtTTSBackend(TTSBackend):
|
|||||||
if bytes(x.id) == settings.audio_device_id.id:
|
if bytes(x.id) == settings.audio_device_id.id:
|
||||||
s['audioDevice'] = x
|
s['audioDevice'] = x
|
||||||
break
|
break
|
||||||
self.tts = QTextToSpeech(engine_name, s, self)
|
if new_backend:
|
||||||
|
self.tts = QTextToSpeech(engine_name, s, self)
|
||||||
|
else:
|
||||||
|
self.tts.setEngine(engine_name, s)
|
||||||
else:
|
else:
|
||||||
self.tts = QTextToSpeech(self)
|
if new_backend:
|
||||||
|
self.tts = QTextToSpeech(self)
|
||||||
|
else:
|
||||||
|
self.tts.setEngine('')
|
||||||
engine_name = self.tts.engine()
|
engine_name = self.tts.engine()
|
||||||
settings = EngineSpecificSettings.create_from_config(engine_name)
|
settings = EngineSpecificSettings.create_from_config(engine_name)
|
||||||
if settings.audio_device_id:
|
if settings.audio_device_id:
|
||||||
@ -63,6 +74,9 @@ class QtTTSBackend(TTSBackend):
|
|||||||
s['audioDevice'] = x
|
s['audioDevice'] = x
|
||||||
self.tts = QTextToSpeech(engine_name, s, self)
|
self.tts = QTextToSpeech(engine_name, s, self)
|
||||||
break
|
break
|
||||||
|
if new_backend:
|
||||||
|
self.tts.sayingWord.connect(self._saying_word)
|
||||||
|
self.tts.stateChanged.connect(self._state_changed)
|
||||||
|
|
||||||
self.tts.setRate(max(-1, min(float(settings.rate), 1)))
|
self.tts.setRate(max(-1, min(float(settings.rate), 1)))
|
||||||
self.tts.setPitch(max(-1, min(float(settings.pitch), 1)))
|
self.tts.setPitch(max(-1, min(float(settings.pitch), 1)))
|
||||||
@ -73,9 +87,10 @@ class QtTTSBackend(TTSBackend):
|
|||||||
if v.name() == settings.voice_name:
|
if v.name() == settings.voice_name:
|
||||||
self.tts.setVoice(v)
|
self.tts.setVoice(v)
|
||||||
break
|
break
|
||||||
self.tts.sayingWord.connect(self._saying_word)
|
|
||||||
self.tts.stateChanged.connect(self.state_changed.emit)
|
|
||||||
self._current_settings = settings
|
self._current_settings = settings
|
||||||
|
|
||||||
def _saying_word(self, word: str, utterance_id: int, start: int, length: int) -> None:
|
def _saying_word(self, word: str, utterance_id: int, start: int, length: int) -> None:
|
||||||
self.saying.emit(start, length)
|
self.saying.emit(start, length)
|
||||||
|
|
||||||
|
def _state_changed(self, state: QTextToSpeech.State) -> None:
|
||||||
|
self.state_changed.emit(state)
|
||||||
|
@ -7,7 +7,7 @@ from enum import Enum, auto
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Literal, NamedTuple
|
from typing import Literal, NamedTuple
|
||||||
|
|
||||||
from qt.core import QLocale, QObject, QTextToSpeech, QVoice, pyqtSignal
|
from qt.core import QApplication, QLocale, QObject, QTextToSpeech, QVoice, pyqtSignal
|
||||||
|
|
||||||
from calibre.constants import islinux, ismacos, iswindows
|
from calibre.constants import islinux, ismacos, iswindows
|
||||||
from calibre.utils.config import JSONConfig
|
from calibre.utils.config import JSONConfig
|
||||||
@ -208,20 +208,30 @@ class TTSBackend(QObject):
|
|||||||
def error_message(self) -> str:
|
def error_message(self) -> str:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def reload_after_configure(self) -> str:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def create_tts_backend(parent: QObject|None = None, force_engine: str | None = None) -> TTSBackend:
|
|
||||||
|
engine_instances: dict[str, TTSBackend] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def create_tts_backend(force_engine: str | None = None) -> TTSBackend:
|
||||||
prefs = load_config()
|
prefs = load_config()
|
||||||
engine_name = prefs.get('engine', '') if force_engine is None else force_engine
|
engine_name = prefs.get('engine', '') if force_engine is None else force_engine
|
||||||
engine_name = engine_name or default_engine_name()
|
engine_name = engine_name or default_engine_name()
|
||||||
if engine_name not in available_engines():
|
if engine_name not in available_engines():
|
||||||
engine_name = default_engine_name()
|
engine_name = default_engine_name()
|
||||||
|
|
||||||
if engine_name == 'speechd':
|
if engine_name == 'speechd':
|
||||||
from calibre.gui2.tts2.speechd import SpeechdTTSBackend
|
if engine_name not in engine_instances:
|
||||||
ans = SpeechdTTSBackend(engine_name, parent)
|
from calibre.gui2.tts2.speechd import SpeechdTTSBackend
|
||||||
|
engine_instances[engine_name] = SpeechdTTSBackend(engine_name, QApplication.instance())
|
||||||
|
ans = engine_instances[engine_name]
|
||||||
else:
|
else:
|
||||||
if engine_name not in available_engines():
|
if 'qt' not in engine_instances:
|
||||||
engine_name = '' # let Qt pick the engine
|
# Bad things happen with more than one QTextToSpeech instance
|
||||||
from calibre.gui2.tts2.qt import QtTTSBackend
|
from calibre.gui2.tts2.qt import QtTTSBackend
|
||||||
ans = QtTTSBackend(engine_name, parent)
|
engine_instances['qt'] = QtTTSBackend(engine_name if engine_name in available_engines() else '', QApplication.instance())
|
||||||
|
ans = engine_instances['qt']
|
||||||
|
if ans.engine_name != engine_name:
|
||||||
|
ans._qt_reload_after_configure(engine_name if engine_name in available_engines() else '')
|
||||||
return ans
|
return ans
|
||||||
|
Loading…
x
Reference in New Issue
Block a user