|
1 | 1 | # A part of NonVisual Desktop Access (NVDA) |
2 | 2 | # This file is covered by the GNU General Public License. |
3 | 3 | # 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, |
5 | 5 | # Joseph Lee, Arnold Loubriat, Leonard de Ruijter |
6 | 6 |
|
7 | 7 | import pkgutil |
8 | 8 | import importlib |
9 | | -from typing import Optional |
| 9 | +from typing import Optional, OrderedDict, Set |
10 | 10 | from locale import strxfrm |
11 | 11 |
|
12 | 12 | import config |
@@ -38,11 +38,10 @@ class VoiceInfo(StringParameterInfo): |
38 | 38 | """Provides information about a single synthesizer voice. |
39 | 39 | """ |
40 | 40 |
|
41 | | - def __init__(self, id, displayName, language=None): |
| 41 | + def __init__(self, id, displayName, language: Optional[str] = None): |
42 | 42 | """ |
43 | 43 | @param language: The ID of the language this voice speaks, |
44 | 44 | C{None} if not known or the synth implements language separate from voices. |
45 | | - @type language: str |
46 | 45 | """ |
47 | 46 | self.language = language |
48 | 47 | super(VoiceInfo, self).__init__(id, displayName) |
@@ -71,10 +70,6 @@ class SynthDriver(driverHandler.Driver): |
71 | 70 | L{supportedNotifications} should specify what notifications the synthesizer provides. |
72 | 71 | Currently, the available notifications are L{synthIndexReached} and L{synthDoneSpeaking}. |
73 | 72 | 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 |
78 | 73 | @ivar pitch: The current pitch; ranges between 0 and 100. |
79 | 74 | @type pitch: int |
80 | 75 | @ivar rate: The current rate; ranges between 0 and 100. |
@@ -102,6 +97,18 @@ class SynthDriver(driverHandler.Driver): |
102 | 97 | #: @type: set of L{extensionPoints.Action} instances |
103 | 98 | supportedNotifications = frozenset() |
104 | 99 | _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]] |
105 | 112 |
|
106 | 113 | @classmethod |
107 | 114 | def LanguageSetting(cls): |
@@ -218,29 +225,28 @@ def cancel(self): |
218 | 225 | """Silence speech immediately. |
219 | 226 | """ |
220 | 227 |
|
221 | | - def _get_language(self): |
| 228 | + def _get_language(self) -> Optional[str]: |
222 | 229 | return self.availableVoices[self.voice].language |
223 | 230 |
|
224 | 231 | def _set_language(self, language): |
225 | 232 | raise NotImplementedError |
226 | 233 |
|
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} |
229 | 236 |
|
230 | 237 | def _get_voice(self): |
231 | 238 | raise NotImplementedError |
232 | 239 |
|
233 | 240 | def _set_voice(self, value): |
234 | 241 | pass |
235 | 242 |
|
236 | | - def _getAvailableVoices(self): |
| 243 | + def _getAvailableVoices(self) -> OrderedDict[str, VoiceInfo]: |
237 | 244 | """fetches an ordered dictionary of voices that the synth supports. |
238 | 245 | @returns: an OrderedDict of L{VoiceInfo} instances representing the available voices, keyed by ID |
239 | | - @rtype: OrderedDict |
240 | 246 | """ |
241 | 247 | raise NotImplementedError |
242 | 248 |
|
243 | | - def _get_availableVoices(self): |
| 249 | + def _get_availableVoices(self) -> OrderedDict[str, VoiceInfo]: |
244 | 250 | if not hasattr(self, '_availableVoices'): |
245 | 251 | self._availableVoices = self._getAvailableVoices() |
246 | 252 | return self._availableVoices |
@@ -449,28 +455,39 @@ def setSynth(name, isFallback=False): |
449 | 455 | prevSynthName = None |
450 | 456 | try: |
451 | 457 | _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: |
452 | 462 | _audioOutputDevice = config.conf["speech"]["outputDevice"] |
453 | 463 | if not isFallback: |
454 | 464 | config.conf["speech"]["synth"] = name |
455 | | - log.info("Loaded synthDriver %s" % name) |
| 465 | + log.info(f"Loaded synthDriver {_curSynth.name}") |
456 | 466 | 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 |
474 | 491 |
|
475 | 492 |
|
476 | 493 | def handlePostConfigProfileSwitch(resetSpeechIfNeeded=True): |
|
0 commit comments