Skip to content

Commit 700ad28

Browse files
authored
Merge 1261b8e into 47af401
2 parents 47af401 + 1261b8e commit 700ad28

3 files changed

Lines changed: 71 additions & 31 deletions

File tree

source/speech/manager.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -331,15 +331,6 @@ def _processSpeechSequence(self, inSeq: SpeechSequence):
331331

332332
outSeq = []
333333
for command in inSeq:
334-
if isinstance(command, LangChangeCommand) and currentSynth.name == 'oneCore':
335-
langCode = command.lang.split('_')[0]
336-
langSupported = False
337-
currentSynthLanguages = currentSynth.availableLanguages
338-
for lang in currentSynthLanguages:
339-
if lang and normalizeLanguage(lang).split('_')[0] == langCode:
340-
langSupported = True
341-
if not langSupported:
342-
log.warning(f"Language {command.lang} not supported by {currentSynth.name} ({currentSynthLanguages})")
343334
if isinstance(command, BaseCallbackCommand):
344335
# When the synth reaches this point, we want to call the callback.
345336
speechIndex = next(self._indexCounter)

source/speechXml.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2016-2021 NV Access Limited
2+
# Copyright (C) 2016-2022 NV Access Limited
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

@@ -14,7 +14,7 @@
1414
import re
1515
import speech
1616
import textUtils
17-
from speech.commands import SpeechCommand
17+
from speech.commands import LangChangeCommand, SpeechCommand
1818
from logHandler import log
1919

