Como Hacer Una Actividad Sugar

Agregar texto hablado

Introducción

Ciertamente una de las Actividades disponibles más populares es Speak (Hablar), la cual toma las palabras que tecleas y las pronuncia en voz alta, mostrando al mismo tiempo una carita que parece hablar.  Te puedes sorprender al saber que poco código de esa Actividad se requiere utilizar para lograr pronunciar las palabras. Si tu Actividad se puede beneficiar al pronunciar palabras en voz alta (hay posibilidades para Actividades educativas y en juegos) este capítulo te enseñará como hacer que eso suceda.  

SpeakActivity.png

Tenemos maneras de hacerte hablar

Un par de maneras y ambas fáciles son:

  • Correr el programa espeak directamente
  • Usar el plugin gstreamer espeak

Ambos métodos tienen sus ventajas. El primero es usar Speak (técnicamente Speak usa el plugin gstreamer si está disponible y si no lo está ejecuta directamente espeak. Para lo que hace Speak, el plugin gstreamer no es realmente necesario). Ejecutar  espeak es claramente el método más simple y puede ser apropiado para tu Actividad. Su gran ventaja es que no tienes que tener el plugin gstreamer instalado. Si tu Actividad necesita correr en otra que la última versión de Sugar, esto puede ser algo para considerar.  

El plugin gstreamer es lo que usa Read Etexts para hacer texto hablado con resaltador.  Para esta aplicación necesitamos ser capaces de hacer cosas que no son posibles solamente corriendo espeak.  Por ejemplo:

  • Necesitamos parar y retomar el habla, ya que la Actividad necesita leer una página completa, no solamente frases simples.
  • Necesitamos resaltar las palabras a medida que son pronunciadas.

Tu puedes pensar que puedes lograr estos objetivos corriendo espeak de a una palabra por vez. Si lo piensas, no te sientas mal porque yo también pensé eso. En una computadora rápida suena horrible, como HAL 9000 tartamudeando al final, antes de ser desactivada. En la XO no sale sonido alguno. 

Originalmente Read Etexts usó speech-dispatcher para hacer lo que hace el plugin gstreamer.  Los desarrolladores del ese programa fueron de mucha ayuda para lograr funcionar el resaltador en Read Etexts, pero speech-dispatcher necesita ser configurado antes de poder usarlo, lo que fue un problema para nosotros. (Hay más de un tipo de software disponible para convertir texto en habla y speech-dispatcher soporta la mayoría de ellos. Esto hace inevitable tener archivos de configuración).  Aleksey Lim de los laboratorios de Sugar tuvo la idea de usar el plugin de gstreamer y fue quién lo escribió. El también reescribió gran parte de Read Etexts de forma que use el plugin si está disponible, use speech-dispatcher si no, y no soporte hablar si ninguno de los dos está disponible.

Correr espeak directamente

Tu puedes correr el programa espeak desde la terminal para probar sus opciones. Para ver que opciones están disponibles para espeak tu puedes usar el comando man:

man espeak

Esto te trae una página del manual donde se describe como correr el programa y que opciones hay disponibles. Las partes de la página man que nos interesan más son las siguientes: 

NAME
       espeak - A multi-lingual software speech synthesizer.

SYNOPSIS
       espeak [options] [<words>]

DESCRIPTION
       espeak is a software speech synthesizer for English,
       and some other languages.

OPTIONS
       -p <integer>
              Pitch adjustment, 0 to 99, default is 50

       -s <integer>
              Speed in words per minute, default is 160

       -v <voice name>
              Use voice file of this name from
              espeak-data/voices

       --voices[=<language code>]
               Lists the available voices. If =<language code>
               is present then only those voices which are
               suitable for that language are listed.

Probemos algunas de estas opciones. Primero obtengamos una lista de voces (Voices):

espeak --voices
Pty Language Age/Gender VoiceName       File        Other Langs
 5  af             M  afrikaans         af
 5  bs             M  bosnian           bs
 5  ca             M  catalan           ca
 5  cs             M  czech             cs
 5  cy             M  welsh-test        cy
 5  de             M  german            de
 5  el             M  greek             el
 5  en             M  default           default
 5  en-sc          M  en-scottish       en/en-sc    (en 4)
 2  en-uk          M  english           en/en       (en 2)
