From 59b1e77f97edbde96f351b2408fcf21415536887 Mon Sep 17 00:00:00
2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= <pvdeejay gmail com>
Date: Sun, 23 Aug 2015 20:23:27 +0200 Subject: [PATCH 1/2] direct
eSpeak support using python-espeak. espeakfactory is based off of
speechdispatcherfactory replacing speechd API calls with espeak
API.
--- src/orca/Makefile.am | 1 + src/orca/espeakfactory.py |
461 ++++++++++++++++++++++++++++++++++++++++++++++
src/orca/guilabels.py | 4 + src/orca/settings.py | 2
+- 4 files changed, 467 insertions(+), 1 deletion(-) create mode
100644 src/orca/espeakfactory.py
diff --git a/src/orca/Makefile.am b/src/orca/Makefile.am index
f9f3086..275e5f7 100644 --- a/src/orca/Makefile.am +++
b/src/orca/Makefile.am @@ -22,6 +22,7 @@ orca_python_PYTHON = \
common_keyboardmap.py \ debug.py \ desktop_keyboardmap.py \ +
espeakfactory.py \ event_manager.py \ eventsynthesizer.py \
find.py \ diff --git a/src/orca/espeakfactory.py
b/src/orca/espeakfactory.py new file mode 100644 index
0000000..4585694 --- /dev/null +++ b/src/orca/espeakfactory.py @@
-0,0 +1,461 @@ +# 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 an Orca speech server for eSpeak backend.""" +
+__id__ = "$Id$" +__version__ = "$Revision$" +__date__ =
"$Date$" +__license__ = "LGPL" + +from gi.repository import
GLib +import re +import time +import os.path + +from . import
chnames +from . import debug +from . import guilabels +from .
import messages +from . import speechserver +from . import
settings +from . import orca_state +from . import
punctuation_settings +from .acss import ACSS + +try: + from
espeak import espeak +except: + _espeak_available = False
+else: + _espeak_available = True + +PUNCTUATION =
re.compile('[^\w\s]', re.UNICODE) +ELLIPSIS =
re.compile('(\342\200\246|\.\.\.\s*)') + +#Parameter bounds
+minRate=80 +maxRate=450 +minPitch=0 +maxPitch=99 + +class
SpeechServer(speechserver.SpeechServer): + # See the parent
class for documentation. + + _active_servers = {} + +
DEFAULT_SERVER_ID = 'default' + _SERVER_NAMES =
{DEFAULT_SERVER_ID: guilabels.DEFAULT_SYNTHESIZER} + + def
getFactoryName(): + return guilabels.ESPEAK +
getFactoryName = staticmethod(getFactoryName) + + def
getSpeechServers(): + servers = [] + default =
SpeechServer._getSpeechServer(SpeechServer.DEFAULT_SERVER_ID) +
if default is not None: + servers.append(default) +
return servers + getSpeechServers =
staticmethod(getSpeechServers) + + def _getSpeechServer(cls,
serverId): + """Return an active server for given id. + +
Attempt to create the server if it doesn't exist yet. Returns
None + when it is not possible to create the server. +
+ """ + if serverId not in cls._active_servers: +
cls(serverId) + # Don't return the instance, unless it is
succesfully added + # to `_active_Servers'. +
return cls._active_servers.get(serverId) + _getSpeechServer =
classmethod(_getSpeechServer) + + def
getSpeechServer(info=None): + if info is not None: +
thisId = info[1] + else: + thisId =
SpeechServer.DEFAULT_SERVER_ID + return
SpeechServer._getSpeechServer(thisId) + getSpeechServer =
staticmethod(getSpeechServer) + + def
shutdownActiveServers(): + for server in
list(SpeechServer._active_servers.values()): +
server.shutdown() + shutdownActiveServers =
staticmethod(shutdownActiveServers) + + # *** Instance methods
*** + + def __init__(self, serverId): +
super(SpeechServer, self).__init__() + self._id =
serverId + self._client = None +
self._current_voice_properties = {} +
self._acss_manipulators = ( + (ACSS.RATE,
self._set_rate), + (ACSS.AVERAGE_PITCH,
self._set_pitch), + (ACSS.GAIN, self._set_volume), +
(ACSS.FAMILY, self._set_family), + ) + if not
_espeak_available: +
debug.println(debug.LEVEL_WARNING, +
"eSpeak interface not installed.") + return +
self._PUNCTUATION_MODE_MAP = { +
settings.PUNCTUATION_STYLE_ALL: espeak.Punctuation.All, +
settings.PUNCTUATION_STYLE_MOST: espeak.Punctuation.Custom, +
settings.PUNCTUATION_STYLE_SOME: espeak.Punctuation.Custom, +
settings.PUNCTUATION_STYLE_NONE: espeak.Punctuation.Any, +
} + self._CALLBACK_TYPE_MAP = { +
espeak.event_SENTENCE: speechserver.SayAllContext.PROGRESS, +
#espeak.event_END: speechserver.SayAllContext.INTERRUPTED, +
espeak.event_MSG_TERMINATED:
speechserver.SayAllContext.COMPLETED, +
#espeak.event_MARK:speechserver.SayAllContext.PROGRESS, +
} + + self._default_voice_name =
guilabels.SPEECH_DEFAULT_VOICE % serverId + + try:
+ self._init() + except: +
debug.println(debug.LEVEL_WARNING, +
"eSpeak failed to initialize:") +
debug.printException(debug.LEVEL_WARNING) + else: +
SpeechServer._active_servers[serverId] = self + +
self._lastKeyEchoTime = None + + def _init(self): +
self._current_voice_properties = {} + mode =
self._PUNCTUATION_MODE_MAP[settings.verbalizePunctuationStyle] +
espeak.set_parameter(espeak.Parameter.Punctuation,mode,0) + +
def updateCapitalizationStyle(self): + """Updates the
capitalization style used by the speech server.""" + pass
+ + def updatePunctuationLevel(self): + """ Punctuation
level changed, inform this speechServer. """ + mode =
self._PUNCTUATION_MODE_MAP[settings.verbalizePunctuationStyle] +
espeak.set_parameter(espeak.Parameter.Punctuation,mode,0) + +
def _paramToPercent(self, current, min, max): + """Convert
a raw parameter value to a percentage given the current, minimum
and maximum raw values. + @param current: The current
value. + @type current: int + @param min: The
minimum value. + @type current: int + @param max:
The maximum value. + @type max: int + """ +
return int(round(float(current - min) / (max - min) * 100)) + +
def _percentToParam(self, percent, min, max): + """Convert
a percentage to a raw parameter value given the current
percentage and the minimum and maximum raw parameter values. +
@param percent: The current percentage. + @type percent:
int + @param min: The minimum raw parameter value. +
@type min: int + @param max: The maximum raw parameter
value. + @type max: int + """ + return
int(round(float(percent) / 100 * (max - min) + min)) + + def
_set_rate(self, acss_rate): + rate =
self._percentToParam(acss_rate, minRate, maxRate) +
espeak.set_parameter(espeak.Parameter.Rate, rate, 0) + + def
_set_pitch(self, acss_pitch): + pitch =
self._percentToParam((acss_pitch *10), minPitch, maxPitch) +
espeak.set_parameter(espeak.Parameter.Pitch, pitch, 0) + + def
_set_volume(self, acss_volume): + volume = int(acss_volume
*10) + espeak.set_parameter(espeak.Parameter.Volume,
volume, 0) + + def _set_family(self, acss_family): +
familyLocale = acss_family.get(speechserver.VoiceFamily.LOCALE) +
if not familyLocale: + import locale +
familyLocale, encoding = locale.getdefaultlocale() + if
familyLocale: + lang = familyLocale.split('_')[0] +
if lang: + espeak.set_voice(lang) + else: +
name = acss_family.get(speechserver.VoiceFamily.NAME) +
if name != self._default_voice_name: +
espeak.set_voice(name) + + def _apply_acss(self, acss): +
if acss is None: + acss =
settings.voices[settings.DEFAULT_VOICE] + current =
self._current_voice_properties + for acss_property, method
in self._acss_manipulators: + value =
acss.get(acss_property) + if value is not None: +
if current.get(acss_property) != value: +
method(value) + current[acss_property] =
value + elif acss_property == ACSS.AVERAGE_PITCH: +
method(5.0) + current[acss_property] = 5.0 +
elif acss_property == ACSS.FAMILY \ + and acss
== settings.voices[settings.DEFAULT_VOICE]: + # We
need to explicitly reset (at least) the family. +
# See bgo#626072. + # + method({})
+ current[acss_property] = {} + + 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. + + Arguments:
+ - oldText: text to be parsed for punctuation. + +
Returns a text string with the punctuation symbols adjusted
accordingly. + """ + + spokenEllipsis =
messages.SPOKEN_ELLIPSIS + " " + newText =
re.sub(ELLIPSIS, spokenEllipsis, oldText) + symbols =
set(re.findall(PUNCTUATION, newText)) + for symbol in
symbols: + try: + level, action =
punctuation_settings.getPunctuationInfo(symbol) +
except: + continue + + if level !=
punctuation_settings.LEVEL_NONE: + # eSpeak should
handle it. + # + continue + +
charName = " %s " % chnames.getCharacterName(symbol) +
if action == punctuation_settings.PUNCTUATION_INSERT: +
charName += symbol + newText = re.sub(symbol,
charName, newText) + + if orca_state.activeScript: +
newText =
orca_state.activeScript.utilities.adjustForDigits(newText) + +
return newText + + def _speak(self, text, acss,
callback=None): + if isinstance(text, ACSS): +
text = '' + text = self.__addVerbalizedPunctuation(text) +
if orca_state.activeScript: + text =
orca_state.activeScript.\ +
utilities.adjustForPronunciation(text) + + # We need to
make several replacements. + text = text.translate({ +
0x1: None, # used for embedded commands + 0x3C:
u"<", # <: because of XML + 0x3E: u">", # >:
because of XML + 0x5B: u" [", # [: [[ indicates
phonemes + }) + + self._apply_acss(acss) +
espeak.set_SynthCallback(callback) + espeak.synth(text) +
+ def _say_all(self, iterator, orca_callback): +
"""Process another sayAll chunk. + + Called by the gidle
thread. + + """ + try: + context, acss =
next(iterator) + except StopIteration: + pass +
else: + def callback(event, pos, len): +
t = self._CALLBACK_TYPE_MAP[event] + if t ==
speechserver.SayAllContext.PROGRESS: + if pos
1: + context.currentOffset =
(context.startOffset +pos -1) + else: +
context.currentOffset = context.startOffset + elif
t == speechserver.SayAllContext.COMPLETED: +
context.currentOffset = context.endOffset +
GLib.idle_add(orca_callback, context, t) + if t ==
speechserver.SayAllContext.COMPLETED: +
GLib.idle_add(self._say_all, iterator, orca_callback) +
self._speak(context.utterance, acss, callback=callback) +
return False # to indicate, that we don't want to be called
again. + + def _cancel(self): + espeak.cancel() + +
def _change_default_speech_rate(self, step, decrease=False): +
acss = settings.voices[settings.DEFAULT_VOICE] + delta =
step * (decrease and -1 or +1) + try: + rate =
acss[ACSS.RATE] + except KeyError: + rate = 50
+ acss[ACSS.RATE] = max(0, min(99, rate + delta)) +
debug.println(debug.LEVEL_CONFIGURATION, +
"Speech rate is now %d" % rate) + + self.speak(decrease
and messages.SPEECH_SLOWER \ + or
messages.SPEECH_FASTER, acss=acss) + + def
_change_default_speech_pitch(self, step, decrease=False): +
acss = settings.voices[settings.DEFAULT_VOICE] + delta =
step * (decrease and -1 or +1) + try: + pitch =
acss[ACSS.AVERAGE_PITCH] + except KeyError: +
pitch = 5 + acss[ACSS.AVERAGE_PITCH] = max(0, min(9, pitch
+ delta)) + debug.println(debug.LEVEL_CONFIGURATION, +
"Speech pitch is now %d" % pitch) + + self.speak(decrease
and messages.SPEECH_LOWER \ + or
messages.SPEECH_HIGHER, acss=acss) + + def
_change_default_speech_volume(self, step, decrease=False): +
acss = settings.voices[settings.DEFAULT_VOICE] + delta =
step * (decrease and -1 or +1) + try: + volume
= acss[ACSS.GAIN] + except KeyError: + volume =
5 + acss[ACSS.GAIN] = max(0, min(9, volume + delta)) +
debug.println(debug.LEVEL_CONFIGURATION, +
"Speech volume is now %d" % volume) + +
self.speak(decrease and messages.SPEECH_SOFTER \ +
or messages.SPEECH_LOUDER, acss=acss) + + def getInfo(self): +
return [self._SERVER_NAMES.get(self._id, self._id), self._id] + +
def getVoiceFamilies(self): + # Always offer the
configured default voice with a language + # set according
to the current locale. + from locale import getlocale,
LC_MESSAGES + locale = getlocale(LC_MESSAGES)[0] +
if locale is None or locale == 'C': + lang = None +
dialect = None + else: + lang, dialect =
locale.split('_') + list_synthesis_voices =
espeak.list_voices() + families =
[speechserver.VoiceFamily({ \ +
speechserver.VoiceFamily.NAME: self._default_voice_name, +
#speechserver.VoiceFamily.GENDER: speechserver.VoiceFamily.MALE,
+ speechserver.VoiceFamily.DIALECT: dialect, +
speechserver.VoiceFamily.LOCALE: lang})] + for voice in
list_synthesis_voices: +
families.append(speechserver.VoiceFamily({ \ +
speechserver.VoiceFamily.NAME: voice.name, +
speechserver.VoiceFamily.DIALECT: voice.variant, +
speechserver.VoiceFamily.LOCALE:
voice.identifier.split("/")[-1]})) + return families + +
def speak(self, text=None, acss=None, interrupt=True): +
#if interrupt: + # self._cancel() + + # "We will
not interrupt a key echo in progress." (Said the comment in +
# speech.py where these next two lines used to live. But the code
here + # suggests we haven't been doing anything with the
lastKeyEchoTime in + # years. TODO - JD: Dig into this and
if it's truly useless, kill it.) + if
self._lastKeyEchoTime: + interrupt = interrupt and
(time.time() - self._lastKeyEchoTime) > 0.5 + + if text: +
self._speak(text, acss) + + def speakUtterances(self,
utteranceList, acss=None, interrupt=True): + #if
interrupt: + # self._cancel() + for utterance in
utteranceList: + if utterance: +
self._speak(utterance, acss) + + def sayAll(self,
utteranceIterator, progressCallback): +
GLib.idle_add(self._say_all, utteranceIterator,
progressCallback) + + def speakCharacter(self, character,
acss=None): + #self._apply_acss(acss) + + name =
chnames.getCharacterName(character) + if not name: +
self.speak(character, acss) + return + + if
orca_state.activeScript: + name =
orca_state.activeScript.\ +
utilities.adjustForPronunciation(name) + self.speak(name,
acss) + + def speakKeyEvent(self, event): + if
event.isPrintableKey() and event.event_string.isupper(): +
acss = settings.voices[settings.UPPERCASE_VOICE] + else: +
acss = ACSS(settings.voices[settings.DEFAULT_VOICE]) + +
event_string = event.getKeyName() + if
orca_state.activeScript: + event_string =
orca_state.activeScript.\ +
utilities.adjustForPronunciation(event_string) + +
lockingStateString = event.getLockingStateString() +
event_string = "%s %s" % (event_string, lockingStateString) +
self.speak(event_string, acss=acss) +
self._lastKeyEchoTime = time.time() + + def
increaseSpeechRate(self, step=5): +
self._change_default_speech_rate(step) + + def
decreaseSpeechRate(self, step=5): +
self._change_default_speech_rate(step, decrease=True) + + def
increaseSpeechPitch(self, step=0.5): +
self._change_default_speech_pitch(step) + + def
decreaseSpeechPitch(self, step=0.5): +
self._change_default_speech_pitch(step, decrease=True) + + def
increaseSpeechVolume(self, step=0.5): +
self._change_default_speech_volume(step) + + def
decreaseSpeechVolume(self, step=0.5): +
self._change_default_speech_volume(step, decrease=True) + +
def stop(self): + self._cancel() + + def
shutdown(self): + pass + + def reset(self, text=None,
acss=None): + pass + diff --git a/src/orca/guilabels.py
b/src/orca/guilabels.py index 8d8d9cf..b5e365e 100644 ---
a/src/orca/guilabels.py +++ b/src/orca/guilabels.py @@ -700,6
+700,10 @@ SPEECH_VOICE_TYPE_UPPERCASE = C_("VoiceType",
"Uppercase") # system. (http://devel.freebsoft.org/speechd)
SPEECH_DISPATCHER = _("Speech Dispatcher")
+# Translators this label refers to the name of particular
speech synthesiser. +# (http://espeak.sourceforge.net/) +ESPEAK =
_("eSpeak") + # Translators: This is a label for a group of
options related to Orca's behavior # when presenting an
application's spell check dialog. SPELL_CHECK = C_("OptionGroup",
"Spell Check") diff --git a/src/orca/settings.py
b/src/orca/settings.py index 045817d..933ec52 100644 ---
a/src/orca/settings.py +++ b/src/orca/settings.py @@ -187,7
+187,7 @@ activeProfile = ['Default', 'default'] profile
= ['Default', 'default']
# Speech -speechFactoryModules =
["speechdispatcherfactory"] +speechFactoryModules =
["speechdispatcherfactory","espeakfactory"] speechServerFactory
= "speechdispatcherfactory" speechServerInfo = None #
None means let the factory decide. enableSpeech =
True -- 2.5.0
From 62136a85f539b27782cc6e6191a732f5ef75b302 Mon Sep 17 00:00:00
2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= <pvdeejay gmail com>
Date: Sun, 23 Aug 2015 21:03:23 +0200 Subject: [PATCH 2/2] Added
author credits and fixes issue where less than and greater
symbols where spoken as their xml entities
--- src/orca/espeakfactory.py | 9 +++++++-- 1 file changed, 7
insertions(+), 2 deletions(-)
diff --git a/src/orca/espeakfactory.py
b/src/orca/espeakfactory.py index 4585694..f4f5008 100644 ---
a/src/orca/espeakfactory.py +++ b/src/orca/espeakfactory.py @@
-1,3 +1,10 @@ +# Orca +# +# Copyright 2015 Peter V??gner
<pvdeejay gmail com> +# Copyright 2006, 2007, 2008, 2009
Brailcom, o.p.s. +# +# Author: Tomas Cerha <cerha brailcom org>
+# # 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 @@ -273,8 +280,6 @@ class
SpeechServer(speechserver.SpeechServer): # We need to make
several replacements. text = text.translate({ 0x1: None, # used
for embedded commands - 0x3C: u"<", # <: because of
XML - 0x3E: u">", # >: because of XML 0x5B: u" [",
# [: [[ indicates phonemes })
-- 2.5.0