Sapi.SpVoice

Allgemeines zum Thema EventGhost
MonsterMagnet
Plugin Developer
Posts: 137
Joined: Fri Feb 10, 2006 12:04 pm

Sapi.SpVoice

Post by MonsterMagnet » Thu Feb 23, 2006 2:12 pm

Code: Select all

import win32com.client

VoiceObj = win32com.client.Dispatch("Sapi.SpVoice")
VoiceObj.Speak ("Hello, this is EventGhost speaking",1)
Zudem Thema findet sich ne Menge in Python.
Damit könnte man auch ne Sprachsteuerung integrieren.

Werde mal versuchen ein ganz einfaches "Sag die Zeit Plugin" zu machen, wenn ich mal etwas durch Python und wx durchgeblickt habe.

Das dürfte also noch ne Weile dauern...

Ist es sinnvoll das hier http://www.cs.unc.edu/~parente/tech/tr02.shtml zu benutzen oder reicht win32com ?

User avatar
Bitmonster
Site Admin
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Post by Bitmonster » Thu Feb 23, 2006 4:05 pm

Gute Sache. Ob du win32com direkt oder über pyTTS verwendest, überlasse ich dir. Man kann solche Module sowohl in das Plugin mit einbinden (einfach den pyTTS Ordner mit in den Plugin-Ordner kopieren) oder auch generell in die EG-Python-Distribution mit aufnehmen, falls es von mehreren Plugins gebraucht wird.

Peter Parente schreibt in der Regel in einem sehr "pythonischen" Stil, was ganz nett ist.

wxPython ist wirklich eine sehr umfangreiche Geschichte. Wenn man sich lange genug damit beschäftigt hat, dann kann man aber GUI-Sachen wirklich schnell und schön damit lösen. Hinterher sieht der Code ganz simpel aus, aber es gibt da doch recht viele Fallstricke am Anfang.

Wie gesagt habe ich auch kein Problem damit, dir die GUI-Sachen für deine Plugins zu schreiben. Ich kann dann gleich Vorteile daraus ziehen, dass EG schon manche Dinge die in wx sehr umständlich oder buggy sind schon in verbesserter Version zur Verfügung hat. Bevor du dich da also Wochen mit beschäftigst, nur um einen kleinen Dialog sauber aussehen zu lassen, verwende die Zeit lieber dafür Funktionalität zu schaffen.

MonsterMagnet
Plugin Developer
Posts: 137
Joined: Fri Feb 10, 2006 12:04 pm

Post by MonsterMagnet » Sun Feb 26, 2006 4:14 pm

O.K. jetzt wird peinlich...

Hab das mal in wx verpackt, so in etwa dachte ich könnte ein plugin aussehen...

Code: Select all

rateList = ['1','2','3','4','5']

import wx
import pyTTS
import time

class MyPanel(wx.Panel):

    def __init__(self, parent, id):

        self.tts = pyTTS.Create()       
        voiceList = self.tts.GetVoiceNames()
        
        wx.Panel.__init__(self, parent, id)
        
        wx.StaticText(self, -1, "Select voice", (10, 10))

        self.voice = wx.ComboBox(self, -1, value=voiceList[0], pos=wx.Point(10, 30),
            size=wx.Size(120, 150), choices=voiceList)

        self.voice.Bind(wx.EVT_COMBOBOX, self.OnEvtVoice, self.voice)
        
        wx.StaticText(self, -1, "Voice rate", pos=wx.Point(150, 10))

        self.rate = wx.ComboBox(self, -1, value=rateList[0], pos=wx.Point(150, 30),
            size=wx.Size(120, 150), choices=rateList)
        
        self.rate.Bind(wx.EVT_COMBOBOX, self.OnEvtRate, self.rate)
        
        wx.StaticText(self, -1, "Say:", pos=wx.Point(10, 70))
        
        self.edit1 = wx.TextCtrl(self, -1, value="The time is", pos=wx.Point(10, 90), size=wx.Size(260,25))

        self.time = wx.CheckBox(self, -1, " Say: Text and Time ",  pos=wx.Point(10, 130))
       
        self.test = wx.Button(self, -1, label="Test",
            pos=wx.Point(10, 160), size=wx.Size(260,28))

        self.test.Bind(wx.EVT_BUTTON, self.OnEvtTest, self.test)
                         
    def OnEvtVoice(self,event):
        self.tts.SetVoiceByName(event.GetString())

    def OnEvtTest(self,event):
        if self.time.IsChecked():
            self.tts.Speak(self.edit1.GetValue() + ' ' + time.strftime("%X"), pyTTS.tts_async)        
        else:
            self.tts.Speak(self.edit1.GetValue(), pyTTS.tts_async)
            
    def OnEvtRate(self,event):
        self.tts.Rate = event.GetString()
        
