Cleanup tts manager event system

Fix event stream when resuming after configure
This commit is contained in:
Kovid Goyal 2024-09-03 18:49:46 +05:30
parent f3b44f3614
commit a617af14d8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 68 additions and 48 deletions

View File

@ -2,23 +2,20 @@
# 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, QSize, Qt, QTextCursor, QTextToSpeech, QToolBar from typing import Literal
from qt.core import QAction, QKeySequence, QPlainTextEdit, QSize, Qt, QTextCursor, 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
from calibre.gui2.tts2.manager import TTSManager from calibre.gui2.tts2.manager import TTSManager
TEXT = '''\ TEXT = '''\
Demonstration 😹 🐈 of DOCX support in calibre Demonstration 🐈 of DOCX support in calibre
This document demonstrates the ability of the calibre DOCX Input plugin to convert the various typographic features in a Microsoft Word This document demonstrates the ability of the calibre DOCX Input plugin to convert the various typographic features in a Microsoft Word
(2007 and newer) document. Convert this document to a modern ebook format, such as AZW3 for Kindles or EPUB for other ebook readers, (2007 and newer) document. Convert this document to a modern ebook format, such as AZW3 for Kindles or EPUB for other ebook readers,
to see it in action. to see it in action.
There is support for images, tables, lists, footnotes, endnotes, links, dropcaps and various types of text and paragraph level formatting.
To see the DOCX conversion in action, simply add this file to calibre using the Add Books button and then click Convert.
Set the output format in the top right corner of the conversion dialog to EPUB or AZW3 and click OK.
''' '''
@ -27,28 +24,19 @@ class MainWindow(MainWindow):
def __init__(self, text): def __init__(self, text):
super().__init__() super().__init__()
self.display = d = QPlainTextEdit(self) self.display = d = QPlainTextEdit(self)
self.page_count = 1
self.toolbar = tb = QToolBar(self) self.toolbar = tb = QToolBar(self)
self.tts = TTSManager(self) self.tts = TTSManager(self)
self.tts.state_changed.connect(self.state_changed, type=Qt.ConnectionType.QueuedConnection) self.tts.state_event.connect(self.state_event, type=Qt.ConnectionType.QueuedConnection)
self.tts.saying.connect(self.saying) self.tts.saying.connect(self.saying)
self.addToolBar(tb) self.addToolBar(tb)
self.setCentralWidget(d) self.setCentralWidget(d)
d.setPlainText(text) d.setPlainText(text)
d.setReadOnly(True) d.setReadOnly(True)
c = d.textCursor() self.create_marked_text()
c.setPosition(0)
marked_text = []
while True:
marked_text.append(c.position())
if not c.movePosition(QTextCursor.MoveOperation.NextWord, QTextCursor.MoveMode.KeepAnchor):
break
marked_text.append(c.selectedText().replace('\u2029', '\n'))
c.setPosition(c.position())
c.setPosition(0)
self.marked_text = marked_text
self.play_action = pa = QAction('Play') self.play_action = pa = QAction('Play')
pa.setShortcut(QKeySequence(Qt.Key.Key_Space)) pa.setShortcut(QKeySequence(Qt.Key.Key_Space))
pa.triggered.connect(self.toggled) pa.triggered.connect(self.play_triggerred)
self.toolbar.addAction(pa) self.toolbar.addAction(pa)
self.stop_action = sa = QAction('Stop') self.stop_action = sa = QAction('Stop')
sa.setShortcut(QKeySequence(Qt.Key.Key_Escape)) sa.setShortcut(QKeySequence(Qt.Key.Key_Escape))
@ -67,31 +55,50 @@ class MainWindow(MainWindow):
self.toolbar.addAction(ra) self.toolbar.addAction(ra)
ra.triggered.connect(self.tts.test_resume_after_reload) ra.triggered.connect(self.tts.test_resume_after_reload)
self.state_changed(self.tts.state)
self.resize(self.sizeHint()) self.resize(self.sizeHint())
def state_changed(self, state): def create_marked_text(self):
self.statusBar().showMessage(str(state))
if state in (QTextToSpeech.State.Ready, QTextToSpeech.State.Paused, QTextToSpeech.State.Error):
self.play_action.setChecked(False)
if state is QTextToSpeech.State.Ready:
c = self.display.textCursor() c = self.display.textCursor()
c.setPosition(0) c.setPosition(0)
marked_text = []
while True:
marked_text.append(c.position())
if not c.movePosition(QTextCursor.MoveOperation.NextWord, QTextCursor.MoveMode.KeepAnchor):
break
marked_text.append(c.selectedText().replace('\u2029', '\n'))
c.setPosition(c.position())
c.setPosition(0)
self.marked_text = marked_text
self.display.setTextCursor(c) self.display.setTextCursor(c)
else:
self.play_action.setChecked(True)
self.stop_action.setEnabled(state in (QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused))
if self.tts.state is QTextToSpeech.State.Paused:
self.play_action.setText('Resume')
elif self.tts.state is QTextToSpeech.State.Speaking:
self.play_action.setText('Pause')
else:
self.play_action.setText('Play')
def toggled(self): def next_page(self):
if self.tts.state is QTextToSpeech.State.Paused: self.page_count += 1
self.display.setPlainText(f'This is page number {self.page_count}. Pages are turned automatically when the end of a page is reached.')
self.create_marked_text()
def update_play_action(self, text):
self.play_action.setText(text)
def state_event(self, ev: Literal['begin', 'end', 'cancel', 'pause', 'resume']):
sb = self.statusBar()
self.statusBar().showMessage((sb.currentMessage() + ' ' + ev).strip())
self.stop_action.setEnabled(ev in ('pause', 'resume', 'begin'))
if ev == 'cancel':
self.update_play_action('Play')
elif ev == 'pause':
self.update_play_action('Resume')
elif ev in ('resume', 'begin'):
self.update_play_action('Pause')
elif ev == 'end':
if self.play_action.text() == 'Pause':
self.next_page()
self.update_play_action('Play')
self.play_triggerred()
def play_triggerred(self):
if self.play_action.text() == 'Resume':
self.tts.resume() self.tts.resume()
elif self.tts.state is QTextToSpeech.State.Speaking: elif self.play_action.text() == 'Pause':
self.tts.pause() self.tts.pause()
else: else:
self.tts.speak_marked_text(self.marked_text) self.tts.speak_marked_text(self.marked_text)

