Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AbiWordActivity.py10
-rw-r--r--icons/speak.svg76
-rw-r--r--speech.py44
-rw-r--r--speech_dispatcher.py116
-rw-r--r--speech_gst.py110
-rw-r--r--speechtoolbar.py194
6 files changed, 550 insertions, 0 deletions
diff --git a/AbiWordActivity.py b/AbiWordActivity.py
index 3861365..beef626 100644
--- a/AbiWordActivity.py
+++ b/AbiWordActivity.py
@@ -49,6 +49,8 @@ from toolbar import InsertToolbar
from toolbar import ParagraphToolbar
from widgets import ExportButtonFactory
from port import chooser
+import speech
+from speechtoolbar import SpeechToolbar
logger = logging.getLogger('write-activity')
@@ -132,6 +134,13 @@ class AbiWordActivity(activity.Activity):
content_box.show_all()
self.floating_image = False
+ if speech.supported:
+ self.speech_toolbar_button = ToolbarButton(icon_name='speak')
+ toolbar_box.toolbar.insert(self.speech_toolbar_button, -1)
+ self.speech_toolbar = SpeechToolbar(self)
+ self.speech_toolbar_button.set_page(self.speech_toolbar)
+ self.speech_toolbar_button.show()
+
separator = gtk.SeparatorToolItem()
separator.props.draw = False
separator.set_expand(True)
@@ -195,6 +204,7 @@ class AbiWordActivity(activity.Activity):
if self.abiword_canvas.get_selection('text/plain')[1] == 0:
logging.error('Setting default font to Sans in new documents')
self.abiword_canvas.set_font_name('Sans')
+ self.abiword_canvas.moveto_bod()
def get_preview(self):
if not hasattr(self.abiword_canvas, 'render_page_to_image'):
diff --git a/icons/speak.svg b/icons/speak.svg
new file mode 100644
index 0000000..5466410
--- /dev/null
+++ b/icons/speak.svg
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="42"
+ height="42"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.48.1 r9760"
+ version="1.0"
+ sodipodi:docname="speak.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ gridtolerance="10000"
+ guidetolerance="10"
+ objecttolerance="10"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="8.6621052"
+ inkscape:cx="20.354648"
+ inkscape:cy="27.567986"
+ inkscape:document-units="px"
+ inkscape:current-layer="g6207"
+ width="42px"
+ height="42px"
+ inkscape:window-width="1432"
+ inkscape:window-height="871"
+ inkscape:window-x="4"
+ inkscape:window-y="25"
+ showgrid="false"
+ inkscape:window-maximized="0" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="g6207"
+ transform="matrix(1.1572772,0,0,1.1572772,-4.2605572,6.7107864)">
+ <path
+ sodipodi:nodetypes="cccc"
+ id="path2327"
+ 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"
+ 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"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path4267"
+ d="m 5.4593796,11.583549 c 32.8803554,0 32.8803554,0.248154 32.8803554,0.248154"
+ style="fill:none;stroke:#ffffff;stroke-width:2.59229159;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+</svg>
diff --git a/speech.py b/speech.py
new file mode 100644
index 0000000..d950fbd
--- /dev/null
+++ b/speech.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2008, 2009 James D. Simmons
+# Copyright (C) 2009 Aleksey S. Lim
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+_logger = logging.getLogger('read-etexts-activity')
+
+supported = True
+
+try:
+ import gst
+ gst.element_factory_make('espeak')
+ from speech_gst import *
+ _logger.info('use gst-plugins-espeak')
+except Exception, e:
+ _logger.info('disable gst-plugins-espeak: %s' % e)
+ try:
+ from speech_dispatcher import *
+ _logger.info('use speech-dispatcher')
+ except Exception, e:
+ supported = False
+ _logger.info('disable speech: %s' % e)
+
+voice = 'default'
+pitch = 0
+rate = 0
+
+highlight_cb = None
+end_text_cb = None
+reset_cb = None
diff --git a/speech_dispatcher.py b/speech_dispatcher.py
new file mode 100644
index 0000000..4fad27f
--- /dev/null
+++ b/speech_dispatcher.py
@@ -0,0 +1,116 @@
+# Copyright (C) 2008 James D. Simmons
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import time
+import threading
+import speechd
+import logging
+
+import speech
+
+_logger = logging.getLogger('read-etexts-activity')
+
+done = True
+
+
+def voices():
+ try:
+ client = speechd.SSIPClient('readetextstest')
+ voices = client.list_synthesis_voices()
+ client.close()
+ return voices
+ except Exception, e:
+ _logger.warning('speech dispatcher not started: %s' % e)
+ return []
+
+
+def say(words):
+ try:
+ client = speechd.SSIPClient('readetextstest')
+ client.set_rate(int(speech.rate))
+ client.set_pitch(int(speech.pitch))
+ client.set_language(speech.voice[1])
+ client.speak(words)
+ client.close()
+ except Exception, e:
+ _logger.warning('speech dispatcher not running: %s' % e)
+
+
+def is_stopped():
+ return done
+
+
+def stop():
+ global done
+ done = True
+
+
+def play(words):
+ global thread
+ thread = EspeakThread(words)
+ thread.start()
+
+
+class EspeakThread(threading.Thread):
+
+ def __init__(self, words):
+ threading.Thread.__init__(self)
+ self.words = words
+
+ def run(self):
+ "This is the code that is executed when the start() method is called"
+ self.client = None
+ try:
+ self.client = speechd.SSIPClient('readetexts')
+ self.client._conn.send_command('SET', speechd.Scope.SELF,
+ 'SSML_MODE', "ON")
+ if speech.voice:
+ self.client.set_language(speech.voice[1])
+ self.client.set_rate(speech.rate)
+ self.client.set_pitch(speech.pitch)
+ self.client.speak(self.words, self.next_word_cb,
+ (speechd.CallbackType.INDEX_MARK,
+ speechd.CallbackType.END))
+ global done
+ done = False
+ while not done:
+ time.sleep(0.1)
+ self.cancel()
+ self.client.close()
+ except Exception, e:
+ _logger.warning('speech-dispatcher client not created: %s' % e)
+
+ def cancel(self):
+ if self.client:
+ try:
+ self.client.cancel()
+ except Exception, e:
+ _logger.warning('speech dispatcher cancel failed: %s' % e)
+
+ def next_word_cb(self, type, **kargs):
+ if type == speechd.CallbackType.INDEX_MARK:
+ mark = kargs['index_mark']
+ word_count = int(mark)
+ gtk.gdk.threads_enter()
+ speech.highlight_cb(word_count)
+ gtk.gdk.threads_leave()
+ elif type == speechd.CallbackType.END:
+ gtk.gdk.threads_enter()
+ speech.reset_cb()
+ gtk.gdk.threads_leave()
+ global done
+ done = True
diff --git a/speech_gst.py b/speech_gst.py
new file mode 100644
index 0000000..b827ad9
--- /dev/null
+++ b/speech_gst.py
@@ -0,0 +1,110 @@
+# Copyright (C) 2009 Aleksey S. Lim
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gst
+import logging
+
+import speech
+
+_logger = logging.getLogger('read-etexts-activity')
+
+
+def get_all_voices():
+ all_voices = {}
+ for voice in gst.element_factory_make('espeak').props.voices:
+ name, language, dialect = voice
+ if dialect != 'none':
+ all_voices[language + '_' + dialect] = name
+ else:
+ all_voices[language] = name
+ return all_voices
+
+
+def _message_cb(bus, message, pipe):
+ if message.type == gst.MESSAGE_EOS:
+ pipe.set_state(gst.STATE_NULL)
+ if speech.end_text_cb != None:
+ speech.end_text_cb()
+ if message.type == gst.MESSAGE_ERROR:
+ pipe.set_state(gst.STATE_NULL)
+ if pipe is play_speaker[1]:
+ speech.reset_cb()
+ elif message.type == gst.MESSAGE_ELEMENT and \
+ message.structure.get_name() == 'espeak-mark':
+ mark = message.structure['mark']
+ speech.highlight_cb(int(mark))
+
+
+def _create_pipe():
+ pipe = gst.Pipeline('pipeline')
+
+ source = gst.element_factory_make('espeak', 'source')
+ pipe.add(source)
+
+ sink = gst.element_factory_make('autoaudiosink', 'sink')
+ pipe.add(sink)
+ source.link(sink)
+
+ bus = pipe.get_bus()
+ bus.add_signal_watch()
+ bus.connect('message', _message_cb, pipe)
+
+ return (source, pipe)
+
+
+def _speech(speaker, words):
+ speaker[0].props.pitch = speech.pitch
+ speaker[0].props.rate = speech.rate
+ speaker[0].props.voice = speech.voice[1]
+ speaker[0].props.text = words
+ speaker[1].set_state(gst.STATE_NULL)
+ speaker[1].set_state(gst.STATE_PLAYING)
+
+
+info_speaker = _create_pipe()
+play_speaker = _create_pipe()
+play_speaker[0].props.track = 2
+
+
+def voices():
+ return info_speaker[0].props.voices
+
+
+def say(words):
+ _speech(info_speaker, words)
+
+
+def play(words):
+ _speech(play_speaker, words)
+
+
+def pause():
+ play_speaker[1].set_state(gst.STATE_PAUSED)
+
+
+def continue_play():
+ play_speaker[1].set_state(gst.STATE_PLAYING)
+
+
+def is_stopped():
+ for i in play_speaker[1].get_state():
+ if isinstance(i, gst.State) and i == gst.STATE_NULL:
+ return True
+ return False
+
+
+def stop():
+ play_speaker[1].set_state(gst.STATE_NULL)
diff --git a/speechtoolbar.py b/speechtoolbar.py
new file mode 100644
index 0000000..ca6eae5
--- /dev/null
+++ b/speechtoolbar.py
@@ -0,0 +1,194 @@
+# Copyright (C) 2006, Red Hat, Inc.
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import simplejson
+from gettext import gettext as _
+import logging
+
+import gtk
+import gconf
+
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.toggletoolbutton import ToggleToolButton
+from sugar.graphics.combobox import ComboBox
+from sugar.graphics.toolcombobox import ToolComboBox
+
+import speech
+
+
+class SpeechToolbar(gtk.Toolbar):
+
+ def __init__(self, activity):
+ gtk.Toolbar.__init__(self)
+ self._activity = activity
+ if not speech.supported:
+ return
+ self.is_paused = False
+ self._cnf_client = gconf.client_get_default()
+ self.load_speech_parameters()
+
+ self.sorted_voices = [i for i in speech.voices()]
+ self.sorted_voices.sort(self.compare_voices)
+ default = 0
+ for voice in self.sorted_voices:
+ if voice[0] == speech.voice[0]:
+ break
+ default = default + 1
+
+ # Play button
+ self.play_btn = ToggleToolButton('media-playback-start')
+ self.play_btn.show()
+ self.play_btn.connect('toggled', self.play_cb)
+ self.insert(self.play_btn, -1)
+ self.play_btn.set_tooltip(_('Play / Pause'))
+
+ # Stop button
+ self.stop_btn = ToolButton('media-playback-stop')
+ self.stop_btn.show()
+ self.stop_btn.connect('clicked', self.stop_cb)
+ self.stop_btn.set_sensitive(False)
+ self.insert(self.stop_btn, -1)
+ self.stop_btn.set_tooltip(_('Stop'))
+
+ self.voice_combo = ComboBox()
+ for voice in self.sorted_voices:
+ self.voice_combo.append_item(voice, voice[0])
+ self.voice_combo.set_active(default)
+
+ self.voice_combo.connect('changed', self.voice_changed_cb)
+ combotool = ToolComboBox(self.voice_combo)
+ self.insert(combotool, -1)
+ combotool.show()
+ speech.reset_buttons_cb = self.reset_buttons_cb
+ speech.end_text_cb = self.reset_buttons_cb
+
+ def compare_voices(self, a, b):
+ if a[0].lower() == b[0].lower():
+ return 0
+ if a[0] .lower() < b[0].lower():
+ return -1
+ if a[0] .lower() > b[0].lower():
+ return 1
+
+ def voice_changed_cb(self, combo):
+ speech.voice = combo.props.value
+ speech.say(speech.voice[0])
+ self.save_speech_parameters()
+
+ def load_speech_parameters(self):
+ speech_parameters = {}
+ data_path = os.path.join(self._activity.get_activity_root(), 'data')
+ data_file_name = os.path.join(data_path, 'speech_params.json')
+ if os.path.exists(data_file_name):
+ f = open(data_file_name, 'r')
+ try:
+ speech_parameters = simplejson.load(f)
+ speech.voice = speech_parameters['voice']
+ finally:
+ f.close()
+ else:
+ speech.voice = self.get_default_voice()
+ logging.error('Default voice %s', speech.voice)
+
+ self._cnf_client.add_dir('/desktop/sugar/speech',
+ gconf.CLIENT_PRELOAD_NONE)
+ speech.pitch = self._cnf_client.get_int('/desktop/sugar/speech/pitch')
+ speech.rate = self._cnf_client.get_int('/desktop/sugar/speech/rate')
+ self._cnf_client.notify_add('/desktop/sugar/speech/pitch', \
+ self.__conf_changed_cb, None)
+ self._cnf_client.notify_add('/desktop/sugar/speech/rate', \
+ self.__conf_changed_cb, None)
+
+ def get_default_voice(self):
+ """Try to figure out the default voice, from the current locale ($LANG)
+ Fall back to espeak's voice called Default."""
+ voices = speech.get_all_voices()
+
+ locale = os.environ.get('LANG', '')
+ language_location = locale.split('.', 1)[0].lower()
+ language = language_location.split('_')[0]
+ variant = ''
+ if language_location.find('_') > -1:
+ variant = language_location.split('_')[1]
+ # if the language is es but not es_es default to es_la (latin voice)
+ if language == 'es' and language_location != 'es_es':
+ language_location = 'es_la'
+
+ best = voices.get(language_location) or voices.get(language) \
+ or 'default'
+ logging.debug('Best voice for LANG %s seems to be %s',
+ locale, best)
+ return [best, language, variant]
+
+ def __conf_changed_cb(self, client, connection_id, entry, args):
+ key = entry.get_key()
+ value = client.get_int(key)
+ if key == '/desktop/sugar/speech/pitch':
+ speech.pitch = value
+ if key == '/desktop/sugar/speech/rate':
+ speech.rate = value
+
+ def save_speech_parameters(self):
+ speech_parameters = {}
+ speech_parameters['voice'] = speech.voice
+ data_path = os.path.join(self._activity.get_activity_root(), 'data')
+ data_file_name = os.path.join(data_path, 'speech_params.json')
+ f = open(data_file_name, 'w')
+ try:
+ simplejson.dump(speech_parameters, f)
+ finally:
+ f.close()
+
+ def reset_buttons_cb(self):
+ logging.error('reset buttons')
+ self.play_btn.set_named_icon('media-playback-start')
+ self.stop_btn.set_sensitive(False)
+ self.play_btn.set_active(False)
+ self.is_paused = False
+
+ def play_cb(self, widget):
+ self.stop_btn.set_sensitive(True)
+ if widget.get_active():
+ self.play_btn.set_named_icon('media-playback-pause')
+ logging.error('Paused %s', self.is_paused)
+ if not self.is_paused:
+ # get the text to speech, if there are a selection,
+ # play selected text, if not, play all
+ abi = self._activity.abiword_canvas
+ selection = abi.get_selection('text/plain')
+ if selection[1] == 0:
+ # nothing selected
+ abi.select_all()
+ text = abi.get_selection('text/plain')[0]
+ abi.moveto_bod()
+ else:
+ text = selection[0]
+ speech.play(text)
+ else:
+ logging.error('Continue play')
+ speech.continue_play()
+ else:
+ self.play_btn.set_named_icon('media-playback-start')
+ self.is_paused = True
+ speech.pause()
+
+ def stop_cb(self, widget):
+ self.stop_btn.set_sensitive(False)
+ self.play_btn.set_named_icon('media-playback-start')
+ self.play_btn.set_active(False)
+ self.is_paused = False
+ speech.stop()