app = wx.PySimpleApp()
frame = wx.Frame(None, -1, "Text to Speech", size = (290, 250))
MyPanel(frame,-1)
frame.Show(True)
app.MainLoop()

User avatar
Bitmonster
Site Admin
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Post by Bitmonster » Sun Feb 26, 2006 6:14 pm

Das sieht schon ganz gut aus. Eigentlich fehlt dir nur noch der Umgang mit wxSizer. Ich mache nächste Woche mal ein Demo-Plugin davon, um dir mal den Aufbau zu zeigen, wie das in EG gedacht ist mit Parametern, etc.

MSSam hört sich echt übel an. :)
Hatte vor längerer Zeit mal nach guten deutschen Stimmen gesucht, aber irgendwie nichts gefunden.

MonsterMagnet
Plugin Developer
Posts: 137
Joined: Fri Feb 10, 2006 12:04 pm

Post by MonsterMagnet » Sun Feb 26, 2006 6:32 pm

Hab jetzt den Dialog als Plugin am laufen. :lol:

Echt genial da braucht man ja nur noch die passenden buttons etc. einfügen.

Ich hatte mal ne deutsche AT&T Stimme die war schon ganz gut, leider
ist an dem Paket beim brennen was verreckt.

MS Mary geht grade so durch...will mir ja nicht die Zeitung vorlesen lassen.

User avatar
Bitmonster
Site Admin
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Post by Bitmonster » Mon Feb 27, 2006 2:43 pm

Ich habe mal etwas mehr damit rumgespielt und muss sagen, die Sache macht ja richtig Spaß. Die Stimmen die MS kostenlos liefert sind natürlich nicht so der Hit, aber ich habe gestern noch ein Demo von RealSpeak-Steffi gefunden und die Stimme ist ja schon ziemlich gut. Selbst Schimpfwörter kriegt die 1A hin. :)

MonsterMagnet
Plugin Developer
Posts: 137
Joined: Fri Feb 10, 2006 12:04 pm

Post by MonsterMagnet » Mon Feb 27, 2006 3:11 pm

Könntest Du bitte mal einen Blick drauf werfen ?

Den Dialog hab ich erstmal vernachlässigt, das kann man ja dann anpassen.

Wie könnte man das mit dem Testbutton am besten lösen ?

Außerdem bin ich mir nicht sicher ob es vielleicht besser ist das Voice Object mal auf "None" zusetzten.

Zeitansage etc. mach ich später.

Code: Select all

import eg
import wx
import win32com.client
import os

rateList = ("-5","-4","-3","-2","-1","1","2","3","4","5")

