mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Abstract the TTSBackend type
This commit is contained in:
parent
661474fd33
commit
5945c0d8b1
@ -4,13 +4,16 @@
|
||||
|
||||
from collections import deque
|
||||
from contextlib import contextmanager
|
||||
from typing import NamedTuple
|
||||
from typing import NamedTuple, TYPE_CHECKING
|
||||
|
||||
from qt.core import QApplication, QDialog, QObject, QTextToSpeech, QWidget, pyqtSignal
|
||||
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.widgets import BusyCursor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from calibre.gui2.tts2.types import TTSBackend
|
||||
|
||||
|
||||
class Utterance(NamedTuple):
|
||||
text: str
|
||||
@ -108,12 +111,12 @@ class TTSManager(QObject):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tts = None
|
||||
self._tts: 'TTSBackend' | None = None
|
||||
self.state = QTextToSpeech.State.Ready
|
||||
self.tracker = Tracker()
|
||||
|
||||
@property
|
||||
def tts(self):
|
||||
def tts(self) -> 'TTSBackend':
|
||||
if self._tts is None:
|
||||
with BusyCursor():
|
||||
from calibre.gui2.tts2.types import create_tts_backend
|
||||
|
@ -2,15 +2,12 @@
|
||||
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
from qt.core import QMediaDevices, QObject, QTextToSpeech, pyqtSignal
|
||||
from qt.core import QMediaDevices, QObject, QTextToSpeech
|
||||
|
||||
from calibre.gui2.tts2.types import EngineSpecificSettings, Voice, qvoice_to_voice
|
||||
from calibre.gui2.tts2.types import EngineSpecificSettings, TTSBackend, Voice, qvoice_to_voice
|
||||
|
||||
|
||||
class QtTTSBackend(QObject):
|
||||
|
||||
saying = pyqtSignal(int, int)
|
||||
state_changed = pyqtSignal(QTextToSpeech.State)
|
||||
class QtTTSBackend(TTSBackend):
|
||||
|
||||
def __init__(self, engine_name: str = '', parent: QObject|None = None):
|
||||
super().__init__(parent)
|
||||
@ -31,18 +28,6 @@ class QtTTSBackend(QObject):
|
||||
def default_output_module(self) -> str:
|
||||
return ''
|
||||
|
||||
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.pause()
|
||||
self.tts.setRate(new_rate)
|
||||
self._current_settings = self._current_settings._replace(rate=new_rate)
|
||||
self._current_settings.save_to_config()
|
||||
self.tts.resume()
|
||||
self.tts.stop(QTextToSpeech.BoundaryHint.Immediate)
|
||||
|
||||
def pause(self) -> None:
|
||||
self.tts.pause()
|
||||
|
||||
|
@ -7,7 +7,7 @@ from enum import Enum, auto
|
||||
from functools import lru_cache
|
||||
from typing import Literal, NamedTuple
|
||||
|
||||
from qt.core import QLocale, QObject, QTextToSpeech, QVoice
|
||||
from qt.core import QLocale, QObject, QTextToSpeech, QVoice, pyqtSignal
|
||||
|
||||
from calibre.constants import islinux, ismacos, iswindows
|
||||
from calibre.utils.config import JSONConfig
|
||||
@ -183,7 +183,33 @@ def default_engine_name() -> str:
|
||||
return 'speechd'
|
||||
|
||||
|
||||
def create_tts_backend(parent: QObject|None = None, force_engine: str | None = None):
|
||||
class TTSBackend(QObject):
|
||||
saying = pyqtSignal(int, int) # offset, length
|
||||
state_changed = pyqtSignal(QTextToSpeech.State)
|
||||
available_voices: dict[str, tuple[Voice, ...]] = {}
|
||||
engine_name: str = ''
|
||||
default_output_module: str = ''
|
||||
|
||||
def __init__(self, engine_name: str = '', parent: QObject|None = None):
|
||||
super().__init__(parent)
|
||||
|
||||
def pause(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def resume(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def stop(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def say(self, text: str) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def error_message(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def create_tts_backend(parent: QObject|None = None, force_engine: str | None = None) -> TTSBackend:
|
||||
prefs = load_config()
|
||||
engine_name = prefs.get('engine', '') if force_engine is None else force_engine
|
||||
engine_name = engine_name or default_engine_name()
|
||||
@ -201,51 +227,5 @@ def create_tts_backend(parent: QObject|None = None, force_engine: str | None = N
|
||||
return ans
|
||||
|
||||
|
||||
def develop(engine_name=''):
|
||||
# {{{
|
||||
marked_text = [2, 'Demonstration', ' ', 16, 'of', ' ', 19, 'DOCX', ' ', 24, 'support', ' ', 32, 'in', ' ', 35, 'calibre', '\n\t', 44, 'This', ' ', 49, 'document', ' ', 58, 'demonstrates', ' ', 71, 'the', ' ', 75, 'ability', ' ', 83, 'of', ' ', 86, 'the', ' ', 90, 'calibre', ' ', 98, 'DOCX', ' ', 103, 'Input', ' ', 109, 'plugin', ' ', 116, 'to', ' ', 119, 'convert', ' ', 127, 'the', ' ', 131, 'various', ' ', 139, 'typographic', ' ', 151, 'features', ' ', 160, 'in', ' ', 163, 'a', ' ', 165, 'Microsoft', ' ', 175, 'Word', ' ', 180, '(2007', ' ', 186, 'and', ' ', 190, 'newer)', ' ', 197, 'document.', ' ', 207, 'Convert', ' ', 215, 'this', ' ', 220, 'document', ' ', 229, 'to', ' ', 232, 'a', ' ', 234, 'modern', ' ', 241, 'ebook', ' ', 247, 'format,', ' ', 255, 'such', ' ', 260, 'as', ' ', 263, 'AZW3', ' ', 268, 'for', ' ', 272, 'Kindles', ' ', 280, 'or', ' ', 283, 'EPUB', ' ', 288, 'for', ' ', 292, 'other', ' ', 298, 'ebook', ' ', 304, 'readers,', ' ', 313, 'to', ' ', 316, 'see', ' ', 320, 'it', ' ', 323, 'in', ' ', 326, 'action.', '\n\t', 335, 'There', ' ', 341, 'is', ' ', 344, 'support', ' ', 352, 'for', ' ', 356, 'images,', ' ', 364, 'tables,', ' ', 372, 'lists,', ' ', 379, 'footnotes,', ' ', 390, 'endnotes,', ' ', 400, 'links,', ' ', 407, 'dropcaps', ' ', 416, 'and', ' ', 420, 'various', ' ', 428, 'types', ' ', 434, 'of', ' ', 437, 'text', ' ', 442, 'and', ' ', 446, 'paragraph', ' ', 456, 'level', ' ', 462, 'formatting.', '\n\t', 475, 'To', ' ', 478, 'see', ' ', 482, 'the', ' ', 486, 'DOCX', ' ', 491, 'conversion', ' ', 502, 'in', ' ', 505, 'action,', ' ', 513, 'simply', ' ', 520, 'add', ' ', 524, 'this', ' ', 529, 'file', ' ', 534, 'to', ' ', 537, 'calibre', ' ', 545, 'using', ' ', 551, 'the', ' ', 555, '“Add', ' ', 560, 'Books”', ' ', 567, 'button', ' ', 574, 'and', ' ', 578, 'then', ' ', 583, 'click', ' ', 589, '“Convert”.', ' ', 601, 'Set', ' ', 605, 'the', ' ', 609, 'output', ' ', 616, 'format', ' ', 623, 'in', ' ', 626, 'the', ' ', 630, 'top', ' ', 634, 'right', ' ', 640, 'corner', ' ', 647, 'of', ' ', 650, 'the', ' ', 654, 'conversion', ' ', 665, 'dialog', ' ', 672, 'to', ' ', 675, 'EPUB', ' ', 680, 'or', ' ', 683, 'AZW3', ' ', 688, 'and', ' ', 692, 'click', ' ', 698, '“OK”.', '\n\t\xa0\n\t'] # noqa }}}
|
||||
|
||||
from calibre.gui2 import Application
|
||||
app = Application([])
|
||||
app.shutdown_signal_received.connect(lambda: app.exit(1))
|
||||
tts = create_tts_backend(force_engine=engine_name)
|
||||
speech_started = False
|
||||
|
||||
def print_saying(s, e):
|
||||
bits = []
|
||||
in_region = False
|
||||
for x in marked_text:
|
||||
if isinstance(x, int):
|
||||
if in_region:
|
||||
if x >= e:
|
||||
break
|
||||
else:
|
||||
if x == s:
|
||||
in_region = True
|
||||
elif x > e:
|
||||
break
|
||||
elif in_region:
|
||||
bits.append(x)
|
||||
print('Saying:', repr(''.join(bits)))
|
||||
|
||||
import sys
|
||||
|
||||
def state_changed(state):
|
||||
nonlocal speech_started
|
||||
print('State changed:', state)
|
||||
if state == QTextToSpeech.State.Speaking:
|
||||
speech_started = True
|
||||
elif state == QTextToSpeech.State.Error:
|
||||
print(tts.error_message(), file=sys.stderr)
|
||||
app.exit(1)
|
||||
elif state == QTextToSpeech.State.Ready:
|
||||
if speech_started:
|
||||
app.quit()
|
||||
tts.saying.connect(print_saying)
|
||||
tts.state_changed.connect(state_changed)
|
||||
tts.speak_marked_text(marked_text)
|
||||
app.exec()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
develop()
|
||||
|
Loading…
x
Reference in New Issue
Block a user