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 os
29
import subprocess
30
import gtk
31
import gobject
32
import pango
33
import json
34
from gettext import gettext as _
35
36
from sugar.graphics import style
37
from sugar.graphics.toolbutton import ToolButton
38
from sugar.graphics.toggletoolbutton import ToggleToolButton
39
from sugar.graphics.radiotoolbutton import RadioToolButton
40
41
from toolkit.toolitem import ToolWidget
42
from toolkit.combobox import ComboBox
43
from toolkit.toolbarbox import ToolbarBox
44
from toolkit.activity import SharedActivity
45
from toolkit.activity_widgets import *
46
47
import eye
48
import glasses
49
import mouth
50
import fft_mouth
51
import waveform_mouth
52
import voice
53
import face
54
import brain
55
import chat
56
import espeak
57
from messenger import Messenger, SERVICE
58
59
logger = logging.getLogger('speak')
60
61
MODE_TYPE = 1
62
MODE_BOT = 2
63
MODE_CHAT = 3
64
MOUTHS = [mouth.Mouth, fft_mouth.FFTMouth, waveform_mouth.WaveformMouth]
65
EYES = [eye.Eye, glasses.Glasses]
66
DELAY_BEFORE_SPEAKING = 1500  # milleseconds
67
68
69
def _is_tablet_mode():
70
    if not os.path.exists('/dev/input/event4'):
71
        return False
72
    try:
73
        output = subprocess.call(
74
            ['evtest', '--query', '/dev/input/event4', 'EV_SW',
75
             'SW_TABLET_MODE'])
76
    except (OSError, subprocess.CalledProcessError):
77
        return False
78
    if str(output) == '10':
79
        return True
80
    return False
81
82
83
class SpeakActivity(SharedActivity):
84
    def __init__(self, handle):
85
        self.notebook = gtk.Notebook()
86
87
        SharedActivity.__init__(self, self.notebook, SERVICE, handle)
88
89
        self._mode = MODE_TYPE
90
        # self._tablet_mode = _is_tablet_mode()
91
        self._tablet_mode = _is_tablet_mode()
92
        self.numeyesadj = None
93
94
        # make an audio device for playing back and rendering audio
95
        self.connect("notify::active", self._activeCb)
96
        self.cfg = {}
97
98
        # make a box to type into
99
        hbox = gtk.HBox()
100
        if self._tablet_mode:
101
            self.entry = gtk.Entry()
102
            hbox.pack_start(self.entry, expand=True)
103
            talk_button = ToolButton('microphone')
104
            talk_button.set_tooltip(_('Speak'))
105
            talk_button.connect('clicked', self._talk_cb)
106
            hbox.pack_end(talk_button, expand=False)
107
        else:
108
            self.entrycombo = gtk.combo_box_entry_new_text()
109
            self.entrycombo.connect("changed", self._combo_changed_cb)
110
            self.entry = self.entrycombo.child
111
            hbox.pack_start(self.entrycombo, expand=True)
112
        self.entry.set_editable(True)
113
        self.entry.connect('activate', self._entry_activate_cb)
114
        self.entry.connect("key-press-event", self._entry_key_press_cb)
115
        self.input_font = pango.FontDescription(str='sans bold 24')
116
        self.entry.modify_font(self.input_font)
117
        hbox.show()
118
119
        self.face = face.View()
120
        self.face.show()
121
122
        # layout the screen
123
        box = gtk.VBox(homogeneous=False)
124
        box.pack_start(hbox, expand=False)
125
        box.pack_start(self.face)
126
127
        self.add_events(gtk.gdk.POINTER_MOTION_HINT_MASK
128
                | gtk.gdk.POINTER_MOTION_MASK)
129
        self.connect("motion_notify_event", self._mouse_moved_cb)
130
131
        box.add_events(gtk.gdk.BUTTON_PRESS_MASK)
132
        box.connect("button_press_event", self._mouse_clicked_cb)
133
134
        # desktop
135
        self.notebook.show()
136
        self.notebook.props.show_border=False
137
        self.notebook.props.show_tabs=False
138
139
        box.show_all()
140
        self.notebook.append_page(box)
141
142
        self.chat = chat.View()
143
        self.chat.show_all()
144
        self.notebook.append_page(self.chat)
145
146
        # make the text box active right away
147
        if not self._tablet_mode:
148
            self.entry.grab_focus()
149
150
        self.entry.connect("move-cursor", self._cursor_moved_cb)
151
        self.entry.connect("changed", self._cursor_moved_cb)
