1- # MathCAT add-on: generates speech, braille, and allows exploration of expressions written in MathML
2- # The goal of this add-on is to replicate/improve upon the functionality of MathPlayer which has been discontinued.
1+ """ MathCAT add-on: generates speech, braille, and allows exploration of expressions written in MathML.
2+ The goal of this add-on is to replicate/improve upon the functionality of MathPlayer which has been discontinued."""
33# Author: Neil Soiffer
44# Copyright: this file is copyright GPL2
55# The code additionally makes use of the MathCAT library (written in Rust) which is covered by the MIT license
4141 PhonemeCommand ,
4242)
4343
44- RE_MP_SPEECH = re .compile (
44+ RE_MATHML_SPEECH = re .compile (
4545 # Break.
4646 r"<break time='(?P<break>\d+)ms'/> ?"
4747 # Pronunciation of characters.
6262 "volume" : VolumeCommand ,
6363 "rate" : RateCommand ,
6464}
65-
66- def ConvertSSMLTextForNVDA (text :str , language :str = "" ):
65+ RE_MATH_LANG = re .compile (r'''<math .*(xml:)?lang=["']([^'"]+)["'].*>''' )
66+ def getLanguageToUse (mathMl :str ) -> str :
67+ """Get the language specified in a math tag if the language pref is Auto, else the language preference."""
68+ mathCATLanguageSetting = "Auto"
69+ try :
70+ # ignore regional differences if the MathCAT language setting doesn't have it.
71+ mathCATLanguageSetting = libmathcat .GetPreference ("Language" )
72+ except Exception as e :
73+ log .error (e )
74+
75+ if mathCATLanguageSetting != 'Auto' :
76+ return mathCATLanguageSetting
77+
78+ languageMatch = RE_MATH_LANG .search (mathMl )
79+ language = languageMatch .group (2 ) if languageMatch else speech .getCurrentLanguage () # seems to be current voice's language
80+ language = language .lower ().replace ("_" , "-" )
81+ return language
82+
83+ def ConvertSSMLTextForNVDA (text :str , language :str = "" ) -> list :
6784 # MathCAT's default rate is 180 wpm.
6885 # Assume that 0% is 80 wpm and 100% is 450 wpm and scale accordingly.
69- # log.info("Speech str: '{}'".format(text))
86+ # log.info(f"Speech str: '{text}'")
87+ if language == "" : # shouldn't happen
88+ language = "en" # fallback to what was being used
89+
90+ mathCATLanguageSetting = "en" # fallback in case GetPreference fails for unknown reasons
91+ try :
92+ mathCATLanguageSetting = libmathcat .GetPreference ("Language" )
93+ except Exception as e :
94+ log .error (e )
95+
7096 synth = getSynth ()
7197 wpm = synth ._percentToParam (synth .rate , 80 , 450 )
7298 breakMulti = 180.0 / wpm
@@ -77,12 +103,20 @@ def ConvertSSMLTextForNVDA(text:str, language:str=""):
77103 use_rate = RateCommand in supported_commands
78104 use_volume = VolumeCommand in supported_commands
79105 use_phoneme = PhonemeCommand in supported_commands
80- use_character = CharacterModeCommand in supported_commands
106+ # as of 7/23, oneCore voices do not implement the CharacterModeCommand despite it being in supported_commands
107+ use_character = CharacterModeCommand in supported_commands and synth .name != 'oneCore'
81108 out = []
82- if language :
83- out .append (LangChangeCommand (language ))
109+ if mathCATLanguageSetting != language :
110+ try :
111+ log .info (f"Setting language to { language } " )
112+ libmathcat .SetPreference ("Language" , language )
113+ out .append (LangChangeCommand (language ))
114+ except Exception as e :
115+ log .error (e )
116+ language = mathCATLanguageSetting # didn't do the 'append'
117+
84118 resetProsody = []
85- for m in RE_MP_SPEECH .finditer (text ):
119+ for m in RE_MATHML_SPEECH .finditer (text ):
86120 if m .lastgroup == "break" :
87121 if use_break :
88122 out .append (BreakCommand (time = int (int (m .group ("break" )) * breakMulti )))
@@ -91,7 +125,7 @@ def ConvertSSMLTextForNVDA(text:str, language:str=""):
91125 if use_character :
92126 out .extend ((CharacterModeCommand (True ), ch , CharacterModeCommand (False )))
93127 else :
94- out .extend ((" " , ch , " " ))
128+ out .extend ((" " , "eigh" if ch == "a" else ch , " " ))
95129 elif m .lastgroup == "beep" :
96130 out .append (BeepCommand (2000 , 50 ))
97131 elif m .lastgroup == "pitch" :
@@ -116,9 +150,14 @@ def ConvertSSMLTextForNVDA(text:str, language:str=""):
116150 # MathCAT puts out spaces between words, the speak command seems to want to glom the strings together at times,
117151 # so we need to add individual " "s to the output
118152 out .extend ((" " , m .group (0 ), " " ))
119- if language :
120- out .append (LangChangeCommand (None ))
121- # log.info("Speech commands: '{}'".format(out))
153+ if mathCATLanguageSetting != language :
154+ try :
155+ libmathcat .SetPreference ("Language" , mathCATLanguageSetting )
156+ out .append (LangChangeCommand (None ))
157+ except Exception as e :
158+ log .error (e )
159+
160+ # log.info(f"Speech commands: '{out}'")
122161 return out
123162
124163class MathCATInteraction (mathPres .MathInteractionNVDAObject ):
@@ -131,17 +170,18 @@ class MathCATInteraction(mathPres.MathInteractionNVDAObject):
131170
132171 def __init__ (self , provider = None , mathMl : Optional [str ]= None ):
133172 super (MathCATInteraction , self ).__init__ (provider = provider , mathMl = mathMl )
134- provider . _setSpeechLanguage (mathMl )
173+ self . _language = getLanguageToUse (mathMl )
135174 self .init_mathml = mathMl
136175
176+
137177 def reportFocus (self ):
138178 super (MathCATInteraction , self ).reportFocus ()
139179 try :
140180 text = libmathcat .DoNavigateCommand ("ZoomIn" )
141- speech .speak (ConvertSSMLTextForNVDA (text , self .provider . _language ))
181+ speech .speak (ConvertSSMLTextForNVDA (text , self ._language ))
142182 except Exception as e :
143183 log .error (e )
144- speech .speakMessage (_ ("Error in speaking math: see NVDA error log for details" ))
184+ speech .speakMessage (_ ("Error in starting navigation of math: see NVDA error log for details" ))
145185
146186
147187 def getBrailleRegions (self , review : bool = False ):
@@ -181,7 +221,7 @@ def script_navigate(self, gesture: KeyboardInputGesture):
181221 modNames = gesture .modifierNames
182222 text = libmathcat .DoNavigateKeyPress (gesture .vkCode ,
183223 "shift" in modNames , "control" in modNames , "alt" in modNames , False )
184- speech .speak (ConvertSSMLTextForNVDA (text , self .provider . _language ))
224+ speech .speak (ConvertSSMLTextForNVDA (text , self ._language ))
185225
186226 # update the braille to reflect the nav position (might be excess code, but it works)
187227 nav_node = libmathcat .GetNavigationMathMLId ()
@@ -295,21 +335,22 @@ def __init__(self):
295335 try :
296336 # IMPORTANT -- SetRulesDir must be the first call to libmathcat
297337 rules_dir = path .join ( path .dirname (path .abspath (__file__ )), "Rules" )
298- log .info ("MathCAT Rules dir: %s" % rules_dir )
338+ log .info (f "MathCAT installed. Using rules dir: { rules_dir } " )
299339 libmathcat .SetRulesDir (rules_dir )
300340 libmathcat .SetPreference ("TTS" , "SSML" )
301-
302341 except Exception as e :
303342 log .error (e )
304343 speech .speakMessage (_ ("MathCAT initialization failed: see NVDA error log for details" ))
344+ self ._language = ""
305345
306346
307347 def getSpeechForMathMl (self , mathml : str ):
308- self ._setSpeechLanguage (mathml )
309348 try :
349+ self ._language = getLanguageToUse (mathml )
310350 libmathcat .SetMathML (mathml )
311351 except Exception as e :
312352 log .error (e )
353+ log .error (f"MathML is { mathml } " )
313354 speech .speakMessage (_ ("Illegal MathML found: see NVDA error log for details" ))
314355 libmathcat .SetMathML ("<math></math>" ) # set it to something
315356 try :
@@ -322,9 +363,9 @@ def getSpeechForMathMl(self, mathml: str):
322363 if PitchCommand in supported_commands :
323364 libmathcat .SetPreference ("CapitalLetters_Pitch" , str (synthConfig ["capPitchChange" ]))
324365 if self ._add_sounds ():
325- return [BeepCommand (800 ,25 )] + ConvertSSMLTextForNVDA (libmathcat .GetSpokenText ()) + [BeepCommand (600 ,15 )]
366+ return [BeepCommand (800 ,25 )] + ConvertSSMLTextForNVDA (libmathcat .GetSpokenText (), self . _language ) + [BeepCommand (600 ,15 )]
326367 else :
327- return ConvertSSMLTextForNVDA (libmathcat .GetSpokenText ())
368+ return ConvertSSMLTextForNVDA (libmathcat .GetSpokenText (), self . _language )
328369
329370 except Exception as e :
330371 log .error (e )
@@ -343,6 +384,7 @@ def getBrailleForMathMl(self, mathml: str):
343384 libmathcat .SetMathML (mathml )
344385 except Exception as e :
345386 log .error (e )
387+ log .error (f"MathML is { mathml } " )
346388 speech .speakMessage (_ ("Illegal MathML found: see NVDA error log for details" ))
347389 libmathcat .SetMathML ("<math></math>" ) # set it to something
348390 try :
@@ -356,11 +398,3 @@ def getBrailleForMathMl(self, mathml: str):
356398 def interactWithMathMl (self , mathml : str ):
357399 MathCATInteraction (provider = self , mathMl = mathml ).setFocus ()
358400 MathCATInteraction (provider = self , mathMl = mathml ).script_navigate (None )
359-
360- def _setSpeechLanguage (self , mathml : str ):
361- # NVDA inserts its notion of the current language into the math tag, so we can't use it
362- # see nvda\source\mathPres\mathPlayer.py for original version of this code
363- # lang = mathPres.getLanguageFromMath(mathml)
364-
365- # it might have changed, so can't just set it in init()
366- self ._language = libmathcat .GetPreference ("Language" )
0 commit comments