Web · Wiki · Activities · Blog · Lists · Chat · Meeting · Bugs · Git · Translate · Archive · People · Donate
1
# Speak.activity
2
# A simple front end to the espeak text-to-speech engine on the XO laptop
3
# http://wiki.laptop.org/go/Speak
4
#
5
# Copyright (C) 2008  Joshua Minor
6
# This file is part of Speak.activity
7
#
8
# Parts of Speak.activity are based on code from Measure.activity
9
# Copyright (C) 2007  Arjun Sarwal - arjun@laptop.org
10
#
11
#     Speak.activity is free software: you can redistribute it and/or modify
12
#     it under the terms of the GNU General Public License as published by
13
#     the Free Software Foundation, either version 3 of the License, or
14
#     (at your option) any later version.
15
#
16
#     Speak.activity is distributed in the hope that it will be useful,
17
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
18
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
#     GNU General Public License for more details.
20
#
21
#     You should have received a copy of the GNU General Public License
22
#     along with Speak.activity.  If not, see <http://www.gnu.org/licenses/>.
23
24
25
from sugar.activity import activity
26
from sugar.presence import presenceservice
27
import logging
28
import gtk
29
import gobject
30
import pango
31
import cjson
32
from gettext import gettext as _
33
34
from sugar.graphics.toolbutton import ToolButton
35
from sugar.graphics.toggletoolbutton import ToggleToolButton
36
from sugar.graphics.radiotoolbutton import RadioToolButton
37
38
from toolkit.toolitem import ToolWidget
39
from toolkit.combobox import ComboBox
40
from toolkit.toolbarbox import ToolbarBox
41
from toolkit.activity import SharedActivity
42
from toolkit.activity_widgets import *
43
44
import eye
45
import glasses
46
import mouth
47
import fft_mouth
48
import waveform_mouth
49
import voice
50
import face
51
import brain
52
import chat
53
import espeak
54
from messenger import Messenger, SERVICE
55
56
logger = logging.getLogger('speak')
57
58
MODE_TYPE = 1
59
MODE_BOT = 2
60
MODE_CHAT = 3
61
62
63
class SpeakActivity(SharedActivity):
64
    def __init__(self, handle):
65
        self.notebook = gtk.Notebook()
66
67
        SharedActivity.__init__(self, self.notebook, SERVICE, handle)
68
69
        self._mode = MODE_TYPE
70
        self.numeyesadj = None
71
72
        # make an audio device for playing back and rendering audio
73
        self.connect("notify::active", self._activeCb)
74
75
        # make a box to type into
76
        self.entrycombo = gtk.combo_box_entry_new_text()
77
        self.entrycombo.connect("changed", self._combo_changed_cb)
78
        self.entry = self.entrycombo.child
79
        self.entry.set_editable(True)
80
        self.entry.connect('activate', self._entry_activate_cb)
81
        self.entry.connect("key-press-event", self._entry_key_press_cb)
82
        self.input_font = pango.FontDescription(str='sans bold 24')
83
        self.entry.modify_font(self.input_font)
84
85
        self.face = face.View()
86
        self.face.show()
87
88
        # layout the screen
89
        box = gtk.VBox(homogeneous=False)
90
        box.pack_start(self.face)
91
        box.pack_start(self.entrycombo, expand=False)
92
93
        self.add_events(gtk.gdk.POINTER_MOTION_HINT_MASK
94
                | gtk.gdk.POINTER_MOTION_MASK)
95
        self.connect("motion_notify_event", self._mouse_moved_cb)
96
97
        box.add_events(gtk.gdk.BUTTON_PRESS_MASK)
98
        box.connect("button_press_event", self._mouse_clicked_cb)
99
100
        # desktop
101
        self.notebook.show()
102
        self.notebook.props.show_border=False
103
        self.notebook.props.show_tabs=False
104
105
        box.show_all()
106
        self.notebook.append_page(box)
107
108
        self.chat = chat.View()
109
        self.chat.show_all()
110
        self.notebook.append_page(self.chat)
111
112
        # make the text box active right away
113
        self.entry.grab_focus()
114
115
        self.entry.connect("move-cursor", self._cursor_moved_cb)
116
        self.entry.connect("changed", self._cursor_moved_cb)
117
118
        # toolbar
119
120
        toolbox = ToolbarBox()
121
122
        toolbox.toolbar.insert(ActivityToolbarButton(self), -1)
123
124
        separator = gtk.SeparatorToolItem()
125
        separator.set_draw(False)
126
        toolbox.toolbar.insert(separator, -1)
127
128
        self.voices = ComboBox()
129
        for name in sorted(voice.allVoices().keys()):
130
            self.voices.append_item(voice.allVoices()[name], name)