class Speech(eg.PluginClass):
    
    def __init__(self):
        maingroup = self.AddGroup(self.name)
        maingroup.AddAction(self.TextToSpeech)

    class TextToSpeech(eg.ActionClass):
        VoiceObj = win32com.client.Dispatch("Sapi.SpVoice")
        text = "This is a test"
        
        def __call__(self, text, rate, id):
            self.VoiceObj.Rate = rate
            self.VoiceObj.Voice = self.VoiceObj.GetVoices().Item(id)
            self.VoiceObj.Speak(text,1)
                        
        def GetLabel(self, text, rate, id):
            return "Speech: %s" % text
        
        def SetupConfigDialog(self, dialog):
            voice_list = self.VoiceObj.GetVoices()
            myvoices=[]
            
            for voice in voice_list:
                myvoices.append (os.path.basename(voice.Id))
    
            text = self.text
            vv = wx.StaticText(dialog, -1, "Voice:")
            combo2 = wx.Choice(dialog, -1,  choices=myvoices)
            combo2.Select(0)
            vr = wx.StaticText(dialog, -1, "Voice rate:")
            combo1 = wx.Choice(dialog, -1,  choices=rateList)
            combo1.Select(5)
            tx = wx.StaticText(dialog, -1, "Text to Speak:")
            edit1 = wx.TextCtrl(dialog, -1, value="EventGhost rocks !")
            test = wx.Button(dialog, -1, label="Test")
            
            dialog.sizer.Add(vv, 1, wx.EXPAND)
            dialog.sizer.Add(combo2, 1, wx.EXPAND)
            dialog.sizer.Add(vr, 1, wx.EXPAND)
            dialog.sizer.Add(combo1, 1, wx.EXPAND)
            dialog.sizer.Add(tx, 1, wx.EXPAND)
            dialog.sizer.Add(edit1, 1, wx.EXPAND) 
            dialog.sizer.Add(test, 1, wx.EXPAND)
            
            test.Bind(wx.EVT_BUTTON, self.OnEvtTest, test)

            def ReturnResult():
                return (edit1.GetValue(),combo1.GetSelection(),combo2.GetSelection())
            return ReturnResult

        def OnEvtTest(self,event):
            self.VoiceObj.Speak(self.text,1)

User avatar
Bitmonster
Site Admin
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Post by Bitmonster » Mon Feb 27, 2006 3:24 pm

Zuerst würde ich das VoiceObj in der Plugin __init__ initialisieren, damit alle Action-Klassen darauf zugreifen können. Die Action-Klassen können das Plugin immer über self.plugin referenzieren. Die Button-Callback-Funktion würde ich *innerhalb* der SetupConfigDialog-Funktion definieren, denn dann kann sie auf alle Variablen die dort definiert sind zurückgreifen. Also in der Art:

Code: Select all

test = wx.Button(dialog, -1, label="Test") 
def OnButton(event):
    self.plugin.VoiceObj.Speak(edit1.GetValue(), 1)
test.Bind(wx.EVT_BUTTON, OnButton) 

User avatar
Bitmonster
Site Admin
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Post by Bitmonster » Mon Feb 27, 2006 3:30 pm

Die SetupConfigDialog hat die gleiche Parameter wie __call__ und GetLabel, aber es ist zusätzlich noch das "dialog" Parameter dabei. Es müsste also heißen:

Code: Select all

SetupConfigDialog(self, dialog, text="EventGhost rocks !", rate=1, id=None): 
Die Default-Werte sind notwendig, da beim ersten Hinzufügen ja noch keine Parameter vorhanden sind. In dem Fall musst du also id auch auf None prüfen um dann einen Vorgabewert zu setzen.

User avatar
Bitmonster
Site Admin
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Post by Bitmonster » Mon Feb 27, 2006 3:40 pm

Und noch einen Tipp, den man in Python immer beachten sollte:
Viele Leute gewöhnen sich an, immer alles über den vollen Pfad zu referenzieren, weil es dank Copy&Paste so einfach ist. Jede unnötige Referenzierung kostet aber Rechenzeit und macht den Code auch nicht unbedingt lesbarer. Sowie man also ein Objekt mehr als einmal braucht, sollte man es erst in eine lokale Variable "zwischen-referenzieren". Also statt:

Code: Select all

            dialog.sizer.Add(vv, 1, wx.EXPAND)
            dialog.sizer.Add(combo2, 1, wx.EXPAND)
            dialog.sizer.Add(vr, 1, wx.EXPAND)
            dialog.sizer.Add(combo1, 1, wx.EXPAND)
            dialog.sizer.Add(tx, 1, wx.EXPAND)
            dialog.sizer.Add(edit1, 1, wx.EXPAND)
            dialog.sizer.Add(test, 1, wx.EXPAND)