152
153
        # toolbar
154
        toolbox = ToolbarBox()
155
156
        toolbox.toolbar.insert(ActivityToolbarButton(self), -1)
157
158
        self.voices = ComboBox()
159
        for name in sorted(voice.allVoices().keys()):
160
            vn = voice.allVoices()[name]
161
            n = name [ : 26 ] + ".."
162
            self.voices.append_item(vn, n)
163
164
        self.voices.select(voice.defaultVoice())
165
        all_voices = self.voices.get_model()
166
        brain_voices = brain.get_voices()
167
168
        mode_type = RadioToolButton(
169
                named_icon='mode-type',
170
                tooltip=_('Type something to hear it'))
171
        mode_type.connect('toggled', self.__toggled_mode_type_cb, all_voices)
172
        toolbox.toolbar.insert(mode_type, -1)
173
174
        mode_robot = RadioToolButton(
175
                named_icon='mode-robot',
176
                group=mode_type,
177
                tooltip=_('Ask robot any question'))
178
        mode_robot.connect('toggled', self.__toggled_mode_robot_cb,
179
                brain_voices)
180
        toolbox.toolbar.insert(mode_robot, -1)
181
182
        mode_chat = RadioToolButton(
183
                named_icon='mode-chat',
184
                group=mode_type,
185
                tooltip=_('Voice chat'))
186
        mode_chat.connect('toggled', self.__toggled_mode_chat_cb, all_voices)
187
        toolbox.toolbar.insert(mode_chat, -1)
188
189
        language_button = ToolbarButton(
190
                page=self.make_language_bar(),
191
                label=_('Language'),
192
                icon_name='module-language')
193
        toolbox.toolbar.insert(language_button, -1)
194
        
195
        voice_button = ToolbarButton(
196
                page=self.make_voice_bar(),
197
                label=_('Voice'),
198
                icon_name='voice')
199
        toolbox.toolbar.insert(voice_button, -1)
200
201
        face_button = ToolbarButton(
202
                page=self.make_face_bar(),
203
                label=_('Face'),
204
                icon_name='face')
205
        toolbox.toolbar.insert(face_button, -1)
206
207
        separator = gtk.SeparatorToolItem()
208
        separator.set_draw(False)
209
        separator.set_expand(True)
210
        toolbox.toolbar.insert(separator, -1)
211
212
        toolbox.toolbar.insert(StopButton(self), -1)
213
214
        toolbox.show_all()
215
        self.toolbar_box = toolbox
216
217
        gtk.gdk.screen_get_default().connect('size-changed',
218
                                             self._configure_cb)
219
220
        self._configure_cb()
221
222
    def _configure_cb(self, event=None):
223
        logger.debug('configure_cb')
224
        if gtk.gdk.screen_width() / 14 < style.GRID_CELL_SIZE:
225
            self.numeyesbar_label.set_label('')
226
        else:
227
            self.numeyesbar_label.set_label(_('Eyes number:'))
228
229
    def new_instance(self):
230
        self.voices.connect('changed', self.__changed_voices_cb)
231
        self.pitchadj.connect("value_changed", self.pitch_adjusted_cb,
232
                              self.pitchadj)
233
        self.rateadj.connect("value_changed", self.rate_adjusted_cb,
234
                             self.rateadj)
235
        self.numeyesadj.connect("value_changed", self.eyes_changed_cb, False)
236
        self.eyes_changed_cb(None, True)
237
        self.mouth_changed_cb(None, True)
238
239
        self.face.look_ahead()
240
241
        # say hello to the user
242
        presenceService = presenceservice.get_instance()
243
        xoOwner = presenceService.get_owner()
244
        if self._tablet_mode:
245
            self.entry.props.text = _("Hello %s.") \
246
                % xoOwner.props.nick.encode('utf-8', 'ignore')
247
        self.face.say_notification(_("Hello %s. Please Type something.") \
248
                                       % xoOwner.props.nick)
249
250
    def resume_instance(self, file_path):
251
        self.cfg = json.loads(file(file_path, 'r').read())
252
253
        status = self.face.status = \
254
            face.Status().deserialize(self.cfg['status'])
255
        self.voices.select(status.voice)
256
        self.pitchadj.value = self.face.status.pitch
257
        self.rateadj.value = self.face.status.rate
258
        self.numeyesadj.value = len(status.eyes)
259
        if status.mouth in MOUTHS:
260
            self.mouth_type[MOUTHS.index(status.mouth)].set_active(True)
261
        if status.eyes[0] in EYES:
