Questo sito utilizza cookies, anche di terze parti, per mostrare pubblicità e servizi in linea con il tuo account. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - Riconoscimento vocale

Guida al Visual Basic .NET

Capitolo 105° - Riconoscimento vocale

<< Precedente Prossimo >>
Questo capitolo è scritto per VB2008!

 

Costruire la grammatica

Per il riconoscimento vocale, la faccenda si fa un po' più complicata. L'oggetto principale su cui si regge tutto il capitolo è System.Speech.Recognition.SpeechRecognitionEngine. Non analizzerò in dettaglio i suoi membri, poiché la gran parte di essi verrà spiegata nel codice che scriverò dopo: basterà dire che per l'inizializzazione, anch'esso dispone di metodi SetInpuTo... identici a quelli di SpeechSynthesizer.
Ora, il computer non può prevedere tutte le possibili combinazioni di parole esistenti, quindi dobbiamo essere noi a fornirgli un "dizionario" su cui basarsi per il riconoscimento. La costruzione di una struttura di questo tipo richiede l'uso di un oggetto particolare: GrammarBuilder. Questa semplice classe aiuta a formare delle frasi che potranno venire catturate e riconosciute dall'Engine attraverso il microfono. Nelle prove che ho fatto, l'engine è riuscito a catturare una sola parola alla volta, ma forse mi sono dimenticato di impostare i tempi giusti di intervallo tra una parola e l'altra. Sta di fatto che il modo più semplice per far riconoscere una qualsiasi parola all'engine consiste nell'aggiungere a GrammarBuilder la lista di tutte le parole contemplate (ossia, solo quelle che vogliamo noi). Ecco un semplice esempio:

Imports System.Speech
Imports System.Speech.Recognition
Imports System.Speech.Synthesis

'...
Dim GrammarBuilder As New GrammarBuilder
GrammarBuilder.Append(New Choices("one", "two", "three", "four")) 

In questo modo si è aggiunta al GrammarBuilder una gamma di parole possibili: one, two, three e four. Usando un oggetto Choices comunichiamo all'engine che ogni parola può essere presa singolarmente. Ho usato dei numeri perchè l'esempio di questo capitolo sarà un programma in grado di riconoscere un numero pronunciato in inglese. A questo proposito, bisogna dire che l'oggetto GrammarBuilder può essere creato per differenti lingue (anche se non ci sono ancora pacchetti per molte lingue diverse), ma esso da solo non è in grado di riconoscere a quale lingua appartengano le parole che il programmatore inserisce: per questo prende come nazionalità di default quella del computer su cui sta correndo. Per il 99% di coloro che leggono questa guida, quindi, GrammarBuilder considererà la cultura corrente come Italiana, poiché il computer ha installata quella. Tuttavia l'engine che useremo è impostato solo per la lingua inglese e questo genererà sicuramente un errore. Perciò, prima di inserire la grammatica di GrammarBuilder nell'engine di riconoscimento vocale, dobbiamo specificare esplicitamente che si tratta di inglese:

GrammarBuilder.Culture = Globalization.CultureInfo.GetCultureInfo("en-US") 

Una volta fatto questo, per formare una nuova grammatica usabile (un insieme di parole in questo caso) bisogna creare un nuovo oggetto Grammar e passare al costruttore GrammarBuilder come parametro:

Dim Grammar As Grammar
'...
Grammar = New Grammar(GrammarBuilder) 

Ora manca la cosa più importante: l'engine di riconoscimento vocale. Useremo la classe SpeechRecognitionEngine, che descriverò direttamente nei commenti del prossimo esempio.

Esempio: Tell me how much

Per questo esempio basta un semplicissimo form, con una label (lblNumber) e due pulsanti (btnStart e btnStop). Questo è il codice:

Imports System.Speech
Imports System.Speech.Recognition
Imports System.Speech.Synthesis

