Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions source/speech/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ def ensureEndUtterance(seq: SpeechSequence):
else:
lastOutSeq = outSeqs[-1] if outSeqs else None
lastCommand = lastOutSeq[-1] if lastOutSeq else None
if not lastCommand or isinstance(lastCommand, (EndUtteranceCommand, ConfigProfileTriggerCommand)):
if lastCommand is None or isinstance(lastCommand, (EndUtteranceCommand, ConfigProfileTriggerCommand)):
# It doesn't make sense to start with or repeat EndUtteranceCommands.
# We also don't want an EndUtteranceCommand immediately after a ConfigProfileTriggerCommand.
return
Expand Down Expand Up @@ -458,13 +458,18 @@ def _buildNextUtterance(self):
# apply any parameters changed before the preemption.
params = self._curPriQueue.paramTracker.getChanged()
utterance.extend(params)
for seq in self._curPriQueue.pendingSequences:
lastSequenceIndexAddedToUtterance = None
for seqIndex, seq in enumerate(self._curPriQueue.pendingSequences):
if isinstance(seq[0], EndUtteranceCommand):
# The utterance ends here.
break
utterance.extend(seq)
lastSequenceIndexAddedToUtterance = seqIndex
# if any items are cancelled, cancel the whole utterance.
if utterance and not self._checkForCancellations(utterance):
log.error(f"Checking for cancellations failed, cancelling sequence: {utterance}")
# Avoid infinite recursion by removing the problematic sequences:
del self._curPriQueue.pendingSequences[:lastSequenceIndexAddedToUtterance + 1]
return self._buildNextUtterance()
return utterance

Expand Down Expand Up @@ -542,7 +547,7 @@ def _getMostRecentlyCancelledUtterance(self) -> Optional[_IndexT]:
)
for index in cancelledIndexes:
if (
not latestCancelledUtteranceIndex
latestCancelledUtteranceIndex is None
or self._isIndexABeforeIndexB(latestCancelledUtteranceIndex, index)
):
latestCancelledUtteranceIndex = index
Expand Down
30 changes: 29 additions & 1 deletion tests/unit/test_speechManager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1139,12 +1139,15 @@ def setUp(self):
config.conf['featureFlag']['cancelExpiredFocusSpeech'] = 1 # yes


class Test_pr11651(unittest.TestCase):
class RegressionTests(unittest.TestCase):
"""Tests to prevent regressions after issues are fixed.
"""

def test_redundantSequenceAfterEndUtterance(self):
"""
Tests that redundant param change and index commands are not emitted as an extra utterance
when the preceeding utterance contained param change commands and an EndUtterance command.
See PR #11651
E.g. speaking a character.
"""
smi = SpeechManagerInteractions(self)
Expand All @@ -1168,3 +1171,28 @@ def test_redundantSequenceAfterEndUtterance(self):
smi.indexReached(1)
smi.doneSpeaking()
smi.pumpAll()

def test_nonSpokenCharacter(self):
"""Test for fix to GH#11752 - NVDA Freeze with unicode value U+000B
Actually, the speech manager receives an empty string for the character U+000B. NVDA
does not have a mapping for this character.
It is questionable whether we should send anything to the synth when there is no content, however
what constitutes 'content' is currently not easy to define.
"""
smi = SpeechManagerInteractions(self)

speechSequence = [
CharacterModeCommand(True),
'',
smi.create_EndUtteranceCommand(expectedToBecomeIndex=1)
]
with smi.expectation():
seqIndexes = smi.speak(speechSequence)
smi.expect_synthSpeak(seqIndexes)


class RegressionTests_withCancellableSpeechEnabled(RegressionTests):
"""Note, while cancellable speech is configurable test with and without it enabled."""
def setUp(self):
super().setUp()
config.conf['featureFlag']['cancelExpiredFocusSpeech'] = 1 # yes