262
            self.eye_type[EYES.index(status.eyes[0])].set_active(True)
263
        self.entry.props.text = self.cfg['text'].encode('utf-8', 'ignore')
264
        if not self._tablet_mode:
265
            for i in self.cfg['history']:
266
                self.entrycombo.append_text(i.encode('utf-8', 'ignore'))
267
268
        self.new_instance()
269
270
    def save_instance(self, file_path):
271
        if self._tablet_mode:
272
            if 'history' in self.cfg:
273
                history = self.cfg['history']  # retain old history
274
            else:
275
                history = []
276
        else:
277
            history = [unicode(i[0], 'utf-8', 'ignore') \
278
                           for i in self.entrycombo.get_model()]
279
        cfg = {'status': self.face.status.serialize(),
280
                'text': unicode(self.entry.props.text, 'utf-8', 'ignore'),
281
                'history': history,
282
                }
283
        file(file_path, 'w').write(json.dumps(cfg))
284
285
    def share_instance(self, connection, is_initiator):
286
        self.chat.messenger = Messenger(connection, is_initiator, self.chat)
287
288
    def _cursor_moved_cb(self, entry, *ignored):
289
        # make the eyes track the motion of the text cursor
290
        index = entry.props.cursor_position
291
        layout = entry.get_layout()
292
        pos = layout.get_cursor_pos(index)
293
        x = pos[0][0] / pango.SCALE - entry.props.scroll_offset
294
        y = entry.get_allocation().y
295
        self.face.look_at(pos=(x, y))
296
297
    def get_mouse(self):
298
        display = gtk.gdk.display_get_default()
299
        screen, mouseX, mouseY, modifiers = display.get_pointer()
300
        return mouseX, mouseY
301
302
    def _mouse_moved_cb(self, widget, event):
303
        # make the eyes track the motion of the mouse cursor
304
        self.face.look_at()
305
        self.chat.look_at()
306
307
    def _mouse_clicked_cb(self, widget, event):
308
        pass
309
310
    def make_language_bar(self):
311
        languagebar = gtk.Toolbar()
312
313
        voices_toolitem = ToolWidget(widget=self.voices)
314
        languagebar.insert(voices_toolitem, -1)
315
        languagebar.show_all()
316
        return languagebar
317
318
    def make_voice_bar(self):
319
        voicebar = gtk.Toolbar()
320
321
        self.pitchadj = gtk.Adjustment(self.face.status.pitch, 0,
322
                espeak.PITCH_MAX, 1, espeak.PITCH_MAX/10, 0)
323
        pitchbar = gtk.HScale(self.pitchadj)
324
        pitchbar.set_draw_value(False)
325
        # pitchbar.set_inverted(True)
326
        pitchbar.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
327
        pitchbar.set_size_request(240, 15)
328
329
        pitchbar_toolitem = ToolWidget(
330
                widget=pitchbar,
331
                label_text=_('Pitch:'))
332
        voicebar.insert(pitchbar_toolitem, -1)
333
334
        self.rateadj = gtk.Adjustment(self.face.status.rate, 0, espeak.RATE_MAX,
335
                1, espeak.RATE_MAX / 10, 0)
336
        ratebar = gtk.HScale(self.rateadj)
337
        ratebar.set_draw_value(False)
338
        # ratebar.set_inverted(True)
339
        ratebar.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
340
        ratebar.set_size_request(240, 15)
341
342
        ratebar_toolitem = ToolWidget(
343
                widget=ratebar,
344
                label_text=_('Rate:'))
345
        voicebar.insert(ratebar_toolitem, -1)
346
347
        voicebar.show_all()
348
        return voicebar
349
350
    def pitch_adjusted_cb(self, get, data=None):
351
        self.face.status.pitch = get.value
352
        self.face.say_notification(_("pitch adjusted"))
353
354
    def rate_adjusted_cb(self, get, data=None):
355
        self.face.status.rate = get.value
356
        self.face.say_notification(_("rate adjusted"))
357
358
    def make_face_bar(self):
359
        facebar = gtk.Toolbar()
360
361
        self.mouth_type = []
362
        self.mouth_type.append(RadioToolButton(
363
            named_icon='mouth',
364
            group=None,
365
            tooltip=_('Simple')))
366
        self.mouth_type[-1].connect('clicked', self.mouth_changed_cb, False)
367
        facebar.insert(self.mouth_type[-1], -1)
368
369
        self.mouth_type.append(RadioToolButton(
370
            named_icon='waveform',
371
            group=self.mouth_type[0],
372
            tooltip=_('Waveform')))
