sábado, 1 de junio de 2019

SINTETIZADOR DE VOZ EN ESPAÑOL PARA PYTHON 3 (OFFLINE)





En este post describiré cómo instalar pyttsx3 con espeak  y pondré unos programas ejemplo, donde el primero es un ejemplo del uso básico, y el segundo es una pequeña aplicación que dice la hora o alguna frace pequeña cada vez que se presiona un botón.

pytssx3 es una librería de interfase entre python 3 y los motores de texto a voz tales como SAPI5 (windows), espeak (Linux o windows) y NSSS (Mac). Ahunque existen otras librerías de conversión de texto a voz como gTTS (Google Text to Speech) con la cual se obtiene una voz mucho más realista, solo se pueden realizar 50 sintetizaciones de voz por día (no good) y requiere de una conexión permanente a internet (no good). Otra diferencia es que gTTS solo puede generar archivos de sonido que uno debe reproducir para escuchar, mientras que en pytssx3 se puede generar la señal audible (hacer hablar) y opcionalmente guardar como archivo de sonido.

Se recomienda usar la versión de python mas reciente. A continuación pondré los pasos a seguir para instalar lo necesario.

1. Instalar espeak.

En FEDORA, correr como root (super usuario) el siguiente comando en una terminal:

      dnf install espeak

con esto se instalan espeak y todas las voces con sus variantes de las voces disponibles.

En UBUNTU o RASPBIAN (Raspberry pi 3) correr como root (super usuario) el siguiente comando en una terminal:
     
      sudo apt install espeak

con esto se instalan espeak y todas las voces con sus variantes de las voces disponibles..

En Windows se debe instalar con el archivo ejecutable, lo puede descargar de la siguiente dirección:

 http://espeak.sourceforge.net/download.html

En el instalador se requiere especificar que voces se quieren instalar. Por ejemplo si quiero instalar el idioma español latino agrego spanish-latin-am. Además existen varios modificadores de voz, como por ejemplo +f5 (variante femenia de en este idioma). Si quisiera instalar una la voz con esa variante,  podría poner spanish-latin-am+f5 en el instalador, como se muestra en la imagen de abajo.

Tambien esta el español de españa que es spanish. Los modificadores aplicables a cualquier voz son los siguientes:
corak, f1, f2, f3, f4, f5, m1, m2, m3, m4, m6, m7, whisper, whisperf, klatt, klatt2, klatt3, klatt4.



Ejemplo: instalasr español latino con modificador +f5  y español latino normal

Si quieres instalar todas las voces en español con todas las variantes, copia y pega esto: 

spanish-latin-am+corak spanish-latin-am+whisper spanish-latin-am+whisperf spanish-latin-am+f1 spanish-latin-am+f2 spanish-latin-am+f3 spanish-latin-am+f4 spanish-latin-am+f5 spanish-latin-am+m1 spanish-latin-am+m2 spanish-latin-am+m3 spanish-latin-am+m4 spanish-latin-am+m5 spanish-latin-am+m6 spanish-latin-am+m7 spanish-latin-am+klatt spanish-latin-am+klatt2 spanish-latin-am+klatt3 panish-latin-am+klatt4 spanish+corak spanish+whisper spanish+whisperf spanish+f1 spanish+f2 spanish+f3 spanish+f4 spanish+f5 spanish+m1 spanish+m2 spanish+m3 spanish+m4 spanish+m5 spanish+m6 spanish+m7 spanish+klatt spanish+klatt2 spanish+klatt3 spanish+klatt4

2.- Instalar librerías de Python 3:

En FEDORA o UBUNTU o RASPBIAN o Windows poner en una terminal o linea de comandos:

       pip3 install pyttsx3

Posiblemente nesecites correrlo como root (super usuario).

En WINDOWS podrías necesitar instalar también pypiwin32 si arroja un error al importar la librería en python, pero es mejor ir a lo seguro e instalarla desde el inicio.

      pip3 install  pypiwin32

EJEMPLO 1 (Uso básico):

En el programa que se muestra más abajo, se ejemplifica el uso de pyttsx3. En este ejemplo se inicia por averiguar cuáes voces que están disponibles. Por ejemplo: en linux la voz en español latino es "spanish-latin-am", en windows es algo parecido a "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens\eSpeak_2" (es posible que tengas que cambiarlo en el programa de abajo, debido a que el nombre depende de cuantos idiomas y variantes hayas instalado). Después se elige la voz en español, en seguida se ajusta la velocidad y volumen de la voz elegida y por último pronuncia la célebre frase "Hola mundo".


