Skip to content

Commit

Permalink
speechManager: Ensure handling of skipped indexes (PR #10730)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
michaelDCurran authored Jan 31, 2020
1 parent c5ef3da commit 6460949
Showing 1 changed file with 38 additions and 8 deletions.
46 changes: 38 additions & 8 deletions source/speech/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import queueHandler
import synthDriverHandler
from .commands import *
from .commands import IndexCommand
from .priorities import Spri, SPEECH_PRIORITIES

class ParamChangeTracker(object):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 6460949

Please sign in to comment.