# 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)
self.cfg = {}
# 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()