würde ich z.B. schreiben:

Code: Select all

            sizer = dialog.sizer
            sizer.Add(vv, 1, wx.EXPAND)
            sizer.Add(combo2, 1, wx.EXPAND)
            sizer.Add(vr, 1, wx.EXPAND)
            sizer.Add(combo1, 1, wx.EXPAND)
            sizer.Add(tx, 1, wx.EXPAND)
            sizer.Add(edit1, 1, wx.EXPAND)
            sizer.Add(test, 1, wx.EXPAND)
Man kann es sogar auf die Spitze treiben (was ich aber bisher auch selten mache):

Code: Select all

            Add = dialog.sizer.Add
            Add(vv, 1, wx.EXPAND)
            Add(combo2, 1, wx.EXPAND)
            Add(vr, 1, wx.EXPAND)
            Add(combo1, 1, wx.EXPAND)
            Add(tx, 1, wx.EXPAND)
            Add(edit1, 1, wx.EXPAND)
            Add(test, 1, wx.EXPAND)

MonsterMagnet
Plugin Developer
Posts: 137
Joined: Fri Feb 10, 2006 12:04 pm

Post by MonsterMagnet » Mon Feb 27, 2006 3:53 pm

Danke !

Na da hab ich ja noch was vor mir... :wink:

Für heute reichts erstmal !

MonsterMagnet
Plugin Developer
Posts: 137
Joined: Fri Feb 10, 2006 12:04 pm

Post by MonsterMagnet » Fri Mar 03, 2006 2:14 pm

Da läßt sich sicher noch einiges verbessern :oops:

Code: Select all

from win32com.client import Dispatch
from time import strftime
import eg
import wx
import os

rateList = ("-5","-4","-3","-2","-1","0","+1","+2","+3","+4","+5")
dateList = ("None","Local time","Local date","Date and time")

