From 824aa0a89e0d424e44f5dbb18124eb8101bf12eb Mon Sep 17 00:00:00 2001 From: Joshua Minor Date: Thu, 10 Jan 2008 09:34:26 +0000 Subject: Speak v3 Lots of major performance enhancements. --- diff --git a/Speak.activity/.DS_Store b/Speak.activity/.DS_Store deleted file mode 100644 index c8c7f65..0000000 --- a/Speak.activity/.DS_Store +++ /dev/null Binary files differ diff --git a/Speak.activity/MANIFEST b/Speak.activity/MANIFEST index 078d389..4fdf708 100644 --- a/Speak.activity/MANIFEST +++ b/Speak.activity/MANIFEST @@ -1,4 +1,9 @@ activity/activity-speak.svg activity/activity.info activity.py -setup.py \ No newline at end of file +audio.py +COPYING +eye.py +mouth.py +setup.py +voice.py \ No newline at end of file diff --git a/Speak.activity/activity.py b/Speak.activity/activity.py index 629219d..fbe96ad 100755 --- a/Speak.activity/activity.py +++ b/Speak.activity/activity.py @@ -1,23 +1,53 @@ -# coding: UTF8 +# Speak.activity +# A simple front end to the espeak text-to-speech engine on the XO laptop +# +# 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. +# +# Foobar 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 Foobar. If not, see . -import commands, subprocess + +import sys +import os +import subprocess import random from sugar.activity import activity from sugar.datastore import datastore from sugar.presence import presenceservice import logging -import sys, os import gtk import gobject import pango +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toolcombobox import ToolComboBox +from sugar.graphics.combobox import ComboBox + import pygst pygst.require("0.10") import gst import audio import eye +import glasses import mouth +import fft_mouth +import waveform_mouth +import voice class SpeakActivity(activity.Activity): def __init__(self, handle): @@ -25,68 +55,257 @@ class SpeakActivity(activity.Activity): activity.Activity.__init__(self, handle) bounds = self.get_allocation() - toolbox = activity.ActivityToolbox(self) - self.set_toolbox(toolbox) - toolbox.show() + # pick a voice that espeak supports + self.voices = voice.allVoices() + #self.voice = random.choice(self.voices.values()) + self.voice = self.voices["Default"] + # make an audio device for playing back and rendering audio + self.active = False + self.connect( "notify::active", self._activeCb ) + self.audio = audio.AudioGrab(datastore, self._jobject) + + #self.proc = None + + # make a box to type into self.entry = gtk.Entry() self.entry.set_editable(True) self.entry.connect('activate', self.entry_activate_cb) self.input_font = pango.FontDescription(str='sans bold 24') self.entry.modify_font(self.input_font) - self.eyes = [eye.Eye(), eye.Eye()] - eyeBox = gtk.HBox() - eyeBox.pack_start(self.eyes[0]) - eyeBox.pack_start(self.eyes[1]) - map(lambda e: e.set_size_request(300,300), self.eyes) + # make an empty box for some eyes + self.eyes = None + self.eyebox = gtk.HBox() - self.ACTIVE = True - self.connect( "notify::active", self._activeCb ) - self.audio = audio.AudioGrab(datastore, self._jobject) - self.mouth = mouth.Mouth(self.audio) + # make an empty box to put the mouth in + self.mouth = None + self.mouthbox = gtk.HBox() + # layout the screen box = gtk.VBox(homogeneous=False) - box.pack_start(eyeBox, expand=False) - box.pack_start(self.mouth) + box.pack_start(self.eyebox, expand=False) + box.pack_start(self.mouthbox) box.pack_start(self.entry, expand=False) self.set_canvas(box) box.show_all() + box.add_events(gtk.gdk.BUTTON_PRESS_MASK | + gtk.gdk.POINTER_MOTION_MASK) + box.connect("motion_notify_event", self._mouse_moved_cb) + box.connect("button_press_event", self._mouse_clicked_cb) + + # make some toolbars + toolbox = activity.ActivityToolbox(self) + self.set_toolbox(toolbox) + toolbox.show() + #activitybar = toolbox.get_activity_toolbar() + + voicebar = self.make_voice_bar() + toolbox.add_toolbar("Voice", voicebar) + voicebar.show() + + facebar = self.make_face_bar() + toolbox.add_toolbar("Face", facebar) + facebar.show() + + # make the text box active right away self.entry.grab_focus() - gobject.timeout_add(100, self._timeout_cb) + # start polling for audio + #gobject.timeout_add(100, self._timeout_cb) + # say hello to the user + self.active = True presenceService = presenceservice.get_instance() xoOwner = presenceService.get_owner() - self.say("Hi %s, my name is Otto. Type something." % xoOwner.props.nick) + self.say("Hello %s, my name is XO. Type something." % xoOwner.props.nick) + + def _mouse_moved_cb(self, widget, event): + map(lambda w: w.queue_draw(), self.eyes) + + def _mouse_clicked_cb(self, widget, event): + pass + + def make_voice_bar(self): + voicebar = gtk.Toolbar() + + # button = ToolButton('change-voice') + # button.set_tooltip("Change Voice") + # button.connect('clicked', self.change_voice_cb) + # voicebar.insert(button, -1) + # button.show() + + combo = ComboBox() + combo.connect('changed', self.voice_changed_cb) + voicenames = self.voices.keys() + voicenames.sort() + for name in voicenames: + combo.append_item(self.voices[name], name) + combo.set_active(voicenames.index(self.voice.friendlyname)) + combotool = ToolComboBox(combo) + voicebar.insert(combotool, -1) + combotool.show() + + self.pitchadj = gtk.Adjustment(50, 0, 99, 1, 10, 0) + self.pitchadj.connect("value_changed", self.pitch_adjusted_cb, self.pitchadj) + pitchbar = gtk.HScale(self.pitchadj) + pitchbar.set_draw_value(False) + #pitchbar.set_inverted(True) + pitchbar.set_update_policy(gtk.UPDATE_DISCONTINUOUS) + pitchbar.set_size_request(240,15) + pitchtool = gtk.ToolItem() + pitchtool.add(pitchbar) + pitchtool.show() + voicebar.insert(pitchtool, -1) + pitchbar.show() + + self.rateadj = gtk.Adjustment(100, 80, 370, 1, 10, 0) + self.rateadj.connect("value_changed", self.rate_adjusted_cb, self.rateadj) + ratebar = gtk.HScale(self.rateadj) + ratebar.set_draw_value(False) + #ratebar.set_inverted(True) + ratebar.set_update_policy(gtk.UPDATE_DISCONTINUOUS) + ratebar.set_size_request(240,15) + ratetool = gtk.ToolItem() + ratetool.add(ratebar) + ratetool.show() + voicebar.insert(ratetool, -1) + ratebar.show() + + return voicebar + + def voice_changed_cb(self, combo): + self.voice = combo.props.value + self.say(self.voice.friendlyname) + + def pitch_adjusted_cb(self, get, data=None): + self.say("pitch adjusted") + + def rate_adjusted_cb(self, get, data=None): + self.say("rate adjusted") + + + def make_face_bar(self): + facebar = gtk.Toolbar() + + self.numeyesadj = None + + # button = ToolButton('change-voice') + # button.set_tooltip("Change Voice") + # button.connect('clicked', self.change_voice_cb) + # facebar.insert(button, -1) + # button.show() + + combo = ComboBox() + combo.connect('changed', self.mouth_changed_cb) + combo.append_item(mouth.Mouth, "Simple") + combo.append_item(waveform_mouth.WaveformMouth, "Waveform") + combo.append_item(fft_mouth.FFTMouth, "Frequency") + combo.set_active(0) + combotool = ToolComboBox(combo) + facebar.insert(combotool, -1) + combotool.show() + + self.eye_shape_combo = ComboBox() + self.eye_shape_combo.connect('changed', self.eyes_changed_cb) + self.eye_shape_combo.append_item(eye.Eye, "Round") + self.eye_shape_combo.append_item(glasses.Glasses, "Glasses") + combotool = ToolComboBox(self.eye_shape_combo) + facebar.insert(combotool, -1) + combotool.show() + + self.numeyesadj = gtk.Adjustment(2, 1, 5, 1, 1, 0) + self.numeyesadj.connect("value_changed", self.eyes_changed_cb, self.numeyesadj) + numeyesbar = gtk.HScale(self.numeyesadj) + numeyesbar.set_draw_value(False) + numeyesbar.set_update_policy(gtk.UPDATE_DISCONTINUOUS) + numeyesbar.set_size_request(240,15) + numeyestool = gtk.ToolItem() + numeyestool.add(numeyesbar) + numeyestool.show() + facebar.insert(numeyestool, -1) + numeyesbar.show() + + self.eye_shape_combo.set_active(0) + + return facebar + + def mouth_changed_cb(self, combo): + mouth_class = combo.props.value + if self.mouth: + self.mouthbox.remove(self.mouth) + self.mouth = mouth_class(self.audio) + self.mouthbox.add(self.mouth) + self.mouth.show() + # enable mouse move events so we can track the eyes while the mouse is over the mouth + self.mouth.add_events(gtk.gdk.POINTER_MOTION_MASK) + # this SegFaults: self.say(combo.get_active_text()) + self.say("mouth changed") + + def eyes_changed_cb(self, ignored, ignored2=None): + if self.numeyesadj is None: + return + + eye_class = self.eye_shape_combo.props.value + if self.eyes: + for eye in self.eyes: + self.eyebox.remove(eye) + + self.eyes = [] + numberOfEyes = int(self.numeyesadj.value) + for i in range(numberOfEyes): + eye = eye_class() + self.eyes.append(eye) + self.eyebox.pack_start(eye) + eye.set_size_request(300,300) + eye.show() + + # this SegFaults: self.say(self.eye_shape_combo.get_active_text()) + self.say("eyes changed") def _timeout_cb(self): - self.mouth.queue_draw(); + # make the mouth update with the latest waveform + # ideally we would only do this when the audio is actually playing + if self.mouth: + self.mouth.queue_draw(); return True def entry_activate_cb(self, entry): + # the user pressed Return, say the text and clear it out text = entry.props.text if text: self.say(text) + # ideally we would clear it after we finish saying it + # so that you would be able to compare the audio to the text + # without having to remember what you typed entry.props.text = '' - def speak(self, widget, data=None): - self.say(random.choice(["Let's go to Annas","Hi Opal, how are you?"])) - def say(self, something): + if self.audio is None or not self.active: + return + # ideally we would stream the audio instead of writing to disk each time... + print self.voice.name, ":", something wavpath = "/tmp/speak.wav" - subprocess.call(["espeak", "-w", wavpath, something]) - #subprocess.call(["playwave", wavpath]) + subprocess.call(["espeak", "-w", wavpath, "-p", str(self.pitchadj.value), "-s", str(self.rateadj.value), "-v", self.voice.name, something], stdout=subprocess.PIPE) self.audio.playfile(wavpath) + # this doesn't seem to work, but would avoid the /tmp/file.wave + # if self.proc: + # self.proc = None + # self.proc = subprocess.Popen(["espeak", "--stdout", "-s", "100", "-v", self.voice.name, something], stdout=subprocess.PIPE) + # print self.proc + # print self.proc.stdout + # print self.proc.stdout.fileno() + # self.audio.playfd(self.proc.stdout.fileno()) def _activeCb( self, widget, pspec ): - if (not self.props.active and self.ACTIVE): + # only generate sound when this activity is active + if (not self.props.active and self.active): self.audio.stop_sound_device() - elif (self.props.active and not self.ACTIVE): + elif (self.props.active and not self.active): self.audio.restart_sound_device() - self.ACTIVE = self.props.active + self.active = self.props.active def on_quit(self, data=None): self.audio.on_quit() diff --git a/Speak.activity/activity/activity-speak.svg b/Speak.activity/activity/activity-speak.svg index ce92ad4..b99ebdf 100644 --- a/Speak.activity/activity/activity-speak.svg +++ b/Speak.activity/activity/activity-speak.svg @@ -5,11 +5,10 @@ ]> - + diff --git a/Speak.activity/activity/activity.info b/Speak.activity/activity/activity.info index 3f7c5ee..6029cec 100644 --- a/Speak.activity/activity/activity.info +++ b/Speak.activity/activity/activity.info @@ -3,5 +3,5 @@ name = Speak service_name = vu.lux.olpc.Speak class = activity.SpeakActivity icon = activity-speak -activity_version = 1 +activity_version = 3 show_launcher = yes diff --git a/Speak.activity/audio.py b/Speak.activity/audio.py index 484285d..e29ac80 100644 --- a/Speak.activity/audio.py +++ b/Speak.activity/audio.py @@ -1,3 +1,27 @@ +# Speak.activity +# A simple front end to the espeak text-to-speech engine on the XO laptop +# +# 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. +# +# Foobar 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 Foobar. If not, see . + +# This code is a stripped down version of the audio grabber from Measure + import pygst pygst.require("0.10") import gst @@ -19,6 +43,8 @@ class AudioGrab(gobject.GObject): self.pipeline = None def playfile(self, filename): + self.stop_sound_device() + # build a pipeline that reads the given file # and sends it to both the real audio output # and a fake one that we use to draw from @@ -44,6 +70,36 @@ class AudioGrab(gobject.GObject): # how do we detect when the sample has finished playing? # we should stop the sound device and stop emitting buffers # to save on CPU and battery usage when there is no audio playing + + def playfd(self, fd): + self.stop_sound_device() + + # build a pipeline that reads the given file + # and sends it to both the real audio output + # and a fake one that we use to draw from + if self.pipeline is None: + p = 'fdsrc name=fd-source ! wavparse ! tee name=tee tee.! audioconvert ! alsasink tee.! queue ! audioconvert name=conv' + self.pipeline = gst.parse_launch(p) + + # make a fakesink to capture audio + fakesink = gst.element_factory_make("fakesink", "fakesink") + fakesink.connect("handoff",self.on_buffer) + fakesink.set_property("signal-handoffs",True) + self.pipeline.add(fakesink) + + # attach it to the pipeline + conv = self.pipeline.get_by_name("conv") + gst.element_link_many(conv, fakesink) + + # set the source file + self.pipeline.get_by_name("fd-source").set_property('fd', fd) + + # play + self.restart_sound_device() + + # how do we detect when the sample has finished playing? + # we should stop the sound device and stop emitting buffers + # to save on CPU and battery usage when there is no audio playing def on_quit(self): self.pipeline.set_state(gst.STATE_NULL) @@ -59,7 +115,8 @@ class AudioGrab(gobject.GObject): return True def stop_sound_device(self): - self.pipeline.set_state(gst.STATE_NULL) + if self.pipeline is not None: + self.pipeline.set_state(gst.STATE_NULL) def restart_sound_device(self): self.pipeline.set_state(gst.STATE_PLAYING) diff --git a/Speak.activity/eye.py b/Speak.activity/eye.py index 824289c..72d9170 100644 --- a/Speak.activity/eye.py +++ b/Speak.activity/eye.py @@ -1,4 +1,24 @@ -#! /usr/bin/python +# Speak.activity +# A simple front end to the espeak text-to-speech engine on the XO laptop +# +# 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. +# +# Foobar 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 Foobar. If not, see . import pygtk import gtk @@ -12,19 +32,33 @@ class Eye(gtk.DrawingArea): gtk.DrawingArea.__init__(self) self.connect("expose_event",self.expose) self.frame = 0 - # instead of listening for mouse move events we poll to see if the mouse has moved - # this is so we can react to the mouse even when it isn't directly over this widget - gobject.timeout_add(100, self._timeout_cb) - self.mousePosition = self.get_mouse() + self.blink = False + + # listen for clicks + self.add_events(gtk.gdk.BUTTON_PRESS_MASK) + self.add_events(gtk.gdk.BUTTON_RELEASE_MASK) + self.connect("button_press_event", self._mouse_pressed_cb) + self.connect("button_release_event", self._mouse_released_cb) + + # Instead of listening for mouse move events we could poll to see if the mouse has moved + # would let us react to the mouse even when it isn't directly over this widget. + # Unfortunately that would cause a lot of CPU usage. So instead we rely on our parent to + # tell us to redraw when the mouse has moved. We still need to call add_events so that + # our parent will get mouse motion events, but we don't connect the callback for them ourselves. + self.add_events(gtk.gdk.POINTER_MOTION_MASK) + # self.connect("motion_notify_event", self._mouse_moved_cb) - def _timeout_cb(self): - # only redraw if the mouse has moved - newPosition = self.get_mouse() - if newPosition != self.mousePosition: - self.queue_draw() - self.mousePosition = newPosition - return True + def _mouse_moved_cb(self, widget, event): + self.queue_draw() + def _mouse_pressed_cb(self, widget, event): + self.blink = True + self.queue_draw() + + def _mouse_released_cb(self, widget, event): + self.blink = False + self.queue_draw() + def get_mouse(self): display = gtk.gdk.display_get_default() screen, mouseX, mouseY, modifiers = display.get_pointer() @@ -34,7 +68,7 @@ class Eye(gtk.DrawingArea): self.frame += 1 bounds = self.get_allocation() - mouseX, mouseY = self.mousePosition + mouseX, mouseY = self.get_mouse() eyeSize = min(bounds.width, bounds.height) outlineWidth = eyeSize/20.0 @@ -77,4 +111,6 @@ class Eye(gtk.DrawingArea): self.context.set_source_rgb(0,0,0) self.context.fill() + self.blink = False + return True diff --git a/Speak.activity/mouth.py b/Speak.activity/mouth.py index 2a42833..453675d 100644 --- a/Speak.activity/mouth.py +++ b/Speak.activity/mouth.py @@ -1,18 +1,31 @@ -#! /usr/bin/python +# Speak.activity +# A simple front end to the espeak text-to-speech engine on the XO laptop +# +# 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. +# +# Foobar 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 Foobar. If not, see . + +# This code is a super-stripped down version of the waveform view from Measure -import pygst -pygst.require("0.10") -import pygtk import gtk import cairo -import gobject -from time import * -from struct import * -import pango -import os -import audioop -from Numeric import * -from FFT import * +from struct import unpack +import numpy.core class Mouth(gtk.DrawingArea): def __init__(self, audioSource): @@ -20,120 +33,28 @@ class Mouth(gtk.DrawingArea): gtk.DrawingArea.__init__(self) self.connect("expose_event",self.expose) self.buffers = [] + self.buffer_size = 256 self.main_buffers = [] - self.integer_buffer = [] + self.newest_buffer = [] audioSource.connect("new-buffer", self._new_buffer) - self.peaks = [] - - self.y_mag = 0.7 - self.freq_range=70 - self.draw_interval = 1 - self.num_of_points = 105 - - self.stop=False - - self.fft_show = False - self.fftx = [] - - self.y_mag_bias_multiplier = 1 #constant to multiply with self.param2 while scaling values - - self.scaleX = "10" - self.scaleY = "10" - - def _new_buffer(self, obj, buf): - self.integer_buffer = list(unpack( str(int(len(buf))/2)+'h' , buf)) - if(len(self.main_buffers)>6301): - del self.main_buffers[0:(len(self.main_buffers)-6301)] - self.main_buffers += self.integer_buffer + 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, bounds): - self.param1 = bounds.height/65536.0 - self.param2 = bounds.height/2.0 - - if(self.stop==False): - - if(self.fft_show==False): - - ######################filtering#################### - weights = [1,2,3,4,3,2,1] - weights_sum = 16.0 - - for i in range(3,len(self.integer_buffer)-3): - self.integer_buffer[i] = (self.integer_buffer[(i-3)]+2*self.integer_buffer[(i-2)] + 3*self.integer_buffer[(i-1)] + 4*self.integer_buffer[(i)]+3*self.integer_buffer[(i+1)] + 2*self.integer_buffer[(i+2)] + self.integer_buffer[(i+3)]) / weights_sum - ################################################### - - self.y_mag_bias_multiplier=1 - self.draw_interval=10 - - #100hz - if(self.freq_range==30): - self.spacing = 60 - self.num_of_points=6300 - - #1khz - if(self.freq_range==50): - self.spacing = 6 - self.num_of_points=630 - - #4khz - if(self.freq_range==70): - self.spacing = 1 - self.num_of_points = 105 - - self.scaleX = str(self.spacing*.104) + " msec" #.104 = 5/48; 5 points per division and 48 khz sampling - - if(len(self.main_buffers)>=self.num_of_points): - del self.main_buffers[0:len(self.main_buffers)-(self.num_of_points+1)] - self.buffers=[] - i=0 - while i= bounds.height): - temp_val_float = bounds.height-25 - if(temp_val_float <= 0): - temp_val_float = 25 - val.append( temp_val_float ) - - self.peaks = val - ################################################# + if len(self.main_buffers) == 0: + self.volume = 0 + else: + self.volume = numpy.core.max(self.main_buffers)# - numpy.core.min(self.main_buffers) def expose(self, widget, event): """This function is the "expose" event handler and does all the drawing.""" - bounds = self.get_allocation() self.processBuffer(bounds) @@ -151,12 +72,21 @@ class Mouth(gtk.DrawingArea): self.context.rectangle(0,0, bounds.width,bounds.height) self.context.fill() - # Draw the waveform + # Draw the mouth + volume = self.volume / 65535. + mouthH = volume * bounds.height + mouthW = volume**2 * (bounds.width/2.) + bounds.width/2. + # T + # L R + # B + Lx,Ly = bounds.width/2 - mouthW/2, bounds.height/2 + Tx,Ty = bounds.width/2, bounds.height/2 - mouthH/2 + Rx,Ry = bounds.width/2 + mouthW/2, bounds.height/2 + Bx,By = bounds.width/2, bounds.height/2 + mouthH/2 self.context.set_line_width(10.0) - count = 0 - for peak in self.peaks: - self.context.line_to(count,bounds.height - peak) - count += self.draw_interval + self.context.move_to(Lx,Ly) + self.context.curve_to(Tx,Ty, Tx,Ty, Rx,Ry) + self.context.curve_to(Bx,By, Bx,By, Lx,Ly) self.context.set_source_rgb(0,0,0) self.context.stroke() diff --git a/build b/build index dd0c031..4bcab6a 100755 --- a/build +++ b/build @@ -1,3 +1,4 @@ #!/bin/sh +rm dist/speak.xo zip -r dist/speak.xo Speak.activity && scp dist/speak.xo luxvu@lux.vu:public_html/olpc/ diff --git a/dist/speak-1.xo b/dist/speak-1.xo new file mode 100644 index 0000000..a8fb3e3 --- /dev/null +++ b/dist/speak-1.xo Binary files differ diff --git a/dist/speak-2.xo b/dist/speak-2.xo new file mode 100644 index 0000000..4f9ef59 --- /dev/null +++ b/dist/speak-2.xo Binary files differ diff --git a/dist/speak-2b.xo b/dist/speak-2b.xo new file mode 100644 index 0000000..e272ca4 --- /dev/null +++ b/dist/speak-2b.xo Binary files differ diff --git a/dist/speak-3.xo b/dist/speak-3.xo new file mode 100644 index 0000000..152c9d4 --- /dev/null +++ b/dist/speak-3.xo Binary files differ -- cgit v0.9.1