373
        self.mouth_type[-1].connect('clicked', self.mouth_changed_cb, False)
374
        facebar.insert(self.mouth_type[-1], -1)
375
376
        self.mouth_type.append(RadioToolButton(
377
            named_icon='frequency',
378
            group=self.mouth_type[0],
379
            tooltip=_('Frequency')))
380
        self.mouth_type[-1].connect('clicked', self.mouth_changed_cb, False)
381
        facebar.insert(self.mouth_type[-1], -1)
382
383
        separator = gtk.SeparatorToolItem()
384
        separator.set_draw(True)
385
        separator.set_expand(False)
386
        facebar.insert(separator, -1)
387
388
        self.eye_type = []
389
        self.eye_type.append(RadioToolButton(
390
            named_icon='eyes',
391
            group=None,
392
            tooltip=_('Round')))
393
        self.eye_type[-1].connect('clicked', self.eyes_changed_cb, False)
394
        facebar.insert(self.eye_type[-1], -1)
395
396
        self.eye_type.append(RadioToolButton(
397
            named_icon='glasses',
398
            group=self.eye_type[0],
399
            tooltip=_('Glasses')))
400
        self.eye_type[-1].connect('clicked', self.eyes_changed_cb, False)
401
        facebar.insert(self.eye_type[-1], -1)
402
403
        separator = gtk.SeparatorToolItem()
404
        separator.set_draw(False)
405
        separator.set_expand(False)
406
        facebar.insert(separator, -1)
407
408
        self.numeyesbar_label = gtk.Label()
409
        self.numeyesbar_label.set_text(_('Eyes number:'))
410
        toolitem = gtk.ToolItem()
411
        toolitem.add(self.numeyesbar_label)
412
        facebar.insert(toolitem, -1)
413
414
        self.numeyesadj = gtk.Adjustment(2, 1, 5, 1, 1, 0)
415
        numeyesbar = gtk.HScale(self.numeyesadj)
416
        numeyesbar.set_draw_value(False)
417
        numeyesbar.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
418
        numeyesbar.set_size_request(240, 15)
419
        toolitem = gtk.ToolItem()
420
        toolitem.add(numeyesbar)
421
        facebar.insert(toolitem, -1)
422
423
        facebar.show_all()
424
        return facebar
425
426
    def _get_active_mouth(self):
427
        for i, button in enumerate(self.mouth_type):
428
            if button.get_active():
429
                return MOUTHS[i]
430
431
    def mouth_changed_cb(self, ignored, quiet):
432
        value = self._get_active_mouth()
433
        if value is None:
434
            return
435
436
        self.face.status.mouth = value
437
        self._update_face()
438
439
        # this SegFaults: self.face.say(combo.get_active_text())
440
        if not quiet:
441
            self.face.say_notification(_("mouth changed"))
442
443
    def _get_active_eyes(self):
444
        for i, button in enumerate(self.eye_type):
445
            if button.get_active():
446
                return EYES[i]
447
448
    def eyes_changed_cb(self, ignored, quiet):
449
        if self.numeyesadj is None:
450
            return
451
452
        value = self._get_active_eyes()
453
        if value is None:
454
            return
455
456
        self.face.status.eyes = [value] * int(self.numeyesadj.value)
457
        self._update_face()
458
459
        # this SegFaults: self.face.say(self.eye_shape_combo.get_active_text())
460
        if not quiet:
461
            self.face.say_notification(_("eyes changed"))
462
463
    def _update_face(self):
464
        self.face.update()
465
        self.chat.update(self.face.status)
466
467
    def _combo_changed_cb(self, combo):
468
        # when a new item is chosen, make sure the text is selected
469
        if not self.entry.is_focus():
470
            if not self._tablet_mode:
471
                self.entry.grab_focus()
472
            self.entry.select_region(0, -1)
473
474
    def _entry_key_press_cb(self, combo, event):
475
        # make the up/down arrows navigate through our history
476
        if self._tablet_mode:
477
            return
478
        keyname = gtk.gdk.keyval_name(event.keyval)
479
        if keyname == "Up":
480
            index = self.entrycombo.get_active()
481
            if index>0:
482
                index-=1
483
            self.entrycombo.set_active(index)
484
            self.entry.select_region(0,-1)
485
            return True
486
        elif keyname == "Down":
487
            index = self.entrycombo.get_active()
488
            if index<len(self.entrycombo.get_model())-1:
489
                index+=1
