Codebase list sugar-write-activity / fa52f26
Add text to speech functionality to Write - SL #3266 Ass discussed with the Learning Team, Write need a inmediate access to Text to Speech, the global tts feature is too indirect. Signed-off-by: Gonzalo Odiard <gonzalo@laptop.org> Gonzalo Odiard 12 years ago
6 changed file(s) with 550 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
4848 from toolbar import ParagraphToolbar
4949 from widgets import ExportButtonFactory
5050 from port import chooser
51 import speech
52 from speechtoolbar import SpeechToolbar
5153
5254 logger = logging.getLogger('write-activity')
5355
130132 content_box.pack_start(image_floating_checkbutton)
131133 content_box.show_all()
132134 self.floating_image = False
135
136 if speech.supported:
137 self.speech_toolbar_button = ToolbarButton(icon_name='speak')
138 toolbar_box.toolbar.insert(self.speech_toolbar_button, -1)
139 self.speech_toolbar = SpeechToolbar(self)
140 self.speech_toolbar_button.set_page(self.speech_toolbar)
141 self.speech_toolbar_button.show()
133142
134143 separator = gtk.SeparatorToolItem()
135144 separator.props.draw = False
194203 if self.abiword_canvas.get_selection('text/plain')[1] == 0:
195204 logging.error('Setting default font to Sans in new documents')
196205 self.abiword_canvas.set_font_name('Sans')
206 self.abiword_canvas.moveto_bod()
197207
198208 def get_preview(self):
199209 if not hasattr(self.abiword_canvas, 'render_page_to_image'):
0 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
1 <!-- Created with Inkscape (http://www.inkscape.org/) -->
2
3 <svg
4 xmlns:dc="http://purl.org/dc/elements/1.1/"
5 xmlns:cc="http://creativecommons.org/ns#"
6 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
7 xmlns:svg="http://www.w3.org/2000/svg"
8 xmlns="http://www.w3.org/2000/svg"
9 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
10 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
11 width="42"
12 height="42"
13 id="svg2"
14 sodipodi:version="0.32"
15 inkscape:version="0.48.1 r9760"
16 version="1.0"
17 sodipodi:docname="speak.svg"
18 inkscape:output_extension="org.inkscape.output.svg.inkscape">
19 <defs
20 id="defs4" />
21 <sodipodi:namedview
22 id="base"
23 pagecolor="#ffffff"
24 bordercolor="#666666"
25 borderopacity="1.0"
26 gridtolerance="10000"
27 guidetolerance="10"
28 objecttolerance="10"
29 inkscape:pageopacity="0.0"
30 inkscape:pageshadow="2"
31 inkscape:zoom="8.6621052"
32 inkscape:cx="20.354648"
33 inkscape:cy="27.567986"
34 inkscape:document-units="px"
35 inkscape:current-layer="g6207"
36 width="42px"
37 height="42px"
38 inkscape:window-width="1432"
39 inkscape:window-height="871"
40 inkscape:window-x="4"
41 inkscape:window-y="25"
42 showgrid="false"
43 inkscape:window-maximized="0" />
44 <metadata
45 id="metadata7">
46 <rdf:RDF>
47 <cc:Work
48 rdf:about="">
49 <dc:format>image/svg+xml</dc:format>
50 <dc:type
51 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
52 </cc:Work>
53 </rdf:RDF>
54 </metadata>
55 <g
56 inkscape:label="Layer 1"
57 inkscape:groupmode="layer"
58 id="layer1">
59 <g
60 id="g6207"
61 transform="matrix(1.1572772,0,0,1.1572772,-4.2605572,6.7107864)">
62 <path
63 sodipodi:nodetypes="cccc"
64 id="path2327"
65 d="M 5.211226,11.583551 C 16.756465,23.75712 27.826101,22.557765 38.711967,11.58355 34.369968,8.2657832 27.814245,-0.12525692 21.961596,5.2556308 13.884782,0.35931958 10.160766,7.6360152 5.211226,11.583551 z"
66 style="fill:#404040;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:2.59229159;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
67 inkscape:connector-curvature="0" />
68 <path
69 id="path4267"
70 d="m 5.4593796,11.583549 c 32.8803554,0 32.8803554,0.248154 32.8803554,0.248154"
71 style="fill:none;stroke:#ffffff;stroke-width:2.59229159;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
72 inkscape:connector-curvature="0" />
73 </g>
74 </g>
75 </svg>
0 # Copyright (C) 2008, 2009 James D. Simmons
1 # Copyright (C) 2009 Aleksey S. Lim
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16
17 import logging
18
19 _logger = logging.getLogger('read-etexts-activity')
20
21 supported = True
22
23 try:
24 import gst
25 gst.element_factory_make('espeak')
26 from speech_gst import *
27 _logger.info('use gst-plugins-espeak')
28 except Exception, e:
29 _logger.info('disable gst-plugins-espeak: %s' % e)
30 try:
31 from speech_dispatcher import *
32 _logger.info('use speech-dispatcher')
33 except Exception, e:
34 supported = False
35 _logger.info('disable speech: %s' % e)
36
37 voice = 'default'
38 pitch = 0
39 rate = 0
40
41 highlight_cb = None
42 end_text_cb = None
43 reset_cb = None
0 # Copyright (C) 2008 James D. Simmons
1 #
2 # This program is free software; you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation; either version 2 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
15
16 import gtk
17 import time
18 import threading
19 import speechd
20 import logging
21
22 import speech
23
24 _logger = logging.getLogger('read-etexts-activity')
25
26 done = True
27
28
29 def voices():
30 try:
31 client = speechd.SSIPClient('readetextstest')
32 voices = client.list_synthesis_voices()
33 client.close()
34 return voices
35 except Exception, e:
36 _logger.warning('speech dispatcher not started: %s' % e)
37 return []
38
39
40 def say(words):
41 try:
42 client = speechd.SSIPClient('readetextstest')
43 client.set_rate(int(speech.rate))
44 client.set_pitch(int(speech.pitch))
45 client.set_language(speech.voice[1])
46 client.speak(words)
47 client.close()
48 except Exception, e:
49 _logger.warning('speech dispatcher not running: %s' % e)
50
51
52 def is_stopped():
53 return done
54
55
56 def stop():
57 global done
58 done = True
59
60
61 def play(words):
62 global thread
63 thread = EspeakThread(words)
64 thread.start()
65
66
67 class EspeakThread(threading.Thread):
68
69 def __init__(self, words):
70 threading.Thread.__init__(self)
71 self.words = words
72
73 def run(self):
74 "This is the code that is executed when the start() method is called"
75 self.client = None
76 try:
77 self.client = speechd.SSIPClient('readetexts')
78 self.client._conn.send_command('SET', speechd.Scope.SELF,
79 'SSML_MODE', "ON")
80 if speech.voice:
81 self.client.set_language(speech.voice[1])
82 self.client.set_rate(speech.rate)
83 self.client.set_pitch(speech.pitch)
84 self.client.speak(self.words, self.next_word_cb,
85 (speechd.CallbackType.INDEX_MARK,
86 speechd.CallbackType.END))
87 global done
88 done = False
89 while not done:
90 time.sleep(0.1)
91 self.cancel()
92 self.client.close()
93 except Exception, e:
94 _logger.warning('speech-dispatcher client not created: %s' % e)
95
96 def cancel(self):
97 if self.client:
98 try:
99 self.client.cancel()
100 except Exception, e:
101 _logger.warning('speech dispatcher cancel failed: %s' % e)
102
103 def next_word_cb(self, type, **kargs):
104 if type == speechd.CallbackType.INDEX_MARK:
105 mark = kargs['index_mark']
106 word_count = int(mark)
107 gtk.gdk.threads_enter()
108 speech.highlight_cb(word_count)
109 gtk.gdk.threads_leave()
110 elif type == speechd.CallbackType.END:
111 gtk.gdk.threads_enter()
112 speech.reset_cb()
113 gtk.gdk.threads_leave()
114 global done
115 done = True
0 # Copyright (C) 2009 Aleksey S. Lim
1 #
2 # This program is free software; you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation; either version 2 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
15
16 import gst
17 import logging
18
19 import speech
20
21 _logger = logging.getLogger('read-etexts-activity')
22
23
24 def get_all_voices():
25 all_voices = {}
26 for voice in gst.element_factory_make('espeak').props.voices:
27 name, language, dialect = voice
28 if dialect != 'none':
29 all_voices[language + '_' + dialect] = name
30 else:
31 all_voices[language] = name
32 return all_voices
33
34
35 def _message_cb(bus, message, pipe):
36 if message.type == gst.MESSAGE_EOS:
37 pipe.set_state(gst.STATE_NULL)
38 if speech.end_text_cb != None:
39 speech.end_text_cb()
40 if message.type == gst.MESSAGE_ERROR:
41 pipe.set_state(gst.STATE_NULL)
42 if pipe is play_speaker[1]:
43 speech.reset_cb()
44 elif message.type == gst.MESSAGE_ELEMENT and \
45 message.structure.get_name() == 'espeak-mark':
46 mark = message.structure['mark']
47 speech.highlight_cb(int(mark))
48
49
50 def _create_pipe():
51 pipe = gst.Pipeline('pipeline')
52
53 source = gst.element_factory_make('espeak', 'source')
54 pipe.add(source)
55
56 sink = gst.element_factory_make('autoaudiosink', 'sink')
57 pipe.add(sink)
58 source.link(sink)
59
60 bus = pipe.get_bus()
61 bus.add_signal_watch()
62 bus.connect('message', _message_cb, pipe)
63
64 return (source, pipe)
65
66
67 def _speech(speaker, words):
68 speaker[0].props.pitch = speech.pitch
69 speaker[0].props.rate = speech.rate
70 speaker[0].props.voice = speech.voice[1]
71 speaker[0].props.text = words
72 speaker[1].set_state(gst.STATE_NULL)
73 speaker[1].set_state(gst.STATE_PLAYING)
74
75
76 info_speaker = _create_pipe()
77 play_speaker = _create_pipe()
78 play_speaker[0].props.track = 2
79
80
81 def voices():
82 return info_speaker[0].props.voices
83
84
85 def say(words):
86 _speech(info_speaker, words)
87
88
89 def play(words):
90 _speech(play_speaker, words)
91
92
93 def pause():
94 play_speaker[1].set_state(gst.STATE_PAUSED)
95
96
97 def continue_play():
98 play_speaker[1].set_state(gst.STATE_PLAYING)
99
100
101 def is_stopped():
102 for i in play_speaker[1].get_state():
103 if isinstance(i, gst.State) and i == gst.STATE_NULL:
104 return True
105 return False
106
107
108 def stop():
109 play_speaker[1].set_state(gst.STATE_NULL)
0 # Copyright (C) 2006, Red Hat, Inc.
1 #
2 # This program is free software; you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation; either version 2 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
15
16 import os
17 import simplejson
18 from gettext import gettext as _
19 import logging
20
21 import gtk
22 import gconf
23
24 from sugar.graphics.toolbutton import ToolButton
25 from sugar.graphics.toggletoolbutton import ToggleToolButton
26 from sugar.graphics.combobox import ComboBox
27 from sugar.graphics.toolcombobox import ToolComboBox
28
29 import speech
30
31
32 class SpeechToolbar(gtk.Toolbar):
33
34 def __init__(self, activity):
35 gtk.Toolbar.__init__(self)
36 self._activity = activity
37 if not speech.supported:
38 return
39 self.is_paused = False
40 self._cnf_client = gconf.client_get_default()
41 self.load_speech_parameters()
42
43 self.sorted_voices = [i for i in speech.voices()]
44 self.sorted_voices.sort(self.compare_voices)
45 default = 0
46 for voice in self.sorted_voices:
47 if voice[0] == speech.voice[0]:
48 break
49 default = default + 1
50
51 # Play button
52 self.play_btn = ToggleToolButton('media-playback-start')
53 self.play_btn.show()
54 self.play_btn.connect('toggled', self.play_cb)
55 self.insert(self.play_btn, -1)
56 self.play_btn.set_tooltip(_('Play / Pause'))
57
58 # Stop button
59 self.stop_btn = ToolButton('media-playback-stop')
60 self.stop_btn.show()
61 self.stop_btn.connect('clicked', self.stop_cb)
62 self.stop_btn.set_sensitive(False)
63 self.insert(self.stop_btn, -1)
64 self.stop_btn.set_tooltip(_('Stop'))
65
66 self.voice_combo = ComboBox()
67 for voice in self.sorted_voices:
68 self.voice_combo.append_item(voice, voice[0])
69 self.voice_combo.set_active(default)
70
71 self.voice_combo.connect('changed', self.voice_changed_cb)
72 combotool = ToolComboBox(self.voice_combo)
73 self.insert(combotool, -1)
74 combotool.show()
75 speech.reset_buttons_cb = self.reset_buttons_cb
76 speech.end_text_cb = self.reset_buttons_cb
77
78 def compare_voices(self, a, b):
79 if a[0].lower() == b[0].lower():
80 return 0
81 if a[0] .lower() < b[0].lower():
82 return -1
83 if a[0] .lower() > b[0].lower():
84 return 1
85
86 def voice_changed_cb(self, combo):
87 speech.voice = combo.props.value
88 speech.say(speech.voice[0])
89 self.save_speech_parameters()
90
91 def load_speech_parameters(self):
92 speech_parameters = {}
93 data_path = os.path.join(self._activity.get_activity_root(), 'data')
94 data_file_name = os.path.join(data_path, 'speech_params.json')
95 if os.path.exists(data_file_name):
96 f = open(data_file_name, 'r')
97 try:
98 speech_parameters = simplejson.load(f)
99 speech.voice = speech_parameters['voice']
100 finally:
101 f.close()
102 else:
103 speech.voice = self.get_default_voice()
104 logging.error('Default voice %s', speech.voice)
105
106 self._cnf_client.add_dir('/desktop/sugar/speech',
107 gconf.CLIENT_PRELOAD_NONE)
108 speech.pitch = self._cnf_client.get_int('/desktop/sugar/speech/pitch')
109 speech.rate = self._cnf_client.get_int('/desktop/sugar/speech/rate')
110 self._cnf_client.notify_add('/desktop/sugar/speech/pitch', \
111 self.__conf_changed_cb, None)
112 self._cnf_client.notify_add('/desktop/sugar/speech/rate', \
113 self.__conf_changed_cb, None)
114
115 def get_default_voice(self):
116 """Try to figure out the default voice, from the current locale ($LANG)
117 Fall back to espeak's voice called Default."""
118 voices = speech.get_all_voices()
119
120 locale = os.environ.get('LANG', '')
121 language_location = locale.split('.', 1)[0].lower()
122 language = language_location.split('_')[0]
123 variant = ''
124 if language_location.find('_') > -1:
125 variant = language_location.split('_')[1]
126 # if the language is es but not es_es default to es_la (latin voice)
127 if language == 'es' and language_location != 'es_es':
128 language_location = 'es_la'
129
130 best = voices.get(language_location) or voices.get(language) \
131 or 'default'
132 logging.debug('Best voice for LANG %s seems to be %s',
133 locale, best)
134 return [best, language, variant]
135
136 def __conf_changed_cb(self, client, connection_id, entry, args):
137 key = entry.get_key()
138 value = client.get_int(key)
139 if key == '/desktop/sugar/speech/pitch':
140 speech.pitch = value
141 if key == '/desktop/sugar/speech/rate':
142 speech.rate = value
143
144 def save_speech_parameters(self):
145 speech_parameters = {}
146 speech_parameters['voice'] = speech.voice
147 data_path = os.path.join(self._activity.get_activity_root(), 'data')
148 data_file_name = os.path.join(data_path, 'speech_params.json')
149 f = open(data_file_name, 'w')
150 try:
151 simplejson.dump(speech_parameters, f)
152 finally:
153 f.close()
154
155 def reset_buttons_cb(self):
156 logging.error('reset buttons')
157 self.play_btn.set_named_icon('media-playback-start')
158 self.stop_btn.set_sensitive(False)
159 self.play_btn.set_active(False)
160 self.is_paused = False
161
162 def play_cb(self, widget):
163 self.stop_btn.set_sensitive(True)
164 if widget.get_active():
165 self.play_btn.set_named_icon('media-playback-pause')
166 logging.error('Paused %s', self.is_paused)
167 if not self.is_paused:
168 # get the text to speech, if there are a selection,
169 # play selected text, if not, play all
170 abi = self._activity.abiword_canvas
171 selection = abi.get_selection('text/plain')
172 if selection[1] == 0:
173 # nothing selected
174 abi.select_all()
175 text = abi.get_selection('text/plain')[0]
176 abi.moveto_bod()
177 else:
178 text = selection[0]
179 speech.play(text)
180 else:
181 logging.error('Continue play')
182 speech.continue_play()
183 else:
184 self.play_btn.set_named_icon('media-playback-start')
185 self.is_paused = True
186 speech.pause()
187
188 def stop_cb(self, widget):
189 self.stop_btn.set_sensitive(False)
190 self.play_btn.set_named_icon('media-playback-start')
191 self.play_btn.set_active(False)
192 self.is_paused = False
193 speech.stop()