Skip to content

Commit 7214a0e

Browse files
authored
Merge 0cf4891 into b7dd32f
2 parents b7dd32f + 0cf4891 commit 7214a0e

6 files changed

Lines changed: 148 additions & 99 deletions

File tree

source/languageHandler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
# languageHandler.py
21
# A part of NonVisual Desktop Access (NVDA)
3-
# Copyright (C) 2007-2018 NV access Limited, Joseph Lee
2+
# Copyright (C) 2007-2021 NV access Limited, Joseph Lee
43
# This file is covered by the GNU General Public License.
54
# See the file COPYING for more details.
65

@@ -16,6 +15,7 @@
1615
import gettext
1716
import globalVars
1817
from logHandler import log
18+
from typing import Optional
1919

2020
#a few Windows locale constants
2121
LOCALE_SLANGUAGE=0x2
@@ -291,7 +291,8 @@ def setLocale(localeName: str) -> None:
291291
def getLanguage() -> str:
292292
return curLang
293293

294-
def normalizeLanguage(lang):
294+
295+
def normalizeLanguage(lang) -> Optional[str]:
295296
"""
296297
Normalizes a language-dialect string in to a standard form we can deal with.
297298
Converts any dash to underline, and makes sure that language is lowercase and dialect is upercase.

source/speech/manager.py

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# A part of NonVisual Desktop Access (NVDA)
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
5-
# Copyright (C) 2006-2020 NV Access Limited
5+
# Copyright (C) 2006-2021 NV Access Limited
66
import typing
77

88
import queueHandler
@@ -12,6 +12,7 @@
1212
from .commands import (
1313
# Commands that are used in this file.
1414
EndUtteranceCommand,
15+
LangChangeCommand,
1516
SynthParamCommand,
1617
BaseCallbackCommand,
1718
ConfigProfileTriggerCommand,
@@ -27,7 +28,6 @@
2728
Any,
2829
List,
2930
Tuple,
30-
Callable,
3131
Optional,
3232
cast,
3333
)
@@ -290,43 +290,49 @@ def _queueSpeechSequence(self, inSeq: SpeechSequence, priority: Spri) -> bool:
290290
return True
291291
return False
292292

293+
def _ensureEndUtterance(self, seq: SpeechSequence, outSeqs, paramsToReplay, paramTracker):
294+
"""
295+
We split at EndUtteranceCommands so the ends of utterances are easily found.
296+
This function ensures the given sequence ends with an EndUtterance command,
297+
Ensures that the sequence also includes an index command at the end,
298+
It places the complete sequence in outSeqs,
299+
It clears the given sequence list ready to build a new one,
300+
And clears the paramsToReplay list
301+
and refills it with any params that need to be repeated if a new sequence is going to be built.
302+
"""
303+
if seq:
304+
# There have been commands since the last split.
305+
lastOutSeq = paramsToReplay + seq
306+
outSeqs.append(lastOutSeq)
307+
paramsToReplay.clear()
308+
seq.clear()
309+
# Re-apply parameters that have been changed from their defaults.
310+
paramsToReplay.extend(paramTracker.getChanged())
311+
else:
312+
lastOutSeq = outSeqs[-1] if outSeqs else None
313+
lastCommand = lastOutSeq[-1] if lastOutSeq else None
314+
if lastCommand is None or isinstance(lastCommand, (EndUtteranceCommand, ConfigProfileTriggerCommand)):
315+
# It doesn't make sense to start with or repeat EndUtteranceCommands.
316+
# We also don't want an EndUtteranceCommand immediately after a ConfigProfileTriggerCommand.
317+
return
318+
if not isinstance(lastCommand, IndexCommand):
319+
# Add an index so we know when we've reached the end of this utterance.
320+
reachedIndex = next(self._indexCounter)
321+
lastOutSeq.append(IndexCommand(reachedIndex))
322+
outSeqs.append([EndUtteranceCommand()])
323+
293324
def _processSpeechSequence(self, inSeq: SpeechSequence):
325+
currentSynth = getSynth()
294326
paramTracker = ParamChangeTracker()
295327
enteredTriggers = []
296328
outSeqs = []
297329
paramsToReplay = []
298330

299-
def ensureEndUtterance(seq: SpeechSequence):
300-
# We split at EndUtteranceCommands so the ends of utterances are easily found.
301-
# This function ensures the given sequence ends with an EndUtterance command,
302-
# Ensures that the sequence also includes an index command at the end,
303-
# It places the complete sequence in outSeqs,
304-
# It clears the given sequence list ready to build a new one,
305-
# And clears the paramsToReplay list
306-
# and refills it with any params that need to be repeated if a new sequence is going to be built.
307-
if seq:
308-
# There have been commands since the last split.
309-
lastOutSeq = paramsToReplay + seq
310-
outSeqs.append(lastOutSeq)
311-
paramsToReplay.clear()
312-
seq.clear()
313-
# Re-apply parameters that have been changed from their defaults.
314-
paramsToReplay.extend(paramTracker.getChanged())
315-
else:
316-
lastOutSeq = outSeqs[-1] if outSeqs else None
317-
lastCommand = lastOutSeq[-1] if lastOutSeq else None
318-
if lastCommand is None or isinstance(lastCommand, (EndUtteranceCommand, ConfigProfileTriggerCommand)):
319-
# It doesn't make sense to start with or repeat EndUtteranceCommands.
320-
# We also don't want an EndUtteranceCommand immediately after a ConfigProfileTriggerCommand.
321-
return
322-
if not isinstance(lastCommand, IndexCommand):
323-
# Add an index so we know when we've reached the end of this utterance.
324-
reachedIndex = next(self._indexCounter)
325-
lastOutSeq.append(IndexCommand(reachedIndex))
326-
outSeqs.append([EndUtteranceCommand()])
327-
328331
outSeq = []
329332
for command in inSeq:
333+
if isinstance(command, LangChangeCommand):
334+
if command.lang not in currentSynth.availableLanguages:
335+
continue
330336
if isinstance(command, BaseCallbackCommand):
331337
# When the synth reaches this point, we want to call the callback.
332338
speechIndex = next(self._indexCounter)
@@ -347,21 +353,21 @@ def ensureEndUtterance(seq: SpeechSequence):
347353
if not command.enter and command.trigger not in enteredTriggers:
348354
log.debugWarning("Request to exit trigger which wasn't entered: %r" % command.trigger.spec)
349355
continue
350-
ensureEndUtterance(outSeq)
356+
self._ensureEndUtterance(outSeq, outSeqs, paramsToReplay, paramTracker)
351357
outSeqs.append([command])
352358
if command.enter:
353359
enteredTriggers.append(command.trigger)
354360
else:
355361
enteredTriggers.remove(command.trigger)
356362
continue
357363
if isinstance(command, EndUtteranceCommand):
358-
ensureEndUtterance(outSeq)
364+
self._ensureEndUtterance(outSeq, outSeqs, paramsToReplay, paramTracker)
359365
continue
360366
if isinstance(command, SynthParamCommand):
361367
paramTracker.update(command)
362368
outSeq.append(command)
363369
# Add the last sequence and make sure the sequence ends the utterance.
364-
ensureEndUtterance(outSeq)
370+
self._ensureEndUtterance(outSeq, outSeqs, paramsToReplay, paramTracker)
365371
# Exit any profile triggers the caller didn't exit.
366372
for trigger in reversed(enteredTriggers):
367373
command = ConfigProfileTriggerCommand(trigger, False)

source/speech/speech.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def speakMessage(
182182
speak(seq, symbolLevel=None, priority=priority)
183183

184184

185-
def getCurrentLanguage():
185+
def getCurrentLanguage() -> str:
186186
synth = getSynth()
187187
language=None
188188
if synth:

source/synthDriverHandler.py

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# A part of NonVisual Desktop Access (NVDA)
22
# This file is covered by the GNU General Public License.
33
# See the file COPYING for more details.
4-
# Copyright (C) 2006-2019 NV Access Limited, Peter Vágner, Aleksey Sadovoy,
4+
# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy,
55
# Joseph Lee, Arnold Loubriat, Leonard de Ruijter
66

77
import pkgutil
88
import importlib
9-
from typing import Optional
9+
from typing import Optional, OrderedDict, Set
1010
from locale import strxfrm
1111

1212
import config
@@ -38,11 +38,10 @@ class VoiceInfo(StringParameterInfo):
3838
"""Provides information about a single synthesizer voice.
3939
"""
4040

41-
def __init__(self, id, displayName, language=None):
41+
def __init__(self, id, displayName, language: Optional[str] = None):
4242
"""
4343
@param language: The ID of the language this voice speaks,
4444
C{None} if not known or the synth implements language separate from voices.
45-
@type language: str
4645
"""
4746
self.language = language
4847
super(VoiceInfo, self).__init__(id, displayName)
@@ -71,10 +70,6 @@ class SynthDriver(driverHandler.Driver):
7170
L{supportedNotifications} should specify what notifications the synthesizer provides.
7271
Currently, the available notifications are L{synthIndexReached} and L{synthDoneSpeaking}.
7372
Both of these must be supported.
74-
@ivar voice: Unique string identifying the current voice.
75-
@type voice: str
76-
@ivar availableVoices: The available voices.
77-
@type availableVoices: OrderedDict of L{VoiceInfo} keyed by VoiceInfo's ID
7873
@ivar pitch: The current pitch; ranges between 0 and 100.
7974
@type pitch: int
8075
@ivar rate: The current rate; ranges between 0 and 100.
@@ -102,6 +97,18 @@ class SynthDriver(driverHandler.Driver):
10297
#: @type: set of L{extensionPoints.Action} instances
10398
supportedNotifications = frozenset()
10499
_configSection = "speech"
100+
# type information for auto property _get_voice
101+
# Unique string identifying the current voice.
102+
voice: str
103+
# type information for auto property _get_availableVoices
104+
# OrderedDict of L{VoiceInfo} keyed by VoiceInfo's ID
105+
availableVoices: OrderedDict[str, VoiceInfo]
106+
# type information for auto property _get_language
107+
# the current voice's language
108+
language: Optional[str]
109+
# type information for auto property _get_availableLanguages
110+
# the set of languages available in the availableVoices
111+
availableLanguages: Set[Optional[str]]
105112

106113
@classmethod
107114
def LanguageSetting(cls):
@@ -218,29 +225,28 @@ def cancel(self):
218225
"""Silence speech immediately.
219226
"""
220227

221-
def _get_language(self):
228+
def _get_language(self) -> Optional[str]:
222229
return self.availableVoices[self.voice].language
223230

224231
def _set_language(self, language):
225232
raise NotImplementedError
226233

227-
def _get_availableLanguages(self):
228-
raise NotImplementedError
234+
def _get_availableLanguages(self) -> Set[Optional[str]]:
235+
return {self.availableVoices[v].language for v in self.availableVoices}
229236

230237
def _get_voice(self):
231238
raise NotImplementedError
232239

233240
def _set_voice(self, value):
234241
pass
235242

236-
def _getAvailableVoices(self):
243+
def _getAvailableVoices(self) -> OrderedDict[str, VoiceInfo]:
237244
"""fetches an ordered dictionary of voices that the synth supports.
238245
@returns: an OrderedDict of L{VoiceInfo} instances representing the available voices, keyed by ID
239-
@rtype: OrderedDict
240246
"""
241247
raise NotImplementedError
242248

243-
def _get_availableVoices(self):
249+
def _get_availableVoices(self) -> OrderedDict[str, VoiceInfo]:
244250
if not hasattr(self, '_availableVoices'):
245251
self._availableVoices = self._getAvailableVoices()
246252
return self._availableVoices
@@ -449,28 +455,39 @@ def setSynth(name, isFallback=False):
449455
prevSynthName = None
450456
try:
451457
_curSynth = getSynthInstance(name)
458+
except: # noqa: E722 # Legacy bare except
459+
log.error(f"setSynth failed for {name}", exc_info=True)
460+
461+
if _curSynth is not None:
452462
_audioOutputDevice = config.conf["speech"]["outputDevice"]
453463
if not isFallback:
454464
config.conf["speech"]["synth"] = name
455-
log.info("Loaded synthDriver %s" % name)
465+
log.info(f"Loaded synthDriver {_curSynth.name}")
456466
return True
457-
except: # noqa: E722 # Legacy bare except
458-
log.error("setSynth", exc_info=True)
459-
# As there was an error loading this synth:
460-
if prevSynthName:
461-
# There was a previous synthesizer, so switch back to that one.
462-
setSynth(prevSynthName, isFallback=True)
463-
else:
464-
# There was no previous synth, so fallback to the next available default synthesizer
465-
# that has not been tried yet.
466-
try:
467-
nextIndex = defaultSynthPriorityList.index(name) + 1
468-
except ValueError:
469-
nextIndex = 0
470-
if nextIndex < len(defaultSynthPriorityList):
471-
newName = defaultSynthPriorityList[nextIndex]
472-
setSynth(newName, isFallback=True)
473-
return False
467+
# As there was an error loading this synth:
468+
elif prevSynthName:
469+
log.info(f"Falling back to previous synthDriver {prevSynthName}")
470+
# There was a previous synthesizer, so switch back to that one.
471+
setSynth(prevSynthName, isFallback=True)
472+
else:
473+
# There was no previous synth, so fallback to the next available default synthesizer
474+
# that has not been tried yet.
475+
findAndSetNextSynth(name)
476+
return False
477+
478+
479+
def findAndSetNextSynth(currentSynthName: str) -> bool:
480+
"""Returns True if the next synth could be found, False if currentSynthName is the last synth
481+
in the defaultSynthPriorityList"""
482+
if currentSynthName in defaultSynthPriorityList:
483+
nextIndex = defaultSynthPriorityList.index(currentSynthName) + 1
484+
else:
485+
nextIndex = 0
486+
if nextIndex < len(defaultSynthPriorityList):
487+
newName = defaultSynthPriorityList[nextIndex]
488+
setSynth(newName, isFallback=True)
489+
return True
490+
return False
474491

475492

476493
def handlePostConfigProfileSwitch(resetSpeechIfNeeded=True):

0 commit comments

Comments
 (0)