Skip to content

Commit 8929919

Browse files
committed
Hack fix to #51 by monkey-patching the eSpeak speak() method.
I'm not totally sure why the fix works. The fix is to save the rate before the underlying speak() call and reset the rate after the call. However, the bug involves `<break>` and the speech isn't finished (probably not even started) by the time the reset happens. But it does work... There is no other change to the eSpeak method, so hopefully there is no interference with other calls to it. Note: because it was easiest, the monkey-patching is done on a espeak instance, not on the class itself. This is also a little safer because it won't happen unless someone is reading math.
1 parent ab6f71e commit 8929919

File tree

1 file changed

+94
-9
lines changed

1 file changed

+94
-9
lines changed

NVDA-addon/addon/globalPlugins/MathCAT/MathCAT.py

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,32 +83,31 @@ def getLanguageToUse(mathMl:str) -> str:
8383
def 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

Comments
 (0)