From 6460949179188c59024bbe92fa2a1dadaf15c6ed Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Sat, 1 Feb 2020 00:41:29 +1000 Subject: [PATCH] speechManager: Ensure handling of skipped indexes (PR #10730) Fixes #10292 ## Summary of the issue: Synths such as OneCore and sapi5 seem to fail to fire callbacks for bookmarks in speech that directly preceed another bookmark, with no text content in between. This can happen if doing a sayAll which contains blank lines in the middle. SpeechManager currently expects that each and every index will be received. But in this case, the index for the blank line is not received, and therefore speech stops. This was not an issue with 2019.2 and below, presumably because we didn't care so much about indexes for each and every utterance. ## Description of this change A list of indexes currently sent to the synth is now tracked. I.e. the index value of all IndexCommand objects that are in the sequence that is sent to synth.speak are added to the indexesSpeaking instance variable on SpeechManager. When a an index is reached, do callbacks for skipped indexes as well. --- source/speech/manager.py | 46 +++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/source/speech/manager.py b/source/speech/manager.py index 468752ed4e8..59dd75b8f87 100644 --- a/source/speech/manager.py +++ b/source/speech/manager.py @@ -8,6 +8,7 @@ import queueHandler import synthDriverHandler from .commands import * +from .commands import IndexCommand from .priorities import Spri, SPEECH_PRIORITIES class ParamChangeTracker(object): @@ -147,6 +148,8 @@ def _reset(self): self._curPriQueue = None #: Maps indexes to BaseCallbackCommands. self._indexesToCallbacks = {} + #: a list of indexes currently being spoken by the synthesizer + self._indexesSpeaking = [] #: Whether to push more speech when the synth reports it is done speaking. self._shouldPushWhenDoneSpeaking = False @@ -287,6 +290,11 @@ def _pushNextSpeech(self, doneSpeaking): return self._pushNextSpeech(True) seq = self._buildNextUtterance() if seq: + # Record all indexes that will be sent to the synthesizer + # So that we can handle any accidentally skipped indexes. + for item in seq: + if isinstance(item, IndexCommand): + self._indexesSpeaking.append(item.index) getSynth().speak(seq) def _getNextPriority(self): @@ -366,16 +374,38 @@ def _removeCompletedFromQueue(self, index): return True, endOfUtterance def _handleIndex(self, index): - valid, endOfUtterance = self._removeCompletedFromQueue(index) - if not valid: - return - callbackCommand = self._indexesToCallbacks.pop(index, None) - if callbackCommand: + # A synth (such as OneCore) may skip indexes + # If before another index, with no text content in between. + # Therefore, detect this and ensure we handle all skipped indexes. + handleIndexes = [] + for oldIndex in list(self._indexesSpeaking): + if oldIndex < index: + log.debugWarning("Handling skipped index %s" % oldIndex) + handleIndexes.append(oldIndex) + handleIndexes.append(index) + valid, endOfUtterance = False, False + for i in handleIndexes: try: - callbackCommand.run() - except: - log.exception("Error running speech callback") + self._indexesSpeaking.remove(i) + except ValueError: + log.debug("Unknown index %s, speech probably cancelled from main thread." % i) + break # try the rest, this is a very unexpected path. + if i != index: + log.debugWarning("Handling skipped index %s" % i) + # we must do the following for each index, any/all of them may be end of utterance, which must + # trigger _pushNextSpeech + _valid, _endOfUtterance = self._removeCompletedFromQueue(i) + valid = valid or _valid + endOfUtterance = endOfUtterance or _endOfUtterance + if _valid: + callbackCommand = self._indexesToCallbacks.pop(i, None) + if callbackCommand: + try: + callbackCommand.run() + except Exception: + log.exception("Error running speech callback") if endOfUtterance: + # Even if we have many indexes, we should only push next speech once. self._pushNextSpeech(False) def _onSynthDoneSpeaking(self, synth=None):