[Sugar-devel] [Sugar 2/2] Implementation of Text to Speech in Sugar Feature - v3

Simon Schampijer simon at schampijer.de
Tue Jan 31 08:17:55 EST 2012


Hi Gonzalo,

thanks for the new patch.

On 01/26/2012 10:18 PM, godiard at sugarlabs.org wrote:
> From: Gonzalo Odiard<godiard at 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.

As discussed with you today, we should for now keep the old-style menu 
as the Shell is still using the gtk2-toolkit and we can not change API 
there anymore.

> * 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<gonzalo at 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

That should be "Copyright (C) 2011 One Laptop Per Child" only.

> +# 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']

As stated in the other mail, I am happy with going back to use 
<alt><shift>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

That should be "Copyright (C) 2011 One Laptop Per Child" only.

> +# 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)

Two '_' for the callback name.

> +
> +    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)

The logging is not needed.

> +    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)

The logging is not needed here anymore.

Regards,
    Simon


More information about the Sugar-devel mailing list