def speak(self, speechSequence: SpeechSequence, priority: Spri): log._speechManagerUnitTest("speak (priority %r): %r", priority, speechSequence) interrupt = self._queueSpeechSequence(speechSequence, priority) self._doRemoveCancelledSpeechCommands() # If speech isn't already in progress, we need to push the first speech. push = self._hasNoMoreSpeech() or not self._synthStillSpeaking() log._speechManagerDebug( f"Will interrupt: {interrupt}" f" Will push: {push}" f" | _indexesSpeaking: {self._indexesSpeaking!r}" f" | _curPriQueue valid: {not self._hasNoMoreSpeech()}" f" | _shouldPushWhenDoneSpeaking: {self._shouldPushWhenDoneSpeaking}" f" | _cancelledLastSpeechWithSynth {self._cancelledLastSpeechWithSynth}" ) if interrupt: log._speechManagerDebug("Interrupting speech") getSynth().cancel() self._indexesSpeaking.clear() self._cancelCommandsForUtteranceBeingSpokenBySynth.clear() push = True if push: log._speechManagerDebug("Pushing next speech") self._pushNextSpeech(True) else: log._speechManagerDebug("Not pushing speech")
def _handleIndex(self, index: int): log._speechManagerDebug(f"Handle index: {index}") # 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 self._isIndexABeforeIndexB(oldIndex, index): log.debugWarning("Handling skipped index %s" % oldIndex) handleIndexes.append(oldIndex) handleIndexes.append(index) valid, endOfUtterance = False, False for i in handleIndexes: try: 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: log._speechManagerUnitTest( f"CallbackCommand Start: {callbackCommand!r}") callbackCommand.run() log._speechManagerUnitTest("CallbackCommand End") except Exception: log.exception("Error running speech callback") self._doRemoveCancelledSpeechCommands() shouldPush = ( endOfUtterance and not self._synthStillSpeaking() # stops double speaking errors ) if shouldPush: if self._indexesSpeaking: log._speechManagerDebug( f"Indexes speaking: {self._indexesSpeaking!r}," f" queue: {self._curPriQueue.pendingSequences}") # Even if we have many indexes, we should only push next speech once. self._pushNextSpeech(False)
def _doRemoveCancelledSpeechCommands(self): if not _shouldCancelExpiredFocusEvents(): return # Don't delete commands while iterating over _cancelCommandsForUtteranceBeingSpokenBySynth. latestCancelledUtteranceIndex = self._getMostRecentlyCancelledUtterance() log._speechManagerDebug(f"Last index: {latestCancelledUtteranceIndex}") if latestCancelledUtteranceIndex is not None: log._speechManagerDebug(f"Cancel and push speech") # Minimise the number of calls to _removeCompletedFromQueue by using the most recently cancelled # utterance index. This will remove all older queued speech also. self._removeCompletedFromQueue(latestCancelledUtteranceIndex) getSynth().cancel() self._cancelledLastSpeechWithSynth = True self._cancelCommandsForUtteranceBeingSpokenBySynth.clear() self._indexesSpeaking.clear() self._pushNextSpeech(True)
def doPreGainFocus(obj, sleepMode=False): oldForeground = api.getForegroundObject() oldFocus = api.getFocusObject() oldTreeInterceptor = oldFocus.treeInterceptor if oldFocus else None api.setFocusObject(obj) if speech.manager._shouldCancelExpiredFocusEvents(): log._speechManagerDebug( "executeEvent: Removing cancelled speech commands.") # ask speechManager to check if any of it's queued utterances should be cancelled # Note: Removing cancelled speech commands should happen after all dependencies for the isValid check # have been updated: # - lastQueuedFocusObject # - obj.WAS_GAIN_FOCUS_OBJ_ATTR_NAME # - api.getFocusAncestors() # These are updated: # - lastQueuedFocusObject & obj.WAS_GAIN_FOCUS_OBJ_ATTR_NAME # - Set in stack: _trackFocusObject, eventHandler.queueEvent # - Which results in executeEvent being called, then doPreGainFocus # - api.getFocusAncestors() via api.setFocusObject() called in doPreGainFocus speech._manager.removeCancelledSpeechCommands() if globalVars.focusDifferenceLevel <= 1: newForeground = api.getDesktopObject().objectInForeground() if not newForeground: log.debugWarning( "Can not get real foreground, resorting to focus ancestors") ancestors = api.getFocusAncestors() if len(ancestors) > 1: newForeground = ancestors[1] else: newForeground = obj api.setForegroundObject(newForeground) executeEvent('foreground', newForeground) if sleepMode: return True #Fire focus entered events for all new ancestors of the focus if this is a gainFocus event for parent in globalVars.focusAncestors[globalVars.focusDifferenceLevel:]: executeEvent("focusEntered", parent) if obj.treeInterceptor is not oldTreeInterceptor: if hasattr(oldTreeInterceptor, "event_treeInterceptor_loseFocus"): oldTreeInterceptor.event_treeInterceptor_loseFocus() if obj.treeInterceptor and obj.treeInterceptor.isReady and hasattr( obj.treeInterceptor, "event_treeInterceptor_gainFocus"): obj.treeInterceptor.event_treeInterceptor_gainFocus() return True
def _getMostRecentlyCancelledUtterance(self) -> Optional[_IndexT]: # Index of the most recently cancelled utterance. latestCancelledUtteranceIndex: Optional[_IndexT] = None log._speechManagerDebug( f"Length of _cancelCommandsForUtteranceBeingSpokenBySynth: " f"{len(self._cancelCommandsForUtteranceBeingSpokenBySynth)} " f"Length of _indexesSpeaking: " f"{len(self._indexesSpeaking)} ") cancelledIndexes = ( index for command, index in self._cancelCommandsForUtteranceBeingSpokenBySynth.items() if command.isCancelled) for index in cancelledIndexes: if (latestCancelledUtteranceIndex is None or self._isIndexABeforeIndexB( latestCancelledUtteranceIndex, index)): latestCancelledUtteranceIndex = index return latestCancelledUtteranceIndex
def _handleIndex(self, index: int): log._speechManagerDebug(f"Handle index: {index}") # 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: 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 speak(self, speechSequence: SpeechSequence, priority: Spri): log._speechManagerDebug( "Speak called: %r", speechSequence) # expensive string to build - defer interrupt = self._queueSpeechSequence(speechSequence, priority) self.removeCancelledSpeechCommands() # If speech isn't already in progress, we need to push the first speech. push = self._curPriQueue is None or 1 > len(self._indexesSpeaking) if interrupt: log._speechManagerDebug("Interrupting speech") getSynth().cancel() self._indexesSpeaking.clear() self._cancelCommandsForUtteranceBeingSpokenBySynth.clear() push = True if push: log._speechManagerDebug("Pushing next speech") self._pushNextSpeech(True) else: log._speechManagerDebug("Not pushing speech")
def removeCancelledSpeechCommands(self): if not _shouldCancelExpiredFocusEvents(): return latestCanceledUtteranceIndex = None log._speechManagerDebug( f"Length of _cancelCommandsForUtteranceBeingSpokenBySynth: " f"{len(self._cancelCommandsForUtteranceBeingSpokenBySynth)} " f"Length of _indexesSpeaking: " f"{len(self._indexesSpeaking)} ") for command, index in self._cancelCommandsForUtteranceBeingSpokenBySynth.items( ): if command.isCancelled: # we must not risk deleting commands while iterating over _cancelCommandsForUtteranceBeingSpokenBySynth if not latestCanceledUtteranceIndex or latestCanceledUtteranceIndex < index: latestCanceledUtteranceIndex = index log._speechManagerDebug(f"Last index: {latestCanceledUtteranceIndex}") if latestCanceledUtteranceIndex is not None: log._speechManagerDebug(f"Cancel and push speech") self._removeCompletedFromQueue(latestCanceledUtteranceIndex) getSynth().cancel() self._cancelCommandsForUtteranceBeingSpokenBySynth.clear() self._indexesSpeaking.clear() self._pushNextSpeech(True)
def _queueSpeechSequence(self, inSeq: SpeechSequence, priority: Spri) -> bool: """ @return: Whether to interrupt speech. """ outSeq = self._processSpeechSequence(inSeq) log._speechManagerDebug("Out Seq: %r", outSeq) # expensive string to build - defer queue = self._priQueues.get(priority) log._speechManagerDebug( f"Current priority: {priority}," f" queLen: {0 if queue is None else len(queue.pendingSequences)}") if not queue: queue = self._priQueues[priority] = _ManagerPriorityQueue(priority) else: log._speechManagerDebug( "current queue: %r", # expensive string to build - defer queue.pendingSequences) first = len(queue.pendingSequences) == 0 queue.pendingSequences.extend(outSeq) if priority is Spri.NOW and first: # If this is the first sequence at Spri.NOW, interrupt speech. return True return False
def _removeCompletedFromQueue( self, index: int) -> Tuple[bool, bool]: # noqa: C901 """Removes completed speech sequences from the queue. @param index: The index just reached indicating a completed sequence. @return: Tuple of (valid, endOfUtterance), where valid indicates whether the index was valid and endOfUtterance indicates whether this sequence was the end of the current utterance. @rtype: (bool, bool) """ # Find the sequence that just completed speaking. if not self._curPriQueue: # No speech in progress. Probably from a previous utterance which was cancelled. return False, False for seqIndex, seq in enumerate(self._curPriQueue.pendingSequences): lastCommand = seq[-1] if isinstance(seq, list) else None if isinstance(lastCommand, IndexCommand): if self._isIndexAAfterIndexB(index, lastCommand.index): log.debugWarning( f"Reached speech index {index :d}, but index {lastCommand.index :d} never handled" ) elif index == lastCommand.index: endOfUtterance = isinstance( self._curPriQueue.pendingSequences[seqIndex + 1][0], EndUtteranceCommand) if endOfUtterance: # Remove the EndUtteranceCommand as well. seqIndex += 1 break # Found it! else: log._speechManagerDebug( "Unknown index. Probably from a previous utterance which was cancelled." ) return False, False if endOfUtterance: # These params may not apply to the next utterance if it was queued separately, # so reset the tracker. # The next utterance will include the commands again if they do still apply. self._curPriQueue.paramTracker = ParamChangeTracker() else: # Keep track of parameters changed so far. # This is necessary in case this utterance is preempted by higher priority speech. for seqIndex in range(seqIndex + 1): seq = self._curPriQueue.pendingSequences[seqIndex] for command in seq: if isinstance(command, SynthParamCommand): self._curPriQueue.paramTracker.update(command) # This sequence is done, so we don't need to track it any more. toRemove = self._curPriQueue.pendingSequences[:seqIndex + 1] log._speechManagerDebug("Removing: %r", seq) if _shouldCancelExpiredFocusEvents(): cancellables = (item for seq in toRemove for item in seq if isinstance(item, _CancellableSpeechCommand)) for item in cancellables: if log.isEnabledFor( log.DEBUG) and _shouldDoSpeechManagerLogging(): # Debug logging for cancelling expired focus events. log._speechManagerDebug( f"Item is in _cancelCommandsForUtteranceBeingSpokenBySynth: " f"{item in self._cancelCommandsForUtteranceBeingSpokenBySynth.keys()}" ) self._cancelCommandsForUtteranceBeingSpokenBySynth.pop( item, None) del self._curPriQueue.pendingSequences[:seqIndex + 1] return True, endOfUtterance