2020
XML_ESCAPES = {
@@ -49,7 +49,8 @@ def _buildInvalidXmlRegexp():
4949
RE_INVALID_XML_CHARS = _buildInvalidXmlRegexp()
5050
REPLACEMENT_CHAR = textUtils.REPLACEMENT_CHAR
5151

52-
def toXmlLang(nvdaLang):
52+
53+
def toXmlLang(nvdaLang: str) -> str:
5354
"""Convert an NVDA language to an XML language.
5455
"""
5556
return nvdaLang.replace("_", "-")
@@ -153,7 +154,7 @@ def _outputTags(self):
153154
self._openTags.append(tag)
154155
self._tagsChanged = False
155156

156-
def generateXml(self, commands):
157+
def generateXml(self, commands) -> str:
157158
"""Generate XML from a sequence of balancer commands and text.
158159
"""
159160
for command in commands:
@@ -235,7 +236,7 @@ class SsmlConverter(SpeechXmlConverter):
235236
"""Converts an NVDA speech sequence to SSML.
236237
"""
237238

238-
def __init__(self, defaultLanguage):
239+
def __init__(self, defaultLanguage: str):
239240
self.defaultLanguage = toXmlLang(defaultLanguage)
240241

241242
def generateBalancerCommands(self, speechSequence):
@@ -254,7 +255,7 @@ def convertCharacterModeCommand(self, command):
254255
else:
255256
return StopEnclosingTextCommand()
256257

257-
def convertLangChangeCommand(self, command):
258+
def convertLangChangeCommand(self, command: LangChangeCommand) -> SetAttrCommand:
258259
lang = command.lang or self.defaultLanguage
259260
lang = toXmlLang(lang)
260261
return SetAttrCommand("voice", "xml:lang", lang)

source/synthDrivers/oneCore.py

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"""
88

99
import os
10-
import sys
10+
from typing import Any, Callable, Generator, List, Optional, Set, Tuple, Union
1111
from collections import OrderedDict
1212
import ctypes
1313
import winreg
@@ -25,6 +25,7 @@
2525
import config
2626
import nvwave
2727
import queueHandler
28+
from speech.types import SpeechSequence
2829
import speech
2930
import speechXml
3031
import languageHandler
@@ -47,6 +48,14 @@
4748
ocSpeech_Callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_wchar_p)
4849

4950
class _OcSsmlConverter(speechXml.SsmlConverter):
51+
def __init__(
52+
self,
53+
defaultLanguage: str,
54+
availableLanguages: Set[str],
55+
):
56+
self.lowerCaseAvailableLanguages = {language.lower() for language in availableLanguages}
57+
self.availableLanguagesWithoutLocale = {language.split("_")[0] for language in self.lowerCaseAvailableLanguages}
58+
super().__init__(defaultLanguage)
5059

5160
def _convertProsody(self, command, attr, default, base=None):
5261
if base is None:
@@ -74,23 +83,40 @@ def convertCharacterModeCommand(self, command):
7483
# Therefore, we don't use it.
7584
return None
7685

77-
def convertLangChangeCommand(self, command):
86+
def convertLangChangeCommand(self, command: LangChangeCommand) -> Optional[speechXml.SetAttrCommand]:
7887
lcid = languageHandler.localeNameToWindowsLCID(command.lang)
7988
if lcid is languageHandler.LCID_NONE:
8089
log.debugWarning(f"Invalid language: {command.lang}")
8190
return None
91+
92+
normalizedLanguage = command.lang.lower().replace("-", "_")
93+
normalizedLanguageWithoutLocale = normalizedLanguage.split("_")[0]
94+
if (
95+
normalizedLanguage not in self.lowerCaseAvailableLanguages
96+
and normalizedLanguageWithoutLocale not in self.availableLanguagesWithoutLocale
97+
):
98+
log.warning(f"Language {command.lang} not supported ({self.lowerCaseAvailableLanguages})")
99+
return None
100+
82101
return super().convertLangChangeCommand(command)
83102

84103
class _OcPreAPI5SsmlConverter(_OcSsmlConverter):
85104

86-
def __init__(self, defaultLanguage, rate, pitch, volume):
87-
super(_OcPreAPI5SsmlConverter, self).__init__(defaultLanguage)
105+
def __init__(
106+
self,
107+
defaultLanguage: str,
108+
availableLanguages: Set[str],
109+
rate: float,
110+
pitch: float,
111+
volume: float,
112+
):
113+
super().__init__(defaultLanguage, availableLanguages)
88114
self._rate = rate
89115
self._pitch = pitch
90116
self._volume = volume
91117

92-
def generateBalancerCommands(self, speechSequence):
93-
commands = super(_OcPreAPI5SsmlConverter, self).generateBalancerCommands(speechSequence)
118+
def generateBalancerCommands(self, speechSequence: SpeechSequence) -> Generator[Any, None, None]:
119+
commands = super().generateBalancerCommands(speechSequence)
94120
# The EncloseAllCommand from SSML must be first.
95121
yield next(commands)
96122
# OneCore didn't provide a way to set base prosody values before API version 5.
@@ -135,6 +161,9 @@ class OneCoreSynthDriver(SynthDriver):
135161
}
136162
supportedNotifications = {synthIndexReached, synthDoneSpeaking}
137163

164+
_ocSpeechToken: Optional[ctypes.POINTER]
165+
_queuedSpeech: List[Union[str, Tuple[Callable[[ctypes.POINTER, float], None], float]]]
166+
138167
@classmethod
139168
def check(cls):
140169
# Only present this as an available synth if this is Windows 10.
@@ -235,11 +264,17 @@ def cancel(self):
235264
if self._player:
236265
self._player.stop()
237266

238-
def speak(self, speechSequence):
267+
def speak(self, speechSequence: SpeechSequence) -> None:
239268
if self.supportsProsodyOptions:
240-
conv = _OcSsmlConverter(self.language)
269+
conv = _OcSsmlConverter(self.language, self.availableLanguages)
241270
else:
242-
conv = _OcPreAPI5SsmlConverter(self.language, self._rate, self._pitch, self._volume)
271+
conv = _OcPreAPI5SsmlConverter(
272+
self.language,
273+
self.availableLanguages,
274+
self._rate,
275+
self._pitch,
276+
self._volume
277+
)
243278
text = conv.convertToXml(speechSequence)
244279
# #7495: Calling WaveOutOpen blocks for ~100 ms if called from the callback
245280
# when the SSML includes marks.
@@ -249,7 +284,7 @@ def speak(self, speechSequence):
249284
self._player.open()
250285
self._queueSpeech(text)
251286

252-
def _queueSpeech(self, item):
287+
def _queueSpeech(self, item: str) -> None:
253288
self._queuedSpeech.append(item)
254289
# We only process the queue here if it isn't already being processed.
255290
if not self._isProcessing:
@@ -433,7 +468,8 @@ def _getVoiceInfoFromOnecoreVoiceString(self, voiceStr):
433468
def _getAvailableVoices(self):
434469
voices = OrderedDict()
435470
# Fetch the full list of voices that Onecore speech knows about.
436-
# Note that it may give back voices that are uninstalled or broken.
471+
# Note that it may give back voices that are uninstalled or broken.
472+
# Refer to _isVoiceValid for information on uninstalled or broken voices.
437473
voicesStr = self._dll.ocSpeech_getVoices(self._ocSpeechToken).split('|')
438474
for index,voiceStr in enumerate(voicesStr):
439475
voiceInfo=self._getVoiceInfoFromOnecoreVoiceString(voiceStr)
@@ -444,14 +480,26 @@ def _getAvailableVoices(self):
444480
voices[voiceInfo.id] = voiceInfo
445481
return voices
446482

447-
def _isVoiceValid(self,ID):
448-
"""
483+
def _isVoiceValid(self, ID: str) -> bool:
484+
r"""
449485
Checks that the given voice actually exists and is valid.
450486
It checks the Registry, and also ensures that its data files actually exist on this machine.
451487
@param ID: the ID of the requested voice.
452-
@type ID: string
453-
@returns: True if the voice is valid, false otherwise.
454-
@rtype: boolean
488+
@returns: True if the voice is valid, False otherwise.
489+
490+
OneCore keeps specific registry caches of OneCore for AT applications.
491+
NVDA's OneCore cache is: `HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4`.
492+
The caches contain a subtree which is meant to mirror the path `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\*`.
493+
494+
For example
495+
`HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4\
496+
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM`
497+
refers to `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM`.
498+
499+
Languages which have been used by an installed copy of NVDA, but uninstalled from the system are kept in the cache.
500+
OneCore will still attempt to use these languages, so we must check if they are valid first.
501+
502+
Refer to https://github.com/nvaccess/nvda/issues/13732#issuecomment-1149386711 for more information.
455503
"""
456504
IDParts = ID.split('\\')
457505
rootKey = getattr(winreg, IDParts[0])

0 commit comments

Comments
 (0)