From 8daeb77c8dfeb0fadca50969296d5ca11a79d3af Mon Sep 17 00:00:00 2001 From: David CM Date: Sun, 25 Aug 2019 11:16:54 -0600 Subject: [PATCH] version 19.8B3 add support for speech change commands. update now volume, rate and speed default parameters are send at start of speak method on ibmeci. delete getVParam and setVParam methods from ibmeci delete parameters sends on _ibmeci, its needed to implement speech commands correctly. update MIN_RATE was changed from 0 to 40 update now settings are set in the base profile only. --- addon/globalPlugins/ibmtts.py | 10 +- addon/installTasks.py | 1 + addon/synthDrivers/_ibmeci.py | 82 ++++++++------- addon/synthDrivers/_settingsDB.py | 9 +- addon/synthDrivers/ibmeci.py | 165 +++++++++++++++--------------- buildVars.py | 2 +- 6 files changed, 141 insertions(+), 128 deletions(-) diff --git a/addon/globalPlugins/ibmtts.py b/addon/globalPlugins/ibmtts.py index 0692894..da581ce 100644 --- a/addon/globalPlugins/ibmtts.py +++ b/addon/globalPlugins/ibmtts.py @@ -31,8 +31,8 @@ def makeSettings(self, settingsSizer): self._setValues() def _setValues(self): - self._ttsPath.SetValue(config.conf['ibmeci']['TTSPath']) - self._dllName.SetValue(config.conf['ibmeci']['dllName']) + self._ttsPath.SetValue(config.conf.profiles[0]['ibmeci']['TTSPath']) + self._dllName.SetValue(config.conf.profiles[0]['ibmeci']['dllName']) def _onBrowseClick(self, evt): # Translators: The message displayed in the dialog that allows you to look for the IBMTTS library. @@ -94,7 +94,7 @@ def _onSetLocalClick(self, evt): if res: self._ttsPath.SetValue("ibmtts") # this parameter is saved even if the user doesn't click accept button. - config.conf['ibmeci']['TTSPath'] = self._ttsPath.GetValue() + config.conf.profiles[0]['ibmeci']['TTSPath'] = self._ttsPath.GetValue() # Translators: The message displayed when copying IBMTTS files to Add-on was successful. gui.messageBox(_("Successfully copied IBMTTS files. The local copy will be used after restart NVDA."), # Translators: The title displayed when copying IBMTTS files to Add-on was successful. @@ -118,8 +118,8 @@ def copyTtsFiles(self): installer.tryCopyFile(sourceFilePath,destFilePath) def onSave(self): - config.conf['ibmeci']['dllName'] = self._dllName.GetValue() - config.conf['ibmeci']['TTSPath'] = self._ttsPath.GetValue() + config.conf.profiles[0]['ibmeci']['dllName'] = self._dllName.GetValue() + config.conf.profiles[0]['ibmeci']['TTSPath'] = self._ttsPath.GetValue() class GlobalPlugin(globalPluginHandler.GlobalPlugin): diff --git a/addon/installTasks.py b/addon/installTasks.py index 0072f22..bc76fe4 100644 --- a/addon/installTasks.py +++ b/addon/installTasks.py @@ -1,6 +1,7 @@ # -*- coding: UTF-8 -*- #Copyright (C) 2009 - 2019 David CM, released under the GPL. # Author: David CM and others. +# note: this file doesn't get settings from the base profile to avoid issues when updating from older versions. from synthDrivers import _settingsDB import config, gui, os, wx, addonHandler diff --git a/addon/synthDrivers/_ibmeci.py b/addon/synthDrivers/_ibmeci.py index 62217f1..b0f3403 100644 --- a/addon/synthDrivers/_ibmeci.py +++ b/addon/synthDrivers/_ibmeci.py @@ -35,8 +35,7 @@ class ECIParam: class ECIVoiceParam: params = range(1,8) eciGender, eciHeadSize, eciPitchBaseline, eciPitchFluctuation, eciRoughness, eciBreathiness, eciSpeed, eciVolume = range(8) - - + class ECIDictVolume: eciMainDict, eciRootDict, eciAbbvDict, eciMainDictExt = range(4) @@ -46,59 +45,63 @@ class ECIMessage: class ECICallbackReturn: eciDataNotProcessed, eciDataProcessed, eciDataAbort= range(3) +# constants +samples=3300 user32 = windll.user32 +WM_PROCESS = 1025 +WM_SILENCE = 1026 +WM_PARAM = 1027 +WM_VPARAM=1028 +WM_COPYVOICE=1029 +WM_KILL=1030 +WM_SYNTH=1031 +WM_INDEX=1032 + +langs={ + 'esp': (131072, _('Castilian Spanish'), 'es_ES', 'es'), + 'esm': (131073, _('Latin American Spanish'), 'es_ME', 'es_CO'), + 'ptb': (458752, _('Brazilian Portuguese'), 'pt_BR', 'pt'), + 'fra': (196608, _('French'), 'fr_FR', 'fr'), + 'frc': (196609, _('French Canadian'), 'fr_CA', ''), + 'fin': (589824, _('Finnish'), 'fi_FI', 'fi'), + 'chs': (393216, _('Chinese'), 'zh_GB', 'zh'), + 'jpn': (524288, _('Japanese'), 'ja_JA', 'jp'), + 'kor': (655360, _('Korean'), 'ko_KO', 'ko'), + 'deu': (262144, _('German'), 'de_DE', 'de'), + 'ita': (327680, _('Italian'), 'it_IT', 'it'), + 'enu': (65536, _('American English'), 'en_US', 'en'), + 'eng': (65537, _('British English'), 'en_UK', '') +} + audioStream = BytesIO() speaking=False eciThread = None +callbackQueue = None +callbackThread = None eciQueue = None eciThreadId = None -callbackThread = None -callbackQueue = None +idleTimer = None onIndexReached = None onDoneSpeaking = None -samples=3300 buffer = create_string_buffer(samples*2) -idleTimer = None + stopped = threading.Event() started = threading.Event() param_event = threading.Event() - - lastindex=0 -langs={'esp': (131072, _('Castilian Spanish'), 'es_ES', 'es'), -'esm': (131073, _('Latin American Spanish'), 'es_ME', 'es_CO'), -'ptb': (458752, _('Brazilian Portuguese'), 'pt_BR', 'pt'), -'fra': (196608, _('French'), 'fr_FR', 'fr'), -'frc': (196609, _('French Canadian'), 'fr_CA', ''), -'fin': (589824, _('Finnish'), 'fi_FI', 'fi'), -'chs': (393216, _('Chinese'), 'zh_GB', 'zh'), -'jpn': (524288, _('Japanese'), 'ja_JA', 'jp'), -'kor': (655360, _('Korean'), 'ko_KO', 'ko'), -'deu': (262144, _('German'), 'de_DE', 'de'), -'ita': (327680, _('Italian'), 'it_IT', 'it'), -'enu': (65536, _('American English'), 'en_US', 'en'), -'eng': (65537, _('British English'), 'en_UK', '')} - avLangs=0 ttsPath="" dllName="" -WM_PROCESS = 1025 -WM_SILENCE = 1026 -WM_PARAM = 1027 -WM_VPARAM=1028 -WM_COPYVOICE=1029 -WM_KILL=1030 -WM_SYNTH=1031 -WM_INDEX=1032 -params = {} -vparams = {} - #We can only have one of each in NVDA. Make this global dll = None handle = None +params = {} +vparams = {} + + class EciThread(threading.Thread): def run(self): global vparams, params, speaking, endMarkersCount @@ -170,13 +173,12 @@ def processEciQueue(): def eciCheck(): global ttsPath, dllName, dll - dllName = config.conf['ibmeci']['dllName'] - ttsPath = config.conf['ibmeci']['TTSPath'] + dllName = config.conf.profiles[0]['ibmeci']['dllName'] + ttsPath = config.conf.profiles[0]['ibmeci']['TTSPath'] if not path.isabs(ttsPath): ttsPath = path.join(path.abspath(path.dirname(__file__)), ttsPath) if path.exists(ttsPath): iniCheck() - print (ttsPath, path.exists(ttsPath)) if not path.exists(ttsPath): return False if dll: return True try: @@ -216,6 +218,7 @@ def eciNew(): def _callbackExec(func, *args, **kwargs): global callbackQueue callbackQueue.put((func, args, kwargs)) + def setLast(lp): global lastindex lastindex = lp @@ -305,9 +308,10 @@ def initialize(indexCallback, doneCallback): callbackThread.start() def speak(text): -#Sometimes the synth slows down for one string of text. Why? -#Trying to fix it here. - if ECIVoiceParam.eciSpeed in vparams: text = b"`vs%d%s" % (vparams[ECIVoiceParam.eciSpeed], text) + # deleted the following fix because is incompatible with NVDA's speech change command. Now send it from speak in ibmeci.py + #Sometimes the synth slows down for one string of text. Why? + #Trying to fix it here. + # if ECIVoiceParam.eciSpeed in vparams: text = b"`vs%d%s" % (vparams[ECIVoiceParam.eciSpeed], text) dll.eciAddText(handle, text) def index(x): diff --git a/addon/synthDrivers/_settingsDB.py b/addon/synthDrivers/_settingsDB.py index a129726..edc1109 100644 --- a/addon/synthDrivers/_settingsDB.py +++ b/addon/synthDrivers/_settingsDB.py @@ -9,4 +9,11 @@ "dllName": "string(default='eci.dll')", "TTSPath": "string(default='ibmtts')" } -config.conf.spec["ibmeci"]=confspec \ No newline at end of file +config.conf.spec["ibmeci"]=confspec + +def setConfig(): + d=config.conf['ibmeci'].dict() + if 'ibmeci' not in config.conf.profiles[0]: config.conf.profiles[0]['ibmeci'] = d + if 'TTSPath' not in config.conf.profiles[0]['ibmeci']: config.conf.profiles[0]['ibmeci']['TTSPath'] = d['TTSPath'] + if 'dllName' not in config.conf.profiles[0]['ibmeci']: config.conf.profiles[0]['ibmeci']['dllName'] = d['dllName'] +setConfig() diff --git a/addon/synthDrivers/ibmeci.py b/addon/synthDrivers/ibmeci.py index 4d6ae62..c0e1e71 100644 --- a/addon/synthDrivers/ibmeci.py +++ b/addon/synthDrivers/ibmeci.py @@ -28,7 +28,7 @@ def unicode(s): punctuation = "-,.?!:;" -minRate=0 +minRate=40 maxRate=156 pause_re = re.compile(br'([a-zA-Z])([-.(),:;!?])( |$)') @@ -37,61 +37,61 @@ def unicode(s): anticrash_res = { re.compile(br'\b(|\d+|\W+)(|un|anti|re)c(ae|\xe6)sur', re.I): br'\1\2seizur', re.compile(br"\b(|\d+|\W+)h'(r|v)[e]", re.I): br"\1h ' \2 e", -# re.compile(r"\b(|\d+|\W+)wed[h]esday", re.I): r"\1wed hesday", -re.compile(br'hesday'): b' hesday', - re.compile(br"\b(|\d+|\W+)tz[s]che", re.I): br"\1tz sche" + # re.compile(r"\b(|\d+|\W+)wed[h]esday", re.I): r"\1wed hesday", + re.compile(br'hesday'): b' hesday', + re.compile(br"\b(|\d+|\W+)tz[s]che", re.I): br"\1tz sche" } english_fixes = { -re.compile(r'(\w+)\.([a-zA-Z]+)'): r'\1 dot \2', -re.compile(r'([a-zA-Z0-9_]+)@(\w+)'): r'\1 at \2', -} -french_fixes = { -re.compile(r'([a-zA-Z0-9_]+)@(\w+)'): r'\1 arobase \2', + re.compile(r'(\w+)\.([a-zA-Z]+)'): r'\1 dot \2', + re.compile(r'([a-zA-Z0-9_]+)@(\w+)'): r'\1 at \2', } +french_fixes = { re.compile(r'([a-zA-Z0-9_]+)@(\w+)'): r'\1 arobase \2' } spanish_fixes = { -#for emails -re.compile(r'([a-zA-Z0-9_]+)@(\w+)'): r'\1 arroba \2', -re.compile(u'([€$]\d{1,3})((\s\d{3})+\.\d{2})'): r'\1 \2', + #for emails + re.compile(r'([a-zA-Z0-9_]+)@(\w+)'): r'\1 arroba \2', + re.compile(u'([€$]\d{1,3})((\s\d{3})+\.\d{2})'): r'\1 \2', } -variants = {1:"Reed", -2:"Shelley", -3:"Bobby", -4:"Rocko", -5:"Glen", -6:"Sandy", -7:"Grandma", -8:"Grandpa"} +variants = { + 1:"Reed", + 2:"Shelley", + 3:"Bobby", + 4:"Rocko", + 5:"Glen", + 6:"Sandy", + 7:"Grandma", + 8:"Grandpa" +} # For langChangeCommand langsAnnotations={ -"en":b"`l1", -"en_US":b"`l1.0", -"en_UK":b"`l1.1", -"en_GB":b"`l1.1", -"es":b"`l2", -"es_ES":b"`l2.0", -"es_ME":b"`l2.1", -"fr":b"`l3", -"fr_FR":b"`l3.0", -"fr_CA":b"`l3.1", -"de":b"`l4", -"de_DE":b"`l4", -"it":b"`l5", -"it_IT":b"`l5", -"zh":b"`l6", -"zh_gb":b"`l6.0", -"pt":b"`l7", -"pt_BR":b"`l7.0", -"pt_PT":b"`l7.1", -"ja":b"`l8", -"ja_ja":b"`l8.0", -"ko":b"`l10", -"ko_ko":b"`l10.0", -"fi":b"`l9", -"fi_FI":b"`l9.0" + "en":b"`l1", + "en_US":b"`l1.0", + "en_UK":b"`l1.1", + "en_GB":b"`l1.1", + "es":b"`l2", + "es_ES":b"`l2.0", + "es_ME":b"`l2.1", + "fr":b"`l3", + "fr_FR":b"`l3.0", + "fr_CA":b"`l3.1", + "de":b"`l4", + "de_DE":b"`l4", + "it":b"`l5", + "it_IT":b"`l5", + "zh":b"`l6", + "zh_gb":b"`l6.0", + "pt":b"`l7", + "pt_BR":b"`l7.0", + "pt_PT":b"`l7.1", + "ja":b"`l8", + "ja_ja":b"`l8.0", + "ko":b"`l10", + "ko_ko":b"`l10.0", + "fi":b"`l9", + "fi_FI":b"`l9.0" } class SynthDriver(synthDriverHandler.SynthDriver): @@ -132,19 +132,20 @@ def __init__(self): self.variant="1" PROSODY_ATTRS = { - speech.PitchCommand: "`vs", - speech.VolumeCommand: "`vv", - speech.RateCommand: "vb", + speech.PitchCommand: b'vb', + speech.VolumeCommand: b'vv', + speech.RateCommand: b'vs', } def speak(self,speechSequence): last = None defaultLanguage=self.language outlist = [] - outlist.append((_ibmeci.speak, (b"`ts0",))) + outlist.append((_ibmeci.speak, (b'`ts0 `pp0 `vb%d `vs%d `vv%d ' %(self.pitch, _ibmeci.getVParam(ECIVoiceParam.eciSpeed), self.volume),))) for item in speechSequence: if isinstance(item, string_types): s = self.processText(unicode(item)) + if not s: continue outlist.append((_ibmeci.speak, (s,))) last = s elif isinstance(item,speech.IndexCommand): @@ -164,11 +165,13 @@ def speak(self,speechSequence): outlist.append((_ibmeci.speak, (b"`ts1" if item.state else b"`ts0",))) elif isinstance(item,speech.BreakCommand): outlist.append((_ibmeci.speak, (b' `p%d ' %item.time,))) - elif isinstance(item,speech.SpeechCommand): - log.debugWarning("Unsupported speech command: %s"%item) + elif type(item) in self.PROSODY_ATTRS: + val = max(0, min(item.newValue, 100)) + if type(item) == speech.RateCommand: val = self.percentToRate(val) + outlist.append((_ibmeci.speak, (b' `%s%d ' %(self.PROSODY_ATTRS[type(item)], val),))) else: - log.error("Unknown speech: %s"%item) - if last is not None and not str(last[-1]) in punctuation: outlist.append((_ibmeci.speak, (b'`p1. ',))) + log.error("Unknown speech: %s" %item) + if last and str(last[-1]) not in punctuation: outlist.append((_ibmeci.speak, (b'`p1. ',))) outlist.append((_ibmeci.setEndStringMark, ())) outlist.append((_ibmeci.synth, ())) _ibmeci.eciQueue.put(outlist) @@ -181,9 +184,8 @@ def processText(self,text): if _ibmeci.params[9] in (196609, 196608): text = resub(french_fixes, text) text = text.replace('quil', 'qil') #Sometimes this string make everything buggy with IBMTTS in French - #if not self._backquoteVoiceTags: text = text.replace(u'‵', ' ') if self._backquoteVoiceTags: - text = "`pp0 `vv%d %s" % (self.getVParam(ECIVoiceParam.eciVolume), text.replace('`', ' ')) #no embedded commands + text = text.replace('`', ' ') #no embedded commands text = resub(anticrash_res, text) #this converts to ansi for anticrash. If this breaks with foreign langs, we can remove it. text = text.encode('mbcs', 'replace') @@ -191,7 +193,7 @@ def processText(self,text): #this converts to ansi for anticrash. If this breaks with foreign langs, we can remove it. text = text.encode('mbcs', 'replace') text = resub(anticrash_res, text) - text = b"`pp0 `vv%d %s" % (self.getVParam(ECIVoiceParam.eciVolume), text.replace(b'`', b' ')) #no embedded commands + text = text.replace(b'`', b' ') #no embedded commands text = pause_re.sub(br'\1 `p1\2\3', text) text = time_re.sub(br'\1:\2 \3', text) return text @@ -224,55 +226,59 @@ def _set_rateBoost(self, enable): def _get_rate(self): - val = self.getVParam(ECIVoiceParam.eciSpeed) + val = _ibmeci.getVParam(ECIVoiceParam.eciSpeed) if self._rateBoost: val=int(round(val/self.RATE_BOOST_MULTIPLIER)) return self._paramToPercent(val, minRate, maxRate) - def _set_rate(self,vl): - val = self._percentToParam(vl, minRate, maxRate) + def percentToRate(self, val): + val = self._percentToParam(val, minRate, maxRate) if self._rateBoost: val = int(round(val *self.RATE_BOOST_MULTIPLIER)) + return val + + def _set_rate(self,val): + val = self.percentToRate(val) self._rate = val - self.setVParam(ECIVoiceParam.eciSpeed, val) + _ibmeci.setVParam(ECIVoiceParam.eciSpeed, val) def _get_pitch(self): - return self.getVParam(ECIVoiceParam.eciPitchBaseline) + return _ibmeci.getVParam(ECIVoiceParam.eciPitchBaseline) def _set_pitch(self,vl): - self.setVParam(ECIVoiceParam.eciPitchBaseline,vl) + _ibmeci.setVParam(ECIVoiceParam.eciPitchBaseline,vl) def _get_volume(self): - return self.getVParam(ECIVoiceParam.eciVolume) + return _ibmeci.getVParam(ECIVoiceParam.eciVolume) def _set_volume(self,vl): - self.setVParam(ECIVoiceParam.eciVolume,int(vl)) + _ibmeci.setVParam(ECIVoiceParam.eciVolume,int(vl)) def _set_inflection(self,vl): vl = int(vl) - self.setVParam(ECIVoiceParam.eciPitchFluctuation,vl) + _ibmeci.setVParam(ECIVoiceParam.eciPitchFluctuation,vl) def _get_inflection(self): - return self.getVParam(ECIVoiceParam.eciPitchFluctuation) + return _ibmeci.getVParam(ECIVoiceParam.eciPitchFluctuation) def _set_hsz(self,vl): vl = int(vl) - self.setVParam(ECIVoiceParam.eciHeadSize,vl) + _ibmeci.setVParam(ECIVoiceParam.eciHeadSize,vl) def _get_hsz(self): - return self.getVParam(ECIVoiceParam.eciHeadSize) + return _ibmeci.getVParam(ECIVoiceParam.eciHeadSize) def _set_rgh(self,vl): vl = int(vl) - self.setVParam(ECIVoiceParam.eciRoughness,vl) + _ibmeci.setVParam(ECIVoiceParam.eciRoughness,vl) def _get_rgh(self): - return self.getVParam(ECIVoiceParam.eciRoughness) + return _ibmeci.getVParam(ECIVoiceParam.eciRoughness) def _set_bth(self,vl): vl = int(vl) - self.setVParam(ECIVoiceParam.eciBreathiness,vl) + _ibmeci.setVParam(ECIVoiceParam.eciBreathiness,vl) def _get_bth(self): - return self.getVParam(ECIVoiceParam.eciBreathiness) + return _ibmeci.getVParam(ECIVoiceParam.eciBreathiness) def _getAvailableVoices(self): o = OrderedDict() @@ -286,14 +292,9 @@ def _get_voice(self): return str(_ibmeci.params[_ibmeci.ECIParam.eciLanguageDialect]) def _set_voice(self,vl): _ibmeci.set_voice(vl) - def getVParam(self,pr): - return _ibmeci.getVParam(pr) - - def setVParam(self, pr,vl): - _ibmeci.setVParam(pr, vl) - + def _get_lastIndex(self): -#fix? + #fix? return _ibmeci.lastindex def cancel(self): @@ -307,9 +308,9 @@ def _set_variant(self, v): global variants self._variant = v if int(v) in variants else "1" _ibmeci.setVariant(int(v)) - self.setVParam(ECIVoiceParam.eciSpeed, self._rate) -# if 'ibmtts' in config.conf['speech']: -# config.conf['speech']['ibmtts']['pitch'] = self.pitch + _ibmeci.setVParam(ECIVoiceParam.eciSpeed, self._rate) + #if 'ibmtts' in config.conf['speech']: + #config.conf['speech']['ibmtts']['pitch'] = self.pitch def _get_variant(self): return self._variant diff --git a/buildVars.py b/buildVars.py index 04fd52d..eaf2521 100644 --- a/buildVars.py +++ b/buildVars.py @@ -19,7 +19,7 @@ # Translators: Long description to be shown for this add-on on add-on information from add-ons manager "addon_description" : _("""This is the IBMTTS synthesizer driver for NVDA."""), # version - "addon_version" : "19.8B2", + "addon_version" : "19.8B3", # Author(s) "addon_author" : u"David CM and others", # URL for the add-on documentation support