131
        self.voices.select(voice.defaultVoice())
132
        all_voices = self.voices.get_model()
133
        brain_voices = brain.get_voices()
134
135
        mode_type = RadioToolButton(
136
                named_icon='mode-type',
137
                tooltip=_('Type something to hear it'))
138
        mode_type.connect('toggled', self.__toggled_mode_type_cb, all_voices)
139
        toolbox.toolbar.insert(mode_type, -1)
140
141
        mode_robot = RadioToolButton(
142
                named_icon='mode-robot',
143
                group=mode_type,
144
                tooltip=_('Ask robot any question'))
145
        mode_robot.connect('toggled', self.__toggled_mode_robot_cb,
146
                brain_voices)
147
        toolbox.toolbar.insert(mode_robot, -1)
148
149
        mode_chat = RadioToolButton(
150
                named_icon='mode-chat',
151
                group=mode_type,
152
                tooltip=_('Voice chat'))
153
        mode_chat.connect('toggled', self.__toggled_mode_chat_cb, all_voices)
154
        toolbox.toolbar.insert(mode_chat, -1)
155
156
        separator = gtk.SeparatorToolItem()
157
        toolbox.toolbar.insert(separator, -1)
158
159
        voices_toolitem = ToolWidget(widget=self.voices)
160
        toolbox.toolbar.insert(voices_toolitem, -1)
161
162
        voice_button = ToolbarButton(
163
                page=self.make_voice_bar(),
164
                label=_('Voice'),
165
                icon_name='voice')
166
        toolbox.toolbar.insert(voice_button, -1)
167
168
        face_button = ToolbarButton(
169
                page=self.make_face_bar(),
170
                label=_('Face'),
171
                icon_name='face')
172
        toolbox.toolbar.insert(face_button, -1)
173
174
        separator = gtk.SeparatorToolItem()
175
        separator.set_draw(False)
176
        separator.set_expand(True)
177
        toolbox.toolbar.insert(separator, -1)
178
179
        toolbox.toolbar.insert(StopButton(self), -1)
180
181
        toolbox.show_all()
182
        self.toolbar_box = toolbox
183
184
    def new_instance(self):
185
        self.voices.connect('changed', self.__changed_voices_cb)
186
        self.pitchadj.connect("value_changed", self.pitch_adjusted_cb, self.pitchadj)
187
        self.rateadj.connect("value_changed", self.rate_adjusted_cb, self.rateadj)
188
        self.mouth_shape_combo.connect('changed', self.mouth_changed_cb, False)
189
        self.mouth_changed_cb(self.mouth_shape_combo, True)
190
        self.numeyesadj.connect("value_changed", self.eyes_changed_cb, False)
191
        self.eye_shape_combo.connect('changed', self.eyes_changed_cb, False)
192
        self.eyes_changed_cb(None, True)
193
194
        self.face.look_ahead()
195
196
        # say hello to the user
197
        presenceService = presenceservice.get_instance()
198
        xoOwner = presenceService.get_owner()
199
        self.face.say_notification(_("Hello %s. Please Type something.") \
200
                % xoOwner.props.nick)
201
202
    def resume_instance(self, file_path):
203
        cfg = cjson.decode(file(file_path, 'r').read())
204
205
        status = self.face.status = face.Status().deserialize(cfg['status'])
206
        self.voices.select(status.voice)
207
        self.pitchadj.value = self.face.status.pitch
208
        self.rateadj.value = self.face.status.rate
209
        self.mouth_shape_combo.select(status.mouth)
210
        self.eye_shape_combo.select(status.eyes[0])
211
        self.numeyesadj.value = len(status.eyes)
212
213
        self.entry.props.text = cfg['text']
214
        for i in cfg['history']:
215
            self.entrycombo.append_text(i)
216
217
        self.new_instance()
218
219
    def save_instance(self, file_path):
220
        cfg = {'status': self.face.status.serialize(),
221
                'text': self.entry.props.text,
222
                'history': map(lambda i: i[0], self.entrycombo.get_model())}
223
        file(file_path, 'w').write(cjson.encode(cfg))
224
225
    def share_instance(self, connection, is_initiator):
226
        self.chat.messenger = Messenger(connection, is_initiator, self.chat)
227
228
    def _cursor_moved_cb(self, entry, *ignored):
229
        # make the eyes track the motion of the text cursor
230
        index = entry.props.cursor_position
231
        layout = entry.get_layout()
232
        pos = layout.get_cursor_pos(index)
233
        x = pos[0][0] / pango.SCALE - entry.props.scroll_offset
234
        y = entry.get_allocation().y
235
        self.face.look_at(pos=(x, y))
