Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8f0921c
Split lineReached into a separate _bookmarkReached function
Jul 15, 2019
ce53201
Change the re_last_pause regex to consume less of a line containing m…
Jul 17, 2019
ff1872c
Add a nice repr to callback commands for easier debugging
Jul 17, 2019
07b90c8
Make the caret follow say all more closely
Jul 16, 2019
3225897
Say all, use obj.selection = updater instead of updater.updateCaret()
Jul 17, 2019
3341210
Update source/speech/__init__.py
LeonarddeR Aug 5, 2019
db74dc6
Apply suggestions from code review
LeonarddeR Aug 5, 2019
7bf925c
Merge remote-tracking branch 'origin/master' into sayAll
Aug 9, 2019
795b6ca
Make the caret per word setting a synth setting instead of global to …
Aug 9, 2019
c1d0fb2
Rename of private _TextCommand class to _TextChunk
Aug 9, 2019
f67bbd3
Rename cb to callback
Aug 9, 2019
4947f97
Add type info to speech.speakTextInfo
Aug 9, 2019
28abd3c
re.UNICODE is now obsolete, no longer use it
Aug 9, 2019
c832f7e
Fix copyright of speech/commands. This is part of speech refactor so …
Aug 9, 2019
7af96e2
linter satisfaction
Aug 9, 2019
3b40b04
Merge remote-tracking branch 'origin/master' into sayAll
Oct 23, 2019
000d680
Merge remote-tracking branch 'origin/master' into sayAll
Nov 6, 2019
4f5d791
Merge remote-tracking branch 'origin/master' into sayAll
LeonarddeR Nov 6, 2019
9e9918b
Merge remote-tracking branch 'origin/master' into sayAll
Nov 21, 2019
21c310d
Linting fix
Nov 21, 2019
1dc834d
Merge remote-tracking branch 'origin/master' into sayAll
LeonarddeR Feb 17, 2020
60f53b5
Fix more merge conflicts
LeonarddeR Feb 18, 2020
5f2154c
Support test_SpeechWithoutPauses
LeonarddeR Feb 18, 2020
aad4c99
Merge remote-tracking branch 'origin/master' into sayAll
LeonarddeR May 2, 2020
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
1 change: 1 addition & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
outputDevice = string(default=default)
autoLanguageSwitching = boolean(default=true)
autoDialectSwitching = boolean(default=false)
increaseSayAllCaretUpdates = boolean(default=false)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a speech synth dependent setting? For example, users may have many synths, and some might work better with this feature than others.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@derekriemer wrote:

Increase caret updates during say all sounds wordy to me. How about "Move cursor by word during say all"

Because while the caret is intended to move per word, the movement is also limited to once per half a second.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, sorry for the comment above, it ended up at the wrong place.

Anyway, making this a per synth setting makes sense to me. I will do that.


[[__many__]]
capPitchChange = integer(default=30,min=-100,max=100)
Expand Down
7 changes: 7 additions & 0 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,12 @@ def makeSettings(self, settingsSizer):
self.useSpellingFunctionalityCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = useSpellingFunctionalityText))
self.useSpellingFunctionalityCheckBox.SetValue(config.conf["speech"][self.driver.name]["useSpellingFunctionality"])

# Translators: This is the label for a checkbox in the
# voice settings panel.
increaseSayAllCaretUpdatesText = _("Increase caret updates during say all")
self.increaseSayAllCaretUpdatesCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = increaseSayAllCaretUpdatesText))
self.increaseSayAllCaretUpdatesCheckBox.Value = config.conf["speech"]["increaseSayAllCaretUpdates"]

def onSave(self):
DriverSettingsMixin.onSave(self)

Expand All @@ -1264,6 +1270,7 @@ def onSave(self):
config.conf["speech"][self.driver.name]["sayCapForCapitals"]=self.sayCapForCapsCheckBox.IsChecked()
config.conf["speech"][self.driver.name]["beepForCapitals"]=self.beepForCapsCheckBox.IsChecked()
config.conf["speech"][self.driver.name]["useSpellingFunctionality"]=self.useSpellingFunctionalityCheckBox.IsChecked()
config.conf["speech"]["increaseSayAllCaretUpdates"] = self.increaseSayAllCaretUpdatesCheckBox.IsChecked()

class KeyboardSettingsPanel(SettingsPanel):
# Translators: This is the label for the keyboard settings panel.
Expand Down
37 changes: 31 additions & 6 deletions source/sayAllHandler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# sayAllHandler.py
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2017 NV Access Limited
# Copyright (C) 2006-2019 NV Access Limited, Babbage B.V.
# This file may be used under the terms of the GNU General Public License, version 2 or later.
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html

Expand All @@ -12,6 +13,8 @@
import api
import textInfos
import queueHandler
from functools import partial
import time