... and many more ...

Ahora que sabemos los nombres de las voces podemos probarlas. ¿Qué tal inglés con  acento francés? 

espeak "Your mother was a hamster and your father \
smelled of elderberries." -v fr

Experimentemos con velociadad y tono (rate and pitch):

espeak "I'm sorry, Dave. I'm afraid I can't \
do that." -s 120 -p 30

Lo siguiente es escribir algo de código Python para correr espeak.  Acá va un pequeño programa adaptado del código en Speak:

import re
import subprocess

PITCH_MAX = 99
RATE_MAX = 99
PITCH_DEFAULT = PITCH_MAX/2
RATE_DEFAULT = RATE_MAX/3

def speak(text, rate=RATE_DEFAULT, pitch=PITCH_DEFAULT,
    voice="default"):

    # espeak uses 80 to 370
    rate = 80 + (370-80) * int(rate) / 100

    subprocess.call(["espeak", "-p", str(pitch),
            "-s", str(rate), "-v", voice,  text],
            stdout=subprocess.PIPE)

def voices():
    out = []
    result = subprocess.Popen(["espeak", "--voices"],
        stdout=subprocess.PIPE).communicate()[0]

    for line in result.split('\n'):
        m = re.match(
            r'\s*\d+\s+([\w-]+)\s+([MF])\s+([\w_-]+)\s+(.+)',
            line)
        if not m:
            continue
        language, gender, name, stuff = m.groups()
        if stuff.startswith('mb/') or \
            name in ('en-rhotic','english_rp',
                'english_wmids'):
            # these voices don't produce sound
            continue
        out.append((language, name))

    return out

def main():
    print voices()
    speak("I'm afraid I can't do that, Dave.")
    speak("Your mother was a hamster, and your father "
        + "smelled of elderberries!",  30,  60,  "fr")

if __name__ == "__main__":
    main()

En el repositorio Git del directorio Adding_TTS este archivo se llama espeak.py.  Cargar este archivo en Eric y ejecutar Run Script  desde el menu de Start (Inicio).  Además de escuchar hablar deberías ver este texto: 

[('af', 'afrikaans'), ('bs', 'bosnian'), ('ca', 'catalan'), ('cs', 'czech'), ('cy', 'welsh-test'), ('de', 'german'), ('el', 'greek'), ('en', 'default'), ('en-sc', 'en-scottish'), ('en-uk', 'english'), ('en-uk-north', 'lancashire'), ('en-us', 'english-us'), ('en-wi', 'en-westindies'), ('eo', 'esperanto'), ('es', 'spanish'), ('es-la', 'spanish-latin-american'), ('fi', 'finnish'), ('fr', 'french'), ('fr-be', 'french'), ('grc', 'greek-ancient'), ('hi', 'hindi-test'), ('hr', 'croatian'), ('hu', 'hungarian'), ('hy', 'armenian'), ('hy', 'armenian-west'), ('id', 'indonesian-test'), ('is', 'icelandic-test'), ('it', 'italian'), ('ku', 'kurdish'), ('la', 'latin'), ('lv', 'latvian'), ('mk', 'macedonian-test'), ('nl', 'dutch-test'), ('no', 'norwegian-test'), ('pl', 'polish'), ('pt', 'brazil'), ('pt-pt', 'portugal'), ('ro', 'romanian'), ('ru', 'russian_test'), ('sk', 'slovak'), ('sq', 'albanian'), ('sr', 'serbian'), ('sv', 'swedish'), ('sw', 'swahihi-test'), ('ta', 'tamil'), ('tr', 'turkish'), ('vi', 'vietnam-test'), ('zh', 'Mandarin'), ('zh-yue', 'cantonese-test')]

La función voices() devuelve una lista de voces como una tupla por voz y elimina voces de la lista que espeak no puede producir solo.  Esta lista de tuplas puede ser usada para llenar un menú derivado. 