490
            self.entrycombo.set_active(index)
491
            self.entry.select_region(0, -1)
492
            return True
493
        return False
494
495
    def _entry_activate_cb(self, entry):
496
        # the user pressed Return, say the text and clear it out
497
        text = entry.props.text
498
        if self._tablet_mode:
499
            self._dismiss_OSK(entry)
500
            timeout = DELAY_BEFORE_SPEAKING
501
        else:
502
            timeout = 100
503
        gobject.timeout_add(timeout, self._speak_the_text, entry, text)
504
505
    def _dismiss_OSK(self, entry):
506
        entry.hide()
507
        entry.show()
508
509
    def _talk_cb(self, button):
510
        text = self.entry.props.text
511
        self._speak_the_text(self.entry, text)
512
513
    def _speak_the_text(self, entry, text):
514
        if text:
515
            self.face.look_ahead()
516
517
            # speak the text
518
            if self._mode == MODE_BOT:
519
                self.face.say(
520
                        brain.respond(self.voices.props.value, text))
521
            else:
522
                self.face.say(text)
523
524
        if text and not self._tablet_mode:
525
            # add this text to our history unless it is the same as
526
            # the last item
527
            history = self.entrycombo.get_model()
528
            if len(history)==0 or history[-1][0] != text:
529
                self.entrycombo.append_text(text)
530
                # don't let the history get too big
531
                while len(history)>20:
532
                    self.entrycombo.remove_text(0)
533
                # select the new item
534
                self.entrycombo.set_active(len(history)-1)
535
        if text:
536
            # select the whole text
537
            entry.select_region(0, -1)
538
539
    def _activeCb(self, widget, pspec):
540
        # only generate sound when this activity is active
541
        if not self.props.active:
542
            self.face.shut_up()
543
            self.chat.shut_up()
544
545
    def _set_voice(self, new_voice):
546
        try:
547
            self.voices.handler_block_by_func(self.__changed_voices_cb)
548
            self.voices.select(new_voice)
549
            self.face.status.voice = new_voice
550
        finally:
551
            self.voices.handler_unblock_by_func(self.__changed_voices_cb)
552
553
    def __toggled_mode_type_cb(self, button, voices_model):
554
        if not button.props.active:
555
            return
556
557
        self._mode = MODE_TYPE
558
        self.chat.shut_up()
559
        self.face.shut_up()
560
        self.notebook.set_current_page(0)
561
562
        old_voice = self.voices.props.value
563
        self.voices.set_model(voices_model)
564
        self._set_voice(old_voice)
565
566
    def __toggled_mode_robot_cb(self, button, voices_model):
567
        if not button.props.active:
568
            return
569
570
        self._mode = MODE_BOT
571
        self.chat.shut_up()
572
        self.face.shut_up()
573
        self.notebook.set_current_page(0)
574
575
        old_voice = self.voices.props.value
576
        self.voices.set_model(voices_model)
577
578
        new_voice = [i[0] for i in voices_model
579
                if i[0].short_name == old_voice.short_name]
580
        if not new_voice:
581
            new_voice = brain.get_default_voice()
582
            sorry = _("Sorry, I can't speak %(old_voice)s, " \
583
                      "let's talk %(new_voice)s instead.") % {
584
                              'old_voice': old_voice.friendlyname,
585
                              'new_voice': new_voice.friendlyname,
586
                              }
587
        else:
588
            new_voice = new_voice[0]
589
            sorry = None
590
591
        self._set_voice(new_voice)
592
593
        if not brain.load(self, self.voices.props.value, sorry):
594
            if sorry:
595
                self.face.say_notification(sorry)
596
597
    def __toggled_mode_chat_cb(self, button, voices_model):
598
        if not button.props.active:
599
            return
600
601
        is_first_session = not self.chat.me.flags() & gtk.MAPPED
602
603
        self._mode = MODE_CHAT
604
        self.face.shut_up()
605
        self.notebook.set_current_page(1)
606
607
        old_voice = self.voices.props.value
608
        self.voices.set_model(voices_model)
609
        self._set_voice(old_voice)
610
611
        if is_first_session:
612
            self.chat.me.say_notification(
613
                    _("You are in off-line mode, share and invite someone."))
614
615
    def __changed_voices_cb(self, combo):
616
        voice = combo.props.value
617
        self.face.set_voice(voice)
618
        if self._mode == MODE_BOT:
619
            brain.load(self, voice)
620
621
622
# activate gtk threads when this module loads
623
gtk.gdk.threads_init()