Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGonzalo Odiard <godiard@sugarlabs.org>2011-02-21 20:11:04 (GMT)
committer Gonzalo Odiard <godiard@sugarlabs.org>2011-02-21 20:11:04 (GMT)
commita04af069262aed24bcceae48d414906a36ec28e2 (patch)
tree332eb32a6a7fa59cac6b9cc33c7c740905fc00fe
parent66b9606d49af508a2414121d8aaa8956de1d037e (diff)
Add text to speech capability to text backend
Too much code stolen from Read EText activity
-rw-r--r--epubadapter.py3
-rw-r--r--evinceadapter.py3
-rw-r--r--readactivity.py15
-rw-r--r--readtoolbar.py71
-rw-r--r--speech.py43
-rw-r--r--speech_dispatcher.py116
-rw-r--r--speech_gst.py87
-rw-r--r--textadapter.py69
8 files changed, 405 insertions, 2 deletions
diff --git a/epubadapter.py b/epubadapter.py
index b03966f..2b1b597 100644
--- a/epubadapter.py
+++ b/epubadapter.py
@@ -49,6 +49,9 @@ class EpubViewer(epubview.EpubView):
def can_highlight(self):
return False
+ def can_do_text_to_speech(self):
+ return False
+
def connect_zoom_handler(self, handler):
self._zoom_handler = handler
self._view_notify_zoom_handler = \
diff --git a/evinceadapter.py b/evinceadapter.py
index c37562c..a4d8aca 100644
--- a/evinceadapter.py
+++ b/evinceadapter.py
@@ -112,6 +112,9 @@ class EvinceViewer():
def can_highlight(self):
return False
+ def can_do_text_to_speech(self):
+ return False
+
def get_zoom(self):
'''
Returns the current zoom level
diff --git a/readactivity.py b/readactivity.py
index 1fb2ffc..8e1ce1c 100644
--- a/readactivity.py
+++ b/readactivity.py
@@ -45,13 +45,16 @@ from sugar import mime
from sugar.datastore import datastore
from sugar.graphics.objectchooser import ObjectChooser
-from readtoolbar import EditToolbar, ViewToolbar
+from readtoolbar import EditToolbar
+from readtoolbar import ViewToolbar
+from readtoolbar import SpeechToolbar
from readsidebar import Sidebar
from readtopbar import TopBar
from readdb import BookmarkManager
import epubadapter
import evinceadapter
import textadapter
+import speech
_HARDWARE_MANAGER_INTERFACE = 'org.laptop.HardwareManager'
_HARDWARE_MANAGER_SERVICE = 'org.laptop.HardwareManager'
@@ -244,6 +247,11 @@ class ReadActivity(activity.Activity):
toolbar_box.toolbar.insert(self._highlight_item, -1)
self._highlight_item.show_all()
+ self.speech_toolbar = SpeechToolbar(self)
+ self.speech_toolbar_button = ToolbarButton(page=self.speech_toolbar,
+ icon_name='speak')
+ toolbar_box.toolbar.insert(self.speech_toolbar_button, -1)
+
separator = gtk.SeparatorToolItem()
separator.props.draw = False
separator.set_expand(True)
@@ -790,7 +798,7 @@ class ReadActivity(activity.Activity):
mimetype = mime.get_for_file(filepath)
if mimetype == 'application/epub+zip':
self._view = epubadapter.EpubViewer()
- elif mimetype == 'text/plain' or mimetype == 'application/zip' :
+ elif mimetype == 'text/plain' or mimetype == 'application/zip':
self._view = textadapter.TextViewer()
else:
self._view = evinceadapter.EvinceViewer()
@@ -833,6 +841,9 @@ class ReadActivity(activity.Activity):
self._view_toolbar._update_zoom_buttons()
if not self._view.can_highlight():
self._highlight_item.hide()
+ if speech.supported and self._view.can_do_text_to_speech():
+ self.speech_toolbar_button.show()
+ self.speech_toolbar_button.show()
def _share_document(self):
"""Share the document."""
diff --git a/readtoolbar.py b/readtoolbar.py
index ed57fde..1da23bf 100644
--- a/readtoolbar.py
+++ b/readtoolbar.py
@@ -20,11 +20,16 @@ import logging
import gobject
import gtk
+from sugar.graphics.combobox import ComboBox
from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.toggletoolbutton import ToggleToolButton
+from sugar.graphics.toolcombobox import ToolComboBox
from sugar.graphics.menuitem import MenuItem
from sugar.graphics import iconentry
from sugar.activity import activity
+import speech
+
class EditToolbar(activity.EditToolbar):
@@ -288,3 +293,69 @@ class ViewToolbar(gtk.Toolbar):
def _fullscreen_cb(self, button):
self.emit('go-fullscreen')
+
+
+class SpeechToolbar(gtk.Toolbar):
+
+ def __init__(self, activity):
+ gtk.Toolbar.__init__(self)
+ voicebar = gtk.Toolbar()
+ self.activity = activity
+ 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] == 'default':
+ break
+ default = default + 1
+
+ # Play button Image
+ play_img = gtk.Image()
+ play_img.show()
+ play_img.set_from_icon_name('media-playback-start',
+ gtk.ICON_SIZE_LARGE_TOOLBAR)
+
+ # Pause button Image
+ pause_img = gtk.Image()
+ pause_img.show()
+ pause_img.set_from_icon_name('media-playback-pause',
+ gtk.ICON_SIZE_LARGE_TOOLBAR)
+
+ # Play button
+ self.play_btn = ToggleToolButton('media-playback-start')
+ self.play_btn.show()
+ self.play_btn.connect('toggled', self.play_cb, [play_img, pause_img])
+ self.insert(self.play_btn, -1)
+ self.play_btn.set_tooltip(_('Play / Pause'))
+
+ 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)
+ speech.voice = self.voice_combo.props.value
+ combotool = ToolComboBox(self.voice_combo)
+ self.insert(combotool, -1)
+ combotool.show()
+
+ 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
+ if self.activity != None:
+ speech.say(speech.voice[0])
+
+ def play_cb(self, widget, images):
+ widget.set_icon_widget(images[int(widget.get_active())])
+
+ if widget.get_active():
+ if speech.is_stopped():
+ speech.play(self.activity._view.get_marked_words())
+ else:
+ speech.stop()
diff --git a/speech.py b/speech.py
new file mode 100644
index 0000000..a1c1e5f
--- /dev/null
+++ b/speech.py
@@ -0,0 +1,43 @@
+# 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 = None
+pitch = 0
+rate = 0
+
+highlight_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..4627c75
--- /dev/null
+++ b/speech_gst.py
@@ -0,0 +1,87 @@
+# 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 _message_cb(bus, message, pipe):
+ if message.type in (gst.MESSAGE_EOS, 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 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/textadapter.py b/textadapter.py
index 2e96447..a6cef2b 100644
--- a/textadapter.py
+++ b/textadapter.py
@@ -9,6 +9,8 @@ import threading
from sugar import mime
from sugar.graphics import style
+import speech
+
PAGE_SIZE = 38
@@ -59,6 +61,14 @@ class TextViewer(gobject.GObject):
self.highlight_tag.set_property('foreground', 'black')
self.highlight_tag.set_property('background', 'yellow')
+ # text to speech initialization
+ self.current_word = 0
+ self.word_tuples = []
+ self.spoken_word_tag = self.textview.get_buffer().create_tag()
+ self.spoken_word_tag.set_property('weight', pango.WEIGHT_BOLD)
+ self.normal_tag = self.textview.get_buffer().create_tag()
+ self.normal_tag.set_property('weight', pango.WEIGHT_NORMAL)
+
def load_document(self, file_path):
file_name = file_path.replace('file://', '')
@@ -94,6 +104,8 @@ class TextViewer(gobject.GObject):
self._pagecount = pagecount + 1
self.set_current_page(0)
+ speech.highlight_cb = self.highlight_next_word
+
def _show_page(self, page_number):
position = self.page_index[page_number]
self._etext_file.seek(position)
@@ -110,6 +122,7 @@ class TextViewer(gobject.GObject):
textbuffer = self.textview.get_buffer()
label_text = label_text + '\n\n\n'
textbuffer.set_text(label_text)
+ self._prepare_text_to_speech(label_text)
def can_highlight(self):
return True
@@ -138,6 +151,62 @@ class TextViewer(gobject.GObject):
def connect_page_changed_handler(self, handler):
self.connect('page-changed', handler)
+ def can_do_text_to_speech(self):
+ return True
+
+ def get_marked_words(self):
+ "Adds a mark between each word of text."
+ i = self.current_word
+ marked_up_text = '<speak> '
+ while i < len(self.word_tuples):
+ word_tuple = self.word_tuples[i]
+ marked_up_text = marked_up_text + '<mark name="' + str(i) + '"/>' \
+ + word_tuple[2]
+ i = i + 1
+ print marked_up_text
+ return marked_up_text + '</speak>'
+
+ def _prepare_text_to_speech(self, page_text):
+ i = 0
+ j = 0
+ word_begin = 0
+ word_end = 0
+ ignore_chars = [' ', '\n', u'\r', '_', '[', '{', ']', '}', '|',
+ '<', '>', '*', '+', '/', '\\']
+ ignore_set = set(ignore_chars)
+ self.word_tuples = []
+ len_page_text = len(page_text)
+ while i < len_page_text:
+ if page_text[i] not in ignore_set:
+ word_begin = i
+ j = i
+ while j < len_page_text and page_text[j] not in ignore_set:
+ j = j + 1
+ word_end = j
+ i = j
+ word_tuple = (word_begin, word_end,
+ page_text[word_begin: word_end])
+ if word_tuple[2] != u'\r':
+ self.word_tuples.append(word_tuple)
+ i = i + 1
+
+ def highlight_next_word(self, word_count):
+ if word_count < len(self.word_tuples):
+ word_tuple = self.word_tuples[word_count]
+ textbuffer = self.textview.get_buffer()
+ iterStart = textbuffer.get_iter_at_offset(word_tuple[0])
+ iterEnd = textbuffer.get_iter_at_offset(word_tuple[1])
+ bounds = textbuffer.get_bounds()
+ textbuffer.apply_tag(self.normal_tag, bounds[0], iterStart)
+ textbuffer.apply_tag(self.spoken_word_tag, iterStart, iterEnd)
+ v_adjustment = self._scrolled.get_vadjustment()
+ max = v_adjustment.upper - v_adjustment.page_size
+ max = max * word_count
+ max = max / len(self.word_tuples)
+ v_adjustment.value = max
+ self.current_word = word_count
+ return True
+
def load_metadata(self, activity):
pass