CURSOR_CARET = 0
CURSOR_REVIEW = 1
Expand Down Expand Up @@ -99,6 +102,8 @@ class _TextReader(object):
11. If there is another page, L{turnPage} calls L{nextLine}.
"""
MAX_BUFFERED_LINES = 10
_lastEndOfWhiteSpaceReachedTime = 0
MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES = 0.5

def __init__(self, cursor):
self.cursor = cursor
Expand Down Expand Up @@ -142,9 +147,19 @@ def nextLine(self):
return
# Call lineReached when we start speaking this line.
# lineReached will move the cursor and trigger reading of the next line.
cb = speech.CallbackCommand(lambda obj=self.reader.obj, state=self.speakTextInfoState.copy(): self.lineReached(obj,bookmark, state))
cb = speech.CallbackCommand(partial(
self.lineReached,
obj=self.reader.obj,
bookmark=bookmark,
state=self.speakTextInfoState.copy()
))
spoke = speech.speakTextInfo(self.reader, unit=textInfos.UNIT_READINGCHUNK,
reason=controlTypes.REASON_SAYALL, _prefixSpeechCommand=cb,
_whiteSpaceReachedCallback=(
self.endOfWhiteSpaceReached
if config.conf["speech"]["increaseSayAllCaretUpdates"]
else None
),
useCache=self.speakTextInfoState)
# Collapse to the end of this line, ready to read the next.
try:
Expand All @@ -168,14 +183,24 @@ def nextLine(self):
# The first buffered line has now started speaking.
self.numBufferedLines -= 1

def lineReached(self, obj, bookmark, state):
# We've just started speaking this line, so move the cursor there.
state.updateObj()
def _bookmarkReached(self, obj, bookmark):
updater = obj.makeTextInfo(bookmark)
if self.cursor == CURSOR_CARET:
updater.updateCaret()
obj.selection = updater
if self.cursor != CURSOR_CARET or config.conf["reviewCursor"]["followCaret"]:
api.setReviewPosition(updater, isCaret=self.cursor==CURSOR_CARET)

def endOfWhiteSpaceReached(self, bookmark):
curTime = time.time()
if (self._lastEndOfWhiteSpaceReachedTime + self.MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES) > curTime:
return
self._lastEndOfWhiteSpaceReachedTime = curTime
self._bookmarkReached(self.reader.obj, bookmark)

def lineReached(self, obj, bookmark, state):
# We've just started speaking this line, so move the cursor there.
state.updateObj()
self._bookmarkReached(obj, bookmark)
if self.numBufferedLines == 0:
# This was the last line spoken, so move on.
self.nextLine()
Expand Down
74 changes: 61 additions & 13 deletions source/speech/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
import characterProcessing
import languageHandler
from .commands import *
from typing import Optional, Callable, Any
from functools import partial
from types import GeneratorType

speechMode_off=0
speechMode_beeps=1
Expand Down Expand Up @@ -766,8 +769,25 @@ def _speakTextInfo_addMath(speechSequence, info, field):
except (NotImplementedError, LookupError):
return

def speakTextInfo(info, useCache=True, formatConfig=None, unit=None, reason=controlTypes.REASON_QUERY, _prefixSpeechCommand=None, onlyInitialFields=False, suppressBlanks=False,priority=None):
re_white_space = re.compile(r"(\s+|$)", re.DOTALL)

class _TextCommand(str):
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
"""str subclass to distinguish normal text from field text when processing text info speech."""

def speakTextInfo(
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
info,
useCache=True,
formatConfig=None,
unit=None,
reason=controlTypes.REASON_QUERY,
_prefixSpeechCommand=None,
_whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None,
onlyInitialFields=False,
suppressBlanks=False,
priority=None
):
onlyCache=reason==controlTypes.REASON_ONLYCACHE
processWhiteSPace: bool = _whiteSpaceReachedCallback is not None
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
if isinstance(useCache,SpeakTextInfoState):
speakTextInfoState=useCache
elif useCache:
Expand Down Expand Up @@ -943,9 +963,9 @@ def speakTextInfo(info, useCache=True, formatConfig=None, unit=None, reason=cont
indentationDone=True
if command:
if inTextChunk:
relativeSpeechSequence[-1]+=command
relativeSpeechSequence[-1] = _TextCommand(relativeSpeechSequence[-1] + command)
else:
relativeSpeechSequence.append(command)
relativeSpeechSequence.append(_TextCommand(command))
inTextChunk=True
elif isinstance(command,textInfos.FieldCommand):
newLanguage=None
Expand Down Expand Up @@ -1007,16 +1027,44 @@ def speakTextInfo(info, useCache=True, formatConfig=None, unit=None, reason=cont
else:
speechSequence.append(indentationSpeech)
if speakTextInfoState: speakTextInfoState.indentationCache=allIndentation
# Don't add this text if it is blank.
relativeBlank=True
for x in relativeSpeechSequence:
if isinstance(x,str) and not isBlank(x):
relativeBlank=False
break
if not relativeBlank:
speechSequence.extend(relativeSpeechSequence)
# only add this text if it is not blank.
notBlank = any(
not isBlank(x) for x in relativeSpeechSequence
if isinstance(x,str)
)
if notBlank:
isTextBlank=False

if not processWhiteSPace:
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
speechSequence.extend(relativeSpeechSequence)
else:
# Add appropriate white space bookmarks
whiteSpaceTracker = info.copy()
whiteSpaceTracker.collapse()
for index, command in list(enumerate(relativeSpeechSequence)):
if not isinstance(command, _TextCommand):
continue
curCommandSequence = []
endOfWhiteSpaceIndexes = [m.end() for m in re_white_space.finditer(command)]
start = 0
for end in endOfWhiteSpaceIndexes:
text = command[start:end]
curCommandSequence.append(text)
whiteSpaceTracker.move(textInfos.UNIT_CHARACTER, len(text))
if whiteSpaceTracker.compareEndPoints(info, "startToEnd") > 0:
break
bookmark = whiteSpaceTracker.bookmark
cb = partial(_whiteSpaceReachedCallback, bookmark=bookmark)
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
curCommandSequence.append(CallbackCommand(cb))
# The whiteSpaceTracker shouldn't move past the end of the info we're speaking.
start = end
relativeSpeechSequence[index] = curCommandSequence
expandedRelativeSpeechSequence = []
for x in relativeSpeechSequence:
if isinstance(x, list):
expandedRelativeSpeechSequence.extend(x)
else:
expandedRelativeSpeechSequence.append(x)
speechSequence.extend(expandedRelativeSpeechSequence)
#Finally get speech text for any fields left in new controlFieldStack that are common with the old controlFieldStack (for closing), if extra detail is not requested
if autoLanguageSwitching and lastLanguage is not None:
speechSequence.append(LangChangeCommand(None))
Expand Down Expand Up @@ -1763,7 +1811,7 @@ def getTableInfoSpeech(tableInfo,oldTableInfo,extraDetail=False):
textList.append(_("row %s")%rowNumber)
return " ".join(textList)

re_last_pause=re.compile(r"^(.*(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)",re.DOTALL|re.UNICODE)
re_last_pause=re.compile(r"^(.*?(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)",re.DOTALL|re.UNICODE)
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved

def speakWithoutPauses(speechSequence,detectBreaks=True):
"""
Expand Down
28 changes: 25 additions & 3 deletions source/speech/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
#Copyright (C) 2006-2019 NV Access Limited
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved

