# 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 os import subprocess import gtk import gobject import pango import json from gettext import gettext as _ from sugar.graphics import style from sugar.graphics.toolbutton import ToolButton from sugar.graphics.toggletoolbutton import ToggleToolButton from sugar.graphics.radiotoolbutton import RadioToolButton from toolkit.toolitem import ToolWidget from toolkit.combobox import ComboBox from toolkit.toolbarbox import ToolbarBox from toolkit.activity import SharedActivity from toolkit.activity_widgets import * import eye import glasses import mouth import fft_mouth import waveform_mouth import voice import face import brain import chat import espeak from messenger import Messenger, SERVICE logger = logging.getLogger('speak') MODE_TYPE = 1 MODE_BOT = 2 MODE_CHAT = 3 MOUTHS = [mouth.Mouth, fft_mouth.FFTMouth, waveform_mouth.WaveformMouth] EYES = [eye.Eye, glasses.Glasses] DELAY_BEFORE_SPEAKING = 1500 # milleseconds def _is_tablet_mode(): if not os.path.exists('/dev/input/event4'): return False try: output = subprocess.call( ['evtest', '--query', '/dev/input/event4', 'EV_SW', 'SW_TABLET_MODE']) except (OSError, subprocess.CalledProcessError): return False if str(output) == '10': return True return False class SpeakActivity(SharedActivity): def __init__(self, handle): self.notebook = gtk.Notebook() SharedActivity.__init__(self, self.notebook, SERVICE, handle) self._mode = MODE_TYPE # self._tablet_mode = _is_tablet_mode() self._tablet_mode = _is_tablet_mode() self.numeyesadj = None # make an audio device for playing back and rendering audio self.connect("notify::active", self._activeCb) # make a box to type into hbox = gtk.HBox() if self._tablet_mode: self.entry = gtk.Entry() hbox.pack_start(self.entry, expand=True) talk_button = ToolButton('microphone') talk_button.set_tooltip(_('Speak')) talk_button.connect('clicked', self._talk_cb) hbox.pack_end(talk_button, expand=False) else: self.entrycombo = gtk.combo_box_entry_new_text() self.entrycombo.connect("changed", self._combo_changed_cb) self.entry = self.entrycombo.child hbox.pack_start(self.entrycombo, expand=True) 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) hbox.show() self.face = face.View() self.face.show() # layout the screen box = gtk.VBox(homogeneous=False) box.pack_start(hbox, expand=False) box.pack_start(self.face) self.add_events(gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.POINTER_MOTION_MASK) self.connect("motion_notify_event", self._mouse_moved_cb) box.add_events(gtk.gdk.BUTTON_PRESS_MASK) box.connect("button_press_event", self._mouse_clicked_cb) # desktop self.notebook.show() self.notebook.props.show_border=False self.notebook.props.show_tabs=False box.show_all() self.notebook.append_page(box) self.chat = chat.View() self.chat.show_all() self.notebook.append_page(self.chat) # make the text box active right away if not self._tablet_mode: self.entry.grab_focus() self.entry.connect("move-cursor", self._cursor_moved_cb) self.entry.connect("changed", self._cursor_moved_cb) # toolbar toolbox = ToolbarBox() toolbox.toolbar.insert(ActivityToolbarButton(self), -1) self.voices = ComboBox() for name in sorted(voice.allVoices().keys()): vn = voice.allVoices()[name] n = name [ : 26 ] + ".." self.voices.append_item(vn, n) self.voices.select(voice.defaultVoice()) all_voices = self.voices.get_model() brain_voices = brain.get_voices() mode_type = RadioToolButton( named_icon='mode-type', tooltip=_('Type something to hear it')) mode_type.connect('toggled', self.__toggled_mode_type_cb, all_voices) toolbox.toolbar.insert(mode_type, -1) mode_robot = RadioToolButton( named_icon='mode-robot', group=mode_type, tooltip=_('Ask robot any question')) mode_robot.connect('toggled', self.__toggled_mode_robot_cb, brain_voices) toolbox.toolbar.insert(mode_robot, -1) mode_chat = RadioToolButton( named_icon='mode-chat', group=mode_type, tooltip=_('Voice chat')) mode_chat.connect('toggled', self.__toggled_mode_chat_cb, all_voices) toolbox.toolbar.insert(mode_chat, -1) language_button = ToolbarButton( page=self.make_language_bar(), label=_('Language'), icon_name='module-language') toolbox.toolbar.insert(language_button, -1) voice_button = ToolbarButton( page=self.make_voice_bar(), label=_('Voice'), icon_name='voice') toolbox.toolbar.insert(voice_button, -1) face_button = ToolbarButton( page=self.make_face_bar(), label=_('Face'), icon_name='face') toolbox.toolbar.insert(face_button, -1) separator = gtk.SeparatorToolItem() separator.set_draw(False) separator.set_expand(True) toolbox.toolbar.insert(separator, -1) toolbox.toolbar.insert(StopButton(self), -1) toolbox.show_all() self.toolbar_box = toolbox gtk.gdk.screen_get_default().connect('size-changed', self._configure_cb) self._configure_cb() def _configure_cb(self, event=None): logger.debug('configure_cb') if gtk.gdk.screen_width() / 14 < style.GRID_CELL_SIZE: self.numeyesbar_label.set_label('') else: self.numeyesbar_label.set_label(_('Eyes number:')) def new_instance(self): self.voices.connect('changed', self.__changed_voices_cb) self.pitchadj.connect("value_changed", self.pitch_adjusted_cb, self.pitchadj) self.rateadj.connect("value_changed", self.rate_adjusted_cb, self.rateadj) self.numeyesadj.connect("value_changed", self.eyes_changed_cb, False) self.eyes_changed_cb(None, True) self.mouth_changed_cb(None, True) self.face.look_ahead() # say hello to the user presenceService = presenceservice.get_instance() xoOwner = presenceService.get_owner() if self._tablet_mode: self.entry.props.text = _("Hello %s.") \ % xoOwner.props.nick.encode('utf-8', 'ignore') self.face.say_notification(_("Hello %s. Please Type something.") \ % xoOwner.props.nick) def resume_instance(self, file_path): self.cfg = json.loads(file(file_path, 'r').read()) status = self.face.status = \ face.Status().deserialize(self.cfg['status']) self.voices.select(status.voice) self.pitchadj.value = self.face.status.pitch self.rateadj.value = self.face.status.rate self.numeyesadj.value = len(status.eyes) if status.mouth in MOUTHS: self.mouth_type[MOUTHS.index(status.mouth)].set_active(True) if status.eyes[0] in EYES: self.eye_type[EYES.index(status.eyes[0])].set_active(True) self.entry.props.text = self.cfg['text'].encode('utf-8', 'ignore') if not self._tablet_mode: for i in self.cfg['history']: self.entrycombo.append_text(i.encode('utf-8', 'ignore')) self.new_instance() def save_instance(self, file_path): if self._tablet_mode: if 'history' in self.cfg: history = self.cfg['history'] # retain old history else: history = [] else: history = [unicode(i[0], 'utf-8', 'ignore') \ for i in self.entrycombo.get_model()] cfg = {'status': self.face.status.serialize(), 'text': unicode(self.entry.props.text, 'utf-8', 'ignore'), 'history': history, } file(file_path, 'w').write(json.dumps(cfg)) def share_instance(self, connection, is_initiator): self.chat.messenger = Messenger(connection, is_initiator, self.chat) 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(pos=(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 self.face.look_at() self.chat.look_at() def _mouse_clicked_cb(self, widget, event): pass def make_language_bar(self): languagebar = gtk.Toolbar() voices_toolitem = ToolWidget(widget=self.voices) languagebar.insert(voices_toolitem, -1) languagebar.show_all() return languagebar def make_voice_bar(self): voicebar = gtk.Toolbar() self.pitchadj = gtk.Adjustment(self.face.status.pitch, 0, espeak.PITCH_MAX, 1, espeak.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) pitchbar_toolitem = ToolWidget( widget=pitchbar, label_text=_('Pitch:')) voicebar.insert(pitchbar_toolitem, -1) 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) ratebar.set_update_policy(gtk.UPDATE_DISCONTINUOUS) ratebar.set_size_request(240, 15) ratebar_toolitem = ToolWidget( widget=ratebar, label_text=_('Rate:')) voicebar.insert(ratebar_toolitem, -1) voicebar.show_all() return voicebar def pitch_adjusted_cb(self, get, data=None): self.face.status.pitch = get.value self.face.say_notification(_("pitch adjusted")) def rate_adjusted_cb(self, get, data=None): self.face.status.rate = get.value self.face.say_notification(_("rate adjusted")) def make_face_bar(self): facebar = gtk.Toolbar() self.mouth_type = [] self.mouth_type.append(RadioToolButton( named_icon='mouth', group=None, tooltip=_('Simple'))) self.mouth_type[-1].connect('clicked', self.mouth_changed_cb, False) facebar.insert(self.mouth_type[-1], -1) self.mouth_type.append(RadioToolButton( named_icon='waveform', group=self.mouth_type[0], tooltip=_('Waveform'))) self.mouth_type[-1].connect('clicked', self.mouth_changed_cb, False) facebar.insert(self.mouth_type[-1], -1) self.mouth_type.append(RadioToolButton( named_icon='frequency', group=self.mouth_type[0], tooltip=_('Frequency'))) self.mouth_type[-1].connect('clicked', self.mouth_changed_cb, False) facebar.insert(self.mouth_type[-1], -1) separator = gtk.SeparatorToolItem() separator.set_draw(True) separator.set_expand(False) facebar.insert(separator, -1) self.eye_type = [] self.eye_type.append(RadioToolButton( named_icon='eyes', group=None, tooltip=_('Round'))) self.eye_type[-1].connect('clicked', self.eyes_changed_cb, False) facebar.insert(self.eye_type[-1], -1) self.eye_type.append(RadioToolButton( named_icon='glasses', group=self.eye_type[0], tooltip=_('Glasses'))) self.eye_type[-1].connect('clicked', self.eyes_changed_cb, False) facebar.insert(self.eye_type[-1], -1) separator = gtk.SeparatorToolItem() separator.set_draw(False) separator.set_expand(False) facebar.insert(separator, -1) self.numeyesbar_label = gtk.Label() self.numeyesbar_label.set_text(_('Eyes number:')) toolitem = gtk.ToolItem() toolitem.add(self.numeyesbar_label) facebar.insert(toolitem, -1) 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) toolitem = gtk.ToolItem() toolitem.add(numeyesbar) facebar.insert(toolitem, -1) facebar.show_all() return facebar def _get_active_mouth(self): for i, button in enumerate(self.mouth_type): if button.get_active(): return MOUTHS[i] def mouth_changed_cb(self, ignored, quiet): value = self._get_active_mouth() if value is None: return self.face.status.mouth = value self._update_face() # this SegFaults: self.face.say(combo.get_active_text()) if not quiet: self.face.say_notification(_("mouth changed")) def _get_active_eyes(self): for i, button in enumerate(self.eye_type): if button.get_active(): return EYES[i] def eyes_changed_cb(self, ignored, quiet): if self.numeyesadj is None: return value = self._get_active_eyes() if value is None: return self.face.status.eyes = [value] * int(self.numeyesadj.value) self._update_face() # this SegFaults: self.face.say(self.eye_shape_combo.get_active_text()) if not quiet: self.face.say_notification(_("eyes changed")) def _update_face(self): self.face.update() self.chat.update(self.face.status) def _combo_changed_cb(self, combo): # when a new item is chosen, make sure the text is selected if not self.entry.is_focus(): if not self._tablet_mode: 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 if self._tablet_mode: return 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) if text: # select the whole text entry.select_region(0, -1) 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 _set_voice(self, new_voice): try: self.voices.handler_block_by_func(self.__changed_voices_cb) self.voices.select(new_voice) self.face.status.voice = new_voice finally: self.voices.handler_unblock_by_func(self.__changed_voices_cb) def __toggled_mode_type_cb(self, button, voices_model): if not button.props.active: return self._mode = MODE_TYPE self.chat.shut_up() self.face.shut_up() self.notebook.set_current_page(0) old_voice = self.voices.props.value self.voices.set_model(voices_model) self._set_voice(old_voice) def __toggled_mode_robot_cb(self, button, voices_model): if not button.props.active: return self._mode = MODE_BOT self.chat.shut_up() self.face.shut_up() self.notebook.set_current_page(0) old_voice = self.voices.props.value self.voices.set_model(voices_model) new_voice = [i[0] for i in voices_model if i[0].short_name == old_voice.short_name] if not new_voice: new_voice = brain.get_default_voice() sorry = _("Sorry, I can't speak %(old_voice)s, " \ "let's talk %(new_voice)s instead.") % { 'old_voice': old_voice.friendlyname, 'new_voice': new_voice.friendlyname, } else: new_voice = new_voice[0] sorry = None self._set_voice(new_voice) if not brain.load(self, self.voices.props.value, sorry): if sorry: self.face.say_notification(sorry) def __toggled_mode_chat_cb(self, button, voices_model): if not button.props.active: return is_first_session = not self.chat.me.flags() & gtk.MAPPED self._mode = MODE_CHAT self.face.shut_up() self.notebook.set_current_page(1) old_voice = self.voices.props.value self.voices.set_model(voices_model) self._set_voice(old_voice) if is_first_session: self.chat.me.say_notification( _("You are in off-line mode, share and invite someone.")) def __changed_voices_cb(self, combo): voice = combo.props.value self.face.set_voice(voice) if self._mode == MODE_BOT: brain.load(self, voice) # activate gtk threads when this module loads gtk.gdk.threads_init()