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
48 | 48 | from toolbar import ParagraphToolbar |
49 | 49 | from widgets import ExportButtonFactory |
50 | 50 | from port import chooser |
51 | import speech | |
52 | from speechtoolbar import SpeechToolbar | |
51 | 53 | |
52 | 54 | logger = logging.getLogger('write-activity') |
53 | 55 | |
130 | 132 | content_box.pack_start(image_floating_checkbutton) |
131 | 133 | content_box.show_all() |
132 | 134 | 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() | |
133 | 142 | |
134 | 143 | separator = gtk.SeparatorToolItem() |
135 | 144 | separator.props.draw = False |
194 | 203 | if self.abiword_canvas.get_selection('text/plain')[1] == 0: |
195 | 204 | logging.error('Setting default font to Sans in new documents') |
196 | 205 | self.abiword_canvas.set_font_name('Sans') |
206 | self.abiword_canvas.moveto_bod() | |
197 | 207 | |
198 | 208 | def get_preview(self): |
199 | 209 | 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() |