Public Class Form2
    'Nuovo Engine di riconoscimento vocale
    Private Engine As New SpeechRecognitionEngine
    'GrammarBuilder per costruire la grammatica
    Private GrammarBuilder As New GrammarBuilder
    'Oggetto Grammar che rappresenta la grammatica
    Private Grammar As Grammar

    'Questo dizionario associa ad ogni parola il
    'corrispondente valore numerico (one=1)
    Private TextNumber As Dictionary(Of String, Int32)
    'Questo array già inizializzato contiene l'elenco di
    'tutte le parole che l'engine può rilevare
    Private Numbers As String() = _
        New String() {"one", "two", "three", "four", _
        "five", "six", "seven", "eight", "nine", "ten", _
        "eleven", "twelve", "thirteen", "fourteen", _
        "fiftheen", "sixteen", "seventeen", "eighteen", _
        "nineteen", "twenty", "thirty", "fourty", "fifty", _
        "sixty", "seventy", "eighty", "ninty", "hundred", _
        "thousand", "reset"}
    'L'ultima parola, reset, serve per porre a 0 il
    'conteggio, nel caso si volesse ripetere

    'Prev ricorda l'ultimo numero immesso
    Private Prev As Int32
    'Result contiene il numero finale
    Private Result As Int32

    Private Sub Form2_Load(ByVal sender As Object, ByVal e As EventArgs) _ 
        Handles MyBase.Load
        'All'avvio del form, si imposta l'input dell'engine sul
        'normale microfono (che deve essere collegato al computer).
        'Anche in questo caso si usa un thread, per lo stesso
        'motivo citato nel capitolo precedente
        Dim T As New Threading.Thread( _ 
            AddressOf Engine.SetInputToDefaultAudioDevice)
        T.Start()
        T.Join()

        'Poi si genera il dizionario che associa le parole ai
        'valori numerici veri e propri. Dato che l'array
        'Numbers contiene i numeri in ordine, sfruttermo
        'qualche for per riempire il dizionario in poche
        'righe di codice
        TextNumber = New Dictionary(Of String, Int32)
        With TextNumber
            'I primi 20 numeri sono in ordine crescente,
            'da 1 a 20. Perciò basta aggiungere 1
            'all'indice I per ottenere il numero che la
            'parola indica
            For I As Int16 = 0 To 19
                .Add(Numbers(I), I + 1)
            Next
            'I successivi sette numeri sono tutti i multipli
            'di 10, da 30 a 90. Con la formula:
            '(I-19)*10 + 20
            'è come se I andasse da 1 a 7 e quindi
            'otteniamo tutte le decine da 20+10 a 20+70
            For I As Int16 = 20 To 26
                .Add(Numbers(I), (I - 19) * 10 + 20)
            Next
            'Infine si aggiungono centinaia e migliaia a parte
            .Add("hundred", 100)
            .Add("thousand", 1000)
        End With

        'Aggiunge tutte le parole-numero al GrammarBuilder
        GrammarBuilder.Append(New Choices(Numbers))
        'Imposta la lingua a inglese
        GrammarBuilder.Culture = _ 
            Globalization.CultureInfo.GetCultureInfo("en-US")
        'Costruisce la nuova "grammatica" con il GrammarBuilder
        Grammar = New Grammar(GrammarBuilder)

        'Questo metodo serve per eliminare tutte le grammatiche
        'già presenti. Anche se quasi sicuramente non ci
        'sarà nessun grammatica precaricata, è sempre
        'meglio farlo prima di aggiungerne di nuove
        Engine.UnloadAllGrammars()
        'Quindi carica la grammatica Grammar. Ora Engine è in
        'grado di riconoscere le parole dell'array Numbers
        Engine.LoadGrammar(Grammar)
        'Parte importantissima: aggiunge l'handler di evento per
        'l'evento SpeechRecognized, che viene lanciato quando
        'l'engine ha ascoltato la voce, l'ha analizzata e ha
        'trovato una corrispondenza valida nella sua grammatica
        AddHandler Engine.SpeechRecognized, AddressOf Speech_Recognized
    End Sub

    Private Sub btnStart_Click(ByVal sender As Object, _ 
        ByVal e As EventArgs) Handles btnStart.Click
        'Fa partire il riconoscimento vocale. Il metodo è asincrono,
        'quindi viene eseguito su un altro thread e non blocca il form
        'chiamante. L'argomento Multiple indica che si effetteranno più
        'riconoscimenti e non uno solo
        Engine.RecognizeAsync(RecognizeMode.Multiple)

        'Disabilita Start e abilita Stop
        btnStart.Enabled = False
        btnStop.Enabled = True
    End Sub

    Private Sub btnStop_Click(ByVal sender As Object, _ 
        ByVal e As EventArgs) Handles btnStop.Click
        'Termina il riconoscimento asincrono
        Engine.RecognizeAsyncCancel()

        'Abilita Start e disabilita Stop
        btnStart.Enabled = True
        btnStop.Enabled = False
    End Sub

    Private Sub Speech_Recognized(ByVal sender As Object, _ 
        ByVal e As SpeechRecognizedEventArgs)
        Dim N As Int32
        'Ottiene il testo, ossia la parola pronunciata
        Dim Text As String = e.Result.Text

        'Se il testo è "reset", annulla tutto
        If Text = "reset" Then
            Result = 0
        End If

        'Se il testo è contenuto nel dizionario, allora
        'è un numero valido
        If TextNumber.ContainsKey(Text) Then
            'Ottiene il numero
            N = TextNumber(Text)
            'Se è 100, significa che si è pronunciato
            '"hundred". Hundred indica le centinaia e perciò
            'sicuramente non si può dire "twenty hundred", né
            '"one thousand hundred": l'unico caso in cui si può
            'usare hundred è dopo una singola cifra, ad esempio
            '"one hundred" o "nine hundred". Quindi controlla che il
            'numero precedente sia compreso tra 1 e 9
            If (N = 100) And (Prev > 0 And Prev < 10) Then
                'Toglie l'unità
                Result -= Prev
                'E la trasforma in centinaia
                Result += Prev * 100
            End If
            'Parimenti, si può usare "thousand" solo dopo un
            'numero minore di mille. Anche se lecito, nessuno direbbe
            '"a thousand thousand", ma piuttosto "a million"
            If (N = 1000) And (Result < 1000) Then
                Result *= 1000
            End If
            'Se il numero è minore di 100, semplicemente lo
            'aggiunge. Se quindi si pronunciano "twenty" e "thirty"
            'di seguito, si otterà 50. Non chiedetemi perchè
            'l'ho fatto così...
            If (N < 100) Then
                Result += N
            End If
        Else
            N = 0
        End If

        Prev = N

        'Imposta il testo della label
        lblNumber.Text = String.Format("{0:N0}", Result)
    End Sub
