-
-
Notifications
You must be signed in to change notification settings - Fork 654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
speechManager: Ensure handling of skipped indexes #10730
Conversation
…s synths like oneCore may refuse to fire callbacks for indexes directly before another index.
See test results for failed build of commit fa4b50d881 |
See test results for failed build of commit a33b34b527 |
This build fixes OneCore but not the eSpeak issue that I describe in #10612. Should I open a new issue or will this be investigated in this PR. |
On the contrary to what I stated in my previous comment, it seems that the eSpeak problem is also resolved by this PR. At least, I cannot reproduce it anymore with this PR's build. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think speech manager is in fairly dire need of unit tests.
source/speech/manager.py
Outdated
def _handleIndex(self, index): | ||
def _handleIndex(self, index, handleSkippedIndexes=True): | ||
try: | ||
self._indexesSpeaking.remove(index) | ||
except ValueError: | ||
log.debugWarning("Unknown index %s" % index) | ||
return | ||
# 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. | ||
if handleSkippedIndexes: | ||
for oldIndex in list(self._indexesSpeaking): | ||
if oldIndex < index: | ||
log.debugWarning("Handling skipped index %s" % oldIndex) | ||
self._handleIndex(oldIndex, False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer if this was not recursive, I think it only adds extra complexity, because this function now does several things and recursing the skipped indexes is just one part. I think that _pushNextSpeech
should only be called once. Despite my initial feeling that _removeCompletedFomQueue
should only be called once and it being less efficient than handling this in bulk, it looks like it necessary. This should be well documented. It seems a shame that we are searching through the sequence for these indexes already (in _removeCompletedFomQueue
). Any way, removing the recursive approach you could achieve the same with:
def _handleIndex(self, index):
# A synth (such as OneCore) may skip indexes
# If before another index, with no text content in between.
# Therefore, collect all skipped indexes.
handleIndexes = []
for oldIndex in self._indexesSpeaking: # no need to take a copy anymore
if oldIndex < index:
log.debugWarning("Handling skipped index %s" % oldIndex)
handleIndexes.append(oldIndex)
handleIndexes.append(index)
# a more concise approach to create handleIndexes:
self._indexesSpeaking.sort() # Note: if we wish to maintain out of order indexes (which I don't think we should allow) use s = sorted()
indexOfIndex = self._indexesSpeaking.index(index)
handleIndexes = self._indexesSpeaking[:indexOfIndex]
valid, endOfUtterance = False, False
for i in handleIndexes:
try:
self._indexesSpeaking.remove(i)
except ValueError:
log.error("Unknown index %s" % 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(index, None)
if callbackCommand:
try:
callbackCommand.run()
except:
log.exception("Error running speech callback")
if endOfUtterance:
# Even if we have many indexes, we should only push next speech once.
self._pushNextSpeech(False)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little concerned about the effect this will have on the timing of the callbacks. Or resulting callbacks being called that were intended to be associated with text that the synth decided not to speak. Though, thinking about this from oncore's side. If there are several indexes in sequence, with no speakable text in between, it would seem like a fairly logical optimization to only call the last one.
I committed your suggested rewrite, however, I still copied
_indexesSpeaking and use log.debug rather than log.error when handling
an exception when removing the item. Reason being that errors were
constantly being rased when cancelling speech, as indexes were being
removed from _indexesSpeaking in the main thread, yet _handleIndex is
run from a background thread.
|
I think there must be something else going on, check Line 331 in cbd8f24
It would also be bad if these callbacks were called from another thread. |
handleIndexes = [] | ||
for oldIndex in list(self._indexesSpeaking): | ||
if oldIndex < index: | ||
log.debugWarning("Handling skipped index %s" % oldIndex) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This message is emitted later as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's best if this is merged, so it can get wider testing over the weekend. Since the plan is to release an RC on Monday. I will merge this and make another Beta shortly. It may warrant a followup, since there seems to be some assumptions that don't hold.
Hi reef,
Will this be merged into master?
|
It's first merged into beta, I'll merge beta into master shortly. |
After investigating this further, it seems that this is not a threading issue. @feerrenrut was correct that everything should be executed on the main thread, and @jcsteh also confirmed to me this was the way it was designed. |
Link to issue number:
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 recieved, 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 how this pull request fixes the issue:
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 go to synth.speak go into a indexesSpeaking instance variable on SpeechManager.
_handleIndex now has an optional handleSkippedIndexes argument, which is True by default. If true, then it looks back through indexesSpeaking, and calls _handleIndex manually for any index lower than the current index.
_handleIndex also removes index from indexesSpeaking.
_reset also clears indexesSpeaking.
Testing performed:
With Onecore, sapi5 and espeak: did a say All on the following content:
Before the fix, NVDa would stop reading at 5.
After the fix, it reaches the end.
Known issues with pull request:
None known.
Change log entry:
None needed.