#!/usr/bin/env python3
#-*- coding: utf-8 -*-
#------------------------------------------------------
#  Desarrollado por Carlos Alberto Rodriguez Martinez
#  http://mecatroncharly.blogspot.com/p/blog-page.html
#
#  Este es un programa que dice "Hola mundo" con voz audible
#------------------------------------------------------

from   platform import system
import pyttsx3

ThisOS = system() # averiguar Sistema operativo actual
engine = pyttsx3.init()    # inicializar motor de voz
#---------------------------------------
#     Encoontrar voces isponibles
#---------------------------------------
voices = engine.getProperty('voices')
male_language_count   = 0
female_language_count = 0
none_language_count   = 0
for voice in voices:
    print("------------------------")
    print(" - ID: %s"        % voice.id)
    print(" - Nombre: %s"    % voice.name)
    print(" - Lenguajes: %s" % voice.languages)
    print(" - Genero: %s"    % voice.gender)
    print(" - Edad: %s"      % voice.age)
    if voice.gender == 'male':
        male_language_count += 1
    if voice.gender == 'female':
        female_language_count +=1
    if voice.gender == 'none':
        none_language_count += 1
print ('Voces Masculinas : %d' % male_language_count)
print ('Voces femeninas  : %d' % female_language_count)
print ('Voces neutras    : %d' % none_language_count)
#--------------------------------------
#      CONFIGURAR VOZ E IDIOMA
#--------------------------------------
if ThisOS == 'Linux':
    engine.setProperty('voice', 'spanish-latin-am+f5') #ASIGNAR Español latino
    # agregar +f5 para simular voz femenina (1-5)
if ThisOS == 'Windows':
    engine.setProperty('voice','HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens\eSpeak_4')
engine.setProperty('rate'  ,140)   # Aumentar velocidad 40%
engine.setProperty('volume',1.0)   # Poner el volumen al 100%: Minimo:0, Maximo: 1
#--------------------------------------
#               HABLAR
#--------------------------------------
engine.say("Hola mundo!")          # Hablar
engine.runAndWait()                # Esperar a que terine de Hablar
engine.stop()                      # Detener motor tts


EJEMPLO 2

Este programa es un poco mas complicado pero mas cool,  la aplicación dice la hora actual o dice cualquier otra cosa que se le indique. Se utiliza una cola FIFO (librería "queue") donde se ponen en cola las fraces (que podría ser la hora o la frase que se indique) cada vez que se presiona el botón correspondiente. La librería "threading" se utiliza para crear un hilo separado del hilo principal. Este hilo se encarga de revisar constantemente si hay fraces en cola, si es asi, llama a la funcion que dice las fraces que haya en cola, dependiendo del numero de veces que se haya presionado el botón. El programa también tiene su control de velocidad, de volumen y selección de voces en español.

Interfase gráfica en windows.
La interfase en Linux cambia un poco debido a que se pueden combinar los mods con las voces.

Interfase gráfica en Linux.



Para correr este ejemplo nesesitas tener instaladas las librerías: "wxpython" y "queue". Este programa fué probado en windows 7 y Linux (Fedora 30) con python 3.6 y 3.7.

Te dejo comentarios en el código fuente para ayudatre a entender que es lo que sucede.


#!/usr/bin/env python3
#-*- coding: utf-8 -*-
#------------------------------------------------------
#  Desarrollado por Carlos Alberto rodriguez Martinez
#  http://mecatroncharly.blogspot.com/p/blog-page.html
#
#  Este es un programa que dice la hora con voz audible
#------------------------------------------------------

import wx
import pyttsx3
from   platform  import system
from   datetime  import datetime
from   queue     import Queue
from   time      import sleep
from   threading import Thread