236
237
    def get_mouse(self):
238
        display = gtk.gdk.display_get_default()
239
        screen, mouseX, mouseY, modifiers = display.get_pointer()
240
        return mouseX, mouseY
241
242
    def _mouse_moved_cb(self, widget, event):
243
        # make the eyes track the motion of the mouse cursor
244
        self.face.look_at()
245
        self.chat.look_at()
246
247
    def _mouse_clicked_cb(self, widget, event):
248
        pass
249
250
    def make_voice_bar(self):
251
        voicebar = gtk.Toolbar()
252
253
        self.pitchadj = gtk.Adjustment(self.face.status.pitch, 0,
254
                espeak.PITCH_MAX, 1, espeak.PITCH_MAX/10, 0)
255
        pitchbar = gtk.HScale(self.pitchadj)
256
        pitchbar.set_draw_value(False)
257
        # pitchbar.set_inverted(True)
258
        pitchbar.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
259
        pitchbar.set_size_request(240, 15)
260
261
        pitchbar_toolitem = ToolWidget(
262
                widget=pitchbar,
263
                label_text=_('Pitch:'))
264
        voicebar.insert(pitchbar_toolitem, -1)
265
266
        self.rateadj = gtk.Adjustment(self.face.status.rate, 0, espeak.RATE_MAX,
267
                1, espeak.RATE_MAX / 10, 0)
268
        ratebar = gtk.HScale(self.rateadj)
269
        ratebar.set_draw_value(False)
270
        # ratebar.set_inverted(True)
271
        ratebar.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
272
        ratebar.set_size_request(240, 15)
273
274
        ratebar_toolitem = ToolWidget(
275
                widget=ratebar,
276
                label_text=_('Rate:'))
277
        voicebar.insert(ratebar_toolitem, -1)
278
279
        voicebar.show_all()
280
        return voicebar
281
282
    def pitch_adjusted_cb(self, get, data=None):
283
        self.face.status.pitch = get.value
284
        self.face.say_notification(_("pitch adjusted"))
285
286
    def rate_adjusted_cb(self, get, data=None):
287
        self.face.status.rate = get.value
288
        self.face.say_notification(_("rate adjusted"))
289
290
    def make_face_bar(self):
291
        facebar = gtk.Toolbar()
292
293
        self.mouth_shape_combo = ComboBox()
294
        self.mouth_shape_combo.append_item(mouth.Mouth, _("Simple"))
295
        self.mouth_shape_combo.append_item(waveform_mouth.WaveformMouth, _("Waveform"))
296
        self.mouth_shape_combo.append_item(fft_mouth.FFTMouth, _("Frequency"))
297
        self.mouth_shape_combo.set_active(0)
298
299
        mouth_shape_toolitem = ToolWidget(
300
                widget=self.mouth_shape_combo,
301
                label_text=_('Mouth:'))
302
        facebar.insert(mouth_shape_toolitem, -1)
303
304
        self.eye_shape_combo = ComboBox()
305
        self.eye_shape_combo.append_item(eye.Eye, _("Round"))
306
        self.eye_shape_combo.append_item(glasses.Glasses, _("Glasses"))
307
        self.eye_shape_combo.set_active(0)
308
309
        eye_shape_toolitem = ToolWidget(
310
                widget=self.eye_shape_combo,
311
                label_text=_('Eyes:'))
312
        facebar.insert(eye_shape_toolitem, -1)
313
314
        self.numeyesadj = gtk.Adjustment(2, 1, 5, 1, 1, 0)
315
        numeyesbar = gtk.HScale(self.numeyesadj)
316
        numeyesbar.set_draw_value(False)
317
        numeyesbar.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
318
        numeyesbar.set_size_request(240, 15)
319
320
        numeyesbar_toolitem = ToolWidget(
321
                widget=numeyesbar,
322
                label_text=_('Eyes number:'))
323
        facebar.insert(numeyesbar_toolitem, -1)
324
325
        facebar.show_all()
326
        return facebar
327
328
    def mouth_changed_cb(self, combo, quiet):
329
        self.face.status.mouth = combo.props.value
330
        self._update_face()
331
332
        # this SegFaults: self.face.say(combo.get_active_text())
333
        if not quiet:
334
            self.face.say_notification(_("mouth changed"))
335
336
    def eyes_changed_cb(self, ignored, quiet):
337
        if self.numeyesadj is None:
338
            return
339
340
        self.face.status.eyes = [self.eye_shape_combo.props.value] \
341
                * int(self.numeyesadj.value)
342
        self._update_face()
343
344
        # this SegFaults: self.face.say(self.eye_shape_combo.get_active_text())
345
        if not quiet:
346
            self.face.say_notification(_("eyes changed"))