La función speak() ajusta el valor de la velocidad (rate) para que puedas ingresar un valor entre 0 y 99 en lugar de entre 80 y 370.  speak() es más compleja en la Actividad Speak que lo que tenemos acá, porque en esa Actividad controla el audio hablado y genera movimientos de la boca basados en la amplitud de la voz.  Realizar los movimientos de la cara es gran parte de lo que hace la Actividad Speak y como no  estamos haciendo eso, precisamos muy poco código para que nuestra Actividad hable. 

Tu puedes usar import espeak para incluir este archivo en tus propias Actividades.

Usar el plugin gstreamer espeak

El plugin gstreamer espeak puede instalarse en Fedora 10 o posterior usando Add/Remove Software.

Cuando hayas realizado esto debes ser capaz de bajar la Actividad Read Etexts (la auténtica, no la versión simplificada que estamos usando en este libro) de ASLO y probar la pestaña Speech.  Debes hacerlo ahora. Se debe parecer a algo así:

 

El libro usado para las capturas de pantalla anteriores en este manual fue Pride and Prejudice de Jane Austen.  Para balancear, el resto de las capturas de pantalla se harán usando The Innocents Abroad de Mark Twain. 1 

Gstreamer es el marco para multimedia.  Si has observado videos en la web debes estar familiarizado con el concepto de streaming media.  En lugar de bajar una canción completa o un vidoe clip completo y luego ejecutarlo, streaming significa que la bajada y la ejecución ocurren al mismo tiempo, con la ejecución un poco detrás de la bajada. Hay muchos tipos diferentes de archivos de medios: MP3's, DivX, WMV, Real Media, y otros.  Por cada tipo de archivo de medios Gstreamer tiene un plugin.

Gstreamer utiliza un concepto llamado pipelining.  La idea es que la salida de un programa puede ser la entrada para otro. Una forma de manejar la situación es poner la salida del primer programa en un archivo temporario y hacer que el segundo programa lo lea. Esto significa que el primer programa debe terminar de ejecutar antes que el segundo pueda empezar. ¿Qué sucedería si los dos programas corren al mismo tiempo y que el segundo lea la información a medida que el primero la escribe? Es posible y el mecanismo para pasar información de un programa a otro se le llama pipe (caño). A una colección de programas que se unen de esta manera  se les llama un pipeline (cañería).  Al programa que alimenta la información en el caño se le llama source (fuente) y al programa que saca los datos del caño se le llama sink (pileta).

El plugin gstreamer espeak usa un caño simple: el texto va a espeak por una punta y el sonido sale por el otro y se envía a tu adaptador de sonido. Puedes pensar que no suena muy diferente de lo que hacíamos antes, pero lo es.  Cuando corres espeak, el programa se carga en memoria, habla el texto que le pasas en la tarjeta de sonido y se descarga. Esta es una de las razones por la cuales no puedes usar espeak una palabra a la vez para lograr habla con palabras resaltadas.  Hay un pequeño retraso mientras el programa se carga. No se nota tanto  si le pasas a espeak una frase o una oración completa para leer, pero si ocurre para cada palabra es muy notorio. Al usar el plugin gstreamer podemos tener espeak cargado en la memoria todo el tiempo, esperando que le enviemos algunas palabras a su caño de entrada. Las va a decir en voz alta y luego espera por el próximo lote.

Como podemos controlar lo que entra en el caño, es posible parar y retomar el habla.

El ejemplo que usaremos acá es nuevamente la versión de Read Etexts, pero en lugar de la Actividad vamos a modificar la versión autónoma. No hay nada especial sobre el plugin  gstreamer que lo haga funcionar solamente con Actividades.  Cualquier programa Python lo puede usar. Estoy incluyendo texto hablado como un tema en este manual porque cada instalación Sugar incluye espeak y muchas Actividades pueden encontrarlo útil. 

En el repositorio Git está el archivo speech.py que luce así: 

import gst

voice = 'default'
pitch = 0