class SayTimeApp(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self,None,-1,"Di la hora")
        self.ThisOS = system()        # averiguar Sistema operativo actual.
        self.createGUI()              # Crear los elementos de la GUI
        self.Lista = Queue()          # Una cola FIFO para que no se traslapen las funciones
        self.thread = Thread(target = self.WrkrThred) # poner funcion WrkrThred en un hilo
        self.thread.daemon = True     # Poner hilo en modo daemon, si el programa termina, tambien el hilo
        self.thread.start()           # Iniciar hilo
        self.VoiceNames = []          # Para guardar los nombres de las voces
        self.voicesIDs  = []          # para guardar los IDs de las voces (winndows)
        self.getVoiceList()           # Averiguar voces disponibles y llenar combobox
  
    def createGUI(self):
        MainPanel = wx.Panel(self) # contenedor de Frames
                               #es util para separar el frame principal de las barras de herramientas y
                               #la barra de estado
        BtnDiHora = wx.Button(MainPanel,
                           label = "Decir la hora",
                           pos   = (125, 10),
                           size  = (300, 50))
        #----------------------------------------------------------------
        BtnDiTexto = wx.Button(MainPanel,
                           label = "Decir esto",
                           pos   = wx.DefaultPosition,
                           size  = wx.DefaultSize)
        self.TxTDecir = wx.TextCtrl(MainPanel,
                                    wx.ID_ANY,
                                    wx.EmptyString,
                                    wx.DefaultPosition,
                                    wx.DefaultSize,
                                    0)
        DecirAlgoSizr = wx.BoxSizer(wx.HORIZONTAL)
        DecirAlgoSizr.Add(BtnDiTexto,  0, wx.ALL, 5)
        DecirAlgoSizr.Add(self.TxTDecir, 1, wx.ALL|wx.CENTER|wx.EXPAND, 5)
        #----------------------------------------------------------------
        self.ComboVoices = wx.ComboBox(MainPanel,
                                       choices = [],
                                       style   = wx.CB_READONLY|wx.ALIGN_CENTER)
        StticBoxVoices  = wx.StaticBox(MainPanel, -1, 'Voz')
        StticBVoicsSizr = wx.StaticBoxSizer(StticBoxVoices, wx.VERTICAL)
        StticBVoicsSizr.Add(self.ComboVoices, 0, wx.ALL|wx.CENTER|wx.EXPAND, 5)
        #----------------------------------------------------------------
        self.SpeedSlider = wx.Slider(MainPanel,
                                     value    = 140,
                                     minValue = 50,
                                     maxValue = 300,
                                     style    = wx.SL_HORIZONTAL|wx.SL_LABELS)
        StticBoxSpeed    = wx.StaticBox(MainPanel, -1, '% Velocidad')
        StticBxSpeedSizr = wx.StaticBoxSizer(StticBoxSpeed, wx.VERTICAL)
        StticBxSpeedSizr.Add(self.SpeedSlider, 0, wx.ALL|wx.CENTER|wx.EXPAND, 5)
        #----------------------------------------------------------------
        self.VolumSlider = wx.Slider(MainPanel,
                                     value    = 100,
                                     minValue = 0,
                                     maxValue = 100,
                                     style    = wx.SL_HORIZONTAL|wx.SL_LABELS)
        StticBoxVolume    = wx.StaticBox(MainPanel, -1, '% Volumen')
        StticBxVolumeSizr = wx.StaticBoxSizer(StticBoxVolume, wx.VERTICAL)
        StticBxVolumeSizr.Add(self.VolumSlider, 0, wx.ALL|wx.CENTER|wx.EXPAND, 5)
        #----------------------------------------------------------------
        if self.ThisOS == 'Linux':   #en linux
            mods = ['f5','f4','f3','f2','f1','klatt4','klatt3','klatt2','klatt',
                    'm7','m6','m5','m4','m3','m2','m1','whisper','whispef','croak',]
            self.ComboMods = wx.ComboBox(MainPanel,
                                       choices = mods,
                                       style   = wx.CB_READONLY|wx.ALIGN_CENTER)
            StticBoxMods  = wx.StaticBox(MainPanel, -1, 'Mods')
            StticBoxModsSizr = wx.StaticBoxSizer(StticBoxMods, wx.VERTICAL)
            StticBoxModsSizr.Add(self.ComboMods, 0, wx.ALL|wx.CENTER|wx.EXPAND, 5)
        #------------------------------------------------------------
        #                    BINDS
        #------------------------------------------------------------
        BtnDiHora .Bind(wx.EVT_BUTTON, self.DiHora)
        BtnDiTexto.Bind(wx.EVT_BUTTON, self.DiTexto)
        #------------------------------------------------------------
        #                    Main layout
        #------------------------------------------------------------
        MainSizer = wx.BoxSizer(wx.VERTICAL)
        MainSizer.Add(BtnDiHora,       0, wx.ALL|wx.CENTER|wx.EXPAND, 5)
        MainSizer.Add(DecirAlgoSizr,   0, wx.ALL|wx.CENTER|wx.EXPAND, 5)
        MainSizer.Add(StticBVoicsSizr, 0, wx.ALL|wx.CENTER|wx.EXPAND, 5)

        if self.ThisOS == 'Linux':   #en linux
            MainSizer.Add(StticBoxModsSizr,   0, wx.ALL|wx.CENTER|wx.EXPAND, 5)
            
        MainSizer.Add(StticBxSpeedSizr,  0, wx.ALL|wx.CENTER|wx.EXPAND, 5)
        MainSizer.Add(StticBxVolumeSizr, 0, wx.ALL|wx.CENTER|wx.EXPAND, 5)
        MainPanel.SetSizer(MainSizer)
        
        MainPanel.Layout()
        MainPanel.Fit()
        self.Layout()
        self.Fit()

    def DiTexto(self,evt):
        Texto = self.TxTDecir.GetValue()
        if Texto != "": #S no hay algo, no hacer nada
            self.Lista.put(Texto)

    def DiHora(self, evt):
        # obtener frace 
        self.Lista.put(self.getcurrTime()) # poner esta frace en cola.
        
    def getcurrTime(self): 
        currentTime   = datetime.now()             #Que hora es ahora
        currminuto    = currentTime.strftime('%M') #obtener minuto como string
        hora   = currentTime.strftime('%I')        #obtener hora como string
        if hora[0] == '0':
            hora = hora[0 : 0 : ] + hora[0 + 1 : :]   #si hay cero a la izquierda, quitarlo
        if currminuto[0] == '0':
            currminuto = currminuto[0 : 0 : ] + currminuto[0 + 1 : :] #si hay cero a la izquierda, quitarlo
        if currminuto == '0':
            currminuto = ''

        texto = ""
        if hora == '1':
            texto = "Es la una "+ str(currminuto)
        else:
            texto = "Son las "+str(hora)+" "+ str(currminuto)
            
        return texto

    def WrkrThred(self): #Esta es la funcion del ilo (thread)
        while True:
            sleep(0.1)
            if self.Lista.empty() == False:           # Mientras que la lista no este vacia
                self.SaySomething(self.Lista.get())   # Decir todo lo que este en cola
            
    def SetupVoice (self):  # configurar voz
        if self.ThisOS == 'Linux':   #en linux
            self.engine.setProperty('voice', self.ComboVoices.GetStringSelection()+"+"+self.ComboMods.GetValue())

        if self.ThisOS == 'Windows': #en windows
            index = self.ComboVoices.GetSelection()                # obtener el indice de seleccion del combobox
            self.engine.setProperty('voice',self.voicesIDs[index]) # obtener el nombe ID de la voz y asignarla

        self.engine.setProperty('rate'  ,self.SpeedSlider.GetValue())     # Asignar % velocidad 
        self.engine.setProperty('volume',self.VolumSlider.GetValue()/100) # Asignar volumen: Minimo:0, Maximo: 1 (por eso /100)
        
    def SaySomething(self, something):
        self.engine = pyttsx3.init()   # inicializar motor de voz.
        self.SetupVoice()              # inicializar voz
        self.engine.say(something)     # Indicar que es lo que se va a decir
        self.engine.runAndWait()       # Esperar a que terine de Hablar
        self.engine.stop()             # Detener motor

    def getVoiceList(self):
        engine = pyttsx3.init()   # inicializar un motor de voz localmente en esta funcion
        voices = engine.getProperty('voices')
        engine.stop()             # Parar el motor

        for voice in voices:
            if voice.name.lower().find("spanish") !=-1: # Restringir las voces al espanol
                self.VoiceNames.append(voice.name)
                self.voicesIDs .append(voice.id)
        self.ComboVoices.SetItems(self.VoiceNames)
        self.ComboVoices.SetSelection(0)

if __name__ == '__main__':
    app = wx.App()
    frame = SayTimeApp()
    frame.Show()
    app.MainLoop()