From a04af069262aed24bcceae48d414906a36ec28e2 Mon Sep 17 00:00:00 2001 From: Gonzalo Odiard Date: Mon, 21 Feb 2011 20:11:04 +0000 Subject: Add text to speech capability to text backend Too much code stolen from Read EText activity --- 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 = ' ' + while i < len(self.word_tuples): + word_tuple = self.word_tuples[i] + marked_up_text = marked_up_text + '' \ + + word_tuple[2] + i = i + 1 + print marked_up_text + return marked_up_text + '' + + 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 -- cgit v0.9.1