rate = -20
highlight_cb = None

def _create_pipe():
    pipeline = 'espeak name=source ! autoaudiosink'
    pipe = gst.parse_launch(pipeline)

    def stop_cb(bus, message):
        pipe.set_state(gst.STATE_NULL)

    def mark_cb(bus, message):
        if message.structure.get_name() == 'espeak-mark':
            mark = message.structure['mark']
            highlight_cb(int(mark))

    bus = pipe.get_bus()
    bus.add_signal_watch()
    bus.connect('message::eos', stop_cb)
    bus.connect('message::error', stop_cb)
    bus.connect('message::element', mark_cb)

    return (pipe.get_by_name('source'), pipe)

def _speech(source, pipe, words):
    source.props.pitch = pitch
    source.props.rate = rate
    source.props.voice = voice
    source.props.text = words;
    pipe.set_state(gst.STATE_PLAYING)

info_source, info_pipe = _create_pipe()
play_source, play_pipe = _create_pipe()

# track for marks
play_source.props.track = 2

def voices():
    return info_source.props.voices

def say(words):
    _speech(info_source, info_pipe, words)
    print words

def play(words):
    _speech(play_source, play_pipe, words)

def is_stopped():
    for i in play_pipe.get_state():
        if isinstance(i, gst.State) and \
            i == gst.STATE_NULL:
            return True
    return False

def stop():
    play_pipe.set_state(gst.STATE_NULL)

def is_paused():
    for i in play_pipe.get_state():
        if isinstance(i, gst.State) and \
            i == gst.STATE_PAUSED:
            return True
    return False

def pause():
    play_pipe.set_state(gst.STATE_PAUSED)

def rate_up():
    global rate
    rate = min(99, rate + 10)

def rate_down():
    global rate
    rate = max(-99, rate - 10)

def pitup():
    global pitch
    pitch = min(99, pitch + 10)

def pitdown():
    global pitch
    pitch = max(-99, pitch - 10)