347
348
    def _update_face(self):
349
        self.face.update()
350
        self.chat.update(self.face.status)
351
352
    def _combo_changed_cb(self, combo):
353
        # when a new item is chosen, make sure the text is selected
354
        if not self.entry.is_focus():
355
            self.entry.grab_focus()
356
            self.entry.select_region(0, -1)
357
358
    def _entry_key_press_cb(self, combo, event):
359
        # make the up/down arrows navigate through our history
360
        keyname = gtk.gdk.keyval_name(event.keyval)
361
        if keyname == "Up":
362
            index = self.entrycombo.get_active()
363
            if index>0:
364
                index-=1
365
            self.entrycombo.set_active(index)
366
            self.entry.select_region(0,-1)
367
            return True
368
        elif keyname == "Down":
369
            index = self.entrycombo.get_active()
370
            if index<len(self.entrycombo.get_model())-1:
371
                index+=1
372
            self.entrycombo.set_active(index)
373
            self.entry.select_region(0, -1)
374
            return True
375
        return False
376
377
    def _entry_activate_cb(self, entry):
378
        # the user pressed Return, say the text and clear it out
379
        text = entry.props.text
380
        if text:
381
            self.face.look_ahead()
382
383
            # speak the text
384
            if self._mode == MODE_BOT:
385
                self.face.say(
386
                        brain.respond(self.voices.props.value, text))
387
            else:
388
                self.face.say(text)
389
390
            # add this text to our history unless it is the same as the last item
391
            history = self.entrycombo.get_model()
392
            if len(history)==0 or history[-1][0] != text:
393
                self.entrycombo.append_text(text)
394
                # don't let the history get too big
395
                while len(history)>20:
396
                    self.entrycombo.remove_text(0)
397
                # select the new item
398
                self.entrycombo.set_active(len(history)-1)
399
            # select the whole text
400
            entry.select_region(0, -1)
401
402
    def _activeCb(self, widget, pspec):
403
        # only generate sound when this activity is active
404
        if not self.props.active:
405
            self.face.shut_up()
406
            self.chat.shut_up()
407
408
    def _set_voice(self, new_voice):
409
        try:
410
            self.voices.handler_block_by_func(self.__changed_voices_cb)
411
            self.voices.select(new_voice)
412
            self.face.status.voice = new_voice
413
        finally:
414
            self.voices.handler_unblock_by_func(self.__changed_voices_cb)
415
416
    def __toggled_mode_type_cb(self, button, voices_model):
417
        if not button.props.active:
418
            return
419
420
        self._mode = MODE_TYPE
421
        self.chat.shut_up()
422
        self.face.shut_up()
423
        self.notebook.set_current_page(0)
424
425
        old_voice = self.voices.props.value
426
        self.voices.set_model(voices_model)
427
        self._set_voice(old_voice)
428
429
    def __toggled_mode_robot_cb(self, button, voices_model):
430
        if not button.props.active:
431
            return
432
433
        self._mode = MODE_BOT
434
        self.chat.shut_up()
435
        self.face.shut_up()
436
        self.notebook.set_current_page(0)
437
438
        old_voice = self.voices.props.value
439
        self.voices.set_model(voices_model)
440
441
        new_voice = [i[0] for i in voices_model
442
                if i[0].short_name == old_voice.short_name]
443
        if not new_voice:
444
            new_voice = brain.get_default_voice()
445
            sorry = _("Sorry, I can't speak %s, let's talk %s instead.") % \
446
                    (old_voice.friendlyname, new_voice.friendlyname)
447
        else:
448
            new_voice = new_voice[0]
449
            sorry = None
450
451
        self._set_voice(new_voice)
452
453
        if not brain.load(self, self.voices.props.value, sorry):
454
            if sorry:
455
                self.face.say_notification(sorry)
456
457
    def __toggled_mode_chat_cb(self, button, voices_model):
458
        if not button.props.active:
459
            return
460
461
        is_first_session = not self.chat.me.flags() & gtk.MAPPED
462
463
        self._mode = MODE_CHAT
464
        self.face.shut_up()
465
        self.notebook.set_current_page(1)
466
467
        old_voice = self.voices.props.value
468
        self.voices.set_model(voices_model)
469
        self._set_voice(old_voice)
470
471
        if is_first_session:
472
            self.chat.me.say_notification(
473
                    _("You are in off-line mode, share and invite someone."))
474
475
    def __changed_voices_cb(self, combo):
476
        voice = combo.props.value
477
        self.face.set_voice(voice)
478
        if self._mode == MODE_BOT:
479
            brain.load(self, voice)
480
481
482
# activate gtk threads when this module loads
483
gtk.gdk.threads_init()