class Speech(eg.PluginClass):
   
    def __init__(self):
        maingroup = self.AddGroup(self.name)
        maingroup.AddAction(self.TextToSpeech)
        try:
            self.VoiceObj = Dispatch("Sapi.SpVoice")
        except:
            eg.PrintError ("Cannot create Voice object") 

    class TextToSpeech(eg.ActionClass):
       
        def __call__(self, id, rate, text, time):
            self.plugin.VoiceObj.Voice = self.plugin.VoiceObj.GetVoices().Item(id)
            self.plugin.VoiceObj.Rate = rate
            if time == 1:
                text+= ", " + strftime("%X")
            elif time == 2:
                text+= ", " + strftime("%x")
            elif time == 3:
                text+= ", " + strftime("%x") + ", " +  strftime("%X")
            self.plugin.VoiceObj.Speak(text, 1)
            
        def GetLabel(self, id, rate, text, time):
            return "Speech: %s" %text
       
        def SetupConfigDialog(self, dialog, id=None, rate=0, text="Enter your text here", time=0):
            voice_list = self.plugin.VoiceObj.GetVoices()
            myvoices=[]
           
            for voice in voice_list:
                myvoices.append (os.path.basename(voice.Id))

            sizer = dialog.sizer
            mySizer = wx.GridSizer(cols=2)
            
            desc1 = wx.StaticText(dialog, -1, "Select Voice:")
            mySizer.Add(desc1, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
                        
            choise1 = wx.Choice(dialog, -1, choices=myvoices)
            choise1.Select(0)
            mySizer.Add(choise1, 0, wx.EXPAND|wx.ALL, 5)

            desc2 = wx.StaticText(dialog, -1, "Select Voice Rate:")
            mySizer.Add(desc2, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)

            choise2 = wx.ComboBox(dialog, -1, choices=rateList)
            choise2.Select(5)
            mySizer.Add(choise2, 0, wx.EXPAND|wx.ALL, 5)            

            desc3 = wx.StaticText(dialog, -1, "Time and date:")
            mySizer.Add(desc3, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
                        
            choise3 = wx.Choice(dialog, -1, choices=dateList)
            choise3.Select(0)
            mySizer.Add(choise3, 0, wx.EXPAND|wx.ALL, 5)
            
            sizer.Add(mySizer, 1, wx.EXPAND)

            textctrl = wx.TextCtrl( dialog, -1, "Enter your text here" )            
            sizer.Add(textctrl, 0, wx.EXPAND|wx.ALL, 5)
            
            button = wx.Button(dialog, -1, label="Test")
            def OnButton(event):
                self.plugin.VoiceObj.Voice = self.plugin.VoiceObj.GetVoices().Item(choise1.GetSelection())
                self.plugin.VoiceObj.Rate = choise2.GetValue()
                text = textctrl.GetValue()                
                time = choise3.GetSelection()
                if time == 1:
                    text+= ", " + strftime("%X")
                elif time == 2:
                    text+= ", " + strftime("%x")
                elif time == 3:
                    text+= ", " + strftime("%x") + ", " +  strftime("%X")
                self.plugin.VoiceObj.Speak(text, 1)                
            button.Bind(wx.EVT_BUTTON, OnButton)
            sizer.Add(button, 0, wx.EXPAND|wx.ALL, 5)
            
            def ReturnResult():                    
                return (choise1.GetSelection(), choise2.GetValue(), textctrl.GetValue(), choise3.GetSelection())
            return ReturnResult


User avatar
Bitmonster
Site Admin
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Post by Bitmonster » Fri Mar 03, 2006 4:48 pm

Sehr schön, das wird ja richtig was.
Ich habe trotzdem mal ein paar Änderungen vorgenommen: :)

Code: Select all

from win32com.client import Dispatch
from time import strftime
import eg
import wx
import os

rateList = ["-5","-4","-3","-2","-1","0","+1","+2","+3","+4","+5"]
dateList = ("None","Local time","Local date","Date and time")


class Speech(eg.PluginClass):
   
    def __init__(self):
        maingroup = self.AddGroup(self.name)
        maingroup.AddAction(self.TextToSpeech)
        try:
            self.VoiceObj = Dispatch("Sapi.SpVoice")
        except:
            eg.PrintError ("Cannot create Voice object")
            return
        
        # enumerate all available voices and store them in a dict with their
        # name as key
        voices = {}
        for voice in self.VoiceObj.GetVoices():
            voices[voice.GetDescription()] = voice
        self.voices = voices


    class TextToSpeech(eg.ActionClass):
       
        def __call__(self, voice_name, rate, text, time, volume):
            text = eg.parseString(text)
            VoiceObj = self.plugin.VoiceObj
            try:
                VoiceObj.Voice = self.plugin.voices[voice_name]
            except:
                eg.PrintError ("Voice with name %s is not available" % voice_name)
            VoiceObj.Rate = rate
            VoiceObj.Volume = volume
            if time == 1:
                text+= ", " + strftime("%X")
            elif time == 2:
                text+= ", " + strftime("%x")
            elif time == 3:
                text+= ", " + strftime("%x") + ", " +  strftime("%X")
            VoiceObj.Speak(text, 1)
           
        def GetLabel(self, voice_name, rate, text, time, volume):
            return "Speak: %s" % text
       
        def SetupConfigDialog(self, dialog, voice_name=None, rate="0", 
                              text="Enter your text here", time=0, volume=100):
            plugin = self.plugin
            VoiceObj = plugin.VoiceObj
            myvoices = plugin.voices.keys()
            myvoices.sort()
            try:
                voice_index = myvoices.index(voice_name)
            except:
                voice_index = 0
            sizer = dialog.sizer
            mySizer = wx.FlexGridSizer(cols=2)
           
            desc1 = wx.StaticText(dialog, -1, "Select Voice:")
            mySizer.Add(desc1, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
                       
            choice1 = wx.Choice(dialog, -1, choices=myvoices)
            choice1.Select(voice_index)
            mySizer.Add(choice1, 0, wx.EXPAND|wx.ALL, 5)

            desc2 = wx.StaticText(dialog, -1, "Select Voice Rate:")
            mySizer.Add(desc2, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)

            choice2 = wx.ComboBox(dialog, -1, choices=rateList)
            choice2.Select(rateList.index(str(rate)))
            mySizer.Add(choice2, 0, wx.EXPAND|wx.ALL, 5)           

            desc3 = wx.StaticText(dialog, -1, "Time and date:")
            mySizer.Add(desc3, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
                       
            choice3 = wx.Choice(dialog, -1, choices=dateList)
            choice3.Select(time)
            mySizer.Add(choice3, 0, wx.EXPAND|wx.ALL, 5)
           
            desc4 = wx.StaticText(dialog, -1, "Volume (percent):")
            mySizer.Add(desc4, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
                       
            vol_ctrl = eg.SpinIntCtrl(dialog, -1, volume, 0, 100)
            mySizer.Add(vol_ctrl, 0, wx.ALL, 5)
            
            sizer.Add(mySizer, 1, wx.EXPAND)

            textctrl = wx.TextCtrl(dialog, -1, text)           
            sizer.Add(textctrl, 0, wx.EXPAND|wx.ALL, 5)
           
            button = wx.Button(dialog, -1, label="Test")
            def OnButton(event):
                self(*ReturnResult())
            button.Bind(wx.EVT_BUTTON, OnButton)
            sizer.Add(button, 0, wx.EXPAND|wx.ALL, 5)
           
            def ReturnResult():                   
                return (choice1.GetStringSelection(), 
                        choice2.GetValue(), 
                        textctrl.GetValue(), 
                        choice3.GetSelection(),
                        vol_ctrl.GetValue())
            return ReturnResult
Zuerst habe ich mal alle verfügbaren Stimmen in ein Dictionary speichern lassen, damit man hinterher einfach über den Namen auf eine Stimme zugreifen kann. Dadurch speichert er jetzt auch die gesetzte Sprache richtig und die Konfiguration sollte sich auf andere Rechner übertragen lassen.

Die zweite Änderung ist, dass ich in der __call__ Routine den text zuerst über eg.parseString laufen lasse. Diese Funktion nimmt die Ersetzungen mit dem Geschweifte-Klammern-Syntax vor. Jetzt kann man also auch Variablen in das Textfeld eintragen.

Der Rest sind ein paar Optimierung des Zugriffes auf Objekte.

Vielleicht sollte man auch noch die Lautstärke auswählbar machen. Die geht bei SAPI glaube ich von 0 bis 100 (also in Prozent)? Dafür bietet sich
eg.SpinIntCtrl(dialog, -1, volume, min=0, max=100)
als Control an.

Lässt du EG mittlerweile vom Source-Code laufen? Ich glaube nämlich win32com.Dispatch läuft so nicht direkt bei den Leuten mit einer EXE Installation von EG, weil dieses dynamische Erzeugen der Wrapper über gen_py nicht drin ist. Dafür musste ich bisher immer das erzeugte Interface-File selber in das Plugin legen.
Last edited by Bitmonster on Fri Mar 03, 2006 6:03 pm, edited 1 time in total.

User avatar
Bitmonster
Site Admin
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Post by Bitmonster » Fri Mar 03, 2006 5:55 pm

Ich habe noch ein paar kleine Änderungen in dem obigen Source vorgenommen. Die anderen Werte wurden nämlich auch nicht wieder in die Controls übertragen, wenn man den Befehl erneut editieren wollte. Ausserdem habe ich gleich mal die Volume-Option mit eingebaut. Ich glaube, so kann es schon in die EG-Distribution aufgenommen werden.

User avatar
Bitmonster
Site Admin
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Post by Bitmonster » Fri Mar 03, 2006 6:09 pm

Und noch eine kleine Änderung. :)
Den Test-Button kann man nämlich viel einfacher realisieren, indem man ReturnResult und die eigentliche __call__-Routine ausnutzt.

Jetzt bin ich am überlegen, ob man die Geschichte nicht gleich in das System-Plugin von EG einbauen sollte. Also als eine direkt in EG eingebaute Funktion. Da müsste man nur darauf aufpassen, ob SAPI wirklich installiert ist und den User u.U. entsprechend informieren.

Post Reply