@@ -83,32 +83,31 @@ def getLanguageToUse(mathMl:str) -> str:
8383def ConvertSSMLTextForNVDA (text :str , language :str = "" ) -> list :
8484 # MathCAT's default rate is 180 wpm.
8585 # Assume that 0% is 80 wpm and 100% is 450 wpm and scale accordingly.
86- # log.info(f"Speech str: '{text}'")
86+ # log.info(f"\nSpeech str: '{text}'")
8787 if language == "" : # shouldn't happen
8888 language = "en" # fallback to what was being used
89-
9089 mathCATLanguageSetting = "en" # fallback in case GetPreference fails for unknown reasons
9190 try :
9291 mathCATLanguageSetting = libmathcat .GetPreference ("Language" )
9392 except Exception as e :
9493 log .error (e )
9594
9695 synth = getSynth ()
96+ _monkeyPatchESpeak ()
9797 wpm = synth ._percentToParam (synth .rate , 80 , 450 )
9898 breakMulti = 180.0 / wpm
99- synthConfig = config .conf ["speech" ][synth .name ]
10099 supported_commands = synth .supportedCommands
101100 use_break = BreakCommand in supported_commands
102101 use_pitch = PitchCommand in supported_commands
103- use_rate = RateCommand in supported_commands
104- use_volume = VolumeCommand in supported_commands
102+ # use_rate = RateCommand in supported_commands
103+ # use_volume = VolumeCommand in supported_commands
105104 use_phoneme = PhonemeCommand in supported_commands
106105 # as of 7/23, oneCore voices do not implement the CharacterModeCommand despite it being in supported_commands
107106 use_character = CharacterModeCommand in supported_commands and synth .name != 'oneCore'
108107 out = []
109108 if mathCATLanguageSetting != language :
110109 try :
111- log .info (f"Setting language to { language } " )
110+ # log.info(f"Setting language to {language}")
112111 libmathcat .SetPreference ("Language" , language )
113112 out .append (LangChangeCommand (language ))
114113 except Exception as e :
@@ -125,7 +124,7 @@ def ConvertSSMLTextForNVDA(text:str, language:str="") -> list:
125124 if use_character :
126125 out .extend ((CharacterModeCommand (True ), ch , CharacterModeCommand (False )))
127126 else :
128- out .extend ((" " , "eigh" if ch == "a" else ch , " " ))
127+ out .extend ((" " , "eigh" if ch == "a" and language == "en" else ch , " " ))
129128 elif m .lastgroup == "beep" :
130129 out .append (BeepCommand (2000 , 50 ))
131130 elif m .lastgroup == "pitch" :
@@ -156,7 +155,6 @@ def ConvertSSMLTextForNVDA(text:str, language:str="") -> list:
156155 out .append (LangChangeCommand (None ))
157156 except Exception as e :
158157 log .error (e )
159-
160158 # log.info(f"Speech commands: '{out}'")
161159 return out
162160
@@ -343,7 +341,6 @@ def __init__(self):
343341 speech .speakMessage (_ ("MathCAT initialization failed: see NVDA error log for details" ))
344342 self ._language = ""
345343
346-
347344 def getSpeechForMathMl (self , mathml : str ):
348345 try :
349346 self ._language = getLanguageToUse (mathml )
@@ -398,3 +395,91 @@ def getBrailleForMathMl(self, mathml: str):
398395 def interactWithMathMl (self , mathml : str ):
399396 MathCATInteraction (provider = self , mathMl = mathml ).setFocus ()
400397 MathCATInteraction (provider = self , mathMl = mathml ).script_navigate (None )
398+
399+ CACHED_SYNTH = None
400+ def _monkeyPatchESpeak ():
401+ global CACHED_SYNTH
402+ currentSynth = getSynth ()
403+ if currentSynth .name != "espeak" or CACHED_SYNTH == currentSynth :
404+ return # already patched
405+
406+ CACHED_SYNTH = currentSynth
407+ currentSynth .speak = patched_speak .__get__ (currentSynth , type (currentSynth ))
408+
409+
410+ from speech .types import SpeechSequence
411+ from speech .commands import (
412+ IndexCommand ,
413+ CharacterModeCommand ,
414+ LangChangeCommand ,
415+ BreakCommand ,
416+ PitchCommand ,
417+ RateCommand ,
418+ VolumeCommand ,
419+ PhonemeCommand ,
420+ )
421+ def patched_speak (self , speechSequence : SpeechSequence ): # noqa: C901
422+ from synthDrivers import _espeak
423+ # log.info(f"patched_speak input: {speechSequence}")
424+ textList : List [str ] = []
425+ langChanged = False
426+ prosody : Dict [str , int ] = {}
427+ # We output malformed XML, as we might close an outer tag after opening an inner one; e.g.
428+ # <voice><prosody></voice></prosody>.
429+ # However, eSpeak doesn't seem to mind.
430+ for item in speechSequence :
431+ if isinstance (item ,str ):
432+ textList .append (self ._processText (item ))
433+ elif isinstance (item , IndexCommand ):
434+ textList .append ("<mark name=\" %d\" />" % item .index )
435+ elif isinstance (item , CharacterModeCommand ):
436+ textList .append ("<say-as interpret-as=\" characters\" >" if item .state else "</say-as>" )
437+ elif isinstance (item , LangChangeCommand ):
438+ langChangeXML = self ._handleLangChangeCommand (item , langChanged )
439+ textList .append (langChangeXML )
440+ langChanged = True
441+ elif isinstance (item , BreakCommand ):
442+ textList .append (f'<break time="{ item .time } ms" />' )
443+ elif type (item ) in self .PROSODY_ATTRS :
444+ if prosody :
445+ # Close previous prosody tag.
446+ textList .append ("</prosody>" )
447+ attr = self .PROSODY_ATTRS [type (item )]
448+ if item .multiplier == 1 :
449+ # Returning to normal.
450+ try :
451+ del prosody [attr ]
452+ except KeyError :
453+ pass
454+ else :
455+ prosody [attr ]= int (item .multiplier * 100 )
456+ if not prosody :
457+ continue
458+ textList .append ("<prosody" )
459+ for attr ,val in prosody .items ():
460+ textList .append (' %s="%d%%"' % (attr ,val ))
461+ textList .append (">" )
462+ elif isinstance (item , PhonemeCommand ):
463+ # We can't use str.translate because we want to reject unknown characters.
464+ try :
465+ phonemes = "" .join ([self .IPA_TO_ESPEAK [char ] for char in item .ipa ])
466+ # There needs to be a space after the phoneme command.
467+ # Otherwise, eSpeak will announce a subsequent SSML tag instead of processing it.
468+ textList .append (u"[[%s]] " % phonemes )
469+ except KeyError :
470+ log .debugWarning ("Unknown character in IPA string: %s" % item .ipa )
471+ if item .text :
472+ textList .append (self ._processText (item .text ))
473+ else :
474+ log .error ("Unknown speech: %s" % item )
475+ # Close any open tags.
476+ if langChanged :
477+ textList .append ("</voice>" )
478+ if prosody :
479+ textList .append ("</prosody>" )
480+ text = u"" .join (textList )
481+ # Added saving old rate and then resetting to that -- work around for https://github.com/nvaccess/nvda/issues/15221
482+ # I'm not clear why this works since _set_rate() is called before the speech is finished speaking
483+ oldRate = getSynth ()._get_rate ()
484+ _espeak .speak (text )
485+ getSynth ()._set_rate (oldRate )
0 commit comments