# Orca # # Copyright 2005-2008 Sun Microsystems Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. """Provides a SpeechServer factory for gnome-speech drivers.""" __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." __license__ = "LGPL" import logging log = logging.getLogger("speech") import gobject import Queue import threading import time _gnomespeechAvailable = False try: import bonobo import ORBit _gnomespeechAvailable = True except: pass # Init the ORB if we need to. With AT-SPI/CORBA, we depend upon the pyatspi # implementation to init the ORB for us. [[[WDW: With AT-SPI/D-Bus, we need # to do it ourselves. I'm not sure how to ask ORBit itself if it's been # init'ed yet, so I do so indirectly by looking for an attribute of pyatspi. # This attribute has been set if AT-SPI/CORBA is in use and it not set if # AT-SPI/D-Bus is in use.]]] # try: import pyatspi poa = pyatspi.Accessibility__POA except AttributeError: if _gnomespeechAvailable: orb = ORBit.CORBA.ORB_init() import chnames import debug import orca import orca_state import punctuation_settings import settings import speech import speechserver from acss import ACSS from orca_i18n import _ # for gettext support ORBit.load_typelib('GNOME_Speech') import GNOME.Speech, GNOME__POA.Speech def _matchLanguage(desired, actual): """Compares a desired language to an actual language and returns a numerical value representing the degree of match. The expected language format is a string similar to the following, where the delimiter can be '-' '_' or '.': de-DE (German for Germany) en-US (English as used in the United States) Additional extensions for variants or dialects are allowed as well. The return value is one of the following: 0 - no match at all 1 - some match 2 - exact match """ if desired == actual: return 2 if not desired or not actual: return 0 # Break the strings into arrays where the delimiters are # - or _. Also lowercase everything to help with string # compares. # desired = desired.lower().replace("-","_").replace(".","_").split("_") actual = actual.lower().replace("-","_").replace(".","_").split("_") matchCount = 0 for i in range(0, min(len(desired), len(actual))): if desired[i] == actual[i]: matchCount += 1 else: break if matchCount == len(desired): return 2 elif matchCount: return 1 else: return 0 class _SayAll: def __init__(self, iterator, context, utteranceId, progressCallback): self.utteranceIterator = iterator self.currentContext = context self.idForCurrentContext = utteranceId self.progressCallback = progressCallback class _Speaker(GNOME__POA.Speech.SpeechCallback): """Implements gnome-speech's SpeechCallback class. The gnome-speech server only allows one speech callback to be registered with a speaker and there's no way to unregister it. So...we need to handle stuff like that on our own. This class handles this for us and also delegates all calls to the 'real' gnome speech speaker. """ def __init__(self, gnome_speaker, voice): # We know what we are doing here, so tell pylint not to flag # the self._this() method call as an error. The disable-msg is # localized to just this method. # # pylint: disable-msg=E1101 self.gnome_speaker = gnome_speaker if settings.enableSpeechCallbacks: gnome_speaker.registerSpeechCallback(self._this()) self.__callbacks = [] self.voiceInfo = [voice.name, voice.language, voice.gender] def registerCallback(self, callback): self.__callbacks.append(callback) def deregisterCallback(self, callback): self.__callbacks.remove(callback) def notify(self, progressType, utteranceId, offset): """Called by GNOME Speech when the GNOME Speech driver generates a callback. Arguments: - progressType: one of GNOME.Speech.speech_callback_speech_started, GNOME.Speech.speech_callback_speech_progress, GNOME.Speech.speech_callback_speech_ended - utteranceId: the id of the utterance (returned by say) - offset: the character offset into the utterance (for progress) """ for callback in self.__callbacks: callback.notify(progressType, utteranceId, offset) def say(self, text): if isinstance(text, unicode): text = text.encode("UTF-8") return self.gnome_speaker.say(text) def stop(self): self.gnome_speaker.stop() def getSupportedParameters(self): return self.gnome_speaker.getSupportedParameters() def getParameterValue(self, name): return self.gnome_speaker.getParameterValue(name) def setParameterValue(self, name, value): return self.gnome_speaker.setParameterValue(name, value) def unref(self): try: self.gnome_speaker.unref() except: pass class SpeechServer(speechserver.SpeechServer): """Provides SpeechServer implementation for gnome-speech.""" # Dictionary of known running servers. The key is the iid and the # value is the SpeechServer instance. We do this to enforce a # singleton instance of any given server. # __activeServers = {} def __activateDriver(iid): if not _gnomespeechAvailable: return None # We know what we are doing here, so tell pylint not to flag # the _narrow method call as a warning. The disable-msg is # localized to just this method. # # pylint: disable-msg=W0212 driver = bonobo.activation.activate_from_id(iid, 0, False) driver = driver._narrow(GNOME.Speech.SynthesisDriver) isInitialized = driver.isInitialized() if not isInitialized: isInitialized = driver.driverInit() if not isInitialized: try: driver.unref() except: pass driver = None return driver __activateDriver = staticmethod(__activateDriver) def __createServer(iid): server = None if iid in SpeechServer.__activeServers: server = SpeechServer.__activeServers[iid] else: driver = SpeechServer.__activateDriver(iid) if driver: server = SpeechServer(driver, iid) SpeechServer.__activeServers[iid] = server return server __createServer = staticmethod(__createServer) def getFactoryName(): """Returns a localized name describing this factory.""" return _("GNOME Speech Services") getFactoryName = staticmethod(getFactoryName) def getSpeechServers(): """Gets available speech servers as a list. The caller is responsible for calling the shutdown() method of each speech server returned. """ # Get a list of all the drivers on the system and find out how many # of them work. # knownServers = [] if _gnomespeechAvailable: knownServers = bonobo.activation.query( "repo_ids.has('IDL:GNOME/Speech/SynthesisDriver:0.3')") for server in knownServers: if server.iid not in SpeechServer.__activeServers: try: SpeechServer.__createServer(server.iid) except: debug.printException(debug.LEVEL_WARNING) return SpeechServer.__activeServers.values() getSpeechServers = staticmethod(getSpeechServers) def getSpeechServer(info=None): """Gets a given SpeechServer based upon the info. See SpeechServer.getInfo() for more info. """ if not _gnomespeechAvailable: return None if info and info[1] in SpeechServer.__activeServers: return SpeechServer.__activeServers[info[1]] server = None gservers = bonobo.activation.query( "repo_ids.has('IDL:GNOME/Speech/SynthesisDriver:0.3')") if len(gservers) == 0: return None gserver = None # All this logic is to attempt to fall back to a working # driver if the desired one cannot be found or is not # not working. # if not info: gserver = gservers[0] else: for s in gservers: if s.iid == info[1]: gserver = s break if not gserver: return None server = None try: server = SpeechServer.__createServer(gserver.iid) except: debug.printException(debug.LEVEL_WARNING) if not server: for s in gservers: try: server = SpeechServer.__createServer(s.iid) if server: break except: debug.printException(debug.LEVEL_WARNING) return server getSpeechServer = staticmethod(getSpeechServer) def shutdownActiveServers(): """Cleans up and shuts down this factory. """ for key in SpeechServer.__activeServers.keys(): server = SpeechServer.__activeServers[key] server.shutdown() shutdownActiveServers = staticmethod(shutdownActiveServers) def __init__(self, driver, iid): speechserver.SpeechServer.__init__(self) self.__speakers = {} self.__pitchInfo = {} self.__rateInfo = {} self.__volumeInfo = {} self.__driver = driver self.__driverName = driver.driverName self.__iid = iid self.__sayAll = None self.__isSpeaking = False self.__eventQueue = Queue.Queue(0) self.__gidleId = 0 self.__gidleLock = threading.Lock() self.__lastResetTime = 0 self.__lastText = None self.textCharIndices = [] def __getRate(self, speaker): """Gets the voice-independent ACSS rate value of a voice.""" if speaker not in self.__rateInfo: return 50.0 [minRate, averageRate, maxRate] = self.__rateInfo[speaker] rate = speaker.getParameterValue("rate") if rate < averageRate: return 50.0 * (rate - minRate) / (averageRate - minRate) elif rate > averageRate: return 50.0 \ + (50.0 * (rate - averageRate) / (maxRate - averageRate)) else: return 50.0 def __setRate(self, speaker, acssRate): """Determines the voice-specific rate setting for the voice-independent ACSS rate value. """ if speaker not in self.__rateInfo: return [minRate, averageRate, maxRate] = self.__rateInfo[speaker] if acssRate < 50.0: rate = minRate + acssRate * (averageRate - minRate) / 50.0 elif acssRate > 50.0: rate = averageRate \ + (acssRate - 50.0) * (maxRate - averageRate) / 50.0 else: rate = averageRate speaker.setParameterValue("rate", rate) def __getPitch(self, speaker): """Gets the voice-specific pitch setting for the voice-independent ACSS pitch value. Returns the voice-specific pitch setting. """ if speaker not in self.__pitchInfo: return 5.0 [minPitch, averagePitch, maxPitch] = self.__pitchInfo[speaker] pitch = speaker.getParameterValue("pitch") if pitch < averagePitch: return 5.0 * (pitch - minPitch) / (averagePitch - minPitch) elif pitch > averagePitch: return 5.0 \ + (5.0 * (pitch - averagePitch) / (maxPitch - averagePitch)) else: return 5.0 def __setPitch(self, speaker, acssPitch): """Determines the voice-specific pitch setting for the voice-independent ACSS pitch value. """ if speaker not in self.__pitchInfo: return [minPitch, averagePitch, maxPitch] = self.__pitchInfo[speaker] if acssPitch < 5.0: pitch = minPitch + acssPitch * (averagePitch - minPitch) / 5.0 elif acssPitch > 5.0: pitch = averagePitch \ + (acssPitch - 5.0) * (maxPitch - averagePitch) / 5.0 else: pitch = averagePitch speaker.setParameterValue("pitch", pitch) def __setVolume(self, speaker, acssGain): """Determines the voice-specific rate setting for the voice-independent ACSS rate value. """ if speaker not in self.__volumeInfo: return [minVolume, averageVolume, maxVolume] = self.__volumeInfo[speaker] volume = minVolume + acssGain * (maxVolume - minVolume) / 10.0 speaker.setParameterValue("volume", volume) def __getSpeaker(self, acss=None): voices = settings.voices defaultACSS = ACSS(voices[settings.DEFAULT_VOICE]) if not acss: acss = defaultACSS if acss.name() in self.__speakers: return self.__speakers[acss.name()] # Search for matching languages first, as that is the most # important thing. We also try to look for the desired language # first, but then fall back to the default language. # languages = [] try: if ACSS.FAMILY in acss: family = acss[ACSS.FAMILY] languages = [family[speechserver.VoiceFamily.LOCALE]] elif ACSS.FAMILY in defaultACSS: family = defaultACSS[ACSS.FAMILY] languages = [family[speechserver.VoiceFamily.LOCALE]] except: pass # [[[TODO: WDW - I'm using an array of languages here because # it will give us the opportunity to provide a match for # gnome-speech driver implementations that do not designate # languages using the combination of ISO 639-1 and 3166-1 # language_region strings (e.g., they use 'english' instead of # "en_US"). Thus, we could detect the default language is # "en_US" and just automatically append "english" and such to # the list to allow us to accomodate these kind of # variants.]]] # if len(languages) == 0: import locale language, encoding = locale.getdefaultlocale() languages = [language] voices = self.__driver.getAllVoices() foundVoices = [] for voice in voices: match = 0 for language in languages: match = max(match, _matchLanguage(voice.language, language)) # Exact matches get put at the beginning, inexact get added to # the end. # if match == 2: foundVoices.insert(0, voice) elif match: foundVoices.append(voice) # If we didn't find any matches, well...punt. # if len(foundVoices): voices = foundVoices else: voices = self.__driver.getAllVoices() # Now search for a matching family name. # familyName = None if ACSS.FAMILY in acss: family = acss[ACSS.FAMILY] familyName = family[speechserver.VoiceFamily.NAME] elif ACSS.FAMILY in defaultACSS: family = defaultACSS[ACSS.FAMILY] familyName = family[speechserver.VoiceFamily.NAME] found = False for voice in voices: if (not familyName) or (voice.name == familyName): found = True break if not found: if len(voices) == 0: return None else: voice = voices[0] s = self.__driver.createSpeaker(voice) # We know what we are doing here, so tell pylint not to flag # the _narrow method call as a warning. The disable-msg is # localized to just this method. # # pylint: disable-msg=W0212 speaker = _Speaker(s._narrow(GNOME.Speech.Speaker), voice) # Turn off punctuation. We do this because we want to handle # spoken punctuation on our own. Only do so if "punctuation mode" # is available (see # bug #528797). # try: parameters = speaker.getSupportedParameters() for parameter in parameters: if parameter.name == "punctuation mode": speaker.setParameterValue("punctuation mode", 0.0) break except: pass speaker.registerCallback(self) # Hack - see if we have created a new speaker for a new voice # for the engine. If so, save away the min/current/max parameters. # Otherwise, use the ones we saved away. The problem is that # each time we get the parameters, the current one reflects the # current value. We only want to save the value the first time # because at that time, the current value is the default value. # saveParameters = True for existingSpeaker in self.__speakers.values(): if existingSpeaker.voiceInfo == speaker.voiceInfo: try: self.__rateInfo[speaker] = \ self.__rateInfo[existingSpeaker] except: pass try: self.__pitchInfo[speaker] = \ self.__pitchInfo[existingSpeaker] except: pass try: self.__volumeInfo[speaker] = \ self.__volumeInfo[existingSpeaker] except: pass saveParameters = False break if saveParameters: parameters = speaker.getSupportedParameters() for parameter in parameters: if parameter.name == "rate": self.__rateInfo[speaker] = \ [parameter.min, parameter.current, parameter.max] elif parameter.name == "pitch": self.__pitchInfo[speaker] = \ [parameter.min, parameter.current, parameter.max] elif parameter.name == "volume": self.__volumeInfo[speaker] = \ [parameter.min, parameter.current, parameter.max] if ACSS.RATE in acss: self.__setRate(speaker, acss[ACSS.RATE]) if ACSS.AVERAGE_PITCH in acss: self.__setPitch(speaker, acss[ACSS.AVERAGE_PITCH]) if ACSS.GAIN in acss: self.__setVolume(speaker, acss[ACSS.GAIN]) self.__speakers[acss.name()] = speaker return speaker def __idleHandler(self): """Called by the gidle thread when there is something to do. The goal is to try to do all AT-SPI interactions on the gidle thread as a means to help prevent hangs.""" # Added in the notify method below. # # thisId = the id of the utterance we sent to gnome-speech # thisType = the type of progress we're getting # offset = character offset into the utterance # (thisId, thisType, offset) = self.__eventQueue.get() offset = self.__adjustTextCharIndex(offset) if self.__sayAll: if self.__sayAll.idForCurrentContext == thisId: context = self.__sayAll.currentContext if thisType == GNOME.Speech.speech_callback_speech_started: context.currentOffset = context.startOffset self.__sayAll.progressCallback( self.__sayAll.currentContext, speechserver.SayAllContext.PROGRESS) elif thisType == GNOME.Speech.speech_callback_speech_progress: context.currentOffset = context.startOffset + offset self.__sayAll.progressCallback( self.__sayAll.currentContext, speechserver.SayAllContext.PROGRESS) elif thisType == GNOME.Speech.speech_callback_speech_ended: try: while True: [self.__sayAll.currentContext, acss] = \ self.__sayAll.utteranceIterator.next() utterance = self.__sayAll.currentContext.utterance logLine = "SPEECH OUTPUT: '" + utterance + "'" debug.println(debug.LEVEL_INFO, logLine) log.info(logLine) if utterance and len(utterance)\ and not utterance.isspace(): self.__sayAll.idForCurrentContext = \ self.__speak(utterance, acss) break except StopIteration: self.__isSpeaking = False context.currentOffset = context.endOffset self.__sayAll.progressCallback( self.__sayAll.currentContext, speechserver.SayAllContext.COMPLETED) self.__sayAll = None except: # This is here to deal with the situation where speech # might have stopped and __sayAll was set to None. # If we don't handle those cases, Say All can stop # working. # pass rerun = True self.__gidleLock.acquire() if self.__eventQueue.empty(): self.__gidleId = 0 rerun = False # destroy and don't call again self.__gidleLock.release() return rerun def notify(self, progressType, utteranceId, offset): """Called by GNOME Speech when the GNOME Speech driver generates a callback. This is for internal use only. Arguments: - progressType: one of GNOME.Speech.speech_callback_speech_started, GNOME.Speech.speech_callback_speech_progress, GNOME.Speech.speech_callback_speech_ended - utteranceId: the id of the utterance (returned by say) - offset: the character offset into the utterance (for progress) """ if progressType == GNOME.Speech.speech_callback_speech_started: self.__isSpeaking = True elif progressType == GNOME.Speech.speech_callback_speech_progress: self.__isSpeaking = True elif (progressType == GNOME.Speech.speech_callback_speech_ended) \ and (not self.__sayAll): self.__isSpeaking = False if self.__sayAll: self.__gidleLock.acquire() self.__eventQueue.put((utteranceId, progressType, offset)) if not self.__gidleId: if settings.gilSleepTime: time.sleep(settings.gilSleepTime) self.__gidleId = gobject.idle_add(self.__idleHandler) self.__gidleLock.release() def getInfo(self): """Returns [driverName, serverId] """ return [self.__driverName, self.__iid] def getVoiceFamilies(self): """Returns a list of speechserver.VoiceFamily instances representing all the voice families known by the speech server. """ families = [] try: for voice in self.__driver.getAllVoices(): props = { speechserver.VoiceFamily.NAME : voice.name, speechserver.VoiceFamily.LOCALE : voice.language } families.append(speechserver.VoiceFamily(props)) except: debug.printException(debug.LEVEL_SEVERE) return families def queueText(self, text="", acss=None): """Adds the text to the queue. Arguments: - text: text to be spoken - acss: acss.ACSS instance; if None, the default voice settings will be used. Otherwise, the acss settings will be used to augment/override the default voice settings. Output is produced by the next call to speak. """ self.speak(text, acss) def queueTone(self, pitch=440, duration=50): """Adds a tone to the queue. Output is produced by the next call to speak. """ pass def queueSilence(self, duration=50): """Adds silence to the queue. Output is produced by the next call to speak. """ pass def speakCharacter(self, character, acss=None): """Speaks a single character immediately. Arguments: - character: text to be spoken - acss: acss.ACSS instance; if None, the default voice settings will be used. Otherwise, the acss settings will be used to augment/override the default voice settings. """ chname = chnames.getCharacterName(character) if orca_state.activeScript and orca_state.usePronunciationDictionary: chname = orca_state.activeScript.\ utilities.adjustForPronunciation(chname) self.speak(chname, acss) def speakUtterances(self, utterances, acss=None, interrupt=True): """Speaks the given list of utterances immediately. Arguments: - utterances: list of strings to be spoken - acss: acss.ACSS instance; if None, the default voice settings will be used. Otherwise, the acss settings will be used to augment/override the default voice settings. - interrupt: if True, stop any speech currently in progress. """ i = 0 for text in utterances: if len(text): self.speak(text, acss, interrupt and (i == 0)) i += 1 def __adjustTextCharIndex(self, offset): """Get the original character index into the text string (before the string was adjusted for verbalized punctuation. Arguments: - offset: character offset into text string Returns the equivalent character index into the original text string. """ equivalentIndex = 0 for i in range(0, len(self.textCharIndices)): equivalentIndex = i if self.textCharIndices[i] >= offset: break return equivalentIndex def __addVerbalizedPunctuation(self, oldText): """Depending upon the users verbalized punctuation setting, adjust punctuation symbols in the given text to their pronounced equivalents. The pronounced text will either replace the punctuation symbol or be inserted before it. In the latter case, this is to retain spoken prosity. If we are moving around by single characters, then always speak the punctuation. We try to detect this by looking for just a single character being spoken. Arguments: - oldText: text to be parsed for punctuation. Returns a text string with the punctuation symbols adjusted accordingly. """ # Translators: we replace the ellipses (both manual and UTF-8) # with a spoken string. The extra space you see at the beginning # is because we need the speech synthesis engine to speak the # new string well. For example, "Open..." turns into # "Open dot dot dot". # oldText = oldText.replace("...", _(" dot dot dot"), 1) oldText = oldText.replace("\342\200\246", _(" dot dot dot"), 1) oldText = oldText.decode("UTF-8") ## Don't speak newlines unless the user is moving through text ## using a right or left arrow key. ## removeNewLines = True if orca_state.lastInputEvent and \ "event_string" in orca_state.lastInputEvent.__dict__: lastKey = orca_state.lastInputEvent.event_string if lastKey == "Left" or lastKey == "Right": removeNewLines = False if removeNewLines: oldText = oldText.replace("\n", "", 1) # Used to keep a list of character offsets into the new text string, # once it has been converted to use verbalized punctuation, one for # each character in the original string. # self.textCharIndices = [] style = settings.verbalizePunctuationStyle newText = '' for i in range(0, len(oldText)): self.textCharIndices.append(len(newText)) try: level, action = \ punctuation_settings.getPunctuationInfo(oldText[i]) # Special case for punctuation in text like filenames or URL's: # isPrev = isNext = isSpecial = False if i > 0: isPrev = not oldText[i - 1].isspace() if i < (len(oldText) - 1): isNext = not oldText[i + 1].isspace() # If this is a period and there is a non-space character # on either side of it, then always speak it. # isSpecial = isPrev and isNext and (oldText[i] == ".") # If this is a dash and the users punctuation level is not # NONE and the previous character is a white space character, # and the next character is a dollar sign or a digit, then # always speak it. See bug #392939. # prevCharMatches = nextCharMatches = False if orca_state.activeScript: currencySymbols = \ orca_state.activeScript.getUnicodeCurrencySymbols() if i == 0: prevCharMatches = True if i > 0: prevCharMatches = oldText[i - 1].isspace() if i < (len(oldText) - 1): nextCharMatches = (oldText[i + 1].isdigit() or \ oldText[i + 1] in currencySymbols) if oldText[i] == "-" and \ style != settings.PUNCTUATION_STYLE_NONE and \ prevCharMatches and nextCharMatches: if isPrev: newText += " " # Translators: this is to be sent to a speech synthesis # engine to prefix a negative number (e.g., "-56" turns # into "minus 56". We cannot always be sure of the type # of the number (floating point, integer, mixed with other # odd characters, etc.), so we need to unfortunately # build up the utterance in this manner. # newText += _("minus") elif (len(oldText) == 1) or isSpecial or (style <= level): if isPrev: newText += " " newText += chnames.getCharacterName(oldText[i]) if (action == punctuation_settings.PUNCTUATION_INSERT) \ and not isNext: newText += oldText[i].encode("UTF-8") if isNext: newText += " " else: newText += oldText[i].encode("UTF-8") except: if (len(oldText) == 1): newText += chnames.getCharacterName(oldText[i]) else: newText += oldText[i].encode("UTF-8") return newText def updatePunctuationLevel(self): """ Punctuation level changed, inform this speechServer. """ # We dont need to do anything, __addVerbalizedPunctuation # already does this work for us. # pass def __speak(self, text=None, acss=None, interrupt=True): """Speaks all queued text immediately. If text is not None, it is added to the queue before speaking. Arguments: - text: optional text to add to the queue before speaking - acss: acss.ACSS instance; if None, the default voice settings will be used. Otherwise, the acss settings will be used to augment/override the default voice settings. - interrupt: if True, stops any speech in progress before speaking the text Returns an id of the thing being spoken or -1 if nothing is to be spoken. """ # If the user has speech turned off, just return. # if not settings.enableSpeech: return -1 speaker = self.__getSpeaker(acss) if acss and ACSS.RATE not in acss: voices = settings.voices defaultACSS = voices[settings.DEFAULT_VOICE] if ACSS.RATE in defaultACSS: self.__setRate(speaker, defaultACSS[ACSS.RATE]) if not text: if interrupt: speech.stop() return -1 text = self.__addVerbalizedPunctuation(text) # Replace no break space characters with plain spaces since some # synthesizers cannot handle them. See bug #591734. # text = text.decode("UTF-8").replace(u'\u00a0', ' ').encode("UTF-8") try: # [[[TODO: WDW - back this stop out for now. The problem is # that we end up clipping too much speech, especially in the # case where we want to speak the contents of a popup before # speaking the object with focus.]]] # #if interrupt: # speaker.stop() self.__lastText = [text, acss] self.__isSpeaking = True return speaker.say(text) except: # On failure, remember what we said, reset our connection to the # speech synthesis driver, and try to say it again. # debug.printException(debug.LEVEL_WARNING) debug.println(debug.LEVEL_WARNING, "Restarting speech...") self.reset() return -1 def speak(self, text=None, acss=None, interrupt=True): """Speaks all queued text immediately. If text is not None, it is added to the queue before speaking. Arguments: - text: optional text to add to the queue before speaking - acss: acss.ACSS instance; if None, the default voice settings will be used. Otherwise, the acss settings will be used to augment/override the default voice settings. - interrupt: if True, stops any speech in progress before speaking the text """ if self.__sayAll: self.stop() if not acss and text and text.decode("UTF-8").isupper(): try: acss = \ orca_state.activeScript.voices[settings.UPPERCASE_VOICE] except: pass self.__speak(text, acss, interrupt) def isSpeaking(self): """"Returns True if the system is currently speaking.""" return self.__isSpeaking def sayAll(self, utteranceIterator, progressCallback): """Iterates through the given utteranceIterator, speaking each utterance one at a time. Arguments: - utteranceIterator: iterator/generator whose next() function returns a new string to be spoken - progressCallback: called as progress is made """ try: [context, acss] = utteranceIterator.next() logLine = "SPEECH OUTPUT: '" + context.utterance + "'" debug.println(debug.LEVEL_INFO, logLine) log.info(logLine) self.__sayAll = _SayAll(utteranceIterator, context, self.__speak(context.utterance, acss), progressCallback) except StopIteration: pass def increaseSpeechPitch(self, step=0.5): """Increases the speech pitch for the default voice. Arguments: - step: the pitch step increment. """ # [[[TODO: richb - this is a hack for now. Need to take min/max # values in account, plus also need to take into account that # different engines provide different pitch ranges.]]] voices = settings.voices acss = voices[settings.DEFAULT_VOICE] speaker = self.__getSpeaker(acss) pitchDelta = settings.speechPitchDelta try: pitch = min(10, self.__getPitch(speaker) + pitchDelta) acss[ACSS.AVERAGE_PITCH] = pitch self.__setPitch(speaker, pitch) debug.println(debug.LEVEL_CONFIGURATION, "speech.increaseSpeechPitch: pitch is now " \ " %d" % pitch) # Translators: this is a short string saying that the speech # synthesis engine is now speaking in a higher pitch. # self.speak(_("higher.")) except: debug.printException(debug.LEVEL_SEVERE) def decreaseSpeechPitch(self, step=0.5): """Decreases the speech pitch for the default voice. Arguments: - step: the pitch step decrement. """ # [[[TODO: WDW - this is a hack for now. Need to take min/max # values in account, plus also need to take into account that # different engines provide different rate ranges.]]] voices = settings.voices acss = voices[settings.DEFAULT_VOICE] speaker = self.__getSpeaker(acss) pitchDelta = settings.speechPitchDelta try: pitch = max(1, self.__getPitch(speaker) - pitchDelta) acss[ACSS.AVERAGE_PITCH] = pitch self.__setPitch(speaker, pitch) debug.println(debug.LEVEL_CONFIGURATION, "speech.decreaseSpeechPitch: pitch is now " \ " %d" % pitch) # Translators: this is a short string saying that the speech # synthesis engine is now speaking in a lower pitch. # self.speak(_("lower.")) except: debug.printException(debug.LEVEL_SEVERE) def increaseSpeechRate(self, step=5): """Increases the speech rate. [[[TODO: WDW - this is a hack for now. Need to take min/max values in account, plus also need to take into account that different engines provide different rate ranges.]]] """ voices = settings.voices acss = voices[settings.DEFAULT_VOICE] speaker = self.__getSpeaker(acss) rateDelta = settings.speechRateDelta try: rate = min(100, self.__getRate(speaker) + rateDelta) acss[ACSS.RATE] = rate self.__setRate(speaker, rate) debug.println(debug.LEVEL_CONFIGURATION, "speech.increaseSpeechRate: rate is now " \ " %d" % rate) # Translators: this is a short string saying that the speech # synthesis engine is now speaking at a faster rate (words # per minute). # self.speak(_("faster.")) except: debug.printException(debug.LEVEL_SEVERE) def decreaseSpeechRate(self, step=5): """Decreases the rate of speech for the given ACSS. If acssName is None, the rate decrease will be applied to all known ACSSs. [[[TODO: WDW - this is a hack for now. Need to take min/max values in account, plus also need to take into account that different engines provide different rate ranges.]]] Arguments: -acssName: the ACSS whose speech rate should be decreased """ voices = settings.voices acss = voices[settings.DEFAULT_VOICE] speaker = self.__getSpeaker(acss) rateDelta = settings.speechRateDelta try: rate = max(1, self.__getRate(speaker) - rateDelta) acss[ACSS.RATE] = rate self.__setRate(speaker, rate) debug.println(debug.LEVEL_CONFIGURATION, "speech.decreaseSpeechRate: rate is now " \ " %d" % rate) # Translators: this is a short string saying that the speech # synthesis engine is now speaking at a slower rate (words # per minute). # self.speak(_("slower.")) except: debug.printException(debug.LEVEL_SEVERE) def stop(self): """Stops ongoing speech and flushes the queue.""" if self.__sayAll: self.__sayAll.progressCallback( self.__sayAll.currentContext, speechserver.SayAllContext.INTERRUPTED) self.__sayAll = None for name in self.__speakers.keys(): try: self.__speakers[name].stop() except: pass self.__isSpeaking = False def shutdown(self): """Shuts down the speech engine.""" if self.__iid in SpeechServer.__activeServers: for speaker in self.__speakers.values(): speaker.stop() speaker.unref() self.__speakers = {} try: self.__driver.unref() except: pass self.__driver = None del SpeechServer.__activeServers[self.__iid] def reset(self, text=None, acss=None): """Resets the speech engine.""" # We might get into a vicious loop of resetting speech, so # we will abort if we see this happening. # if (time.time() - self.__lastResetTime) < 20: debug.println(debug.LEVEL_SEVERE, "Something looks wrong with speech. Aborting.") debug.printStack(debug.LEVEL_ALL) orca.die(50) else: self.__lastResetTime = time.time() speakers = self.__speakers self.shutdown() servers = [] if _gnomespeechAvailable: servers = bonobo.activation.query( "repo_ids.has('IDL:GNOME/Speech/SynthesisDriver:0.3')") for server in servers: if server.iid == self.__iid: try: self.__driver = self.__activateDriver(self.__iid) self.__speakers = {} for name in speakers.keys(): self.__getSpeaker(speakers[name]) if text: self.speak(text, acss) break except: debug.printException(debug.LEVEL_SEVERE) self.__driver = None