Closed LeonarddeR closed 3 weeks ago
I'm running a build that contains this PR and am receiving an error pretty frequently when trying to navigate with speech normalization enabled.
Could not execute function event_gainFocus defined in appModules.nvda module; kwargs: {}
Traceback (most recent call last):
File "eventHandler.pyc", line 127, in next
File "NVDAObjects__init__.pyc", line 1306, in event_gainFocus
File "NVDAObjects__init__.pyc", line 1157, in reportFocus
File "speech\speech.pyc", line 694, in speakObject
File "speech\speech.pyc", line 733, in getObjectSpeech
File "speech\speech.pyc", line 660, in getObjectPropertiesSpeech
File "speech\speech.pyc", line 1975, in getPropertiesSpeech
File "speech\speech.pyc", line 1975, in
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "eventHandler.pyc", line 127, in next
File "appModules\nvda.pyc", line 211, in event_gainFocus
File "eventHandler.pyc", line 134, in next
File "extensionPoints\util.pyc", line 216, in callWithSupportedKwargs
File "NVDAObjects__init__.pyc", line 1306, in event_gainFocus
File "NVDAObjects__init__.pyc", line 1157, in reportFocus
File "speech\speech.pyc", line 694, in speakObject
File "speech\speech.pyc", line 733, in getObjectSpeech
File "speech\speech.pyc", line 660, in getObjectPropertiesSpeech
File "speech\speech.pyc", line 1975, in getPropertiesSpeech
File "speech\speech.pyc", line 1975, in
CC @codeofdusk
It looks like there is an erroneous type annotation in getPropertiesSpeech
that miss-leaded me. getKeyboardShortcutsSpeech
can now also return character mode commands, so the getPropertiesSpeech
doesn't only return strings now.
@Qchristensen Could you have a look at the proposed documentation changes?
Hi - testing code rabbit on this PR for an AI review. I've tried to adjust it to only pick up serious issues rather than linting. Apologies for the noise if it picks up junk.
@coderabbitai review
@seanbudd, I have identified the issue causing the error reported by Simon818. The getPropertiesSpeech
function attempts to normalize all items in the textList
, but some items are not strings, leading to a type error. Here is the updated code for the getPropertiesSpeech
function to ensure only strings are passed to unicodeNormalize
:
def getPropertiesSpeech( # noqa: C901
reason: OutputReason = OutputReason.QUERY,
**propertyValues
) -> SpeechSequence:
textList: SpeechSequence = []
name: Optional[str] = propertyValues.get('name')
if name:
textList.append(name)
if 'role' in propertyValues:
role: controlTypes.Role = propertyValues['role']
speakRole=True
elif '_role' in propertyValues:
speakRole=False
role: controlTypes.Role = propertyValues['_role']
else:
speakRole=False
role=controlTypes.Role.UNKNOWN
role = controlTypes.Role(role)
value: Optional[str] = propertyValues.get('value') if role not in controlTypes.silentValuesForRoles else None
cellCoordsText: Optional[str] = propertyValues.get('cellCoordsText')
rowNumber = propertyValues.get('rowNumber')
columnNumber = propertyValues.get('columnNumber')
includeTableCellCoords = propertyValues.get('includeTableCellCoords', True)
if role == controlTypes.Role.CHARTELEMENT:
speakRole = False
roleText: Optional[str] = propertyValues.get('roleText')
if (
speakRole
and (
roleText
or reason not in (
OutputReason.SAYALL,
OutputReason.CARET,
OutputReason.FOCUS,
OutputReason.QUICKNAV
)
or not (
name
or value
or cellCoordsText
or rowNumber
or columnNumber
)
or role not in controlTypes.silentRolesOnFocus
)
and (
role != controlTypes.Role.MATH
or reason not in (
OutputReason.CARET,
OutputReason.SAYALL
)
)):
textList.append(roleText if roleText else role.displayString)
if value:
textList.append(value)
states = propertyValues.get('states')
realStates=propertyValues.get('_states',states)
negativeStates=propertyValues.get('negativeStates',set())
if states is not None or negativeStates:
if states is None:
states = set()
labelStates = controlTypes.processAndLabelStates(role, realStates, reason, states, negativeStates)
textList.extend(labelStates)
description: Optional[str] = propertyValues.get('description')
if description:
textList.append(description)
keyboardShortcut: Optional[str] = propertyValues.get('keyboardShortcut')
textList.extend(getKeyboardShortcutsSpeech(keyboardShortcut))
if includeTableCellCoords and cellCoordsText:
textList.append(cellCoordsText)
if cellCoordsText or rowNumber or columnNumber:
tableID = propertyValues.get("_tableID")
sameTable = (tableID and tableID == _speechState.oldTableID)
if tableID and not sameTable:
_speechState.oldTableID = tableID
rowSpan = propertyValues.get("rowSpan") or 1
columnSpan = propertyValues.get("columnSpan") or 1
if rowNumber and (
not sameTable
or rowNumber != _speechState.oldRowNumber
or rowSpan != _speechState.oldRowSpan
):
rowHeaderText: Optional[str] = propertyValues.get("rowHeaderText")
if rowHeaderText:
textList.append(rowHeaderText)
if includeTableCellCoords and not cellCoordsText:
rowNumberTranslation: str = _("row %s") % rowNumber
textList.append(rowNumberTranslation)
if rowSpan>1 and columnSpan<=1:
rowSpanAddedTranslation: str = _("through {endRow}").format(endRow=rowNumber + rowSpan - 1)
textList.append(rowSpanAddedTranslation)
_speechState.oldRowNumber = rowNumber
_speechState.oldRowSpan = rowSpan
if columnNumber and (
not sameTable
or columnNumber != _speechState.oldColumnNumber
or columnSpan != _speechState.oldColumnSpan
):
columnHeaderText: Optional[str] = propertyValues.get("columnHeaderText")
if columnHeaderText:
textList.append(columnHeaderText)
if includeTableCellCoords and not cellCoordsText:
colNumberTranslation: str = _("column %s") % columnNumber
textList.append(colNumberTranslation)
if columnSpan>1 and rowSpan<=1:
colSpanAddedTranslation: str = _("through {endCol}").format(endCol=columnNumber + columnSpan - 1)
textList.append(colSpanAddedTranslation)
_speechState.oldColumnNumber = columnNumber
_speechState.oldColumnSpan = columnSpan
if includeTableCellCoords and not cellCoordsText and rowSpan>1 and columnSpan>1:
rowColSpanTranslation: str = _("through row {row} column {column}").format(
row=rowNumber + rowSpan - 1,
column=columnNumber + columnSpan - 1
)
textList.append(rowColSpanTranslation)
rowCount=propertyValues.get('rowCount',0)
columnCount=propertyValues.get('columnCount',0)
rowAndColumnCountText = _rowAndColumnCountText(rowCount, columnCount)
if rowAndColumnCountText:
textList.append(rowAndColumnCountText)
if rowCount or columnCount:
_speechState.oldTableID = None
isCurrent = propertyValues.get('current', controlTypes.IsCurrent.NO)
if isCurrent != controlTypes.IsCurrent.NO:
textList.append(isCurrent.displayString)
hasDetails = propertyValues.get('hasDetails', False)
if hasDetails:
detailsRoles: _AnnotationRolesT = propertyValues.get("detailsRoles", tuple())
if detailsRoles:
roleStrings = (role.displayString if role else _("details") for role in detailsRoles)
for roleString in roleStrings:
textList.append(
_("has %s") % roleString
)
else:
textList.append(
_("has details")
)
placeholder: Optional[str] = propertyValues.get('placeholder', None)
if placeholder:
textList.append(placeholder)
indexInGroup=propertyValues.get('positionInfo_indexInGroup',0)
similarItemsInGroup=propertyValues.get('positionInfo_similarItemsInGroup',0)
if 0<indexInGroup<=similarItemsInGroup:
itemPosTranslation: str = _("{number} of {total}").format(
number=indexInGroup,
total=similarItemsInGroup
)
textList.append(itemPosTranslation)
if 'positionInfo_level' in propertyValues:
level=propertyValues.get('positionInfo_level',None)
role=propertyValues.get('role',None)
if level is not None:
levelTranslation: str = _('level %s') % level
if (
role in (controlTypes.Role.TREEVIEWITEM, controlTypes.Role.LISTITEM)
and level != _speechState.oldTreeLevel
):
textList.insert(0, levelTranslation)
_speechState.oldTreeLevel = level
else:
textList.append(levelTranslation)
errorMessage: str | None = propertyValues.get("errorMessage", None)
if errorMessage:
textList.append(errorMessage)
if config.conf["speech"]["unicodeNormalization"]:
textList = [
unicodeNormalize(t) if isinstance(t, str) else t
for t in textList
]
types.logBadSequenceTypes(textList)
return textList
This update ensures that only strings are passed to unicodeNormalize
, preventing the type error.
The recent changes introduce Unicode normalization to NVDA's speech and braille output, enhancing the handling of complex characters like ligatures and composed characters. This feature can be enabled via the settings dialog for both speech and braille, ensuring compatibility with various synthesizers and braille tables.
File Path | Change Summary |
---|---|
source/braille.py |
Added Unicode normalization handling for braille translation, including offset processing. |
source/config/configSpec.py |
Introduced a unicodeNormalization feature flag for different sections, including speech and braille. |
source/gui/settingsDialogs.py |
Added unicodeNormalizationCombo control for managing Unicode normalization settings in the GUI. |
source/speech/speech.py |
Integrated Unicode normalization in speech processing based on configuration settings. |
source/textUtils.py |
Added utilities for Unicode normalization, including UnicodeNormalizationOffsetConverter class. |
tests/unit/test_textUtils.py |
Added tests for UnicodeNormalizationOffsetConverter to verify normalization scenarios. |
user_docs/en/changes.md |
Documented the addition of Unicode normalization support for speech and braille. |
user_docs/en/userGuide.md |
Added sections explaining Unicode normalization settings for speech and braille. |
Objective | Addressed | Explanation |
---|---|---|
Add optional Unicode normalization before passing strings to speech or braille (#16466) | ✅ | |
Ensure normalization handles ligatures and composed characters correctly (#16466) | ✅ | |
Provide settings to enable/disable Unicode normalization for both speech and braille (#16466) | ✅ |
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?
It looks mainly as noise to me honestly
I am getting empty braille line when reading given message in thunderbird. I encountered likely problem earlier as well with squirrelmail (web mail).
This happens when braille is tethered to review and unicode normalization is enabled for braille.
Here are log lines (running from source with current main branch code):
Input: kb(laptop):downArrow DEBUGWARNING - NVDAObjects.IAccessible.normalizeIA2TextFormatField (15:11:47.970) - MainThread (13568): Unsupported value for text-align attribute: "start" IO - speech.speech.speak (15:11:47.971) - MainThread (13568): Speaking ['removed original text\xa0\n'] DEBUGWARNING - NVDAObjects.IAccessible.normalizeIA2TextFormatField (15:11:47.992) - MainThread (13568): Unsupported value for text-align attribute: "start" DEBUGWARNING - braille.BrailleHandler._handlePendingUpdate (15:11:47.992) - MainThread (13568): Region update failed for ReviewCursorManagerRegion ('removed original text\xa0 '), object probably died Traceback (most recent call last): File "braille.py", line 2621, in _handlePendingUpdate region.update() File "braille.py", line 1378, in update super(TextInfoRegion, self).update() File "braille.py", line 503, in update converter = UnicodeNormalizationOffsetConverter(self.rawText) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "textUtils.py", line 439, in init self.computedStrToEncodedOffsets, self.computedEncodedToStrOffsets = self._calculateOffsets() ^^^^^^^^^^^^^^^^^^^^^^^^ File "textUtils.py", line 496, in _calculateOffsets originMultiplier = min(originPartLen / normalizedPartLen, 1)
ZeroDivisionError: division by zero
IO - braille.BrailleBuffer.update (15:11:47.993) - MainThread (13568):
Braille regions text: ['removed original text\xa0 ']
IO - braille.BrailleHandler.update (15:11:47.993) - MainThread (13568):
Braille window dots: -
Thanks for reporting. Easily reproducible on the python console:
textUtils.UnicodeNormalizationOffsetConverter("removed original text\xa0 ")
I will handle this as part of #16584
Hi, viewing a FB post starting with styled chars, I noticed that requesting window title to NVDA I get the not normalized string. Anyway, the post is correctly readable by voice, that is a great, great enhancement!
We only normalize objects and TextInfo, not messages. This can be changed if we agree that moving by character should report normalized characters instead, because then it is much easier to apply normalization over every speech sequence, including character by character navigation. However I'm still reluctant to do so.
@LeonarddeR Can you edit the PR description to add that it closes #4631 and #11570, in case this gets reverted?
These are not yet closed. #16584 will probably close them.
Link to issue number:
Closes #16466
Summary of the issue:
Several speech synthesizers and braille tables are unable to speak or braille some characters, such as ligatures ("ij") or decomposed characters (latin letters with a modifier to add acute, diaeresis, etc.). Also, italic or bold Unicode characters can't be spoken or brailled by default.
Description of user facing changes
None by default. If unicode normalization is enabled for speech, speech output for objects and text navigation is normalized. For braille, normalization is applied for all braille output.
The reason for speech to apply normalization only for objects and text navigation is chosen on purpose, as for individual character navigation or text selection, we really want to pass the original character to the synthesizer. If we don't Unicode bold and italic characters are read as their normalized counterparts, which makes it impossible to distinguish them. This problem is less relevant when working with braille.
Description of development approach
UnicodeNormalizationOffsetConverter
totextUtils
with tests. It stores the normalized version of a given string and based on diffing, calculates offset mappings between the original and normalized strings.UnicodeNormalizationOffsetConverter
when translating braille with normalization on. The several mappings (braille to raw pos, raw to braille pos) are processed to account for normalization.getTextInfoSpeech
andgetPropertiesSpeech
.Testing strategy:
Known issues with pull request:
None known.
Code Review Checklist:
Summary by CodeRabbit
New Features
Documentation