End Class 

Eseguendo questo codice e parlando bene nel microfono, si dovrebbe ottenere un risultato discreto (alcune parole, comunque, si confondono). Nonostante il codice sia esatto, tuttavia, System.Speech rimane un namespace strano, poiché le sue classi generano spesso errori incomprensibili. Se siete stati così fortunati da aver riecevuto, come me, una TargetInvocationException nell'evento SpeechRecognized, mi sarete grati per il codice che propongo qui, un'alternativa più di quella sopra, ma almeno più sicura:

Imports System.Speech
Imports System.Speech.Recognition
Imports System.Speech.Synthesis

Public Class Form2
    'Nuovo Engine di riconoscimento vocale
    Private Engine As New SpeechRecognitionEngine
    'GrammarBuilder per costruire la grammatica
    Private GrammarBuilder As New GrammarBuilder
    'Oggetto Grammar che rappresenta la grammatica
    Private Grammar As Grammar

    'Questo dizionario associa ad ogni parola il
    'corrispondente valore numerico (one=1)
    Private TextNumber As Dictionary(Of String, Int32)
    'Questo array già inizializzato contiene l'elenco di
    'tutte le parole che l'engine può rilevare
    Private Numbers As String() = _
        New String() {"one", "two", "three", "four", _
        "five", "six", "seven", "eight", "nine", "ten", _
        "eleven", "twelve", "thirteen", "fourteen", _
        "fiftheen", "sixteen", "seventeen", "eighteen", _
        "nineteen", "twenty", "thirty", "fourty", "fifty", _
        "sixty", "seventy", "eighty", "ninty", "hundred", _
        "thousand", "reset"}
    'L'ultima parola, reset, serve per porre a 0 il
    'conteggio, nel caso si volesse ripetere

    'Delegato che servirà dopo
    Private Delegate Sub SetLabel(ByVal Res As RecognitionResult)
    'Prev ricorda l'ultimo numero immesso
    Private Prev As Int32
    'Result contiene il numero finale
    Private Result As Int32

    Private Sub Form2_Load(ByVal sender As Object, _ 
        ByVal e As System.EventArgs) Handles MyBase.Load
        'All'avvio del form, si imposta l'input dell'engine sul
        'normale microfono (che deve essere collegato al computer).
        'Anche in questo caso si usa un thread, per lo stesso
        'motivo citato nel capitolo precedente
        Dim T As New Threading.Thread( _ 
            AddressOf Engine.SetInputToDefaultAudioDevice)
        T.Start()
        T.Join()

        'Poi si genera il dizionario che associa le parole ai
        'valori numerici veri e propri. Dato che l'array
        'Numbers contiene i numeri in ordine, sfruttermo
        'qualche for per riempire il dizionario in poche
        'righe di codice
        TextNumber = New Dictionary(Of String, Int32)
        With TextNumber
            'I primi 20 numeri sono in ordine crescente,
            'da 1 a 20. Perciò basta aggiungere 1
            'all'indice I per ottenere il numero che la
            'parola indica
            For I As Int16 = 0 To 19
                .Add(Numbers(I), I + 1)
            Next
            'I successivi sette numeri sono tutti i multipli
            'di 10, da 30 a 90. Con la formula:
            '(I-19)*10 + 20
            'è come se I andasse da 1 a 7 e quindi
            'otteniamo tutte le decine da 20+10 a 20+70
            For I As Int16 = 20 To 26
                .Add(Numbers(I), (I - 19) * 10 + 20)
            Next
            'Infine si aggiungono centinaia e migliaia a parte
            .Add("hundred", 100)
            .Add("thousand", 1000)
        End With

        'Aggiunge tutte le parole-numero al GrammarBuilder
        GrammarBuilder.Append(New Choices(Numbers))
        'Imposta la lingua a inglese
        GrammarBuilder.Culture = Globalization.CultureInfo.GetCultureInfo("en-US")
        'Costruisce la nuova "grammatica" con il GrammarBuilder
        Grammar = New Grammar(GrammarBuilder)

        'Questo metodo serve per eliminare tutte le grammatiche
        'già presenti. Anche se quasi sicuramente non ci
        'sarà nessun grammatica precaricata, è sempre
        'meglio farlo prima di aggiungerne di nuove
        Engine.UnloadAllGrammars()
        'Quindi carica la grammatica Grammar. Ora Engine è in
        'grado di riconoscere le parole dell'array Numbers
        Engine.LoadGrammar(Grammar)
        'Parte importantissima: aggiunge l'handler di evento per
        'l'evento SpeechRecognized, che viene lanciato quando
        'l'engine ha ascoltato la voce, l'ha analizzata e ha
        'trovato una corrispondenza valida nella sua grammatica
        AddHandler Engine.SpeechRecognized, AddressOf Speech_Recognized
    End Sub

    Private Sub btnStart_Click(ByVal sender As Object, _ 
        ByVal e As EventArgs) Handles btnStart.Click
        'Fa partire il riconoscimento vocale. Il metodo è asincrono,
        'quindi viene eseguito su un altro thread e non blocca il form
        'chiamante. L'argomento Multiple indica che si effetteranno più
        'riconoscimenti e non uno solo
        Engine.RecognizeAsync(RecognizeMode.Multiple)

        'Disabilita Start e abilita Stop
        btnStart.Enabled = False
        btnStop.Enabled = True
    End Sub

    Private Sub btnStop_Click(ByVal sender As Object, _ 
        ByVal e As EventArgs) Handles btnStop.Click
        'Termina il riconoscimento asincrono
        Engine.RecognizeAsyncCancel()

        'Abilita Start e disabilita Stop
        btnStart.Enabled = True
        btnStop.Enabled = False
    End Sub

    Private Sub Speech_Recognized(ByVal sender As Object, _ 
        ByVal e As SpeechRecognizedEventArgs)
        'Può capitare che dopo l'esecuzione di questo evento,
        'sia generata un'eccezione TargetInvocationException, causata
        'dall'engine, il quale lancia un evento uguale prima che
        'questo sia terminato. Usando un thread risolviamo tutto
        Dim T As New Threading.Thread(AddressOf InvokeSetLabel)
        T.Start(e.Result)
    End Sub

    Private Sub InvokeSetLabel(ByVal Res As RecognitionResult)
        'Ovviamente questi stupido tipo di errori ci fa usare
        'una via alternativa sprecando molto codice in più.
        'Dato che, come sapete, non si può accedere ai
        'controlli di un form da un thread differente da quello
        'in cui sono stati creati, dobbiamo usare Invoke
        'per far eseguire lo stesso compito al thread principale
        'partendo da questo thread secondario.
        'Per chi non si ricorda i delegate, Invoke permette di
        'far correre un metodo nel thread dell'oggetto da cui è
        'richiamato (Me, ossia il form). In questo caso usiamo
        'il delegato di tipo SetLabel che punta ad AnalyuzeText
        'e gli passiamo dierettamente Res come parametro
        Me.Invoke(New SetLabel(AddressOf AnalyzeText), _ 
            New Object() {Res})
    End Sub

    Private Sub AnalyzeText(ByVal Res As RecognitionResult)
        Dim N As Int32
        'Ottiene il testo, ossia la parola pronunciata
        Dim Text As String = Res.Text

        'Se il testo è "reset", annulla tutto
        If Text = "reset" Then
            Result = 0
        End If

        'Se il testo è contenuto nel dizionario, allora
        'è un numero valido
        If TextNumber.ContainsKey(Text) Then
            'Ottiene il numero
            N = TextNumber(Text)
            'Se è 100, significa che si è pronunciato
            '"hundred". Hundred indica le centinaia e perciò
            'sicuramente non si può dire "twenty hundred", né
            '"one thousand hundred": l'unico caso in cui si può
            'usare hundred è dopo una singola cifra, ad esempio
            '"one hundred" o "nine hundred". Quindi controlla che il
            'numero precedente sia compreso tra 1 e 9
            If (N = 100) And (Prev > 0 And Prev < 10) Then
                'Toglie l'unità
                Result -= Prev
                'E la trasforma in centinaia
                Result += Prev * 100
            End If
            'Parimenti, si può usare "thousand" solo dopo un
            'numero minore di mille. Anche se lecito, nessuno direbbe
            '"a thousand thousand", ma piuttosto "a million"
            If (N = 1000) And (Result < 1000) Then
                Result *= 1000
            End If
            'Se il numero è minore di 100, semplicemente lo
            'aggiunge. Se quindi si pronunciano "twenty" e "thirty"
            'di seguito, si otterà 50. Non chiedetemi perchè
            'l'ho fatto così...
            If (N < 100) Then
                Result += N
            End If
        Else
            N = 0
        End If

        Prev = N

        'Imposta il testo della label
        lblNumber.Text = String.Format("{0:N0}", Result)
    End Sub
End Class 

Potrebbe anche verificarsi un altro errore, qui:

Me.Invoke(New SetLabel(AddressOf AnalyzeText), New Object() {Res}) 

di tipo FormatException, che non c'entra assolutamente niente con quello che si sta facendo. Se anche voi siete così fortunati, chiudete visual studio e riprovateci domani (con me ha funzionato XD).

<< Precedente Prossimo >>
A proposito dell'autore

Programmatore e analista .NET 2005/2008/2010 (in particolare C# e VB.NET), anche nell'implementazione Mono per Linux. Conoscenze approfondite di Pascal, PHP, XML, HTML 4.01/5, CSS 2.1/3, Javascript (e jQuery). Conoscenze buone di C, LUA, GML, Ruby, XNA, AJAX e Assembly 68000. Competenze basilari di C++, SQL, Hlsl, Java.