From: Gonzalo Odiard <godi...@gmail.com> Changes:
* Fixes in schema descriptions. * Frame position value. * UI fixes according to different reviews. * Use a SugarMenuItem to mimic MenuItem behaviour because in the gtk3 palettes can coexist menues and other widgets. This is not mandatory now but will do easier migration. * Fixed max/min values in configurations. I was using values from espeak documentation but should use from gst-plugins-espeak Signed-off-by: Gonzalo Odiard <gonz...@laptop.org> --- data/sugar.schemas.in | 24 ++++ extensions/deviceicon/Makefile.am | 1 + extensions/deviceicon/speech.py | 230 ++++++++++++++++++++++++++++++++++++ extensions/globalkey/Makefile.am | 1 + extensions/globalkey/speech.py | 23 ++++ src/jarabe/model/Makefile.am | 1 + src/jarabe/model/speech.py | 235 +++++++++++++++++++++++++++++++++++++ src/jarabe/view/keyhandler.py | 29 +----- 8 files changed, 516 insertions(+), 28 deletions(-) create mode 100644 extensions/deviceicon/speech.py create mode 100644 extensions/globalkey/speech.py create mode 100644 src/jarabe/model/speech.py diff --git a/data/sugar.schemas.in b/data/sugar.schemas.in index aaef381..226b41f 100644 --- a/data/sugar.schemas.in +++ b/data/sugar.schemas.in @@ -368,5 +368,29 @@ </locale> </schema> + <schema> + <key>/schemas/desktop/sugar/speech/pitch</key> + <applyto>/desktop/sugar/speech/pitch</applyto> + <owner>sugar</owner> + <type>int</type> + <default>0</default> + <locale name="C"> + <short>Pitch value for the speech sugar service</short> + <long>Pitch value used by the speech service in Sugar</long> + </locale> + </schema> + + <schema> + <key>/schemas/desktop/sugar/speech/rate</key> + <applyto>/desktop/sugar/speech/rate</applyto> + <owner>sugar</owner> + <type>int</type> + <default>0</default> + <locale name="C"> + <short>Rate value for the speech sugar service</short> + <long>Rate value used by the speech service in Sugar</long> + </locale> + </schema> + </schemalist> </gconfschemafile> diff --git a/extensions/deviceicon/Makefile.am b/extensions/deviceicon/Makefile.am index 118d866..7ed1f77 100644 --- a/extensions/deviceicon/Makefile.am +++ b/extensions/deviceicon/Makefile.am @@ -5,5 +5,6 @@ sugar_PYTHON = \ battery.py \ network.py \ speaker.py \ + speech.py \ touchpad.py \ volume.py diff --git a/extensions/deviceicon/speech.py b/extensions/deviceicon/speech.py new file mode 100644 index 0000000..6311fd6 --- /dev/null +++ b/extensions/deviceicon/speech.py @@ -0,0 +1,230 @@ +# Copyright (C) 2011 One Laptop Per Child +# Copyright (C) 2011 Gonzalo Odiard +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +import glib +import gtk +import gconf +import gobject + +from sugar.graphics.icon import Icon +from sugar.graphics.tray import TrayIcon +from sugar.graphics.palette import Palette +from sugar.graphics.xocolor import XoColor +from sugar.graphics import style + +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.model import speech + + +_ICON_NAME = 'microphone' + + +class SpeechDeviceView(TrayIcon): + + FRAME_POSITION_RELATIVE = 150 + + def __init__(self): + client = gconf.client_get_default() + self._color = XoColor(client.get_string('/desktop/sugar/user/color')) + TrayIcon.__init__(self, icon_name=_ICON_NAME, xo_color=self._color) + self.set_palette_invoker(FrameWidgetInvoker(self)) + self._manager = speech.get_speech_manager() + self._icon_widget.connect('button-release-event', + self.__button_release_event_cb) + + def create_palette(self): + label = glib.markup_escape_text(_('Speech')) + palette = SpeechPalette(label, manager=self._manager) + palette.set_group_id('frame') + return palette + + def __button_release_event_cb(self, widget, event): + self.palette_invoker.notify_right_click() + return True + + +class SugarMenuItem(gtk.EventBox): + + __gsignals__ = { + 'clicked': (gobject.SIGNAL_RUN_FIRST, None, []) + } + + def __init__(self, icon_name, label_text): + gtk.EventBox.__init__(self) + self._sensitive = True + vbox = gtk.VBox() + hbox = gtk.HBox() + vbox.set_border_width(style.DEFAULT_PADDING) + self.icon = Icon() + self.icon.props.icon_name = icon_name + hbox.pack_start(self.icon, expand=False, fill=False, + padding=style.DEFAULT_PADDING) + align = gtk.Alignment(xalign=0.0, yalign=0.5, xscale=0.0, yscale=0.0) + text = '<span foreground="%s">' % style.COLOR_WHITE.get_html() + \ + label_text + '</span>' + self.label = gtk.Label() + self.label.set_use_markup(True) + self.label.set_markup(text) + align.add(self.label) + hbox.pack_start(align, expand=True, fill=True, + padding=style.DEFAULT_PADDING) + vbox.pack_start(hbox, expand=False, fill=False, + padding=style.DEFAULT_PADDING) + self.add(vbox) + self.id_bt_release_cb = self.connect('button-release-event', + self.__button_release_cb) + self.id_enter_notify_cb = self.connect('enter-notify-event', + self.__enter_notify_cb) + self.id_leave_notify_cb = self.connect('leave-notify-event', + self.__leave_notify_cb) + self.modify_bg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color()) + self.show_all() + self.set_above_child(True) + + def __button_release_cb(self, widget, event): + self.emit('clicked') + + def __enter_notify_cb(self, widget, event): + self.modify_bg(gtk.STATE_NORMAL, + style.COLOR_BUTTON_GREY.get_gdk_color()) + + def __leave_notify_cb(self, widget, event): + self.modify_bg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color()) + + def set_icon(self, icon_name): + self.icon.props.icon_name = icon_name + + def set_label(self, label_text): + text = '<span foreground="%s">' % style.COLOR_WHITE.get_html() + \ + label_text + '</span>' + self.label.set_markup(text) + + def set_sensitive(self, sensitive): + if self._sensitive == sensitive: + return + + self._sensitive = sensitive + if sensitive: + self.handler_unblock(self.id_bt_release_cb) + self.handler_unblock(self.id_enter_notify_cb) + self.handler_unblock(self.id_leave_notify_cb) + else: + self.handler_block(self.id_bt_release_cb) + self.handler_block(self.id_enter_notify_cb) + self.handler_block(self.id_leave_notify_cb) + self.modify_bg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color()) + + +class SpeechPalette(Palette): + + def __init__(self, primary_text, manager): + Palette.__init__(self, label=primary_text) + + self._manager = manager + self._manager.connect('play', self._set_buttons_state, 'play') + self._manager.connect('stop', self._set_buttons_state, 'stop') + self._manager.connect('pause', self._set_buttons_state, 'pause') + + vbox = gtk.VBox() + self.set_content(vbox) + + vbox_menu = gtk.VBox() + # TODO: private!!! + self._content.set_border_width(0) + + self._play_pause_button = SugarMenuItem('player_play', + _('Say selected text')) + self._play_pause_button.connect('clicked', self.__play_clicked_cb) + vbox_menu.add(self._play_pause_button) + + self._stop_button = SugarMenuItem('player_stop', _('Stop playback')) + self._stop_button.connect('clicked', self.__stop_clicked_cb) + self._stop_button.set_sensitive(False) + vbox_menu.add(self._stop_button) + + vbox.pack_start(vbox_menu, expand=True, fill=True, + padding=style.DEFAULT_SPACING) + + vbox.pack_start(gtk.HSeparator(), expand=True, fill=True, padding=0) + + hbox_controls = gtk.HBox() + vbox_controls = gtk.VBox() + self._adj_pitch = gtk.Adjustment(value=self._manager.get_pitch(), + lower=self._manager.MIN_PITCH, + upper=self._manager.MAX_PITCH) + self._hscale_pitch = gtk.HScale(self._adj_pitch) + self._hscale_pitch.set_draw_value(False) + + vbox_controls.pack_start(gtk.Label(_('Pitch'))) + vbox_controls.pack_start(self._hscale_pitch) + + self._adj_rate = gtk.Adjustment(value=self._manager.get_rate(), + lower=self._manager.MIN_RATE, + upper=self._manager.MAX_RATE) + self._hscale_rate = gtk.HScale(self._adj_rate) + self._hscale_rate.set_draw_value(False) + + vbox_controls.pack_start(gtk.Label(_('Rate'))) + vbox_controls.pack_start(self._hscale_rate) + hbox_controls.pack_start(vbox_controls, expand=True, fill=True, + padding=style.DEFAULT_PADDING) + vbox.pack_start(hbox_controls, expand=True, fill=True, + padding=style.DEFAULT_PADDING) + hbox_controls.set_border_width(style.DEFAULT_SPACING) + vbox.show_all() + + self._adj_pitch.connect('value_changed', self.__adj_pitch_changed_cb) + self._adj_rate.connect('value_changed', self.__adj_rate_changed_cb) + + def __adj_pitch_changed_cb(self, adjustement): + self._manager.set_pitch(int(adjustement.value)) + + def __adj_rate_changed_cb(self, adjustement): + self._manager.set_rate(int(adjustement.value)) + + def __play_clicked_cb(self, widget): + if self._manager.is_paused: + self._manager.restart() + elif not self._manager.is_playing: + self._manager.say_selected_text() + else: + self._manager.pause() + + def __stop_clicked_cb(self, widget): + self._manager.stop() + + def _set_buttons_state(self, manager, signal): + if signal == 'play': + self._play_pause_button.set_icon('player_pause') + self._play_pause_button.set_label(_('Pause playback')) + self._stop_button.set_sensitive(True) + + elif signal == 'pause': + self._play_pause_button.set_icon('player_play') + self._play_pause_button.set_label(_('Say selected text')) + self._stop_button.set_sensitive(True) + + elif signal == 'stop': + self._play_pause_button.set_icon('player_play') + self._play_pause_button.set_label(_('Say selected text')) + self._stop_button.set_sensitive(False) + + +def setup(tray): + tray.add_device(SpeechDeviceView()) diff --git a/extensions/globalkey/Makefile.am b/extensions/globalkey/Makefile.am index 69afac2..b6cbbd6 100644 --- a/extensions/globalkey/Makefile.am +++ b/extensions/globalkey/Makefile.am @@ -3,4 +3,5 @@ sugardir = $(pkgdatadir)/extensions/globalkey sugar_PYTHON = \ __init__.py \ screenshot.py \ + speech.py \ viewsource.py diff --git a/extensions/globalkey/speech.py b/extensions/globalkey/speech.py new file mode 100644 index 0000000..020fa81 --- /dev/null +++ b/extensions/globalkey/speech.py @@ -0,0 +1,23 @@ +# Copyright (C) 2011 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from jarabe.model import speech + +BOUND_KEYS = ['<alt>s'] + + +def handle_key_press(key): + speech.get_speech_manager().say_selected_text() diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am index 92e8712..2fc6b1c 100644 --- a/src/jarabe/model/Makefile.am +++ b/src/jarabe/model/Makefile.am @@ -16,4 +16,5 @@ sugar_PYTHON = \ screen.py \ session.py \ sound.py \ + speech.py \ telepathyclient.py diff --git a/src/jarabe/model/speech.py b/src/jarabe/model/speech.py new file mode 100644 index 0000000..b9e3e25 --- /dev/null +++ b/src/jarabe/model/speech.py @@ -0,0 +1,235 @@ +# Copyright (C) 2011 One Laptop Per Child +# Copyright (C) 2011 Gonzalo Odiard +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging + +import gconf +import gst +import gtk +import gobject + + +DEFAULT_PITCH = 0 + + +DEFAULT_RATE = 0 + +_speech_manager = None + + +class SpeechManager(gobject.GObject): + + __gtype_name__ = 'SpeechManager' + + __gsignals__ = { + 'play': (gobject.SIGNAL_RUN_FIRST, None, []), + 'pause': (gobject.SIGNAL_RUN_FIRST, None, []), + 'stop': (gobject.SIGNAL_RUN_FIRST, None, []) + } + + MIN_PITCH = -100 + MAX_PITCH = 100 + + MIN_RATE = -100 + MAX_RATE = 100 + + def __init__(self, **kwargs): + gobject.GObject.__init__(self, **kwargs) + self._player = _GstSpeechPlayer() + self._player.connect('play', self._update_state, 'play') + self._player.connect('stop', self._update_state, 'stop') + self._player.connect('pause', self._update_state, 'pause') + self._voice_name = self._player.get_default_voice() + self._pitch = DEFAULT_PITCH + self._rate = DEFAULT_RATE + self._is_playing = False + self._is_paused = False + self.restore() + + def _update_state(self, player, signal): + self._is_playing = (signal == 'play') + self._is_paused = (signal == 'pause') + self.emit(signal) + + def get_is_playing(self): + return self._is_playing + + is_playing = gobject.property(type=bool, getter=get_is_playing, + setter=None, default=False) + + def get_is_paused(self): + return self._is_paused + + is_paused = gobject.property(type=bool, getter=get_is_paused, + setter=None, default=False) + + def get_pitch(self): + return self._pitch + + def get_rate(self): + return self._rate + + def set_pitch(self, pitch): + self._pitch = pitch + self.save() + + def set_rate(self, rate): + self._rate = rate + self.save() + + def say_text(self, text): + if text: + self._player.speak(self._pitch, self._rate, self._voice_name, text) + + def say_selected_text(self): + clipboard = gtk.clipboard_get(selection='PRIMARY') + clipboard.request_text(self._primary_selection_cb) + + def pause(self): + self._player.pause_sound_device() + + def restart(self): + self._player.restart_sound_device() + + def stop(self): + self._player.stop_sound_device() + + def _primary_selection_cb(self, clipboard, text, user_data): + logging.debug('SpeechManager._primary_selection_cb: %r', text) + self.say_text(text) + + def save(self): + client = gconf.client_get_default() + client.set_int('/desktop/sugar/speech/pitch', self._pitch) + client.set_int('/desktop/sugar/speech/rate', self._rate) + logging.debug('saving speech configuration pitch %s rate %s', + self._pitch, self._rate) + + def restore(self): + client = gconf.client_get_default() + self._pitch = client.get_int('/desktop/sugar/speech/pitch') + self._rate = client.get_int('/desktop/sugar/speech/rate') + logging.debug('loading speech configuration pitch %s rate %s', + self._pitch, self._rate) + + +class _GstSpeechPlayer(gobject.GObject): + + __gsignals__ = { + 'play': (gobject.SIGNAL_RUN_FIRST, None, []), + 'pause': (gobject.SIGNAL_RUN_FIRST, None, []), + 'stop': (gobject.SIGNAL_RUN_FIRST, None, []) + } + + def __init__(self): + gobject.GObject.__init__(self) + self._pipeline = None + + def restart_sound_device(self): + if self._pipeline is None: + logging.debug('Trying to restart not initialized sound device') + return + + self._pipeline.set_state(gst.STATE_PLAYING) + self.emit('play') + + def pause_sound_device(self): + if self._pipeline is None: + return + + self._pipeline.set_state(gst.STATE_PAUSED) + self.emit('pause') + + def stop_sound_device(self): + if self._pipeline is None: + return + + self._pipeline.set_state(gst.STATE_NULL) + self.emit('stop') + + def make_pipeline(self, command): + if self._pipeline is not None: + self.stop_sound_device() + del self._pipeline + + self._pipeline = gst.parse_launch(command) + + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message::element', self.__pipe_message_cb) + + def __pipe_message_cb(self, bus, message): + if message.structure.get_name() == 'espeak-mark' and \ + message.structure['mark'] == 'end': + self.emit('stop') + + def speak(self, pitch, rate, voice_name, text): + # TODO workaround for http://bugs.sugarlabs.org/ticket/1801 + if not [i for i in text if i.isalnum()]: + return + text = text + '<mark name="end>"></mark>' + + self.make_pipeline('espeak name=espeak ! autoaudiosink') + src = self._pipeline.get_by_name('espeak') + + logging.debug('pitch=%d rate=%d voice=%s text=%s', pitch, rate, + voice_name, text) + + src.props.text = text + src.props.pitch = pitch + src.props.rate = rate + src.props.voice = voice_name + src.props.track = 2 # track for marks + + self.restart_sound_device() + + def get_all_voices(self): + all_voices = {} + for voice in gst.element_factory_make('espeak').props.voices: + name, language, dialect = voice + if dialect != 'none': + all_voices[language + '_' + dialect] = name + else: + all_voices[language] = name + return all_voices + + def get_default_voice(self): + """Try to figure out the default voice, from the current locale ($LANG) + Fall back to espeak's voice called Default.""" + voices = self.get_all_voices() + + locale = os.environ.get('LANG', '') + language_location = locale.split('.', 1)[0].lower() + language = language_location.split('_')[0] + # if the language is es but not es_es default to es_la (latin voice) + if language == 'es' and language_location != 'es_es': + language_location = 'es_la' + + best = voices.get(language_location) or voices.get(language) \ + or 'default' + logging.debug('Best voice for LANG %s seems to be %s', + locale, best) + return best + + +def get_speech_manager(): + global _speech_manager + + if _speech_manager is None: + _speech_manager = SpeechManager() + return _speech_manager diff --git a/src/jarabe/view/keyhandler.py b/src/jarabe/view/keyhandler.py index d79bfe6..a71f260 100644 --- a/src/jarabe/view/keyhandler.py +++ b/src/jarabe/view/keyhandler.py @@ -60,13 +60,9 @@ _actions_table = { '<alt><shift>f': 'frame', '<alt><shift>q': 'quit_emulator', 'XF86Search': 'open_search', - '<alt><shift>o': 'open_search', - '<alt><shift>s': 'say_text', + '<alt><shift>o': 'open_search' } -SPEECH_DBUS_SERVICE = 'org.laptop.Speech' -SPEECH_DBUS_PATH = '/org/laptop/Speech' -SPEECH_DBUS_INTERFACE = 'org.laptop.Speech' _instance = None @@ -77,7 +73,6 @@ class KeyHandler(object): self._key_pressed = None self._keycode_pressed = 0 self._keystate_pressed = 0 - self._speech_proxy = None self._key_grabber = KeyGrabber() self._key_grabber.connect('key-pressed', @@ -114,28 +109,6 @@ class KeyHandler(object): sound.set_volume(volume) sound.set_muted(volume == 0) - def _get_speech_proxy(self): - if self._speech_proxy is None: - bus = dbus.SessionBus() - speech_obj = bus.get_object(SPEECH_DBUS_SERVICE, SPEECH_DBUS_PATH, - follow_name_owner_changes=True) - self._speech_proxy = dbus.Interface(speech_obj, - SPEECH_DBUS_INTERFACE) - return self._speech_proxy - - def _on_speech_err(self, ex): - logging.error('An error occurred with the ESpeak service: %r', ex) - - def _primary_selection_cb(self, clipboard, text, user_data): - logging.debug('KeyHandler._primary_selection_cb: %r', text) - if text: - self._get_speech_proxy().SayText(text, reply_handler=lambda: None, - error_handler=self._on_speech_err) - - def handle_say_text(self, event_time): - clipboard = gtk.clipboard_get(selection='PRIMARY') - clipboard.request_text(self._primary_selection_cb) - def handle_previous_window(self, event_time): self._tabbing_handler.previous_activity(event_time) -- 1.7.7.5 _______________________________________________ Sugar-devel mailing list Sugar-devel@lists.sugarlabs.org http://lists.sugarlabs.org/listinfo/sugar-devel