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
Support for Windows OneCore voices included in Windows 10. #7110
Conversation
This uses a C++/CX dll to access the UWP SpeechSynthesizer class. There are other UWP APIs we might like to access in future (e.g. OCR), so rather than making this dll specific to OneCore speech, it's called nvdaHelperLocalWin10. The build system for this dll makes it easy to add other components in future. In addition, this required code to generate balanced XML from an NVDA speech sequence. Although we use SSML for eSpeak, eSpeak happily accepts unbalanced (malformed) XML. OneCore speech does not. This code is in the speechXml module. This might eventually be reused to replace the ugly balanced XML code in the SAPI5 driver.
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.
Attribute values should be also escaped, which will require " in xml_escapes
source/synthDrivers/oneCore.py
Outdated
""" | ||
# comtypes.BSTR.from_address seems to cause a crash for some reason. Not sure why. | ||
# Just access the string ourselves. | ||
val = ctypes.wstring_at(address) |
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.
Would there ever be the case where a NULL char might be in that string? probably not, but you could use SysStringLen to get the length and pass it to wstring_at.
source/synthDrivers/oneCore.py
Outdated
super(SynthDriver, self).terminate() | ||
# Drop the ctypes function instance for the callback, | ||
# as it is holding a reference to an instance method, which causes a reference cycle. | ||
self._dll.ocSpeech_terminate(self._handle) |
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.
Perhaps move this line above the comment as the comment is only about the callback.
ComPtr<IBufferByteAccess> bufferByteAccess; | ||
insp.As(&bufferByteAccess); | ||
byte* bytes = nullptr; | ||
bufferByteAccess->Buffer(&bytes); |
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.
Can inst.as fail? I.e. that interface is not supported? and perhaps then the pointer would be NULL?
@feerrenrut, would you mind reviewing 3b43394 and c02a788? These fix a reported issue where invalid characters break the OneCore Speech driver. @michaelDCurran originally reviewed this, but he's away for a week or so now and it'd be good to get this in ASAP. The first commit ensures that errors get logged and don't break the driver for all subsequent speech. Previously, there was no logging and speech just went silent. The second fixes the actual problem with invalid characters. |
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.
As discussed, the changes look good.
…eeded for OneCore Speech.
@feerrenrut, would you mind reviewing ba62552? It updates the build system to bundle some required redistributable VC2015 components that aren't installed on all systems. Two users confirmed that this fixes the problem for them; see this nvda-devel thread. |
…indows, as otherwise, OneCore throws an error.
…indows 10 (unless running from source).
@feerrenrut, would you mind reviewing three more commits? Sorry. :(
|
source/synthDrivers/oneCore.py
Outdated
@@ -81,6 +84,13 @@ def convertCharacterModeCommand(self, command): | |||
# Therefore, we don't use it. | |||
return None | |||
|
|||
def convertLangChangeCommand(self, command): | |||
lcid = languageHandler.localeNameToWindowsLCID(command.lang) | |||
if lcid in (0, languageHandler.LOCALE_CUSTOM_UNSPECIFIED): |
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.
How do you get a value of zero and what does it represent? Is there a test for this case?
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.
Honestly, I'm not sure. I did this because the other place we use that function checks for 0. It seems we can return 0 on Windows XP (where we implement all of that function ourselves), but that doesn't apply here, since this is always Windows 10. On Vista and later, we use the Windows LocaleNameToLCID function. That function says it can return 0 for invalid parameter values, but I don't know how to trigger this.
What do you think? On one hand, it might be better to remove the 0 so users notice potential unknown problems and report them. On the other hand, if we do get 0, there's probably nothing we can do and we'll end up just ignoring the language anyway.
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.
In that case I think its best to keep checking for it and logging. It might be worth adding the return value to the log message. I guess we could name the zero something like INVALID_PARAM_VALUES
or include some of what you said here as a comment to save the next person from having to look up what zero means.
@feerrenrut, as discussed, I've simplified |
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 these comments really help those who aren't familiar with this area.
@@ -1,34 +0,0 @@ | |||
#tests/unit/synthDrivers/test_oneCore.py |
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 guess this was removed by accident?
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.
No; it was intentional. I got rid of the oneCore synth driver tests and instead added tests for localeNameToWindowsLCID , since the method I was testing in oneCore is really just a thin wrapper around the latter.
source/languageHandler.py
Outdated
# #6259: LOCALE_CUSTOM_UNSPECIFIED denotes custom locale in Windows 10, thus returns "unknown language" or an odd description (observed for Aragonese). | ||
# See https://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.lcid(v=vs.110).aspx. | ||
if LCID not in (0, LOCALE_CUSTOM_UNSPECIFIED): | ||
if LCID is not LCID_NONE: |
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 this is much clearer
As we discussed yesterday, don't forget to update the readme. I had to ensure that I installed |
…on the Windows 10 Tools and SDK.
Readme updated. |
This uses a C++/CX dll to access the UWP SpeechSynthesizer class. There are other UWP APIs we might like to access in future (e.g. OCR), so rather than making this dll specific to OneCore speech, it's called nvdaHelperLocalWin10. The build system for this dll makes it easy to add other components in future.
In addition, this required code to generate balanced XML from an NVDA speech sequence. Although we use SSML for eSpeak, eSpeak happily accepts unbalanced (malformed) XML. OneCore speech does not. This code is in the speechXml module. This might eventually be reused to replace the ugly balanced XML code in the SAPI5 driver.
Fixes #6159.