"""Commands that can be embedded in a speech sequence for changing synth parameters, playing sounds or running other callbacks."""

Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
from abc import ABCMeta, abstractmethod
import config
import languageHandler
from synthDriverHandler import getSynth
from inspect import signature
from functools import partial
from types import FunctionType, MethodType
from typing import Union

class SpeechCommand(object):
"""The base class for objects that can be inserted between strings of text to perform actions, change voice parameters, etc.
Expand All @@ -36,7 +40,8 @@ def __init__(self,index):
@param index: the value of this index
@type index: integer
"""
if not isinstance(index,int): raise ValueError("index must be int, not %s"%type(index))
if not isinstance(index,int):
raise TypeError("index must be int, not %s"%type(index))
self.index=index

def __repr__(self):
Expand Down Expand Up @@ -245,6 +250,8 @@ def run(self):
otherwise it will block production of further speech and or other functionality in NVDA.
"""

SUPPORTED_CALLBACK_TYPES = (FunctionType, MethodType, partial)

class CallbackCommand(BaseCallbackCommand):
"""
Call a function when speech reaches this point.
Expand All @@ -253,12 +260,27 @@ class CallbackCommand(BaseCallbackCommand):
otherwise it will block production of further speech and or other functionality in NVDA.
"""

def __init__(self, callback):
def __init__(self, callback: Union[SUPPORTED_CALLBACK_TYPES]):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is a class with a call supported by this union?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The only way to acoomplish this seems to be the use of collections.abc.Callable, but that also applies to classes itself. I'm reluctant to do this.

if not isinstance(callback, SUPPORTED_CALLBACK_TYPES):
raise TypeError(
"callback must be one of %s, not %s"
% (", ".join(SUPPORTED_CALLBACK_TYPES), type(callback))
)
self._callback = callback

def run(self,*args, **kwargs):
return self._callback(*args,**kwargs)

def __repr__(self):
if isinstance(self._callback, partial):
callBackName = "partial[{}]".format(self._callback.func.__qualname__)
else:
callBackName = self._callback.__qualname__
return "CallbackCommand({name}{signature})".format(
name=callBackName,
signature=str(signature(self._callback))
)

class BeepCommand(BaseCallbackCommand):
"""Produce a beep.
"""
Expand Down