def prepare_highlighting(label_text):
    i = 0
    j = 0
    word_begin = 0
    word_end = 0
    current_word = 0
    word_tuples = []
    omitted = [' ', '\n', u'\r', '_', '[', '{', ']',\
        '}', '|', '<', '>', '*', '+', '/', '\' ]
    omitted_chars = set(omitted)
    while i < len(label_text):
        if label_text[i] not in omitted_chars:
            word_begin = i
            j = i
            while j < len(label_text) and \
                 label_text[j] not in omitted_chars:
                 j = j + 1
                 word_end = j
                 i = j
            word_t = (word_begin, word_end, \
                 label_text[word_begin: word_end].strip())
            if word_t[2] != u'\r':
                 word_tuples.append(word_t)
        i = i + 1
    return word_tuples

def add_word_marks(word_tuples):
    "Adds a mark between each word of text."
    i = 0
    marked_up_text = '<speak> '
    while i < len(word_tuples):
        word_t = word_tuples[i]
        marked_up_text = marked_up_text + \
            '<mark name="' + str(i) + '"/>' + word_t[2]
        i = i + 1
    return marked_up_text + '</speak>'

Hay otro archivo llamado ReadEtextsTTS.py que luce así:

import sys
import os
import zipfile
import pygtk
import gtk
import getopt
import pango
import gobject
import time
import speech

speesupported = True

try:
    import gst
    gst.element_factory_make('espeak')
    print 'speech supported!'
except Exception, e:
    speesupported = False
    print 'speech not supported!'

page=0
PAGE_SIZE = 45

class ReadEtextsActivity():
    def __init__(self):
        "The entry point to the Activity"
        speech.highlight_cb = self.highlight_next_word
        # print speech.voices()

    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()
            tag = textbuffer.create_tag()
            tag.set_property('weight', pango.WEIGHT_BOLD)
            tag.set_property( 'foreground', "white")
            tag.set_property( 'background', "black")
            iterStart = \
                textbuffer.get_iter_at_offset(word_tuple[0])
            iterEnd = \
                textbuffer.get_iter_at_offset(word_tuple[1])
            bounds = textbuffer.get_bounds()
            textbuffer.remove_all_tags(bounds[0], bounds[1])
            textbuffer.apply_tag(tag, iterStart, iterEnd)
            v_adjustment = \
                self.scrolled_window.get_vadjustment()
            max = v_adjustment.upper - \
                v_adjustment.page_size
            max = max * word_count
            max = max / len(self.word_tuples)
            v_adjustment.value = max
        return True

    def keypress_cb(self, widget, event):
        "Respond when the user presses one of the arrow keys"
        global done
        global speesupported
        keyname = gtk.gdk.keyval_name(event.keyval)
        if keyname == 'KP_End' and speesupported:
            if speech.is_paused() or speech.is_stopped():
                speech.play(self.words_on_page)
            else:
                speech.pause()
            return True
        if keyname == 'plus':
            self.font_increase()
            return True
        if keyname == 'minus':
            self.font_decrease()
            return True
        if speesupported and speech.is_stopped() == False \
            and speech.is_paused == False:
            # If speech is in progress, ignore other keys.
            return True
        if keyname == '7':
            speech.pitdown()
            speech.say('Pitch Adjusted')
            return True
        if keyname == '8':
            speech.pitup()
            speech.say('Pitch Adjusted')
            return True
        if keyname == '9':
            speech.rate_down()
            speech.say('Rate Adjusted')
            return True
        if keyname == '0':
            speech.rate_up()
            speech.say('Rate Adjusted')
            return True
        if keyname == 'KP_Right':
            self.page_next()
            return True
        if keyname == 'Page_Up' or keyname == 'KP_Up':
            self.page_previous()
            return True
        if keyname == 'KP_Left':
            self.page_previous()
            return True
        if keyname == 'Page_Down' or keyname == 'KP_Down':
            self.page_next()
            return True
        if keyname == 'Up':
            self.scroll_up()
            return True
        if keyname == 'Down':
            self.scroll_down()
            return True
        return False

    def page_previous(self):
        global page
        page=page-1
        if page < 0: page=0
        self.show_page(page)
        v_adjustment = \
            self.scrolled_window.get_vadjustment()
        v_adjustment.value = v_adjustment.upper - \
            v_adjustment.page_size

    def page_next(self):
        global page
        page=page+1
        if page >= len(self.page_index): page=0
        self.show_page(page)
        v_adjustment = \
            self.scrolled_window.get_vadjustment()
        v_adjustment.value = v_adjustment.lower

    def font_decrease(self):
        font_size = self.font_desc.get_size() / 1024
        font_size = font_size - 1
        if font_size < 1:
            font_size = 1
        self.font_desc.set_size(font_size * 1024)
        self.textview.modify_font(self.font_desc)

    def font_increase(self):
        font_size = self.font_desc.get_size() / 1024
        font_size = font_size + 1
        self.font_desc.set_size(font_size * 1024)
        self.textview.modify_font(self.font_desc)

    def scroll_down(self):
        v_adjustment = \
            self.scrolled_window.get_vadjustment()
        if v_adjustment.value == v_adjustment.upper - \
            v_adjustment.page_size:
            self.page_next()
            return
        if v_adjustment.value < v_adjustment.upper - \
            v_adjustment.page_size:
            new_value = v_adjustment.value + \
                v_adjustment.step_increment
            if new_value > v_adjustment.upper - \
                v_adjustment.page_size:
                new_value = v_adjustment.upper - \
                    v_adjustment.page_size
            v_adjustment.value = new_value

    def scroll_up(self):
        v_adjustment = \
            self.scrolled_window.get_vadjustment()
        if v_adjustment.value == v_adjustment.lower:
            self.page_previous()
            return
        if v_adjustment.value > v_adjustment.lower:
            new_value = v_adjustment.value - \
                v_adjustment.step_increment
            if new_value < v_adjustment.lower:
                new_value = v_adjustment.lower
            v_adjustment.value = new_value

    def show_page(self, page_number):
        global PAGE_SIZE, current_word
        position = self.page_index[page_number]
        self.etext_file.seek(position)
        linecount = 0
        label_text = ''
        textbuffer = self.textview.get_buffer()
        while linecount < PAGE_SIZE:
            line = self.etext_file.readline()
            label_text = label_text + \
                unicode(line, 'iso-8859-1')
            linecount = linecount + 1
        textbuffer.set_text(label_text)
        self.textview.set_buffer(textbuffer)
        self.word_tuples = \
            speech.prepare_highlighting(label_text)
        self.words_on_page = \
            speech.add_word_marks(self.word_tuples)

    def save_extracted_file(self, zipfile, filename):
        "Extract the file to a temp directory for viewing"
        filebytes = zipfile.read(filename)
        f = open("/tmp/" + filename, 'w')
        try:
            f.write(filebytes)
        finally:
            f.close()

    def read_file(self, filename):
        "Read the Etext file"
        global PAGE_SIZE

        if zipfile.is_zipfile(filename):
            self.zf = zipfile.ZipFile(filename, 'r')
            self.book_files = self.zf.namelist()
            self.save_extracted_file(self.zf, \
                self.book_files[0])
            currentFileName = "/tmp/" + self.book_files[0]
        else:
            currentFileName = filename

        self.etext_file = open(currentFileName,"r")
        self.page_index = [ 0 ]
        linecount = 0
        while self.etext_file:
            line = self.etext_file.readline()
            if not line:
                break
            linecount = linecount + 1
            if linecount >= PAGE_SIZE:
                position = self.etext_file.tell()
                self.page_index.append(position)
                linecount = 0
        if filename.endswith(".zip"):
            os.remove(currentFileName)

    def delete_cb(self, widget, event, data=None):
        speech.stop()
        return False

    def destroy_cb(self, widget, data=None):
        speech.stop()
        gtk.main_quit()

    def main(self, file_path):
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.connect("delete_event", self.delete_cb)
        self.window.connect("destroy", self.destroy_cb)
        self.window.set_title("Read Etexts Activity")
        self.window.set_size_request(800, 600)
        self.window.set_border_width(0)
        self.read_file(file_path)
        self.scrolled_window = gtk.ScrolledWindow(
            hadjustment=None, vadjustment=None)
        self.textview = gtk.TextView()
        self.textview.set_editable(False)
        self.textview.set_left_margin(50)
        self.textview.set_cursor_visible(False)
        self.textview.connect("key_press_event",
            self.keypress_cb)
        self.font_desc = pango.FontDescription("sans 12")
        self.textview.modify_font(self.font_desc)
        self.show_page(0)
        self.scrolled_window.add(self.textview)
        self.window.add(self.scrolled_window)
        self.textview.show()
        self.scrolled_window.show()
        self.window.show()
        gtk.main()

if __name__ == "__main__":
    try:
        opts, args = getopt.getopt(sys.argv[1:], "")
        ReadEtextsActivity().main(args[0])
    except getopt.error, msg:
        print msg
        print "This program has no options"
        sys.exit(2)

El programa ReadEtextsTTS tiene solamente unos pocos cambios para habilitarlo para hablar. El primero verifica la existencia del plugin gstreamer:

speesupported = True

try:
    import gst
    gst.element_factory_make('espeak')
    print 'speech supported!'
except Exception, e:
    speesupported = False
    print 'speech not supported!'

Este código detecta si el plugin está instalado intentando importar de las librerías de Python la llamada "gst". Si la importación falla arroja una Exception (excepción) y capturamos la Exception y la usamos para establecer una variable llamada speesupported  a False (falso).  Podemos verificar el valor de la variable en otras partes del programa para que el programa trabaje con texto hablado si está disponible y sin él, si no lo está. Hacer que un programa trabaje en ambientes diferentes haciendo este tipo de chequeos se le llama "degradación elegante" (degrading gracefully).  Procesar excepciones en las importaciones es una técnica habitual en Python para lograr esto, Si quieres que tu Actividad corra con versiones anteriores de Sugar puedes llegar a usarlo. 

El siguiente trozo de código que vamos a analizar resalta una palabra en el área de visualización del texto y lo pagina para mantener visible la palabra resaltada.

class ReadEtextsActivity():
    def __init__(self):
        "The entry point to the Activity"
        speech.highlight_cb = self.highlight_next_word
        # print speech.voices()

    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()
            tag = textbuffer.create_tag()
            tag.set_property('weight', pango.WEIGHT_BOLD)
            tag.set_property( 'foreground', "white")
            tag.set_property( 'background', "black")
            iterStart = \
                textbuffer.get_iter_at_offset(word_tuple[0])
            iterEnd = \
                textbuffer.get_iter_at_offset(word_tuple[1])
            bounds = textbuffer.get_bounds()
            textbuffer.remove_all_tags(bounds[0], bounds[1])
            textbuffer.apply_tag(tag, iterStart, iterEnd)
            v_adjustment = \
                self.scrolled_window.get_vadjustment()
            max = v_adjustment.upper - v_adjustment.page_size
            max = max * word_count
            max = max / len(self.word_tuples)
            v_adjustment.value = max
        return True

En el método  __init__() asignamos una variable llamada highlight_cb en speech.py con un método llamado highlight_next_word().  Esto le da a speech.py una forma de llamar a ese método cada vez que una nueva palabra en el área de texto debe ser resaltada.

Si le quitas el comentario a la siguiente línea, se imprime a la terminal la lista de tuplas conteniendo los nombres de las voces. No estamos permitiendo que el usuario cambie las voces en esta aplicación, pero no sería difícil agregar esa característica. 

A continuación el código para el método para resaltar las palabras.  Lo que hace es mirar en la lista de tuplas que contienen la posición inicial y final de cada palabra (offsets) en el buffer de texto del área de texto.  El que llama a este método pasa el número de una palabra (por ejemplo la primer palabra en el buffer es la palabra 0, la segunda es la palabra 1 y así sucesivamente). Este método busca esa entrada en la lista, obtiene sus posiciones de inicio y fin, elimina cualquier resaltado anterior y resalta el nuevo texto. Adicionalmente determina que fracción es del total de palabras y desplaza el área de texto lo suficiente para asegurarse que la palabra esté visible.

Por supuesto este método funciona mejor en páginas sin demasiadas líneas en blanco, que por suerte son la mayoría. No funciona tan bien en carátulas. Un programador con experiencia seguramente defina una forma más elegante y confiable para hacer este paginado. Avísenme si definen algo. 

Más abajo vemos el código que recibe caracteres del teclado del usuario y hace cosas relacionadas con el habla con ellos:

    def keypress_cb(self, widget, event):
        "Respond when the user presses one of the arrow keys"
        global done
        global speesupported
        keyname = gtk.gdk.keyval_name(event.keyval)
        if keyname == 'KP_End' and speesupported:
            if speech.is_paused() or speech.is_stopped():
                speech.play(self.words_on_page)
            else:
                speech.pause()
            return True
        if speesupported and speech.is_stopped() == False \
            and speech.is_paused == False:
            # If speech is in progress, ignore other keys.
            return True
        if keyname == '7':
            speech.pitdown()
            speech.say('Pitch Adjusted')
            return True
        if keyname == '8':
            speech.pitup()
            speech.say('Pitch Adjusted')
            return True
        if keyname == '9':
            speech.rate_down()
            speech.say('Rate Adjusted')
            return True
        if keyname == '0':
            speech.rate_up()
            speech.say('Rate Adjusted')
            return True

Como puedes ver, las funciones a las que llamamos están todas en el archivo speech.py que importamos.  No tienes que entender completamente como operan estas funciones para usarlas en tus propias Actividades. Nota que como está escrito el código previene al uuario de cambiar el tono o la velocidad una vez que se comenzó a hablar.  Nota también que hay dos métodos diferentes en speech.py para hablar.  play() es el método para tener texto hablado con resaltado de las palabras. say() es para decir frases cortas producidas por la interfaz de usuario, en este caso con ajuste de tono (Pitch adjusted) y de velocidad (Rate adjusted).  Por supuesto si pones un código como este en tu Actividad debes usar la función _() de forma que estas frases puedan ser traducidas a otros idiomas. 

Hay un poco más de código que precisamos para hacer texto hablado con resaltador que: necesitamos preparar las palabras que van a ser pronunciadas para resaltarlas en el área de texto.

    def show_page(self, page_number):
        global PAGE_SIZE, current_word
        position = self.page_index[page_number]
        self.etext_file.seek(position)
        linecount = 0
        label_text = ''
        textbuffer = self.textview.get_buffer()
        while linecount < PAGE_SIZE:
            line = self.etext_file.readline()
            label_text = label_text + unicode(line, \
                'iso-8859-1')
            linecount = linecount + 1
        textbuffer.set_text(label_text)
        self.textview.set_buffer(textbuffer)
        self.word_tuples = \
            speech.prepare_highlighting(label_text)
        self.words_on_page = \
            speech.add_word_marks(self.word_tuples)

El comienzo de este método lee una página de testo en un string llamado label_text y lo coloca en el buffer del área de visualización del texto. Las últimas dos líneas separan el texto en palabras, separando la puntuación y colocando cada palabra y sus offsets de posición de incio y de fin en una tupla. Las tuplas se agregan a una lista. 

speech.add_word_marks() convierte las palabras en la lista en un documento en formato SSML (Speech Synthesis Markup Language).  SSML es un estándar para agregar etiquetas (tipo las etiquetas usadas para hacer páginas web) al texto para decirle al software de hablar que hacer con el texto. Estamos usando una parte muy pequeña de este estándar para producir un documento marcado con una marca (etiqueta) entre cada palabra, como esto:

<speak>
    <mark name="0"/>The<mark name="1"/>quick<mark name-"2"/>
    brown<mark name="3"/>fox<mark name="4"/>jumps
</speak>

Cuando espeak lee este archivo hace un callback en nuestro programa cada vez que lee una de las etiquetas.  La llamada (callback) va a contener el número de la palabra en la lista de tuplas (word_tuples) el que obtiene del atributo name (nombre) de la etiqueta (mark).  De esta forma el método llamado sabe que palabra resaltar. La ventaja de usar el nombre en lugar de sólo resaltar la siguiente palabra en el área de visualización del texto es que si espeak falla al hacer uno de los callback, el resaltado no pierde el sincronismo. Esto era un problema con speech-dispatcher.

Un callback es lo que parece ser. Cuando un programa llama a otro le puede pasar una función o método propio que quiere que el segundo programa llame si ocurre algo.

Para probar el nuevo programa ejecuta:

./ReadEtextsTTS.py bookfile

desde la terminal. Puedes ajustar el tono y velocidad hacia arriba y hacia abajo usando las teclas 7, 8, 9 y 0 en la línea superior del teclado. Debe decir "Pitch Adjusted" o "Rate Adjusted" cuando lo haces. Puedes iniciar, parar y retomar el hablar con resaltador usando la tecla End (fin) en el teclado. (En la laptop XO las teclas de juego (game) se corresponden con el teclado numérico de un teclado normal.  Esto hace práctico el uso de esas teclas  cuando la XO está doblada en modo tablet y el teclado no está disponible). No puedes cambiar el tono o la velocidad cuando hablar está en progreso. Los intentos de hacerlo  serán ignorados. El programa en acción luce así: 

Esto nos trae al fin del tema del texto hablado. Si quieres ver más, el repositorio Git para este libro tiene algunos programas ejemplo más que usan el plugin gstreamer espeak. Estos ejemplos fueron creados por el autor del plugin y muestran otras formas en las que lo puedes usar. Hay un programa "coro" que demuestra múltiples voces hablando al mismo tiempo.

 2

  1. NT: The Innocents Abroad, or The New Pilgrims' Progress o Los inocentes en el extranjero es un libro de viajes con crónicas de humor de Mark Twain publicado en 1869.^
  2. Traducido Olga Mattos, Uruguay^