View File

@ -111,7 +111,6 @@ class ResumeData:
class TTSManager(QObject): class TTSManager(QObject):
state_changed = pyqtSignal(QTextToSpeech.State)
state_event = pyqtSignal(str) state_event = pyqtSignal(str)
saying = pyqtSignal(int, int) saying = pyqtSignal(int, int)
@ -120,6 +119,20 @@ class TTSManager(QObject):
self._tts: 'TTSBackend' | None = None self._tts: 'TTSBackend' | None = None
self.state = QTextToSpeech.State.Ready self.state = QTextToSpeech.State.Ready
self.tracker = Tracker() self.tracker = Tracker()
self._resuming_after_configure = False
def emit_state_event(self, event: str) -> None:
if self._resuming_after_configure:
if event == 'cancel':
self.state_event.emit(event)
self._resuming_after_configure = False
elif event == 'begin':
self.state_event.emit('resume')
self._resuming_after_configure = False
elif event == 'pause':
self.state_event.emit(event)
else:
self.state_event.emit(event)
@property @property
def tts(self) -> 'TTSBackend': def tts(self) -> 'TTSBackend':
@ -160,6 +173,7 @@ class TTSManager(QObject):
rd = ResumeData() rd = ResumeData()
rd.is_speaking = self._tts is not None and self.state in ( rd.is_speaking = self._tts is not None and self.state in (
QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused) QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused)
self._resuming_after_configure = True
if self.state is not QTextToSpeech.State.Paused: if self.state is not QTextToSpeech.State.Paused:
self.tts.pause() self.tts.pause()
yield rd yield rd
@ -216,19 +230,18 @@ class TTSManager(QObject):
prev_state, self.state = self.state, state prev_state, self.state = self.state, state
if state is QTextToSpeech.State.Error: if state is QTextToSpeech.State.Error:
error_dialog(self, _('Read aloud failed'), self.tts.error_message(), show=True) error_dialog(self, _('Read aloud failed'), self.tts.error_message(), show=True)
self.state_changed.emit(state)
if state is QTextToSpeech.State.Paused: if state is QTextToSpeech.State.Paused:
self.state_event.emit('pause') self.emit_state_event('pause')
elif state is QTextToSpeech.State.Speaking: elif state is QTextToSpeech.State.Speaking:
if prev_state is QTextToSpeech.State.Paused: if prev_state is QTextToSpeech.State.Paused:
self.state_event.emit('resume') self.emit_state_event('resume')
elif prev_state is QTextToSpeech.State.Ready: elif prev_state is QTextToSpeech.State.Ready:
self.state_event.emit('begin') self.emit_state_event('begin')
elif state is QTextToSpeech.State.Ready: elif state is QTextToSpeech.State.Ready:
if prev_state in (QTextToSpeech.State.Paused, QTextToSpeech.State.Speaking): if prev_state in (QTextToSpeech.State.Paused, QTextToSpeech.State.Speaking):
self.state_event.emit('end') self.emit_state_event('end')
elif state is QTextToSpeech.State.Error: elif state is QTextToSpeech.State.Error:
self.state_event.emit('cancel') self.emit_state_event('cancel')
def _saying(self, offset: int, length: int) -> None: def _saying(self, offset: int, length: int) -> None:
self.tracker.boundary_reached(offset) self.tracker.boundary_reached(offset)