# 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 . from sugar.activity import activity from sugar.presence import presenceservice import logging import gtk import gobject import pango import cjson from gettext import gettext as _ from sugar.graphics.toolbutton import ToolButton from sugar.graphics.toolcombobox import ToolComboBox from sugar.graphics.combobox import ComboBox import eye import glasses import mouth import fft_mouth import waveform_mouth import voice import face import chat from collab import CollabActivity from messenger import Messenger, SERVICE logger = logging.getLogger('speak') CHAT_TOOLBAR = 3 class SpeakActivity(CollabActivity): def __init__(self, handle): CollabActivity.__init__(self, SERVICE, handle) bounds = self.get_allocation() # pick a voice that espeak supports self.voices = voice.allVoices() # make an audio device for playing back and rendering audio self.connect( "notify::active", self._activeCb ) # make a box to type into self.entrycombo = gtk.combo_box_entry_new_text() self.entrycombo.connect("changed", self._combo_changed_cb) self.entry = self.entrycombo.child self.entry.set_editable(True) self.entry.connect('activate', self._entry_activate_cb) self.entry.connect("key-press-event", self._entry_key_press_cb) self.input_font = pango.FontDescription(str='sans bold 24') self.entry.modify_font(self.input_font) self.face = face.View() self.face.show() # layout the screen box = gtk.VBox(homogeneous=False) box.pack_start(self.face) box.pack_start(self.entrycombo, expand=False) box.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.POINTER_MOTION_MASK) box.connect("motion_notify_event", self._mouse_moved_cb) box.connect("button_press_event", self._mouse_clicked_cb) # desktop self.notebook = gtk.Notebook() self.notebook.show() self.notebook.props.show_border = False self.notebook.props.show_tabs = False self.set_canvas(self.notebook) box.show_all() self.notebook.append_page(box) self.chat = chat.View() self.chat.show_all() self.notebook.append_page(self.chat) # make some toolbars toolbox = activity.ActivityToolbox(self) self.set_toolbox(toolbox) toolbox.show() #activitybar = toolbox.get_activity_toolbar() toolbox.connect('current-toolbar-changed', self._toolbar_changed_cb) voicebar = self.make_voice_bar() toolbox.add_toolbar("Voice", voicebar) voicebar.show() facebar = self.make_face_bar() toolbox.add_toolbar("Face", facebar) facebar.show() chatbar = chat.Toolbar(self.chat) toolbox.add_toolbar(_('Chat'), chatbar) chatbar.show() # make the text box active right away self.entry.grab_focus() self.entry.connect("move-cursor", self._cursor_moved_cb) self.entry.connect("changed", self._cursor_moved_cb) # try to catch all mouse-moved events so the eyes will track wherever you go # this doesn't work for some reason I don't understand # it gets mouse motion over lots of stuff, but not sliders or comboboxes # import time # self.window.set_events(self.window.get_events() | gtk.gdk.POINTER_MOTION_MASK) # def event_filter(event, user_data=None): # map(lambda w: w.queue_draw(), self.eyes) # print time.asctime(), time.time(), event.get_coords(), event.get_root_coords() # return gtk.gdk.FILTER_CONTINUE # self.window.add_filter(event_filter) # map(lambda c: c.forall(lambda w: w.add_events(gtk.gdk.POINTER_MOTION_MASK)), self.window.get_children()) # start polling for mouse movement # self.mouseX = None # self.mouseY = None # def poll_mouse(): # display = gtk.gdk.display_get_default() # screen, mouseX, mouseY, modifiers = display.get_pointer() # if self.mouseX != mouseX or self.mouseY != mouseY: # self.mouseX = mouseX # self.mouseY = mouseY # map(lambda w: w.queue_draw(), self.eyes) # return True # gobject.timeout_add(100, poll_mouse) # XXX do it after(possible) read_file() invoking # have to rely on calling read_file() from map_cb in sugar-toolkit self.connect_after('map', self.connect_to) def connect_to(self, widget): self.voice_combo.connect('changed', self.voice_changed_cb) self.pitchadj.connect("value_changed", self.pitch_adjusted_cb, self.pitchadj) self.rateadj.connect("value_changed", self.rate_adjusted_cb, self.rateadj) self.mouth_shape_combo.connect('changed', self.mouth_changed_cb, False) self.mouth_changed_cb(self.mouth_shape_combo, True) self.numeyesadj.connect("value_changed", self.eyes_changed_cb, False) self.eye_shape_combo.connect('changed', self.eyes_changed_cb, False) self.eyes_changed_cb(None, True) self.face.look_ahead() # say hello to the user presenceService = presenceservice.get_instance() xoOwner = presenceService.get_owner() self.face.say(_("Hello %s. Type something.") % xoOwner.props.nick) def write_file(self, file_path): cfg = { 'status' : self.face.status.serialize(), 'text' : self.entry.props.text, 'history' : map(lambda i: i[0], self.entrycombo.get_model()) } file(file_path, 'w').write(cjson.encode(cfg)) def read_file(self, file_path): cfg = cjson.decode(file(file_path, 'r').read()) def pick_combo_item(combo, col, obj): for i, item in enumerate(combo.get_model()): if item[col] == obj: combo.set_active(i) return logger.warning("Unrecognized loaded value: %s" % obj) status = self.face.status = face.Status().deserialize(cfg['status']) pick_combo_item(self.voice_combo, 1, status.voice.friendlyname) self.pitchadj.value = self.face.status.pitch self.rateadj.value = self.face.status.rate pick_combo_item(self.mouth_shape_combo, 0, status.mouth) pick_combo_item(self.eye_shape_combo, 0, status.eyes[0]) self.numeyesadj.value = len(status.eyes) self.entry.props.text = cfg['text'] for i in cfg['history']: self.entrycombo.append_text(i) def _cursor_moved_cb(self, entry, *ignored): # make the eyes track the motion of the text cursor index = entry.props.cursor_position layout = entry.get_layout() pos = layout.get_cursor_pos(index) x = pos[0][0] / pango.SCALE - entry.props.scroll_offset y = entry.get_allocation().y self.face.look_at(x, y) def get_mouse(self): display = gtk.gdk.display_get_default() screen, mouseX, mouseY, modifiers = display.get_pointer() return mouseX, mouseY def _mouse_moved_cb(self, widget, event): # make the eyes track the motion of the mouse cursor x,y = self.get_mouse() self.face.look_at(x, y) def _mouse_clicked_cb(self, widget, event): pass def make_voice_bar(self): voicebar = gtk.Toolbar() # button = ToolButton('change-voice') # button.set_tooltip("Change Voice") # button.connect('clicked', self.change_voice_cb) # voicebar.insert(button, -1) # button.show() self.voice_combo = ComboBox() voicenames = self.voices.keys() voicenames.sort() for name in voicenames: self.voice_combo.append_item(self.voices[name], name) self.voice_combo.set_active(voicenames.index( self.face.status.voice.friendlyname)) combotool = ToolComboBox(self.voice_combo) voicebar.insert(combotool, -1) combotool.show() self.pitchadj = gtk.Adjustment(self.face.status.pitch, 0, face.PITCH_MAX, 1, face.PITCH_MAX/10, 0) pitchbar = gtk.HScale(self.pitchadj) pitchbar.set_draw_value(False) #pitchbar.set_inverted(True) pitchbar.set_update_policy(gtk.UPDATE_DISCONTINUOUS) pitchbar.set_size_request(240,15) pitchtool = gtk.ToolItem() pitchtool.add(pitchbar) pitchtool.show() voicebar.insert(pitchtool, -1) pitchbar.show() self.rateadj = gtk.Adjustment(self.face.status.rate, 0, face.RATE_MAX, 1, face.RATE_MAX/10, 0) ratebar = gtk.HScale(self.rateadj) ratebar.set_draw_value(False) #ratebar.set_inverted(True) ratebar.set_update_policy(gtk.UPDATE_DISCONTINUOUS) ratebar.set_size_request(240,15) ratetool = gtk.ToolItem() ratetool.add(ratebar) ratetool.show() voicebar.insert(ratetool, -1) ratebar.show() return voicebar def voice_changed_cb(self, combo): self.face.status.voice = combo.props.value self.face.say(self.face.status.voice.friendlyname) def pitch_adjusted_cb(self, get, data=None): self.face.status.pitch = get.value self.face.say(_("pitch adjusted")) def rate_adjusted_cb(self, get, data=None): self.face.status.rate = get.value self.face.say(_("rate adjusted")) def make_face_bar(self): facebar = gtk.Toolbar() self.numeyesadj = None self.mouth_shape_combo = ComboBox() self.mouth_shape_combo.append_item(mouth.Mouth, _("Simple")) self.mouth_shape_combo.append_item(waveform_mouth.WaveformMouth, _("Waveform")) self.mouth_shape_combo.append_item(fft_mouth.FFTMouth, _("Frequency")) self.mouth_shape_combo.set_active(0) combotool = ToolComboBox(self.mouth_shape_combo) facebar.insert(combotool, -1) combotool.show() self.eye_shape_combo = ComboBox() self.eye_shape_combo.append_item(eye.Eye, _("Round")) self.eye_shape_combo.append_item(glasses.Glasses, _("Glasses")) combotool = ToolComboBox(self.eye_shape_combo) facebar.insert(combotool, -1) combotool.show() self.numeyesadj = gtk.Adjustment(2, 1, 5, 1, 1, 0) numeyesbar = gtk.HScale(self.numeyesadj) numeyesbar.set_draw_value(False) numeyesbar.set_update_policy(gtk.UPDATE_DISCONTINUOUS) numeyesbar.set_size_request(240,15) numeyestool = gtk.ToolItem() numeyestool.add(numeyesbar) numeyestool.show() facebar.insert(numeyestool, -1) numeyesbar.show() self.eye_shape_combo.set_active(0) return facebar def mouth_changed_cb(self, combo, quiet): self.face.status.mouth = combo.props.value self.face.update() # this SegFaults: self.face.say(combo.get_active_text()) if not quiet: self.face.say(_("mouth changed")) def eyes_changed_cb(self, ignored, quiet): if self.numeyesadj is None: return self.face.status.eyes = [self.eye_shape_combo.props.value] \ * int(self.numeyesadj.value) self.face.update() # this SegFaults: self.face.say(self.eye_shape_combo.get_active_text()) if not quiet: self.face.say(_("eyes changed")) def _combo_changed_cb(self, combo): # when a new item is chosen, make sure the text is selected if not self.entry.is_focus(): self.entry.grab_focus() self.entry.select_region(0,-1) def _entry_key_press_cb(self, combo, event): # make the up/down arrows navigate through our history keyname = gtk.gdk.keyval_name(event.keyval) if keyname == "Up": index = self.entrycombo.get_active() if index>0: index-=1 self.entrycombo.set_active(index) self.entry.select_region(0,-1) return True elif keyname == "Down": index = self.entrycombo.get_active() if index20: self.entrycombo.remove_text(0) # select the new item self.entrycombo.set_active(len(history)-1) # select the whole text entry.select_region(0,-1) def _synth_cb(self, callback_type, index_mark=None): print "synth callback:", callback_type, index_mark def _activeCb( self, widget, pspec ): # only generate sound when this activity is active if not self.props.active: self.face.shut_up() self.chat.shut_up() def _toolbar_changed_cb(self, widget, index): if index == CHAT_TOOLBAR: self.face.shut_up() self.chat.update(self.face.status) self.notebook.set_current_page(1) else: self.chat.shut_up() self.notebook.set_current_page(0) def on_tube(self, tube_conn, initiating): self.chat.messenger = Messenger(tube_conn, initiating, self.chat) #def on_quit(self, data=None): # self.audio.on_quit() # activate gtk threads when this module loads gtk.gdk.threads_init()