From 6814e865787cd1daad3006070f9de4af93cc7d56 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Fri, 10 Jul 2009 15:32:18 +0000 Subject: Separate espeak command ang gst-espeak code --- diff --git a/Speak.activity/activity.py b/Speak.activity/activity.py index 5f2540d..886b4a2 100644 --- a/Speak.activity/activity.py +++ b/Speak.activity/activity.py @@ -32,8 +32,7 @@ import cjson from gettext import gettext as _ from sugar.graphics.toolbutton import ToolButton -from port.combobox import ToolComboBox -from port.combobox import ComboBox +from port.widgets import ComboBox, ToolComboBox from port.activity import SharedActivity import eye @@ -45,7 +44,7 @@ import voice import face import brain import chat -import audio +import espeak from messenger import Messenger, SERVICE logger = logging.getLogger('speak') @@ -248,7 +247,7 @@ class SpeakActivity(SharedActivity): combotool.show() self.pitchadj = gtk.Adjustment(self.face.status.pitch, 0, - audio.PITCH_MAX, 1, audio.PITCH_MAX/10, 0) + espeak.PITCH_MAX, 1, espeak.PITCH_MAX/10, 0) pitchbar = gtk.HScale(self.pitchadj) pitchbar.set_draw_value(False) #pitchbar.set_inverted(True) @@ -260,8 +259,8 @@ class SpeakActivity(SharedActivity): voicebar.insert(pitchtool, -1) pitchbar.show() - self.rateadj = gtk.Adjustment(self.face.status.rate, 0, audio.RATE_MAX, - 1, audio.RATE_MAX/10, 0) + self.rateadj = gtk.Adjustment(self.face.status.rate, 0, espeak.RATE_MAX, + 1, espeak.RATE_MAX/10, 0) ratebar = gtk.HScale(self.rateadj) ratebar.set_draw_value(False) #ratebar.set_inverted(True) @@ -412,8 +411,5 @@ class SpeakActivity(SharedActivity): if index == BOT_TOOLBAR: self.bot.update_voice() - #def on_quit(self, data=None): - # self.audio.on_quit() - # activate gtk threads when this module loads gtk.gdk.threads_init() diff --git a/Speak.activity/audio.py b/Speak.activity/audio.py deleted file mode 100644 index 8cb1426..0000000 --- a/Speak.activity/audio.py +++ /dev/null @@ -1,195 +0,0 @@ -# Speak.activity -# A simple front end to the espeak text-to-speech engine on the XO laptop -# http://wiki.laptop.org/go/Speak -# -# Copyright (C) 2008 Joshua Minor -# This file is part of Speak.activity -# -# Parts of Speak.activity are based on code from Measure.activity -# Copyright (C) 2007 Arjun Sarwal - arjun@laptop.org -# -# Speak.activity 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 3 of the License, or -# (at your option) any later version. -# -# Speak.activity 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 Speak.activity. If not, see . - -# This code is a stripped down version of the audio grabber from Measure - -import subprocess -import pygst -pygst.require("0.10") -import gst -import pygtk -import gtk, gobject -import signal, os -import time -import dbus -import logging -from struct import * - -logger = logging.getLogger('speak') - -try: - import gst - gst.element_factory_make('espeak') - - PITCH_MAX = 200 - RATE_MAX = 200 - PITCH_DEFAULT = PITCH_MAX/2 - RATE_DEFAULT = RATE_MAX/2 -except: - PITCH_MAX = 99 - RATE_MAX = 99 - PITCH_DEFAULT = PITCH_MAX/2 - RATE_DEFAULT = RATE_MAX/3 - -class AudioGrab(gobject.GObject): - __gsignals__ = { - 'new-buffer': (gobject.SIGNAL_RUN_FIRST, None, [gobject.TYPE_PYOBJECT]) - } - - def __init__(self): - gobject.GObject.__init__(self) - self.pipeline = None - - def speak(self, status, text): - try: - self._speak(status, text) - except Exception, e: - logger.error('Cannot speak "%s": %s' % (text, e)) - - # espeak uses 80 to 370 - rate = 80 + (370-80) * int(status.rate) / 100 - wavpath = "/tmp/speak.wav" - subprocess.call(["espeak", "-w", wavpath, "-p", str(status.pitch), "-s", str(rate), "-v", status.voice.name, text], stdout=subprocess.PIPE) - self._playfile(wavpath) - - def _speak(self, status, text): - pitch = int(status.pitch) - 100 - rate = int(status.rate) - 100 - - logger.debug('pitch=%d rate=%d voice=%s text=%s' % (pitch, rate, - status.voice.name, text)) - - self.stop_sound_device() - self._quiet = False - - # build a pipeline that reads the given file - # and sends it to both the real audio output - # and a fake one that we use to draw from - p = 'espeak name=espeak ' \ - '! wavenc ! decodebin ' \ - '! tee name=tee ' \ - 'tee.! audioconvert ' \ - '! alsasink ' \ - 'tee.! queue ' \ - '! audioconvert name=conv' - self.pipeline = gst.parse_launch(p) - - espeak = self.pipeline.get_by_name('espeak') - espeak.props.text = text - espeak.props.pitch = pitch - espeak.props.rate = rate - espeak.props.voice = status.voice.name - - # make a fakesink to capture audio - fakesink = gst.element_factory_make("fakesink", "fakesink") - fakesink.connect("handoff",self.on_buffer) - fakesink.set_property("signal-handoffs",True) - self.pipeline.add(fakesink) - - bus = self.pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message', self._gstmessage_cb) - - # attach it to the pipeline - conv = self.pipeline.get_by_name("conv") - gst.element_link_many(conv, fakesink) - - # play - self.restart_sound_device() - - # how do we detect when the sample has finished playing? - # we should stop the sound device and stop emitting buffers - # to save on CPU and battery usage when there is no audio playing - - - def _playfile(self, filename): - self.stop_sound_device() - self._quiet = False - - # build a pipeline that reads the given file - # and sends it to both the real audio output - # and a fake one that we use to draw from - p = 'filesrc name=file-source ! decodebin ! tee name=tee tee.! audioconvert ! alsasink tee.! queue ! audioconvert name=conv' - self.pipeline = gst.parse_launch(p) - - # make a fakesink to capture audio - fakesink = gst.element_factory_make("fakesink", "fakesink") - fakesink.connect("handoff",self.on_buffer) - fakesink.set_property("signal-handoffs",True) - self.pipeline.add(fakesink) - - bus = self.pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message', self._gstmessage_cb) - - # attach it to the pipeline - conv = self.pipeline.get_by_name("conv") - gst.element_link_many(conv, fakesink) - - # set the source file - self.pipeline.get_by_name("file-source").set_property('location', filename) - - # play - self.restart_sound_device() - - # how do we detect when the sample has finished playing? - # we should stop the sound device and stop emitting buffers - # to save on CPU and battery usage when there is no audio playing - - - def _gstmessage_cb(self, bus, message): - type = message.type - - if type == gst.MESSAGE_EOS: - # END OF SOUND FILE - self.stop_sound_device() - elif type == gst.MESSAGE_ERROR: - self.stop_sound_device() - - def on_quit(self): - self.pipeline.set_state(gst.STATE_NULL) - - def _new_buffer(self, buf): - if not self._quiet: - # pass captured audio to anyone who is interested via the main thread - self.emit("new-buffer", buf) - return False - - def on_buffer(self,element,buffer,pad): - # we got a new buffer of data, ask for another - gobject.timeout_add(100, self._new_buffer, str(buffer)) - return True - - def stop_sound_device(self): - if self.pipeline is None: - return - - self.pipeline.set_state(gst.STATE_NULL) - # Shut theirs mouths down - self._new_buffer('') - self._quiet = True - - def restart_sound_device(self): - self.pipeline.set_state(gst.STATE_NULL) - self.pipeline.set_state(gst.STATE_PLAYING) - diff --git a/Speak.activity/brain.py b/Speak.activity/brain.py index 20000a9..c9ef2b4 100644 --- a/Speak.activity/brain.py +++ b/Speak.activity/brain.py @@ -24,7 +24,7 @@ from gettext import gettext as _ import logging logger = logging.getLogger('speak') -from port.combobox import ToolComboBox +from port.widgets import ToolComboBox import bot.aiml import voice diff --git a/Speak.activity/espeak.py b/Speak.activity/espeak.py new file mode 100644 index 0000000..c57aa2a --- /dev/null +++ b/Speak.activity/espeak.py @@ -0,0 +1,102 @@ +# 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 pygst +pygst.require("0.10") +import gst +import gobject +import subprocess + +import logging +logger = logging.getLogger('speak') + +supported = True + +class AudioGrab(gobject.GObject): + __gsignals__ = { + 'new-buffer': (gobject.SIGNAL_RUN_FIRST, None, [gobject.TYPE_PYOBJECT]) + } + + def __init__(self): + gobject.GObject.__init__(self) + self.pipeline = None + self.quiet = True + + def restart_sound_device(self): + self.quiet = False + + self.pipeline.set_state(gst.STATE_NULL) + self.pipeline.set_state(gst.STATE_PLAYING) + + def stop_sound_device(self): + if self.pipeline is None: + return + + self.pipeline.set_state(gst.STATE_NULL) + # Shut theirs mouths down + self._new_buffer('') + + self.quiet = True + + def make_pipeline(self, cmd): + # build a pipeline that reads the given file + # and sends it to both the real audio output + # and a fake one that we use to draw from + self.pipeline = gst.parse_launch(cmd) + + def on_buffer(element, buffer, pad): + # we got a new buffer of data, ask for another + gobject.timeout_add(100, self._new_buffer, str(buffer)) + return True + + # make a fakesink to capture audio + fakesink = gst.element_factory_make("fakesink", "fakesink") + fakesink.connect("handoff",on_buffer) + fakesink.set_property("signal-handoffs",True) + self.pipeline.add(fakesink) + + # attach it to the pipeline + conv = self.pipeline.get_by_name("conv") + gst.element_link_many(conv, fakesink) + + def gstmessage_cb(bus, message): + if message.type in (gst.MESSAGE_EOS, gst.MESSAGE_ERROR): + logger.debug(message.type) + self.stop_sound_device() + + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', gstmessage_cb) + + def _new_buffer(self, buf): + if not self.quiet: + # pass captured audio to anyone who is interested + self.emit("new-buffer", buf) + return False + +# load proper espeak plugin +try: + import gst + gst.element_factory_make('espeak') + from espeak_gst import AudioGrabGst as AudioGrab + from espeak_gst import * + logger.info('use gst-plugins-espeak') +except Exception, e: + logger.info('disable gst-plugins-espeak: %s' % e) + if subprocess.call('which espeak', shell=True) == 0: + from espeak_cmd import AudioGrabCmd as AudioGrab + from espeak_cmd import * + else: + logger.info('disable espeak_cmd') + supported = False diff --git a/Speak.activity/espeak_cmd.py b/Speak.activity/espeak_cmd.py new file mode 100644 index 0000000..f391e5b --- /dev/null +++ b/Speak.activity/espeak_cmd.py @@ -0,0 +1,71 @@ +# 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 re +import subprocess + +import logging +logger = logging.getLogger('speak') + +import espeak + +PITCH_MAX = 99 +RATE_MAX = 99 +PITCH_DEFAULT = PITCH_MAX/2 +RATE_DEFAULT = RATE_MAX/3 + +class AudioGrabCmd(espeak.AudioGrab): + def speak(self, status, text): + self.make_pipeline( + 'filesrc name=file-source ' \ + '! decodebin ' \ + '! tee name=tee ' \ + 'tee.! audioconvert' \ + '! alsasink ' \ + 'tee.! queue ' \ + '! audioconvert name=conv') + + # espeak uses 80 to 370 + rate = 80 + (370-80) * int(status.rate) / 100 + wavpath = "/tmp/speak.wav" + + subprocess.call(["espeak", "-w", wavpath, "-p", str(status.pitch), + "-s", str(rate), "-v", status.voice.name, text], + stdout=subprocess.PIPE) + + self.stop_sound_device() + + # set the source file + self.pipeline.get_by_name("file-source").props.location = wavpath + + # play + self.restart_sound_device() + +def voices(): + out = [] + result = subprocess.Popen(["espeak", "--voices"], stdout=subprocess.PIPE) \ + .communicate()[0] + + for line in result.split('\n'): + m = re.match(r'\s*\d+\s+([\w-]+)\s+([MF])\s+([\w_-]+)\s+(.+)', line) + if not m: + continue + language, gender, name, stuff = m.groups() + if stuff.startswith('mb/') or \ + name in ('en-rhotic','english_rp','english_wmids'): + # these voices don't produce sound + continue + out.append((language, name)) + + return out diff --git a/Speak.activity/espeak_gst.py b/Speak.activity/espeak_gst.py new file mode 100644 index 0000000..1c9c738 --- /dev/null +++ b/Speak.activity/espeak_gst.py @@ -0,0 +1,66 @@ +# 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 logging +logger = logging.getLogger('speak') + +import pygst +pygst.require("0.10") +import gst +import espeak + +PITCH_MAX = 200 +RATE_MAX = 200 +PITCH_DEFAULT = PITCH_MAX/2 +RATE_DEFAULT = RATE_MAX/2 + +class AudioGrabGst(espeak.AudioGrab): + def speak(self, status, text): + self.stop_sound_device() + + self.make_pipeline( + 'espeak name=espeak ' \ + '! wavenc ! decodebin ' \ + '! tee name=tee ' \ + 'tee.! audioconvert ' \ + '! alsasink ' \ + 'tee.! queue ' \ + '! audioconvert name=conv') + + src = self.pipeline.get_by_name('espeak') + + pitch = int(status.pitch) - 100 + rate = int(status.rate) - 100 + + logger.debug('pitch=%d rate=%d voice=%s text=%s' % (pitch, rate, + status.voice.name, text)) + + src.props.text = text + src.props.pitch = pitch + src.props.rate = rate + src.props.voice = status.voice.name + + self.restart_sound_device() + +def voices(): + out = [] + + for i in gst.element_factory_make('espeak').props.voices: + name, language, dialect = i + if name in ('en-rhotic','english_rp','english_wmids'): + # these voices don't produce sound + continue + out.append((language, name)) + + return out diff --git a/Speak.activity/face.py b/Speak.activity/face.py index ffb88c3..ab63412 100644 --- a/Speak.activity/face.py +++ b/Speak.activity/face.py @@ -29,7 +29,7 @@ from gettext import gettext as _ import sugar.graphics.style as style -import audio +import espeak import eye import glasses import mouth @@ -44,8 +44,8 @@ FACE_PAD = 2 class Status: def __init__(self): self.voice = voice.DEFAULT - self.pitch = audio.PITCH_DEFAULT - self.rate = audio.RATE_DEFAULT + self.pitch = espeak.PITCH_DEFAULT + self.rate = espeak.RATE_DEFAULT self.eyes = [eye.Eye] * 2 self.mouth = mouth.Mouth @@ -99,13 +99,13 @@ class View(gtk.EventBox): self.connect('size-allocate', self._size_allocate_cb) - self._audio = audio.AudioGrab() + self._audio = espeak.AudioGrab() # make an empty box for some eyes self._eyes = None self._eyebox = gtk.HBox() self._eyebox.show() - + # make an empty box to put the mouth in self._mouth = None self._mouthbox = gtk.HBox() diff --git a/Speak.activity/port/combobox.py b/Speak.activity/port/combobox.py deleted file mode 100644 index 141ddf3..0000000 --- a/Speak.activity/port/combobox.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright (C) 2007, One Laptop Per Child -# -# 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 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., 59 Temple Place - Suite 330, -# Boston, MA 02111-1307, USA. - -""" -STABLE. -""" - -import gobject -import gtk - -from sugar.graphics import style - -class ComboBox(gtk.ComboBox): - def __init__(self): - gtk.ComboBox.__init__(self) - - self._text_renderer = None - self._icon_renderer = None - - self._model = gtk.ListStore(gobject.TYPE_PYOBJECT, - gobject.TYPE_STRING, - gtk.gdk.Pixbuf, - gobject.TYPE_BOOLEAN) - self.set_model(self._model) - - self.set_row_separator_func(self._is_separator) - - def get_value(self): - """ - Parameters - ---------- - None : - - Returns: - -------- - value : - - """ - row = self.get_active_item() - if not row: - return None - return row[0] - - value = gobject.property( - type=object, getter=get_value, setter=None) - - def _get_real_name_from_theme(self, name, size): - icon_theme = gtk.icon_theme_get_default() - width, height = gtk.icon_size_lookup(size) - info = icon_theme.lookup_icon(name, max(width, height), 0) - if not info: - raise ValueError("Icon '" + name + "' not found.") - fname = info.get_filename() - del info - return fname - - def append_item(self, action_id, text, icon_name=None, file_name=None): - """ - Parameters - ---------- - action_id : - - text : - - icon_name=None : - - file_name=None : - - Returns - ------- - None - - """ - if not self._icon_renderer and (icon_name or file_name): - self._icon_renderer = gtk.CellRendererPixbuf() - - settings = self.get_settings() - w, h = gtk.icon_size_lookup_for_settings( - settings, gtk.ICON_SIZE_MENU) - self._icon_renderer.props.stock_size = max(w, h) - - self.pack_start(self._icon_renderer, False) - self.add_attribute(self._icon_renderer, 'pixbuf', 2) - - if not self._text_renderer and text: - self._text_renderer = gtk.CellRendererText() - self.pack_end(self._text_renderer, True) - self.add_attribute(self._text_renderer, 'text', 1) - - if icon_name or file_name: - if text: - size = gtk.ICON_SIZE_MENU - else: - size = gtk.ICON_SIZE_LARGE_TOOLBAR - width, height = gtk.icon_size_lookup(size) - - if icon_name: - file_name = self._get_real_name_from_theme(icon_name, size) - - pixbuf = gtk.gdk.pixbuf_new_from_file_at_size( - file_name, width, height) - else: - pixbuf = None - - self._model.append([action_id, text, pixbuf, False]) - - def append_separator(self): - """ - Parameters - ---------- - None - - Returns - ------- - None - - """ - self._model.append([0, None, None, True]) - - def get_active_item(self): - """ - Parameters - ---------- - None - - Returns - ------- - Active_item : - - """ - index = self.get_active() - if index == -1: - index = 0 - - row = self._model.iter_nth_child(None, index) - if not row: - return None - return self._model[row] - - def remove_all(self): - """ - Parameters - ---------- - None - - Returns - ------- - None - - """ - self._model.clear() - - def _is_separator(self, model, row): - return model[row][3] - - def select(self, value, column=0, silent_cb=None): - for i, item in enumerate(self.get_model()): - if item[column] != value: - continue - try: - if silent_cb: - try: - self.handler_block_by_func(silent_cb) - except Exception, e: - print e - silent_cb = None - self.set_active(i) - finally: - if silent_cb: - self.handler_unblock_by_func(silent_cb) - break - -class ToolComboBox(gtk.ToolItem): - __gproperties__ = { - 'label-text' : (str, None, None, None, - gobject.PARAM_WRITABLE), - } - - def __init__(self, combo=None, **kwargs): - self.label = None - self._label_text = '' - - gobject.GObject.__init__(self, **kwargs) - - self.set_border_width(style.DEFAULT_PADDING) - - hbox = gtk.HBox(False, style.DEFAULT_SPACING) - - self.label = gtk.Label(self._label_text) - hbox.pack_start(self.label, False) - self.label.show() - - if combo: - self.combo = combo - else: - self.combo = ComboBox() - - hbox.pack_start(self.combo) - self.combo.show() - - self.add(hbox) - hbox.show() - - def do_set_property(self, pspec, value): - if pspec.name == 'label-text': - self._label_text = value - if self.label: - self.label.set_text(self._label_text) diff --git a/Speak.activity/port/widgets.py b/Speak.activity/port/widgets.py new file mode 100644 index 0000000..f029e46 --- /dev/null +++ b/Speak.activity/port/widgets.py @@ -0,0 +1,306 @@ +# 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 gtk +import hippo +import gobject +import logging + +from sugar.graphics import style +from sugar.graphics import palette +from sugar.graphics import toolbutton +from sugar.graphics import radiotoolbutton +from sugar.graphics import icon +from sugar.graphics import toggletoolbutton +from sugar.graphics import combobox + +def labelize(text, widget): + box = hippo.CanvasBox() + box.props.spacing = style.DEFAULT_SPACING + + text = hippo.CanvasText(text=text) + text.props.color = style.COLOR_SELECTION_GREY.get_int() + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + box.append(text) + + box.append(widget, hippo.PACK_EXPAND) + + return box + +class Entry(hippo.CanvasWidget): + def __init__(self, text=None, frame_color=style.COLOR_WHITE.get_gdk_color(), + **kwargs): + hippo.CanvasWidget.__init__(self, **kwargs) + + self.entry = gtk.Entry() + self.entry.modify_bg(gtk.STATE_INSENSITIVE, frame_color) + + self.props.widget = self.entry + + if text: + self.text = text + + def get_text(self): + return self.entry.props.text + + def set_text(self, value): + self.entry.props.text = value + + text = gobject.property(type=str, setter=set_text, getter=get_text) + text = property(get_text, set_text) + +class TextView(hippo.CanvasWidget): + def __init__(self, text=None, **kwargs): + hippo.CanvasWidget.__init__(self, **kwargs) + + self.view = gtk.TextView() + self.view.props.left_margin = style.DEFAULT_SPACING + self.view.props.right_margin = style.DEFAULT_SPACING + self.view.props.wrap_mode = gtk.WRAP_WORD + self.view.props.accepts_tab = False + self.view.show() + + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_shadow_type(gtk.SHADOW_OUT) + scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scrolled_window.add(self.view) + + self.props.widget = scrolled_window + + if text: + self.text = text + + def get_text(self): + return self.view.props.buffer.props.text + + def set_text(self, value): + self.view.props.buffer.props.text = value or '' + + text = gobject.property(type=str, setter=set_text, getter=get_text) + text = property(get_text, set_text) + +class Image(hippo.CanvasWidget): + def __init__(self, pal=None, tooltip=None, pixbuf=None, **kwargs): + self.image = gtk.Image() + self._invoker = palette.CanvasInvoker() + + hippo.CanvasBox.__init__(self, widget=self.image) + + self._invoker._position_hint = self._invoker.AT_CURSOR + self._invoker.attach(self) + + self.palette_class = None + + self.connect('destroy', self._destroy_cb) + + if pal: + self.palette = pal + if tooltip: + self.tooltip = tooltip + if pixbuf: + self.pixbuf = pixbuf + + def _destroy_cb(self, widget): + if self._invoker is not None: + self._invoker.detach() + + def create_palette(self): + if self.palette_class is None: + return None + if isinstance(self.palette_class, tuple): + return self.palette_class[0](*self.palette_class[1:]) + else: + return self.palette_class() + + def get_palette(self): + return self._invoker.palette + + def set_palette(self, palette): + self._invoker.palette = palette + + palette = gobject.property(type=object, + setter=set_palette, getter=get_palette) + palette = property(get_palette, set_palette) + + def get_tooltip(self): + return self._invoker.palette and self._invoker.palette.primary_text + + def set_tooltip(self, text): + self.set_palette(palette.Palette(text)) + + tooltip = gobject.property(type=str, setter=set_tooltip, getter=get_tooltip) + tooltip = property(get_tooltip, set_tooltip) + + def set_pixbuf(self, value): + self.image.set_from_pixbuf(value) + self.props.box_width = value.get_width() + self.props.box_height = value.get_height() + + pixbuf = gobject.property(type=object, setter=set_pixbuf, getter=None) + pixbuf = property(None, set_pixbuf) + +class ToolButton(toolbutton.ToolButton): + def __init__(self, + icon_name, + size=gtk.ICON_SIZE_SMALL_TOOLBAR, + padding=None, + **kwargs): + + toolbutton.ToolButton.__init__(self, **kwargs) + + image = icon.Icon(icon_name=icon_name, icon_size=size) + image.show() + + # The alignment is a hack to work around gtk.ToolButton code + # that sets the icon_size when the icon_widget is a gtk.Image + alignment = gtk.Alignment(0.5, 0.5) + alignment.show() + alignment.add(image) + + self.set_icon_widget(alignment) + + sizes = { gtk.ICON_SIZE_SMALL_TOOLBAR: style.SMALL_ICON_SIZE, + gtk.ICON_SIZE_LARGE_TOOLBAR: style.STANDARD_ICON_SIZE } + + if padding is not None and sizes.has_key(size): + button_size = sizes[size] + style.DEFAULT_SPACING + padding + self.set_size_request(button_size, button_size) + +class RadioToolButton(radiotoolbutton.RadioToolButton): + def __init__(self, + icon_name, + size=gtk.ICON_SIZE_SMALL_TOOLBAR, + padding=None, + **kwargs): + + radiotoolbutton.RadioToolButton.__init__(self, **kwargs) + + image = icon.Icon(icon_name=icon_name, icon_size=size) + image.show() + + # The alignment is a hack to work around gtk.ToolButton code + # that sets the icon_size when the icon_widget is a gtk.Image + alignment = gtk.Alignment(0.5, 0.5) + alignment.show() + alignment.add(image) + + self.set_icon_widget(alignment) + + sizes = { gtk.ICON_SIZE_SMALL_TOOLBAR: style.SMALL_ICON_SIZE, + gtk.ICON_SIZE_LARGE_TOOLBAR: style.STANDARD_ICON_SIZE } + + if padding is not None and sizes.has_key(size): + button_size = sizes[size] + style.DEFAULT_SPACING + padding + self.set_size_request(button_size, button_size) + +class ToolWidget(gtk.ToolItem): + def __init__(self, widget): + gtk.ToolItem.__init__(self) + self.add(widget) + widget.show() + +class ToggleToolButton(toggletoolbutton.ToggleToolButton): + def __init__(self, named_icon=None, tooltip=None, palette=None, **kwargs): + toggletoolbutton.ToggleToolButton.__init__(self, named_icon, **kwargs) + + if tooltip: + self.set_tooltip(tooltip) + if palette: + self.set_palette(palette) + +class Palette(palette.Palette): + def __init__(self, **kwargs): + palette.Palette.__init__(self, **kwargs) + + def popup(self, immediate=False, state=None): + if not self.props.invoker: + if _none_invoker.palette: + _none_invoker.palette.popdown(immediate=True) + _none_invoker.palette = self + self.props.invoker = _none_invoker + palette.Palette.popup(self, immediate, state) + +class _NoneInvoker(palette.Invoker): + def __init__(self): + palette.Invoker.__init__(self) + self._position_hint = palette.Invoker.AT_CURSOR + + def get_rect(self): + return gtk.gdk.Rectangle(0, 0, 0, 0) + + def get_toplevel(self): + return None + +_none_invoker = _NoneInvoker() + +class ComboBox(combobox.ComboBox): + def __init__(self, **kwargs): + combobox.ComboBox.__init__(self, **kwargs) + + def select(self, value, column=0, silent_cb=None): + for i, item in enumerate(self.get_model()): + if item[column] != value: + continue + try: + if silent_cb: + try: + self.handler_block_by_func(silent_cb) + except Exception, e: + print e + silent_cb = None + self.set_active(i) + finally: + if silent_cb: + self.handler_unblock_by_func(silent_cb) + break + +class ToolComboBox(gtk.ToolItem): + __gproperties__ = { + 'label-text' : (str, None, None, None, + gobject.PARAM_WRITABLE), + } + + def __init__(self, combo=None, **kwargs): + self.label = None + self._label_text = '' + + gobject.GObject.__init__(self, **kwargs) + + self.set_border_width(style.DEFAULT_PADDING) + + hbox = gtk.HBox(False, style.DEFAULT_SPACING) + + self.label = gtk.Label(self._label_text) + hbox.pack_start(self.label, False) + self.label.show() + + if combo: + self.combo = combo + else: + self.combo = ComboBox() + + hbox.pack_start(self.combo) + self.combo.show() + + self.add(hbox) + hbox.show() + + def do_set_property(self, pspec, value): + if pspec.name == 'label-text': + self._label_text = value + if self.label: + self.label.set_text(self._label_text) diff --git a/Speak.activity/voice.py b/Speak.activity/voice.py index ecd3d5d..fd58b5e 100644 --- a/Speak.activity/voice.py +++ b/Speak.activity/voice.py @@ -17,21 +17,18 @@ # 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 Speak.activity. If not, see . - -import subprocess -import pygst -pygst.require("0.10") -import gst import re, os from gettext import gettext as _ import logging logger = logging.getLogger('speak') +import espeak + # Lets trick gettext into generating entries for the voice names we expect espeak to have # If espeak actually has new or different names then they won't get translated, but they # should still show up in the interface. @@ -86,31 +83,15 @@ class Voice: friendlyname = friendlyname.replace('english-wisper','whisper') friendlyname = friendlyname.capitalize() self.friendlyname = _(friendlyname) - + def allVoices(): - if len(_allVoices) == 0: - try: - for i in gst.element_factory_make('espeak').props.voices: - name, language, dialect = i - if name in ('en-rhotic','english_rp','english_wmids'): - # these voices don't produce sound - continue - voice = Voice(language, name) - _allVoices[voice.friendlyname] = voice - except Exception, e: - logger.warning('Can not find gst-espeak, ' \ - 'fallback to espeak command: %s' % e) - - result = subprocess.Popen(["espeak", "--voices"], stdout=subprocess.PIPE).communicate()[0] - for line in result.split('\n'): - m = re.match(r'\s*\d+\s+([\w-]+)\s+([MF])\s+([\w_-]+)\s+(.+)', line) - if m: - language, gender, name, stuff = m.groups() - if stuff.startswith('mb/') or name in ('en-rhotic','english_rp','english_wmids'): - # these voices don't produce sound - continue - voice = Voice(language, name) - _allVoices[voice.friendlyname] = voice + if _allVoices: + return _allVoices + + for language, name in espeak.voices(): + voice = Voice(language, name) + _allVoices[voice.friendlyname] = voice + return _allVoices def _defaultVoice(): @@ -132,7 +113,7 @@ def _defaultVoice(): lang = os.environ["LANG"] except: lang = "" - + best = voices[_("Default")] for voice in voices.values(): voiceMetric = fit(voice.language, lang) -- cgit v0.9.1