Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorflavio <fdanesse@gmail.com>2012-08-04 16:27:48 (GMT)
committer flavio <fdanesse@gmail.com>2012-08-04 16:27:48 (GMT)
commit663b6c907c1f75502b1ccc0135a9532634830a88 (patch)
treecce7e17271e1fefafa51f4c1182b29df22e90f5e
GnomeSpeak BaseHEADmaster
-rw-r--r--.gitignore5
-rw-r--r--JAMediaReproductor.py361
-rw-r--r--MplayerReproductor.py346
-rw-r--r--Sound.py81
-rw-r--r--Speak.py197
-rw-r--r--Widgets.py423
-rw-r--r--activity/activity-speak.svg14
-rw-r--r--activity/activity.info23
-rw-r--r--face.py438
-rw-r--r--icons/edit-description.svg14
-rw-r--r--icons/face.svg103
-rw-r--r--icons/mode-chat.svg128
-rw-r--r--icons/mode-robot.svg119
-rw-r--r--icons/mode-type.svg82
-rw-r--r--icons/voice.svg66
-rw-r--r--setup.py27
16 files changed, 2427 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bbc801c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.pyc
+*.pyo
+*.mo
+*~
+*.bak
diff --git a/JAMediaReproductor.py b/JAMediaReproductor.py
new file mode 100644
index 0000000..83d75f6
--- /dev/null
+++ b/JAMediaReproductor.py
@@ -0,0 +1,361 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# JAMediaReproductor.py por:
+# Flavio Danesse <fdanesse@gmail.com>
+# CeibalJAM! - Uruguay
+#
+# 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
+
+# Se remplaza:
+# Depends: python-gst0.10,
+# gstreamer0.10-plugins-good,
+# gstreamer0.10-plugins-ugly,
+# gstreamer0.10-plugins-bad,
+# gstreamer0.10-ffmpeg
+
+# Con:
+# Depends: python-gi,
+# gir1.2-gstreamer-1.0,
+# gir1.2-gst-plugins-base-1.0,
+# gstreamer1.0-plugins-good,
+# gstreamer1.0-plugins-ugly,
+# gstreamer1.0-plugins-bad,
+# gstreamer1.0-libav
+
+# https://wiki.ubuntu.com/Novacut/GStreamer1.0#Using_GStreamer_1.0_from_Python
+# http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer/html/GstBus.html#gst-bus-create-watch
+# http://www.roojs.org/seed/gir-1.1-gtk-2.0/Gst.MessageType.html#expand
+
+import os
+
+import gi
+gi.require_version('Gst', '1.0')
+
+from gi.repository import GObject
+from gi.repository import Gst
+from gi.repository import GstVideo
+
+GObject.threads_init()
+Gst.init(None)
+
+class JAMediaReproductor(GObject.GObject):
+ """
+ Reproductor de Audio, Video y Streaming de
+ Radio y Television. Implementado sobre:
+
+ python 2.7.3
+ Gtk 3
+ Gstreamer 1.0
+ """
+
+ __gsignals__ = {"endfile":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, []),
+ "estado":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE,
+ (GObject.TYPE_STRING,)),
+ "newposicion":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE,
+ (GObject.TYPE_INT,)),
+ "volumen":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE,
+ (GObject.TYPE_FLOAT,))}
+
+ # Estados: playing, paused, None
+
+ def __init__(self, ventana_id):
+ """ Recibe el id de un DrawingArea
+ para mostrar el video. """
+
+ GObject.GObject.__init__(self)
+ self.name = "JAMediaReproductor"
+ self.ventana_id = ventana_id
+ self.pipeline = None
+ self.estado = None
+
+ self.duracion = 0
+ self.posicion = 0
+ self.actualizador = None
+
+ self.player = None
+ self.bus = None
+
+ self.set_pipeline()
+
+ def set_pipeline(self):
+ """Crea el pipe de Gst. (playbin)"""
+
+ if self.pipeline: del(self.pipeline)
+
+ self.pipeline = Gst.Pipeline()
+ self.bus = self.pipeline.get_bus()
+ self.bus.add_signal_watch()
+ self.bus.connect('message', self.on_mensaje)
+
+ self.bus.enable_sync_message_emission()
+ self.bus.connect('sync-message', self.sync_message)
+
+ self.player = Gst.ElementFactory.make("playbin", "player")
+ self.pipeline.add(self.player)
+
+ def sync_message(self, bus, mensaje):
+ """Captura los mensajes en el bus del pipe Gst."""
+
+ try:
+ if mensaje.get_structure().get_name() == 'prepare-window-handle':
+ mensaje.src.set_window_handle(self.ventana_id)
+ return
+ except:
+ pass
+
+ if mensaje.type == Gst.MessageType.STATE_CHANGED:
+ old, new, pending = mensaje.parse_state_changed()
+ if old == Gst.State.PAUSED and new == Gst.State.PLAYING:
+ if self.estado != new:
+ self.estado = new
+ self.emit("estado", "playing")
+ self.new_handle(True)
+ return
+ elif old == Gst.State.READY and new == Gst.State.PAUSED:
+ if self.estado != new:
+ self.estado = new
+ self.emit("estado", "paused")
+ self.new_handle(False)
+ return
+ elif old == Gst.State.READY and new == Gst.State.NULL:
+ if self.estado != new:
+ self.estado = new
+ self.emit("estado", "None")
+ self.new_handle(False)
+ return
+ elif old == Gst.State.PLAYING and new == Gst.State.PAUSED:
+ if self.estado != new:
+ self.estado = new
+ self.emit("estado", "paused")
+ self.new_handle(False)
+ return
+ elif old == Gst.State.NULL and new == Gst.State.READY:
+ pass
+ elif old == Gst.State.PAUSED and new == Gst.State.READY:
+ pass
+ else:
+ #print ">>>", old, new, pending
+ return
+
+ elif mensaje.type == Gst.MessageType.ASYNC_DONE:
+ #print mensaje.get_structure().get_name()
+ return
+ elif mensaje.type == Gst.MessageType.NEW_CLOCK:
+ return
+ elif mensaje.type == Gst.MessageType.STREAM_STATUS:
+ #print mensaje.parse_stream_status()
+ return
+ elif mensaje.type == Gst.MessageType.TAG:
+ return
+ elif mensaje.type == Gst.MessageType.ERROR:
+ err, debug = mensaje.parse_error()
+ print "***", 'sync_message'
+ print err, debug
+ self.new_handle(False)
+ #self.pipeline.set_state(Gst.State.NULL)
+ #self.pipeline.set_state(Gst.State.READY)
+ #return self.set_pipeline()
+ return
+ elif mensaje.type == Gst.MessageType.EOS:
+ return
+ else:
+ try:
+ nombre = mensaje.get_structure().get_name()
+ if nombre == "playbin-stream-changed":
+ #print "Nuevo src:", mensaje.get_structure().to_string()
+ pass
+ elif nombre == "have-window-handle":
+ pass
+ elif nombre == "prepare-window-handle":
+ pass
+ else:
+ pass
+ except:
+ print "sync_message", mensaje.type
+ return
+
+ def on_mensaje(self, bus, mensaje):
+ """Captura los mensajes en el bus del pipe Gst."""
+
+ #if mensaje.type == Gst.MessageType.ASYNC_DONE:
+ # print mensaje.get_structure().get_name()
+ #elif mensaje.type == Gst.MessageType.NEW_CLOCK:
+ # pass
+
+ if mensaje.type == Gst.MessageType.ELEMENT:
+ nombre = mensaje.get_structure().get_name()
+ # playbin-stream-changed , prepare-window-handle, have-window-handle
+ #if nombre == 'prepare-window-handle':
+ # mensaje.src.set_window_handle(self.ventana_id)
+ #else:
+ # print nombre
+
+ '''
+ elif mensaje.type == Gst.MessageType.STATE_CHANGED:
+ old, new, pending = mensaje.parse_state_changed()
+ if old == Gst.State.PAUSED and new == Gst.State.PLAYING:
+ if self.estado != new:
+ self.estado = new
+ #self.emit("estado", "playing")
+ elif old == Gst.State.READY and new == Gst.State.PAUSED:
+ if self.estado != new:
+ self.estado = new
+ #self.emit("estado", "paused")
+ else:
+ if self.F: print old, new, pending
+ pass
+ elif mensaje.type == Gst.MessageType.STREAM_STATUS:
+ pass
+ elif mensaje.type == Gst.MessageType.TAG:
+ #print mensaje.get_structure().to_string()
+ pass'''
+
+ if mensaje.type == Gst.MessageType.EOS:
+ #self.pipeline.seek_simple(Gst.Format.TIME,
+ #Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 0)
+ self.new_handle(False)
+ self.set_pipeline()
+ self.emit("endfile")
+
+ elif mensaje.type == Gst.MessageType.QOS:
+ pass
+ elif mensaje.type == Gst.MessageType.WARNING:
+ print mensaje.get_structure().to_string()
+ #self.new_handle(False)
+ #self.pipeline.set_state(Gst.State.NULL)
+ #self.set_pipeline()
+
+ elif mensaje.type == Gst.MessageType.ERROR:
+ err, debug = mensaje.parse_error()
+ print "***", 'sync_message'
+ print err, debug
+ self.new_handle(False)
+ #self.pipeline.set_state(Gst.State.NULL)
+ #self.set_pipeline()
+ self.pipeline.set_state(Gst.State.READY)
+
+ elif mensaje.type == Gst.MessageType.LATENCY:
+ #print mensaje.type
+ pass
+ else:
+ #print "on_mensaje", mensaje.type
+ pass
+
+ def load(self, uri):
+ """Carga un archivo o stream en el pipe de Gst."""
+
+ self.stop()
+ if os.path.exists(uri):
+ direccion = Gst.filename_to_uri(uri)
+ self.player.set_property("uri", direccion)
+ self.play()
+ else:
+ # FIXME: Funciona con la radio pero no con la Tv
+ direccion = uri
+ #Gst.uri_get_protocol(uri)
+ if Gst.uri_is_valid(uri):
+ self.player.set_property("uri", direccion)
+ self.play()
+ #print Gst.uri_protocol_is_supported(uri, Gst.uri_get_protocol(uri))
+ #print Gst.uri_protocol_is_valid(Gst.uri_get_protocol(uri))
+ #self.player.set_property("buffer-size", 1024)
+ #self.player.set_property("force-aspect-ratio", False)
+
+ def play(self):
+ """Pone el pipe de Gst en Gst.State.PLAYING"""
+
+ self.pipeline.set_state(Gst.State.PLAYING)
+
+ def stop(self):
+ """Pone el pipe de Gst en Gst.State.NULL"""
+
+ self.pipeline.set_state(Gst.State.NULL)
+ self.pipeline.set_state(Gst.State.READY)
+
+ def pause(self):
+ """Pone el pipe de Gst en Gst.State.PAUSED"""
+
+ self.pipeline.set_state(Gst.State.PAUSED)
+
+ def pause_play(self):
+ """Llama a play() o pause()
+ segun el estado actual del pipe de Gst."""
+
+ if self.estado == Gst.State.PAUSED \
+ or self.estado == Gst.State.NULL \
+ or self.estado == Gst.State.READY:
+ self.play()
+ elif self.estado == Gst.State.PLAYING:
+ self.pause()
+ else:
+ print self.estado
+
+ def new_handle(self, reset):
+ """Elimina o reinicia la funcion que
+ envia los datos de actualizacion para
+ la barra de progreso del reproductor."""
+
+ if self.actualizador:
+ GObject.source_remove(self.actualizador)
+ self.actualizador = None
+ if reset:
+ self.actualizador = GObject.timeout_add(300, self.handle)
+
+ def handle(self):
+ """Funcion que envia los datos de actualizacion para
+ la barra de progreso del reproductor."""
+
+ bool1, valor1 = self.pipeline.query_duration(Gst.Format.TIME)
+ bool2, valor2 = self.pipeline.query_position(Gst.Format.TIME)
+
+ duracion = float(valor1)
+ posicion = float(valor2)
+
+ pos = 0
+ try:
+ pos = int(posicion * 100 / duracion)
+ except:
+ pass
+
+ if pos < 0 or pos > self.duracion: return True
+
+ if self.duracion != duracion: self.duracion = duracion
+
+ if pos != self.posicion:
+ self.posicion = pos
+ self.emit("newposicion", self.posicion)
+ # print "***", gst.video_convert_frame(self.player.get_property("frame"))
+
+ return True
+
+ def set_position(self, posicion):
+ """Funcion que permite desplazarse por
+ el archivo que se esta reproduciendo."""
+
+ if self.duracion < posicion:
+ self.emit("newposicion", self.posicion)
+ return
+ posicion = self.duracion * posicion / 100
+ self.pipeline.seek_simple(Gst.Format.TIME,
+ Gst.SeekFlags.FLUSH, posicion)
+
+ def get_volumen(self):
+ pass
+
+ def set_volumen(self, valor):
+ pass
+ \ No newline at end of file
diff --git a/MplayerReproductor.py b/MplayerReproductor.py
new file mode 100644
index 0000000..366fb1d
--- /dev/null
+++ b/MplayerReproductor.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# MplayerReproductor.py por:
+# Flavio Danesse <fdanesse@gmail.com>
+# CeibalJAM! - Uruguay
+
+# 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 time
+import os
+import subprocess
+#import platform
+
+import gi
+from gi.repository import GObject
+
+STDOUT = "/tmp/jamediaout%d" % time.time()
+STDERR = "/dev/null"
+MPLAYER = "mplayer"
+
+#if "olpc" in platform.platform(): MPLAYER = "./mplayer"
+#if "Strawberry" in platform.platform(): MPLAYER = "./mplayer"
+
+class MplayerReproductor(GObject.GObject):
+ """
+ Reproductor de Audio, Video y Streaming de
+ Radio y Television. Implementado sobre:
+
+ python 2.7.3
+ Gtk 3
+ mplayer (a traves de python.subprocess)
+ """
+
+ __gsignals__ = {"endfile":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, []),
+ "estado":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE,
+ (GObject.TYPE_STRING,)),
+ "newposicion":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE,
+ (GObject.TYPE_INT,)),
+ "volumen":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE,
+ (GObject.TYPE_FLOAT,))}
+
+ # Estados: playing, paused, None
+
+ def __init__(self, ventana_id):
+ """ Recibe el id de un DrawingArea
+ para mostrar el video. """
+
+ GObject.GObject.__init__(self)
+ self.name = "MplayerReproductor"
+ self.ventana_id = ventana_id
+ self.mplayer = None
+ self.salida = None
+ self.entrada = None
+ self.estado = None
+ self.duracion = 0
+ self.posicion = 0
+ self.volumen = 0
+ self.actualizador = None
+ self.uri = None
+
+ def stop(self):
+ """Detiene todo."""
+
+ try:
+ if self.entrada:
+ self.entrada.write('%s 0\n' % "quit")
+ self.entrada.flush()
+ self.new_handle(False)
+ except Exception, e:
+ #print "HA OCURRIDO UN ERROR EN QUIT DEL REPRODUCTOR", e
+ pass
+ self.posicion = 0
+ if os.path.exists(STDOUT): os.unlink(STDOUT)
+ import commands
+ commands.getoutput('killall mplayer')
+ self.estado = None
+ self.emit("estado", "None")
+
+ def load(self, uri):
+ """Carga y Reproduce un archivo o streaming."""
+
+ self.stop()
+ self.uri = uri
+ if os.path.exists(self.uri):
+ uri = "%s%s%s" % ("\"", self.uri, "\"")
+
+ cache_pantalla = "%s -cache %i -wid %i" % (MPLAYER, 1024, self.ventana_id)
+ estructura = "%s -slave -idle -nolirc -rtc -nomouseinput -noconsolecontrols -nojoystick" % (cache_pantalla)
+ self.mplayer = subprocess.Popen(estructura, shell = True, stdin = subprocess.PIPE,
+ stdout = open(STDOUT,"w+b"), stderr=open(STDOUT,"r+b"), universal_newlines=True)
+ self.entrada = self.mplayer.stdin
+ self.salida = open(STDOUT,"r")
+ self.entrada.write("loadfile %s 0\n" % uri)
+ self.entrada.flush()
+ self.new_handle(True)
+
+ def handle(self):
+ """Consulta el estado y progreso del
+ la reproduccion actual."""
+
+ if not self.entrada.closed:
+ # Control por tiempo
+ #self.entrada.write("%s 0\n" % ("get_time_length"))
+ #self.entrada.flush()
+ #duracion = self.salida.readline()
+ #if "ANS_LENGTH" in duracion:
+ # duracion = float(duracion.split("=")[1]) # Duración en Segundos
+ # print "dur", duracion
+
+ #self.entrada.write("%s 0\n" % ("get_time_pos"))
+ #self.entrada.flush()
+ #posicion = self.salida.readline()
+ #if "ANS_TIME_POSITION" in posicion:
+ # posicion = float(posicion.split("=")[1]) # Posición en Segundos
+ # print "pos", posicion
+
+ self.entrada.write("%s 0\n" % ("get_property percent_pos"))
+ self.entrada.flush()
+ linea = self.salida.readline()
+ if linea:
+ if "ANS_percent_pos" in linea:
+ "Información sobre el porcentaje Reproducido hasta el momento. Ejemplo:"
+ "ANS_percent_pos=0"
+ self.get_progress_in_mplayer(linea)
+ self.get_volumen()
+
+ elif "Video: no video" in linea or "Audio only file format detected" in linea:
+ "Cuando no hay video en la fuente. Ejemplo"
+ "Audio only file format detected."
+ "Video: no video"
+ #self.emit("video", False)
+ pass
+
+ elif "Cache" in linea:
+ "Información Sobre Carga de caché. Ejemplo:"
+ "Cache fill: 6.25% (65536 bytes)"
+ #self.get_progress_cache_in_mplayer(linea)
+ pass
+
+ elif "Movie-Aspect" in linea:
+ "Información sobre el aspecto del video. Ejemplo:"
+ "Movie-Aspect is 1.78:1 - prescaling to correct movie aspect."
+ #self.emit("video", True)
+ pass
+
+ elif "Starting playback" in linea:
+ "Cuando comienza la Reproducción. Ejemplo:"
+ "Starting playback..."
+ self.estado = "playing"
+ self.emit("estado", "playing")
+
+ elif "AO:" in linea:
+ "Información Sobre Audio en la pista. Ejemplo:"
+ "AO: [pulse] 44100Hz 2ch s16le (2 bytes per sample)"
+ pass
+
+ elif "VO:" in linea:
+ "Información sobre Video en la pista. Ejemplo:"
+ "VO: [xv] 428x240 => 428x240 Planar YV12"
+ pass
+
+ elif "Resolving" in linea:
+ "Información sobre Resolución de Streamings. Ejemplo:"
+ "Resolving radio1.oceanofm.com for AF_INET6..."
+ pass
+
+ elif "Connecting" in linea:
+ "Información sobre Conexión a un Streaming. Ejemplo:"
+ "Connecting to server main-office.rautemusik.fm[87.230.101.9]: 80..."
+ pass
+
+ elif "Name" in linea:
+ "El nombre de una streaming de Radio. Ejemplo:"
+ "Name : #MUSIK.MAIN - WWW.RAUTEMUSIK.FM - 24H TOP 40 POP HITS 80S 90S DANCE HOUSE ROCK RNB AND MORE!"
+ pass
+
+ elif "Playing" in linea:
+ "La Pista que se está reproduciendo. Ejemplo:"
+ "Playing /media/4E432D364BC64012/E - Videos/Tylor swift/Back to December-Taylor Swift Lyrics."
+ pass
+
+ elif "Genre" in linea or "Website" in linea or "Bitrate" in linea:
+ "Información Sobre un Streaming de Radio. Ejemplo:"
+ "Genre : Pop Rock Top 40 RnB 80s"
+ "Website: http://www.RauteMusik.FM/"
+ "Bitrate: 128kbit/s"
+ pass
+
+ elif "Opening" in linea or "AUDIO" in linea or "Selected" in linea:
+ "Información sobre Codecs Utilizados. Ejemplo:"
+ "Opening video decoder: [ffmpeg] FFmpeg's libavcodec codec family"
+ "Selected video codec: [ffh264] vfm: ffmpeg (FFmpeg H.264)"
+ "Opening audio decoder: [ffmpeg] FFmpeg/libavcodec audio decoders"
+ "AUDIO: 44100 Hz, 2 ch, s16le, 98.6 kbit/6.99% (ratio: 12323->176400)"
+ "Selected audio codec: [ffaac] afm: ffmpeg (FFmpeg AAC (MPEG-2/MPEG-4 Audio))"
+ pass
+
+ else:
+ "Información Diversa. Ejemplo:"
+ "Failed to open /dev/rtc: Permission denied (it should be readable by the us"
+ "Unsupported PixelFormat 61"
+ "Unsupported PixelFormat 53"
+ "Unsupported PixelFormat 81"
+ "eo (h264), -vid 0"
+ "[lavf] stream 1: audio (aac), -aid 0"
+ "VIDEO: [H264] 640x480 0bpp 30.000 fps 218.3 kbps (26.6 kbyte/s)"
+ "Clip info:"
+ "starttime: 0"
+ "totalduration: 226"
+ "totaldatarate: 338"
+ "bytelength: 9570679"
+ "canseekontime: true"
+ "sourcedata: BC8280065HH1341966475963833"
+ "purl: "
+ "pmsg: "
+ "Load subtitles in /media/4E432D364BC64012/E - Videos/Tylor swift/"
+ "=========================================================================="
+ "libavcodec version 53.35.0 (external)"
+ "Mismatching header version 53.32.2"
+ "=========================================================================="
+ "=========================================================================="
+ "=========================================================================="
+ "A: 0.0 V: 0.0 A-V: 0.014 ct: 0.000 0/ 0 ??% ??% ??,?% 0 0 90%"
+ "A: 123.2 V: 123.2 A-V: -0.000 ct: 0.033 0/ 0 2% 1% 0.4% 0 0 50%"
+
+ "Cuando no se puede Acceder a un Streaming. Ejemplo:"
+ "Failed to get value of property 'percent_pos'."
+ "Failed to get value of property 'percent_ANS_ERFailed to get value of"
+ "prANS_ERROR=PROPERTY_UNAFailed toANS_ERROR=PROPERTY_UNAVAILABLE"
+ "ANS_ERROR=PROPERTY_UNAVAILABLE"
+ pass
+
+ return True
+
+ def get_progress_in_mplayer(self, linea):
+ """Obtiene el progreso de la reproduccion y lo
+ envia en una señal para actualizar la barra de
+ progreso."""
+
+ pos = 0
+ try:
+ if "Cache size" in linea: return
+ pos = int(linea.split('=')[1])
+ if pos != self.posicion:
+ self.posicion = pos
+ self.emit("newposicion", self.posicion)
+ if self.posicion >= 100:
+ self.emit("endfile")
+ except Exception, e:
+ print "Error en Progreso de Reproducción: %s" % (e)
+
+ def pause_play(self):
+ """Llama a play() o pause()
+ segun el estado actual del reproductor."""
+
+ try:
+ if self.entrada:
+ if self.estado == "playing": # pausa
+ self.pause()
+ elif self.estado == "paused":
+ self.pause(True)
+ self.estado = "playing"
+ self.emit("estado", "playing")
+ else:
+ #if self.uri: self.load(self.uri)
+ pass
+ except Exception, e:
+ print "HA OCURRIDO UN ERROR EN PAUSE_PLAY DEL REPRODUCTOR", e
+
+ def pause(self, reset = False):
+ """Pone en pause o unpause a mplayer"""
+
+ self.entrada.write('pause 0\n')
+ self.entrada.flush()
+ self.new_handle(reset)
+ self.estado = "paused"
+ self.emit("estado", "paused")
+
+ def play(self):
+ """No hace nada. mplayer utiliza:
+ pause, unpause y load en lugar de play."""
+
+ pass
+
+ def new_handle(self, reset):
+ """Elimina o reinicia la funcion que
+ envia los datos de actualizacion para
+ la barra de progreso del reproductor."""
+
+ if self.actualizador:
+ GObject.source_remove(self.actualizador)
+ self.actualizador = None
+ if reset:
+ self.actualizador = GObject.timeout_add(35, self.handle)
+
+ def set_position(self, posicion):
+ """Funcion que permite desplazarse por
+ el archivo que se esta reproduciendo."""
+
+ # FIXME: Actualmente no funciona bien
+ posicion = int(posicion)
+ if posicion != self.posicion:
+ self.posicion = posicion
+ self.entrada.write('seek %s %i 0\n' % (posicion, 1))
+ self.entrada.flush()
+
+ def get_volumen(self):
+ """Obtiene el volumen de reproducción.
+ Lo hace solo al reproducir el primer archivo
+ o streaming y envía el dato para actualizar
+ el control de volúmen."""
+
+ if self.volumen != 0: return
+ if self.entrada:
+ self.entrada.write("%s 0\n" % ("get_property volume"))
+ self.entrada.flush()
+ linea = self.salida.readline()
+ if "ANS_volume" in linea:
+ valor = float(linea.split("=")[1])
+ if self.volumen == 0:
+ self.volumen = valor
+ self.emit('volumen', valor)
+
+ def set_volumen(self, valor):
+ """Cambia el volúmen de Reproducción."""
+
+ if self.entrada:
+ if valor != self.volumen:
+ self.volumen = valor
+ self.entrada.write("%s %s0\n" % ("set_property volume", valor))
+ self.entrada.flush()
+ \ No newline at end of file
diff --git a/Sound.py b/Sound.py
new file mode 100644
index 0000000..546c5b2
--- /dev/null
+++ b/Sound.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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 gi
+from gi.repository import GObject
+
+from JAMediaReproductor import JAMediaReproductor
+from MplayerReproductor import MplayerReproductor
+
+PITCH_MAX = 200
+RATE_MAX = 200
+wavpath = "/tmp/speak.wav"
+
+def make_file(pitch, rate, voice, text):
+ """Genera el archivo de audio."""
+
+ rate = 60 + int(((175 - 80) * 2) * rate / RATE_MAX)
+ command = "espeak -w %s -p %s -s %s -v%s \"%s\"" % (wavpath,
+ str(pitch), str(rate), voice, text)
+ ret = os.system(command)
+ file = open(wavpath, "r")
+ file.flush()
+ #print file.readlines()
+ os.fsync(file)
+ file.close()
+ return ret
+
+class Sound(GObject.GObject):
+ """Interactua entre espeak y JAMediaReproductor."""
+
+ def __init__(self, xid):
+
+ GObject.GObject.__init__(self)
+
+ self.pitch = 0
+ self.rate = 0
+ self.voice = "es"
+ self.text = ""
+ self.estado = None
+
+ self.player = MplayerReproductor(xid) #JAMediaReproductor(xid)
+
+ def set_voice(self, value):
+ """Cuando cambia el lenguaje."""
+
+ self.voice = value
+
+ def set_pitch(self, value):
+ """Cuando cambia pitch."""
+
+ self.pitch = value
+
+ def set_rate(self, value):
+ """Cuando cambia rate."""
+
+ self.rate = value
+
+ def speak(self, text):
+ """Cuando habla."""
+
+ self.text = text
+
+ if not make_file(self.pitch, self.rate, self.voice, self.text):
+ self.player.load(wavpath)
+ \ No newline at end of file
diff --git a/Speak.py b/Speak.py
new file mode 100644
index 0000000..9a0a5a2
--- /dev/null
+++ b/Speak.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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 sys
+import os
+
+import gi
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import GObject
+from gi.repository import GdkX11
+
+from Widgets import Speaking
+from Widgets import Toolbar
+from Widgets import ToolbarPitchRate
+from Widgets import ToolbarFace
+from Sound import Sound
+
+BASE = os.path.dirname(__file__)
+
+GObject.threads_init()
+Gdk.threads_init()
+
+class Speak(Gtk.Window):
+
+ def __init__(self):
+ Gtk.Window.__init__(self)
+
+ self.set_title("Hablar con Sara")
+ self.set_icon_from_file(os.path.join(BASE, "icons", "face.svg"))
+ self.set_resizable(True)
+ self.set_border_width(0)
+ self.set_size_request(640, 480)
+ self.modify_bg(0, Gdk.Color(65000, 28260, 30516))
+
+ self.add_events(
+ Gdk.EventMask.POINTER_MOTION_HINT_MASK |
+ Gdk.EventMask.POINTER_MOTION_MASK)
+
+ self.toolbar = None
+ self.toolbarpitchrate = None
+ self.toolbarface = None
+ self.notebook = None
+ self.speaking = None
+ self.sound = None
+
+ self.dinamic_toolbars = []
+
+ self.setup_init()
+
+ def setup_init(self):
+ """Se crean los objetos y se empaqueta todo."""
+
+ self.toolbar = Toolbar()
+ self.toolbarpitchrate = ToolbarPitchRate()
+ self.toolbarface = ToolbarFace()
+ self.notebook = Gtk.Notebook()
+ self.notebook.set_show_border(False)
+ self.notebook.set_show_tabs(False)
+
+ vbox = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)
+ vbox.pack_start(self.toolbar, False, False,0)
+ vbox.pack_start(self.toolbarpitchrate, False, False,0)
+ vbox.pack_start(self.toolbarface, False, False,0)
+
+ self.speaking = Speaking() # Pagina inicial en el notebook
+ self.notebook.append_page(self.speaking, Gtk.Label(""))
+ vbox.pack_start(self.notebook, True, True,0)
+
+ self.dinamic_toolbars = [self.toolbarpitchrate, self.toolbarface]
+
+ self.add(vbox)
+
+ self.show_all()
+
+ map(self.hide_widgets, self.dinamic_toolbars)
+
+ xid = self.speaking.face.get_property('window').get_xid()
+ self.sound = Sound(xid)
+
+ self.sound.set_pitch(100)
+ self.sound.set_rate(100)
+ self.sound.set_voice('es')
+
+ self.connect("motion_notify_event", self.mouse_moved)
+
+ self.toolbar.connect("switch-voice", self.switch_voice)
+ #self.toolbar.connect("switch-desktop", # cambiar entre speaking, chat y robot)
+ self.toolbar.connect("switch-toolbar", self.switch_toolbar)
+
+ self.toolbarpitchrate.connect("pitch", self.switch_pitch)
+ self.toolbarpitchrate.connect("rate", self.switch_rate)
+
+ self.toolbarface.connect("number-eyes", self.number_eyes)
+ self.toolbarface.connect("switch-type-mouth", self.switch_type_mouth)
+ self.toolbarface.connect("switch-type-eyes", self.switch_type_eyes)
+
+ self.speaking.connect("speak", self.speak)
+
+ self.connect("delete_event", self.delete_event)
+ self.connect("destroy", self.salir)
+
+ def speak(self, widget, value):
+ """Cuando el usuario ingresa texto para hablar."""
+
+ self.sound.speak(value)
+
+ def switch_pitch(self, widget, value):
+ """cuando cambia pitch."""
+
+ self.sound.set_pitch(value)
+ self.sound.speak("Campo Modificado.")
+
+ def switch_rate(self, widget, value):
+ """Cuando cambia rate."""
+
+ self.sound.set_rate(value)
+ self.sound.speak("Velocidad Modificada.")
+
+ def number_eyes(self, widget, value):
+ """Recibe la cantidad de ojos que deben dibujarse."""
+
+ self.speaking.face.set_number_eyes(value)
+ self.sound.speak("Ojos Modificados.")
+
+ def switch_type_mouth(self, widget, value):
+ """Recibe el tipo de dibujo para la boca."""
+
+ self.speaking.face.set_type_mouth(value)
+ self.sound.speak("Boca Modificada.")
+
+ def switch_type_eyes(self, widget, value):
+ """Recibe el tipo de ojos a dibujar."""
+
+ self.speaking.face.set_type_eyes(value)
+ self.sound.speak("Ojos Modificados.")
+
+ def switch_toolbar(self, widget, value):
+ """Para que se muestre la toolbar pitch-rate o face"""
+
+ if value == "pitch-rate":
+ if self.toolbarpitchrate.get_visible():
+ self.toolbarpitchrate.hide()
+ else:
+ map(self.hide_widgets, self.dinamic_toolbars)
+ self.toolbarpitchrate.show()
+
+ elif value == "face":
+ if self.toolbarface.get_visible():
+ self.toolbarface.hide()
+ else:
+ map(self.hide_widgets, self.dinamic_toolbars)
+ self.toolbarface.show()
+
+ def hide_widgets(self, objeto):
+ """Esta funcion es llamada desde self.switch_toolbar()"""
+
+ if objeto.get_visible(): objeto.hide()
+
+ def switch_voice(self, widget, value):
+ """Cuando cambia el lenguaje."""
+
+ self.sound.set_voice(value)
+ self.sound.speak("Voz Modificada.")
+
+ def delete_event(self, widget = None, event = None, data = None):
+ self.salir()
+ return False
+
+ def salir(self, widget = None, senial = None):
+ sys.exit(0)
+
+ def mouse_moved(self, widget, event):
+ """Le dice a la cara hacia donde mirar."""
+
+ pos = (event.x, event.y)
+ self.speaking.look_at(pos)
+
+if __name__=="__main__":
+ Speak()
+ Gtk.main()
+ \ No newline at end of file
diff --git a/Widgets.py b/Widgets.py
new file mode 100644
index 0000000..fb87cbf
--- /dev/null
+++ b/Widgets.py
@@ -0,0 +1,423 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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 commands
+
+import gi
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import Pango
+from gi.repository import GdkPixbuf
+from gi.repository import GObject
+
+from face import Face
+
+BASE = os.path.dirname(__file__)
+
+PITCH_MAX = 200
+RATE_MAX = 200
+
+def get_voices():
+ """Devuelve un diccionario del tipo: {"portugal": "pt-pt", . . .}"""
+
+ ret = commands.getoutput('espeak --voices')
+
+ voces = []
+ for linea in ret .split("\n"):
+ voz = linea.split(" ")
+
+ vv = []
+ for valor in voz:
+ if valor: vv.append(valor)
+
+ voces.append(vv)
+
+ voces = voces[1:]
+
+ voices = {}
+ for voz in voces:
+ if voz[2] != "M":
+ voices[voz[2]] = voz[1]
+ else:
+ voices[voz[3]] = voz[1]
+
+ return voices
+
+def get_separador(draw = False, ancho = 0, expand = False):
+ """ Devuelve un separador generico."""
+
+ separador = Gtk.SeparatorToolItem()
+ separador.props.draw = draw
+ separador.set_size_request(ancho, -1)
+ separador.set_expand(expand)
+ return separador
+
+def get_boton(archivo, flip = False, color = Gdk.Color(65000, 65000, 65000)):
+ """ Devuelve un toolbarbutton generico."""
+
+ boton = Gtk.ToolButton()
+ imagen = Gtk.Image()
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(archivo, 32, 32)
+ if flip: pixbuf = pixbuf.flip(True)
+ imagen.set_from_pixbuf(pixbuf)
+ imagen.modify_bg(0, color)
+ boton.set_icon_widget(imagen)
+ imagen.show()
+ boton.show()
+ return boton
+
+class Speaking(Gtk.Box):
+ """Cara con Entrada de texto."""
+
+ __gsignals__ = {"speak":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING, ))}
+
+ def __init__(self):
+
+ Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
+
+ self.entrycombo = None
+ self.entry = None
+ self.face = None
+
+ self.entrycombo = Gtk.ComboBoxText.new_with_entry()
+
+ self.entry = self.entrycombo.get_child()
+ self.entry.can_activate_accel(True)
+
+ font = Pango.FontDescription('sans bold 24')
+ self.entry.modify_font(font)
+
+ self.face = Face()
+
+ self.pack_start(self.face, True, True, 0)
+ self.pack_start(self.entrycombo, False, True, 0)
+
+ self.show_all()
+
+ self.entrycombo.connect("changed", self._combo_changed_cb)
+
+ self.entry.connect('activate', self._entry_activate_cb)
+ self.entry.connect("key-press-event", self._entry_key_press_cb)
+ self.entry.connect("move-cursor", self._cursor_moved_cb)
+ self.entry.connect("changed", self._cursor_moved_cb)
+
+ def _cursor_moved_cb(self, entry):
+ """Mira lo que el usuario escribe."""
+
+ index = entry.get_property('cursor_position')
+ layout = entry.get_layout()
+ pos = layout.get_cursor_pos(index)
+ x = pos[0].x / Pango.SCALE - entry.get_property('scroll_offset')
+ y = entry.get_allocation().y
+ self.face.look_at(pos=(x, y))
+
+ def _combo_changed_cb(self, combo):
+ if not self.entry.is_focus():
+ self.entry.grab_focus()
+ self.entry.select_region(0, -1)
+
+ def look_at(self, pos):
+ """Le dice a la cara hacia donde mirar."""
+
+ self.face.look_at(pos)
+
+ def _entry_activate_cb(self, entry):
+ """Cuando el usuario presiona enter en
+ la entrada de texto."""
+
+ text = entry.get_text()
+
+ if text:
+ self.emit('speak', text)
+
+ history = self.entrycombo.get_model()
+ if len(history) == 0 or history[-1][0] != text:
+ self.entrycombo.append_text(text)
+ while len(history) > 20:
+ self.entrycombo.remove_text(0)
+ self.entrycombo.set_active(len(history) - 1)
+ entry.select_region(0, -1)
+
+ def _entry_key_press_cb(self, combo, event):
+ """Para navegar el historial de entradas
+ en la entrada de texto, con el teclado."""
+
+ keyname = Gdk.keyval_name(event.keyval)
+ index = self.entrycombo.get_active()
+
+ if keyname == "Up":
+ if index > 0: index -= 1
+ self.entrycombo.set_active(index)
+ self.entry.select_region(0, -1)
+ return True
+
+ elif keyname == "Down":
+ if index < len(self.entrycombo.get_model()) - 1: index += 1
+ self.entrycombo.set_active(index)
+ self.entry.select_region(0, -1)
+ return True
+
+ return False
+
+
+class Toolbar(Gtk.Toolbar):
+ """Toolbar Principal."""
+
+ __gsignals__ = {"switch-voice":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING, )),
+ "switch-desktop":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING, )),
+ "switch-toolbar":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING, ))}
+
+ def __init__(self):
+
+ Gtk.Toolbar.__init__(self)
+ self.modify_bg(0, Gdk.Color(0, 0, 0))
+
+ self.voices = get_voices()
+
+ self.insert(get_separador(draw = False, ancho = 32, expand = False), -1)
+
+ archivo = os.path.join(BASE, "icons", "face.svg")
+ boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0))
+ self.insert(boton, -1)
+
+ archivo = os.path.join(BASE, "icons", "mode-type.svg")
+ boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0))
+ self.insert(boton, -1)
+ #boton.connect("clicked", self.emit_switch_desktop, "speaking")
+
+ archivo = os.path.join(BASE, "icons", "mode-robot.svg")
+ boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0))
+ self.insert(boton, -1)
+ #boton.connect("clicked", self.emit_switch_desktop, "robot")
+
+ archivo = os.path.join(BASE, "icons", "mode-chat.svg")
+ boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0))
+ self.insert(boton, -1)
+ #boton.connect("clicked", self.emit_switch_desktop, "chat")
+
+ item = Gtk.ToolItem()
+ combo = Gtk.ComboBoxText()
+ voces = self.voices.keys()
+ for key in sorted(voces):
+ combo.append_text(key)
+ combo.set_active(0)
+ combo.connect('changed', self.emit_switch_voice)
+ item.add(combo)
+ self.insert(item, -1)
+
+ archivo = os.path.join(BASE, "icons", "voice.svg")
+ boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0))
+ self.insert(boton, -1)
+ boton.connect("clicked", self.emit_switch_toolbar, "pitch-rate")
+
+ archivo = os.path.join(BASE, "icons", "face.svg")
+ boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0))
+ self.insert(boton, -1)
+ boton.connect("clicked", self.emit_switch_toolbar, "face")
+
+ self.show_all()
+
+ def emit_switch_desktop(self, widget, value):
+ """Cambiar entre chat, speaking y robot."""
+
+ self.emit("switch-desktop", value)
+
+ def emit_switch_toolbar(self, widget, value):
+ """Para que se muestre la toolbar pitch-rate o face."""
+
+ self.emit("switch-toolbar", value)
+
+ def emit_switch_voice(self, widget):
+ """Cuando cambia el lenguaje."""
+
+ self.emit("switch-voice", self.voices[widget.get_active_text()])
+
+class ToolbarPitchRate(Gtk.Toolbar):
+ """Toolbar con opciones pitch y rate."""
+
+ __gsignals__ = {"pitch":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_INT, )),
+ "rate":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_INT, ))}
+
+ def __init__(self):
+
+ Gtk.Toolbar.__init__(self)
+ self.modify_bg(0, Gdk.Color(0, 0, 0))
+
+ self.pitch = 0
+ self.rate = 0
+
+ self.insert(get_separador(draw = False, ancho = 32, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ label = Gtk.Label("Pitch:")
+ label.modify_fg(0, Gdk.Color(65000, 65000, 65000))
+ item.add(label)
+ self.insert(item, -1)
+
+ self.insert(get_separador(draw = False, ancho = 5, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ pitchadj = Gtk.Adjustment(self.pitch, 0,
+ PITCH_MAX, 1, PITCH_MAX / 10, 0)
+ pitchbar = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL)
+ pitchbar.set_adjustment(pitchadj)
+ pitchbar = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL)
+ pitchbar.set_adjustment(pitchadj)
+ pitchbar.set_draw_value(False)
+ pitchbar.set_size_request(240, 15)
+ item.add(pitchbar)
+ self.insert(item, -1)
+
+ self.insert(get_separador(draw = False, ancho = 5, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ label = Gtk.Label("Rate:")
+ label.modify_fg(0, Gdk.Color(65000, 65000, 65000))
+ item.add(label)
+ self.insert(item, -1)
+
+ self.insert(get_separador(draw = False, ancho = 5, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ rateadj = Gtk.Adjustment(self.rate, 0,
+ RATE_MAX, 1, RATE_MAX / 10, 0)
+ ratebar = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL)
+ ratebar.set_adjustment(rateadj)
+ ratebar.set_draw_value(False)
+ ratebar.set_size_request(240, 15)
+ item.add(ratebar)
+ self.insert(item, -1)
+
+ self.show_all()
+
+ pitchadj.connect("value_changed", self.pitch_adjusted)
+ rateadj.connect("value_changed", self.rate_adjusted)
+
+ def pitch_adjusted(self, widget):
+ """Cuando cambia pitch."""
+
+ self.pitch = int(widget.get_value())
+ self.emit('pitch', self.pitch)
+ #self.face.say_notification(_("pitch adjusted"))
+
+ def rate_adjusted(self, widget):
+ """Cuando cambia rate."""
+
+ self.rate = int(widget.get_value())
+ self.emit('rate', self.rate)
+ #self.face.say_notification(_("rate adjusted"))
+
+class ToolbarFace(Gtk.Toolbar):
+ """Toolbar con opciones para la cara."""
+
+ __gsignals__ = {"number-eyes":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_INT, )),
+ "switch-type-mouth":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING, )),
+ "switch-type-eyes":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING, ))}
+
+ def __init__(self):
+
+ Gtk.Toolbar.__init__(self)
+ self.modify_bg(0, Gdk.Color(0, 0, 0))
+
+ self.insert(get_separador(draw = False, ancho = 32, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ label = Gtk.Label("Mouth:")
+ label.modify_fg(0, Gdk.Color(65000, 65000, 65000))
+ item.add(label)
+ self.insert(item, -1)
+
+ self.insert(get_separador(draw = False, ancho = 5, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ combo = Gtk.ComboBoxText()
+ combo.append_text("Simple")
+ combo.append_text("Waveform")
+ combo.append_text("Frequency")
+ combo.set_active(0)
+ combo.connect('changed', self.emit_switch_type_mouth)
+ item.add(combo)
+ self.insert(item, -1)
+
+ self.insert(get_separador(draw = False, ancho = 5, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ label = Gtk.Label("Eyes:")
+ label.modify_fg(0, Gdk.Color(65000, 65000, 65000))
+ item.add(label)
+ self.insert(item, -1)
+
+ self.insert(get_separador(draw = False, ancho = 5, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ combo = Gtk.ComboBoxText()
+ combo.append_text("Round")
+ combo.append_text("Glasses")
+ combo.set_active(0)
+ combo.connect('changed', self.emit_switch_type_eyes)
+ item.add(combo)
+ self.insert(item, -1)
+
+ self.insert(get_separador(draw = False, ancho = 5, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ label = Gtk.Label("Eyes number:")
+ label.modify_fg(0, Gdk.Color(65000, 65000, 65000))
+ item.add(label)
+ self.insert(item, -1)
+
+ self.insert(get_separador(draw = False, ancho = 5, expand = False), -1)
+
+ item = Gtk.ToolItem()
+ numeyesadj = Gtk.Adjustment(2, 1, 5, 1, 1, 0)
+ numeyesbar = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL)
+ numeyesbar.set_adjustment(numeyesadj)
+ numeyesbar.set_draw_value(False)
+ numeyesbar.set_size_request(240, 15)
+ item.add(numeyesbar)
+ self.insert(item, -1)
+
+ self.show_all()
+
+ numeyesadj.connect("value_changed", self.eyes_changed_number)
+
+ def emit_switch_type_mouth(self, widget):
+ """Cuando cambia el tipo de boca."""
+
+ self.emit("switch-type-mouth", widget.get_active_text())
+
+ def emit_switch_type_eyes(self, widget):
+ """Cuando cambia el tipo de ojos."""
+
+ self.emit("switch-type-eyes", widget.get_active_text())
+
+ def eyes_changed_number(self, widget):
+ """Cuando se cambia la cantidad de ojos."""
+
+ self.emit('number-eyes', int(widget.get_value()))
+ #self.face.say_notification(_("eyes changed"))
+ \ No newline at end of file
diff --git a/activity/activity-speak.svg b/activity/activity-speak.svg
new file mode 100644
index 0000000..82d12a5
--- /dev/null
+++ b/activity/activity-speak.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#000000">
+]>
+<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50">
+ <ellipse fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" cx="13" cy="17" rx="10" ry="10"/>
+ <ellipse fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" cx="37" cy="17" rx="10" ry="10"/>
+ <circle fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" cx="17" cy="15" r="2"/>
+ <circle fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" cx="41" cy="15" r="2"/>
+ <ellipse fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" cx="26" cy="39" rx="20" ry="4"/>
+</svg>
+
diff --git a/activity/activity.info b/activity/activity.info
new file mode 100644
index 0000000..d39ea5f
--- /dev/null
+++ b/activity/activity.info
@@ -0,0 +1,23 @@
+[Activity]
+implement = speak
+name = Speak
+summary = An animated face that speaks whatever you type
+description = Speak is a talking face for the XO laptop. Anything you type will
+ be spoken aloud using the XO's speech synthesizer, espeak. You can
+ adjust the accent, rate and pitch of the voice as well as the shape
+ of the eyes and mouth.
+ This is a great way to experiment with the speech synthesizer,
+ learn to type or just have fun making a funny face for your XO.
+homepage = http://wiki.sugarlabs.org/go/Activities/Speak
+license = GPLv3+
+
+version = 41
+stability = stable
+
+icon = activity-speak
+exec = sugar-activity Speak.Speak
+
+# original activity.info options
+activity_version = 41
+bundle_id = vu.lux.olpc.Speak
+
diff --git a/face.py b/face.py
new file mode 100644
index 0000000..0a32781
--- /dev/null
+++ b/face.py
@@ -0,0 +1,438 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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 math
+
+import gi
+from gi.repository import Gtk
+from gi.repository import Gdk
+
+class Face(Gtk.EventBox):
+ """La cara."""
+
+ def __init__(self):
+
+ Gtk.EventBox.__init__(self)
+
+ self.eyes_box = None
+ self.mouth = None
+
+ self.type_eyes = 'Round'
+ self.type_mouth = 'Simple'
+
+ self.eyes_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ mouthbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ box.pack_start(self.eyes_box, True, True, 0)
+ box.pack_start(mouthbox, True, True, 0)
+
+ self.mouth = Mouth()
+ mouthbox.pack_start(self.mouth, True, True, 0)
+
+ self.add(box)
+
+ self.set_number_eyes(2)
+
+ self.show_all()
+
+ def set_type_mouth(self, tipo):
+ """Actualizar el tipo de boca.
+ tipo puede ser 'Simple' o 'Waveform' o 'Frequency'"""
+
+ self.type_mouth = tipo
+ self.mouth.tipo = self.type_mouth
+
+ def set_type_eyes(self, tipo):
+ """Actualizar el tipo de ojos.
+ tipo puede ser 'Round' o 'Glasses'"""
+
+ self.type_eyes = tipo
+ for child in self.eyes_box.get_children():
+ child.set_type(self.type_eyes)
+
+ def set_number_eyes(self, number):
+ """Actualizar la cantidad de ojos."""
+
+ while len(self.eyes_box.get_children()) < number:
+ self.add_eye()
+
+ while len(self.eyes_box.get_children()) > number:
+ self.del_eye()
+
+ self.set_type_eyes(self.type_eyes)
+
+ def add_eye(self):
+ """Agrega un ojo."""
+
+ self.eyes_box.pack_start(Eye(), True, True, 0)
+
+ def del_eye(self):
+ """Quita un ojo."""
+
+ childs = self.eyes_box.get_children()
+ if childs:
+ eye = childs[-1]
+ self.eyes_box.remove(eye)
+ eye.destroy()
+
+ def look_at(self, pos):
+ """Mirar hacia pos"""
+
+ x, y = pos
+ map(lambda e, x=x, y=y: e.look_at(x, y), self.eyes_box.get_children())
+
+
+class Eye(Gtk.DrawingArea):
+ """El ojo.
+ self.tipo puede ser 'Round' o 'Glasses'."""
+
+ def __init__(self):
+
+ Gtk.DrawingArea.__init__(self)
+
+ self.x, self.y = 0, 0
+ self.fill_color = (49000, 52000, 18000)
+ self.tipo = 'Round'
+
+ self.show_all()
+
+ def set_type(self, tipo):
+ """Cambia el tipo de ojo.
+ tipo puede ser 'Round' o 'Glasses'"""
+
+ self.tipo = tipo
+ self.queue_draw()
+
+ def look_at(self, x, y):
+ """ Look. . ."""
+
+ self.x = x
+ self.y = y
+ self.queue_draw()
+
+ def computePupil(self):
+ """La Pupila."""
+
+ rect = self.get_allocation()
+
+ if self.x is None or self.y is None:
+ if rect.x + rect.width / 2 < rect.width / 2:
+ cx = rect.width * 0.6
+ else:
+ cx = rect.width * 0.4
+ return cx, rect.height * 0.6
+
+ EYE_X, EYE_Y = self.translate_coordinates(
+ self.get_toplevel(), rect.width / 2, rect.height / 2)
+ EYE_HWIDTH = rect.width
+ EYE_HHEIGHT = rect.height
+ BALL_DIST = EYE_HWIDTH / 4
+
+ dx = self.x - EYE_X
+ dy = self.y - EYE_Y
+
+ if dx or dy:
+ angle = math.atan2(dy, dx)
+ cosa = math.cos(angle)
+ sina = math.sin(angle)
+ h = math.hypot(EYE_HHEIGHT * cosa, EYE_HWIDTH * sina)
+ x = (EYE_HWIDTH * EYE_HHEIGHT) * cosa / h
+ y = (EYE_HWIDTH * EYE_HHEIGHT) * sina / h
+ dist = BALL_DIST * math.hypot(x, y)
+
+ if dist < math.hypot(dx, dy):
+ dx = dist * cosa
+ dy = dist * sina
+
+ return rect.width / 2 + dx, rect.height / 2 + dy
+
+ def do_draw(self, context):
+ rect = self.get_allocation()
+ eyeSize = min(rect.width, rect.height)
+
+ outlineWidth = eyeSize / 20.0
+ pupilSize = eyeSize / 10.0
+ pupilX, pupilY = self.computePupil()
+ dX = pupilX - rect.width / 2.
+ dY = pupilY - rect.height / 2.
+ distance = math.sqrt(dX * dX + dY * dY)
+ limit = eyeSize / 2 - outlineWidth * 2 - pupilSize
+ if distance > limit:
+ pupilX = rect.width / 2 + dX * limit / distance
+ pupilY = rect.height / 2 + dY * limit / distance
+
+ r,g,b = self.fill_color
+ context.set_source_rgba(r,g,b,1)
+ context.rectangle(0, 0, rect.width, rect.height)
+ context.fill()
+
+ if self.tipo == 'Round':
+ # eye ball
+ context.set_source_rgb(1, 1, 1)
+ context.arc(rect.width / 2,
+ rect.height / 2,
+ eyeSize / 2 - outlineWidth / 2,
+ 0, 360)
+ context.fill()
+
+ # outline
+ context.set_source_rgb(0, 0, 0)
+ context.set_line_width(outlineWidth)
+ context.arc(rect.width / 2,
+ rect.height / 2,
+ eyeSize / 2 - outlineWidth / 2,
+ 0, 360)
+ context.stroke()
+
+ elif self.tipo == 'Glasses':
+ def roundrect(x1, y1, x2, y2):
+ context.move_to(x1, (y1 + y2) / 2.)
+ context.curve_to(x1, y1, x1, y1, (x1 + x2) / 2., y1)
+ context.curve_to(x2, y1, x2, y1, x2, (y1 + y2) / 2.)
+ context.curve_to(x2, y2, x2, y2, (x1 + x2) / 2., y2)
+ context.curve_to(x1, y2, x1, y2, x1, (y1 + y2) / 2.)
+
+ # eye ball
+ context.set_source_rgb(1, 1, 1)
+ roundrect(outlineWidth,
+ outlineWidth,
+ rect.width - outlineWidth,
+ rect.height - outlineWidth)
+ context.fill()
+
+ # outline
+ context.set_source_rgb(0, 0, 0)
+ context.set_line_width(outlineWidth)
+ roundrect(outlineWidth,
+ outlineWidth,
+ rect.width - outlineWidth,
+ rect.height - outlineWidth)
+ context.stroke()
+
+ # pupil
+ context.arc(pupilX, pupilY, pupilSize, 0, 360)
+ context.set_source_rgb(0, 0, 0)
+ context.fill()
+
+ return True
+
+class Mouth(Gtk.DrawingArea):
+ """La boca."""
+
+ def __init__(self):
+
+ Gtk.DrawingArea.__init__(self)
+
+ self.fill_color = (49000, 52000, 18000)
+ self.volume = 0
+ self.tipo = "Simple"
+
+ self.show_all()
+
+ def do_draw(self, context):
+
+ rect = self.get_allocation()
+
+ #self.processBuffer()
+
+ # background
+ r,g,b = self.fill_color
+ context.set_source_rgba(r,g,b,1)
+ context.paint()
+
+ # Draw the mouth
+ volume = self.volume / 30000.
+ mouthH = volume * rect.height
+ mouthW = volume ** 2 * (rect.width / 2.) + rect.width / 2.
+ # T
+ # L R
+ # B
+ Lx, Ly = rect.width / 2 - mouthW / 2, rect.height / 2
+ Tx, Ty = rect.width / 2, rect.height / 2 - mouthH / 2
+ Rx, Ry = rect.width / 2 + mouthW / 2, rect.height / 2
+ Bx, By = rect.width / 2, rect.height / 2 + mouthH / 2
+ context.set_line_width(min(rect.height / 10.0, 10))
+ context.move_to(Lx, Ly)
+ context.curve_to(Tx, Ty, Tx, Ty, Rx, Ry)
+ context.curve_to(Bx, By, Bx, By, Lx, Ly)
+ context.set_source_rgb(0, 0, 0)
+ context.close_path()
+ context.stroke()
+
+ return True
+
+ '''
+ def _new_buffer(self, obj, buf):
+ if len(buf) < 28:
+ self.newest_buffer = []
+ else:
+ self.newest_buffer = list(unpack(str(int(len(buf)) / 2) + 'h',
+ buf))
+ self.main_buffers += self.newest_buffer
+ if(len(self.main_buffers) > self.buffer_size):
+ del self.main_buffers[0:(len(self.main_buffers) - \
+ self.buffer_size)]
+
+ self.queue_draw()
+ return True
+
+ def processBuffer(self):
+ if len(self.main_buffers) == 0 or len(self.newest_buffer) == 0:
+ self.volume = 0
+ else:
+ self.volume = numpy.core.max(self.main_buffers) # -\
+ # numpy.core.min(self.main_buffers)
+
+
+
+
+class WaveformMouth(Mouth):
+ def __init__(self, audioSource, fill_color):
+
+ Mouth.__init__(self, audioSource, fill_color)
+
+ self.buffer_size = 100
+ self.peaks = []
+
+ self.stop = False
+
+ self.y_mag_bias_multiplier = 1
+ self.y_mag = 0.7
+
+ self.show_all()
+
+ def draw_wave(self, context):
+ rect = self.get_allocation()
+ self.param1 = rect.height / 65536.0
+ self.param2 = rect.height / 2.0
+
+ # background
+ context.set_source_rgba(*self.fill_color.get_rgba())
+ context.paint()
+
+ # Draw the waveform
+ context.set_line_width(min(rect.height / 10.0, 10))
+ count = 0
+ buflen = float(len(self.main_buffers))
+ for value in self.main_buffers:
+ peak = float(self.param1 * value * self.y_mag) +\
+ self.y_mag_bias_multiplier * self.param2
+
+ if peak >= rect.height:
+ peak = rect.height
+ if peak <= 0:
+ peak = 0
+
+ x = count / buflen * rect.width
+ context.line_to(x, rect.height - peak)
+
+ count += 1
+ context.set_source_rgb(0, 0, 0)
+ context.stroke()
+
+ return True
+
+
+class FFTMouth(Mouth):
+ def __init__(self, audioSource, fill_color):
+
+ Mouth.__init__(self, audioSource, fill_color)
+
+ self.peaks = []
+
+ self.y_mag = 1.7
+ self.freq_range = 70
+ self.draw_interval = 1
+ self.num_of_points = 105
+
+ self.stop = False
+
+ #constant to multiply with self.param2 while scaling values
+ self.y_mag_bias_multiplier = 1
+
+ self.fftx = []
+
+ self.scaleX = "10"
+ self.scaleY = "10"
+
+ self.show_all()
+ self.connect('draw', self.draw_fftmouth)
+
+ def newprocessBuffer(self, rect):
+ self.param1 = rect.height / 65536.0
+ self.param2 = rect.height / 2.0
+
+ if(self.stop == False):
+
+ Fs = 48000
+ nfft = 65536
+ self.newest_buffer = self.newest_buffer[0:256]
+ self.fftx = fft(self.newest_buffer, 256, -1)
+
+ self.fftx = self.fftx[0:self.freq_range * 2]
+ self.draw_interval = rect.width / (self.freq_range * 2.)
+
+ NumUniquePts = ceil((nfft + 1) / 2)
+ self.buffers = abs(self.fftx) * 0.02
+ self.y_mag_bias_multiplier = 0.1
+ self.scaleX = "hz"
+ self.scaleY = ""
+
+ if(len(self.buffers) == 0):
+ return False
+
+ # Scaling the values
+ val = []
+ for i in self.buffers:
+ temp_val_float = float(self.param1 * i * self.y_mag) +\
+ self.y_mag_bias_multiplier * self.param2
+
+ if(temp_val_float >= rect.height):
+ temp_val_float = rect.height - 25
+ if(temp_val_float <= 0):
+ temp_val_float = 25
+ val.append(temp_val_float)
+
+ self.peaks = val
+
+ def draw_fftmouth(self, widget, context):
+ rect = widget.get_allocation()
+
+ self.newprocessBuffer(rect)
+
+ # background
+ context.set_source_rgba(*self.fill_color.get_rgba())
+ context.paint()
+
+ # Draw the waveform
+ context.set_line_width(min(rect.height / 10.0, 10))
+ context.set_source_rgb(0, 0, 0)
+ count = 0
+ for peak in self.peaks:
+ context.line_to(rect.width / 2 + count,
+ rect.height / 2 - peak)
+ count += self.draw_interval
+ context.stroke()
+ count = 0
+ for peak in self.peaks:
+ context.line_to(rect.width / 2 - count,
+ rect.height / 2 - peak)
+ count += self.draw_interval
+ context.stroke()
+
+ return True''' \ No newline at end of file
diff --git a/icons/edit-description.svg b/icons/edit-description.svg
new file mode 100644
index 0000000..cf59acc
--- /dev/null
+++ b/icons/edit-description.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"
+[
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#A0A0A0">
+]>
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="55px" height="55px" viewBox="0px, 0px, 55px, 55px">
+ <g id="edit-description">
+ <path d="M19.849,50.763 C16.351,50.757 12.578,50.766 12.578,50.766 C12.578,50.766 12.016,50.75 11.391,50.359 C10.766,49.969 10.75,49.109 10.75,49.109 C10.75,49.109 10.767,46.339 10.768,43.613 C9.035,43.557 7.983,43.622 6.656,42.516 C6.656,42.516 5.985,41.844 6.078,41.062 C6.172,40.281 6.953,39.691 6.953,39.691 C8.243,39.106 9.485,40.004 10.769,40.088 L10.768,43.613 C13.038,43.782 16.125,43.625 17.297,42.531 C17.297,42.531 17.864,42.023 17.918,41.439 C17.972,40.855 17.719,40.359 17.719,40.359 C17.719,40.359 17.348,39.868 17.094,39.734 C16.84,39.6 16.337,39.441 15.594,39.656 C14.851,39.871 14.172,40.125 12.782,40.172 C11.391,40.219 10.769,40.088 10.769,40.088 L10.771,30.37 C9.037,30.314 7.984,30.435 6.656,29.328 C6.656,29.328 6.091,28.813 6.11,27.906 C6.129,26.999 6.746,26.4 7.604,26.277 C8.462,26.155 9.435,26.663 10.771,26.845 L10.771,30.37 C12.22,30.428 14.936,30.415 16.299,29.835 C17.569,29.296 17.646,29.017 17.867,28.54 C18.089,28.063 17.663,27.058 17.441,26.836 C17.22,26.615 16.64,26.172 15.959,26.308 C15.277,26.444 14.817,26.751 13.215,26.887 C11.614,27.024 10.771,26.845 10.771,26.845 L10.773,16.842 C9.039,16.786 7.969,16.811 6.641,15.703 C6.641,15.703 5.969,15.094 6.078,14.281 C6.188,13.469 6.953,12.92 6.953,12.92 C8.244,12.334 9.488,13.232 10.774,13.317 L10.773,16.842 C12.296,16.891 16.078,16.443 16.674,16.17 C17.271,15.897 17.611,15.727 17.85,15.079 C18.089,14.432 17.884,13.819 17.646,13.478 C17.407,13.137 16.743,12.66 16.112,12.711 C15.482,12.762 14.664,13.12 13.386,13.29 C12.108,13.461 10.774,13.317 10.774,13.317 C10.74,10.6 10.775,6.424 10.797,5.625 C10.819,4.826 11.653,4.281 12.397,4.25 C13.141,4.219 19.849,4.25 19.849,4.25 L19.849,50.763 z" stroke="none" fill="&fill_color;"/>
+ <path d="M37.539,50.772 L33.089,50.776 L23.815,50.769 L23.815,4.25 L41.993,4.25 C45.161,4.256 47.676,6.6 48.091,9.837 C44.391,14.527 42.021,17.643 38.267,22.561 C37.445,23.637 33.901,27.981 33.237,29.13 C32.572,30.279 30.721,35.315 29.512,38.128 C29.177,38.9 29.395,39.856 30.087,40.428 C31.004,40.961 31.524,40.868 31.887,40.825 C32.25,40.781 34.33,39.263 35.526,38.479 C37.099,37.449 38.952,36.632 40.38,35.399 C41.683,34.273 46.638,27.29 47.957,25.754 L48.087,25.587 L48.087,44.656 C48.09,48.043 45.921,50.777 43.001,50.779 z" stroke="none" fill="&fill_color;"/>
+ <path d="M52.796,13.98 C52.806,13.97 52.851,13.898 52.851,13.898 C52.893,13.827 52.92,13.743 52.92,13.651 C52.92,13.624 52.925,13.594 52.92,13.568 C52.881,13.233 52.598,12.832 52.192,12.511 C51.786,12.191 51.33,12.018 50.997,12.058 C50.854,12.076 50.654,12.222 50.654,12.222 L36.078,30.999 L34.517,34.851 C34.497,34.897 34.509,34.949 34.548,34.981 C34.587,35.012 34.64,35.017 34.682,34.989 L38.122,32.776 z" stroke="none" fill="&fill_color;"/>
+ </g>
+ <defs/>
+</svg>
diff --git a/icons/face.svg b/icons/face.svg
new file mode 100644
index 0000000..6e2503f
--- /dev/null
+++ b/icons/face.svg
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="50"
+ height="50"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="face.svg">
+ <metadata
+ id="metadata18">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs16">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 25 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="50 : 25 : 1"
+ inkscape:persp3d-origin="25 : 16.666667 : 1"
+ id="perspective20" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1278"
+ inkscape:window-height="735"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="5.22"
+ inkscape:cx="-8.5249042"
+ inkscape:cy="25"
+ inkscape:window-x="0"
+ inkscape:window-y="16"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg2" />
+ <ellipse
+ fill="#FFFFFF"
+ stroke="#000000"
+ stroke-width="3.5"
+ cx="13"
+ cy="17"
+ rx="10"
+ ry="10"
+ id="ellipse4"
+ style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
+ <ellipse
+ fill="#FFFFFF"
+ stroke="#000000"
+ stroke-width="3.5"
+ cx="37"
+ cy="17"
+ rx="10"
+ ry="10"
+ id="ellipse6"
+ style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
+ <circle
+ fill="#FFFFFF"
+ stroke="#000000"
+ stroke-width="3.5"
+ cx="17"
+ cy="15"
+ r="2"
+ id="circle8" />
+ <circle
+ fill="#FFFFFF"
+ stroke="#000000"
+ stroke-width="3.5"
+ cx="41"
+ cy="15"
+ r="2"
+ id="circle10" />
+ <ellipse
+ fill="#FFFFFF"
+ stroke="#000000"
+ stroke-width="3.5"
+ cx="26"
+ cy="39"
+ rx="20"
+ ry="4"
+ id="ellipse12"
+ style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
+</svg>
diff --git a/icons/mode-chat.svg b/icons/mode-chat.svg
new file mode 100644
index 0000000..9b38013
--- /dev/null
+++ b/icons/mode-chat.svg
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="50"
+ height="50"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="mode-chat.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.1">
+ <metadata
+ id="metadata17">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs15">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 25 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="50 : 25 : 1"
+ inkscape:persp3d-origin="25 : 16.666667 : 1"
+ id="perspective19" />
+ <inkscape:perspective
+ id="perspective2475"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 27.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ </defs>
+ <sodipodi:namedview
+ inkscape:window-height="735"
+ inkscape:window-width="1278"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="2.33"
+ inkscape:cx="-48.243501"
+ inkscape:cy="20.573512"
+ inkscape:window-x="0"
+ inkscape:window-y="16"
+ inkscape:current-layer="svg2"
+ inkscape:window-maximized="0" />
+ <path
+ style="fill:#fff2f2;fill-opacity:1;stroke:#010101;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;display:inline"
+ d="m 7.7871373,45.936639 c 0.656039,1.243688 5.7975727,0.0637 7.9321037,-1.579441 2.021984,-1.545974 3.084921,-2.802618 4.170944,-2.802618 1.089871,0 12.062648,0.993223 20.138081,-6.169863 6.949967,-6.161226 9.400977,-14.8854 4.347936,-23.143191 C 39.324131,3.9891325 31.07074,2.4388395 24.292955,3.0002275 15.043002,3.7742935 2.9399533,11.944637 3.3757093,24.255214 c 0.253951,7.245135 3.229211,9.8707 4.734637,11.554862 1.502541,1.690639 4.3719877,1.690639 2.8781057,6.032755 -0.58678,1.704673 -3.6919007,3.150245 -3.2013147,4.093808 z"
+ display="inline"
+ id="path2468" />
+ <ellipse
+ style="fill:#ffffff;stroke:#010101;stroke-width:3.08741363;stroke-miterlimit:4;stroke-dasharray:none"
+ sodipodi:ry="10"
+ sodipodi:rx="10"
+ sodipodi:cy="12"
+ sodipodi:cx="12"
+ cx="12"
+ cy="12"
+ rx="10"
+ ry="10"
+ id="ellipse4"
+ transform="matrix(0.6477914,0,0,0.6477914,10.158787,8.6181926)" />
+ <ellipse
+ style="fill:#ffffff;stroke:#010101;stroke-width:3.08741363;stroke-miterlimit:4;stroke-dasharray:none"
+ sodipodi:ry="10"
+ sodipodi:rx="10"
+ sodipodi:cy="12"
+ sodipodi:cx="36"
+ cx="36"
+ cy="12"
+ rx="10"
+ ry="10"
+ id="ellipse6"
+ transform="matrix(0.6477914,0,0,0.6477914,10.158787,8.6181926)" />
+ <circle
+ style="fill:#ffffff;stroke:#010101;stroke-width:3.39615512;stroke-miterlimit:4;stroke-dasharray:none"
+ sodipodi:ry="2"
+ sodipodi:rx="2"
+ sodipodi:cy="10"
+ sodipodi:cx="16"
+ cx="16"
+ cy="10"
+ r="2"
+ id="circle8"
+ transform="matrix(0.6477914,0,0,0.6477914,9.7794379,8.6181926)" />
+ <circle
+ style="fill:#ffffff;stroke:#010101;stroke-width:3.39615512;stroke-miterlimit:4;stroke-dasharray:none"
+ sodipodi:ry="2"
+ sodipodi:rx="2"
+ sodipodi:cy="10"
+ sodipodi:cx="40"
+ cx="40"
+ cy="10"
+ r="2"
+ id="circle10"
+ transform="matrix(0.6477914,0,0,0.6477914,9.8553077,8.6940624)" />
+ <path
+ sodipodi:open="true"
+ sodipodi:end="2.7133165"
+ sodipodi:start="0.37458313"
+ transform="matrix(0.6789713,0,0,0.9654291,13.305764,57.674017)"
+ d="m 42.271638,-34.758097 a 27.896996,19.957081 0 0 1 -51.3400478,0.986253"
+ sodipodi:ry="19.957081"
+ sodipodi:rx="27.896996"
+ sodipodi:cy="-42.060085"
+ sodipodi:cx="16.309013"
+ id="path3304"
+ style="fill:none;stroke:#010101;stroke-width:2.47026812;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+</svg>
diff --git a/icons/mode-robot.svg b/icons/mode-robot.svg
new file mode 100644
index 0000000..420e922
--- /dev/null
+++ b/icons/mode-robot.svg
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ width="55"
+ height="55"
+ viewBox="0 0 50 50"
+ id="Layer_1"
+ xml:space="preserve"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="activity-x2o.svg"><metadata
+ id="metadata29"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1278"
+ inkscape:window-height="735"
+ id="namedview27"
+ showgrid="false"
+ inkscape:zoom="4.7454545"
+ inkscape:cx="-9.3773946"
+ inkscape:cy="27.5"
+ inkscape:window-x="0"
+ inkscape:window-y="16"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="Layer_1" /><defs
+ id="defs6"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective31" /></defs>
+
+<path
+ d="M 16.723374,46.806292 C 16.855649,43.252225 16.987924,39.723192 17.120199,35.827457 C 18.481703,31.220865 14.052534,27.950574 12.887395,23.658144 C 11.742257,15.774242 14.12395,9.9649462 24.924432,9.1078802 C 31.587506,10.578246 39.012306,11.18182 35.770993,23.922695 C 33.371992,27.717361 29.197982,29.558066 29.421786,35.166081 C 29.131102,39.169447 28.433422,43.376312 27.437659,47.732218 C 24.508943,48.617185 22.012584,50.3051 16.723374,46.806292 z"
+ id="path3190"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 16.975898,44.822165 C 17.216398,44.902332 17.456899,44.982498 17.697399,45.062665 C 18.795738,45.428778 19.981952,45.513553 21.064402,45.784166 C 21.980159,46.013105 23.234291,45.784166 24.190905,45.784166 C 25.361118,45.784166 26.304322,45.626976 27.437659,45.303165 C 27.697639,45.228885 27.902688,45.22566 28.159159,45.182915"
+ id="path3198"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 17.133479,42.262299 C 17.446278,42.340499 18.01959,42.784415 18.493954,42.942537 C 19.169057,43.167571 19.817619,43.326596 20.492151,43.495229 C 21.270392,43.689789 22.112653,43.581905 22.872982,43.835348 C 23.350903,43.994655 24.015112,43.920378 24.531061,43.920378 C 25.193603,43.920378 25.919587,43.489619 26.529258,43.367685 C 27.078211,43.257894 27.52687,42.992633 28.059792,42.814992 C 28.4145,42.696756 28.384141,42.500073 28.655,42.432359"
+ id="path3208"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 17.04845,39.243746 C 17.281316,39.156421 18.824413,39.738142 19.174191,39.796439 C 21.305683,40.151687 23.560017,40.051528 25.721476,40.051528 C 26.543207,40.051528 28.142494,40.244009 28.867574,39.881468"
+ id="path3212"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 16.835875,36.480281 C 17.629072,36.480281 19.916025,37.250319 21.257418,37.585667 C 22.381947,37.866799 23.7113,37.415608 24.828665,37.415608 C 25.529754,37.415608 26.609622,37.21247 27.294525,37.075489 C 27.935027,36.947389 28.643047,36.97219 29.250208,36.8204"
+ id="path3214"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 18.536469,13.607299 C 18.431764,14.02612 17.651319,13.812211 17.346053,14.117477 C 15.831697,15.631833 15.846313,16.88531 15.390371,18.709079"
+ id="path3224"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 30.440623,14.202506 C 31.28182,15.6045 32.362472,14.492321 33.246603,18.028842 C 33.50682,19.069712 33.181971,20.263448 32.906484,21.08991"
+ id="path3232"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 19.726884,35.885074 C 19.736112,34.55356 19.556825,33.306212 19.556825,31.973709 C 19.556825,30.810705 19.488084,29.690611 19.301736,28.572522 C 19.167181,27.765193 19.046647,26.926717 19.046647,26.106661 C 19.046647,25.440348 18.706528,24.823394 18.706528,24.150979"
+ id="path3238"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 26.274169,34.09945 C 26.274169,34.042764 26.274169,33.986078 26.274169,33.929391 C 26.274169,32.646146 27.391115,30.652024 27.719674,29.337789 C 27.77636,29.111043 27.833047,28.884297 27.889733,28.657551 C 28.108768,27.781413 28.43707,27.052701 28.82506,26.27672 C 29.050142,25.826556 29.032745,25.360951 29.165178,24.831216 C 29.283218,24.359058 29.420267,24.188534 29.420267,23.72583"
+ id="path3252"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 20.662211,24.576127 C 22.797072,25.109842 24.88686,23.067902 25.338843,21.25997 C 25.569635,20.336803 25.260119,19.624725 24.318487,19.389317 C 23.614169,19.213237 23.492585,19.692489 23.38316,20.239614 C 23.190462,21.203106 23.569982,22.268338 23.893338,23.130623 C 24.189988,23.921688 24.21253,24.294549 24.998724,24.491097 C 25.602008,24.641918 26.15078,24.498609 26.614288,24.150979"
+ id="path3258"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 13.264629,29.762937 C 10.707218,28.739973 7.3550018,30.448514 5.1017805,31.293472"
+ id="path3268"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 9.0131455,23.640801 C 7.534669,23.04941 5.5299153,22.960563 3.911365,22.960563"
+ id="path3270"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 9.8634423,17.858783 C 7.4097472,16.877305 4.8810938,14.092164 2.5508902,12.927062"
+ id="path3272"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 15.64546,9.0156967 C 15.577739,8.9886084 14.270342,4.936068 14.114926,4.5941536"
+ id="path3276"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 20.067003,7.4851625 C 19.434985,7.2323553 20.407122,3.0721788 20.407122,1.873204"
+ id="path3278"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 27.379555,8.3354593 C 27.158812,8.2471618 28.229852,3.6895085 28.229852,2.7235007"
+ id="path3280"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 33.161573,9.5258747 C 32.98088,9.4535975 35.089459,5.8216307 35.372345,5.4444503"
+ id="path3282"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 37.072938,11.736646 C 35.91726,11.274375 39.824038,9.6778439 42.344778,8.1653999"
+ id="path3284"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 39.11365,16.498308 C 37.874839,16.002784 41.690775,15.648011 43.025016,15.648011"
+ id="path3286"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 37.242998,26.871928 C 38.346419,27.239736 38.801386,28.600377 39.623829,29.422819"
+ id="path3290"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 34.01187,30.443175 C 35.488246,30.9353 37.740219,33.713713 38.603472,34.864718"
+ id="path3292"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 11.223917,11.566587 C 10.939233,10.903218 8.5659634,8.8015698 7.8227301,7.3151032"
+ id="path3294"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ d="M 37.923235,21.089911 C 38.834054,20.372818 40.830076,21.430029 42.514837,21.430029"
+ id="path3296"
+ style="fill:#FFFFFF;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.81818182;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /></svg> \ No newline at end of file
diff --git a/icons/mode-type.svg b/icons/mode-type.svg
new file mode 100644
index 0000000..ddf7dbc
--- /dev/null
+++ b/icons/mode-type.svg
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ width="55"
+ height="55"
+ viewBox="0 0 50 50"
+ id="Layer_1"
+ xml:space="preserve"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="mode-type.svg"><metadata
+ id="metadata29"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
+ pagecolor="#878787"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0.42352941"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1278"
+ inkscape:window-height="735"
+ id="namedview27"
+ showgrid="false"
+ inkscape:zoom="4.7454545"
+ inkscape:cx="-5.7247776"
+ inkscape:cy="38.193871"
+ inkscape:window-x="0"
+ inkscape:window-y="16"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="Layer_1"
+ inkscape:snap-grids="true" /><defs
+ id="defs6"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective31" /><inkscape:perspective
+ id="perspective3618"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective3643"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective3668"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /></defs>
+
+<text
+ xml:space="preserve"
+ style="font-size:23.4211235px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;font-family:URW Gothic L;-inkscape-font-specification:URW Gothic L Semi-Bold"
+ x="1.8150109"
+ y="36.040466"
+ id="text2834-5"
+ sodipodi:linespacing="125%"
+ transform="scale(0.94064439,1.063101)"><tspan
+ sodipodi:role="line"
+ id="tspan2836-48"
+ x="1.8150109"
+ y="36.040466"
+ style="font-size:23.4211235px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;font-family:URW Gothic L;-inkscape-font-specification:URW Gothic L Semi-Bold">ABC</tspan></text>
+</svg> \ No newline at end of file
diff --git a/icons/voice.svg b/icons/voice.svg
new file mode 100644
index 0000000..d32f370
--- /dev/null
+++ b/icons/voice.svg
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="voice.svg"><metadata
+ id="metadata17"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs15"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective19" />
+
+
+</defs><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1278"
+ inkscape:window-height="735"
+ id="namedview13"
+ showgrid="false"
+ inkscape:zoom="4.7454545"
+ inkscape:cx="-9.3773946"
+ inkscape:cy="27.5"
+ inkscape:window-x="0"
+ inkscape:window-y="16"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg2" /><g
+ id="g5"
+ style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none">
+ <path
+ style="fill:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path7"
+ d="m 48.143,36.983 c 0,3.3 -2.7,6 -6,6 H 12.705 c -3.3,0 -6,-2.7 -6,-6 v -18.77 c 0,-3.3 2.7,-6 6,-6 h 29.438 c 3.3,0 6,2.7 6,6 v 18.77 z" />
+ <path
+ style="fill:none;stroke:#010101;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path9"
+ d="m 48.143,36.983 c 0,3.3 -2.7,6 -6,6 H 12.705 c -3.3,0 -6,-2.7 -6,-6 v -18.77 c 0,-3.3 2.7,-6 6,-6 h 29.438 c 3.3,0 6,2.7 6,6 v 18.77 z" />
+ </g><path
+ style="fill:#ffffff;stroke:#010101;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path11"
+ d="m 8.007,27.598 c 10.118,0 6.243,9.431 11.213,9.431 4.375,0 2.154,-19.154 6.279,-19.154 4.875,0 1.719,19.154 6.719,19.154 4.406,0 1.781,-11.529 5.156,-11.529 3.375,0 2,4.875 4.25,4.875 2.375,0 0.25,-3.375 5.75,-3.375" /></svg> \ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..5cae440
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+# 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 <http://www.gnu.org/licenses/>.
+
+from sugar3.activity import bundlebuilder
+bundlebuilder.start()