Utilizar o NodeMcu para criar um web server com as funcionalidades de reconhecimento de voz que existem no Google Chrome é uma estratégia interessante. Entretanto, esta solução deixou de funcionar com uma atualização do navegador. De qualquer forma, neste post mostrarei como fiz para implementá-la, apenas a título de curiosidade.

Caso você não tenha lido o post sobre as diferentes formas de fazer reconhecimento de voz, passe lá para entender tudo que vou falar.


Características

Esta solução utiliza um web server criado pelo NodeMcu e o Web Speech API do Google.

nodemcu comandando led pelo web server
Exemplo de um web server do NodeMcu

O Web Speech API possibilita o uso do reconhecimento de voz nos navegadores por meio do código em html/javascript. Como o web server é feito em html, podemos facilmente utilizar esta API e criar uma função de reconhecimento de voz.

Considerando que esse método ainda funcione:

Como foi dito no outro post, essa é uma solução mesclada. Portanto, a principal desvantagem é a falta de praticidade em se usar o sistema. E, a vantagem é que esta é uma solução simples e é tão boa quanto outras que mostrei aqui (links no fim do tópico abaixo).

Para alcançar o resultado, não é necessário instalar nenhum programa específico (desconsiderando o navegador e a IDE do NodeMcu). É necessário apenas comprar a plaquinha e o restante é feito sem custo.

Abaixo falarei o porquê deste método ter parado de funcionar. 


Problema

Em uma atualização um pouco recente do Google Chrome, em relação à presente data do post, foi proibido o acesso ao microfone pelas páginas que não utilizam o protocolo HTTPS. Já comentei sobre o HTTP em outros posts, e basicamente o HTTPS é um protocolo mais seguro e confiável de transmissão de dados.

Mas o que isso altera? Acontece que o NodeMcu é capaz de criar apenas web server com o protocolo http (pelos meios convencionais). Então, fica impossível a página acessar o microfone para fazer o reconhecimento de voz.

Pesquisei se havia alguma maneira de desabilitar esse recurso. Entretanto, não obtive resultados satisfatórios. Busquei também uma forma de criar um web server com o protocolo https, mas a única alternativa que encontrei é um tanto quanto complicada. Deixo aqui o link para quem tiver interesse.

Considerei também a possibilidade de utilizar outros navegadores. Inclusive, o Mozilla Firefox também tem uma API para reconhecimento de voz. Novamente, o desfecho foi infeliz. A API do Mozilla estava apresentando alguns erros e a API do Google no Mozilla não teve efeito (considerando minhas versões).

E, além disso, o Internet Explorer ou o Edge, não possuem compatibilidade para nenhuma das API’s. Ou seja, é praticamente um beco sem saída.

De qualquer forma, não deixa de ser interessante observar como criei a página (html) e o processamento dos comandos. Inclusive, esse foi um projeto que fiz pra faculdade (na época funcionava).

Vou deixar abaixo os links para os outros posts de reconhecimento de voz (até a data do post), que funcionam de verdade e são bem úteis (na ordem do melhor pro “pior” na minha opinião): extensão do Chromeusando o Processing e o módulo de reconhecimento de voz.


Como fazer

O reconhecimento de voz é feito no web server e os dados são enviados para o NodeMcu, onde são processados. Recomendo o curso NodeMcu Básico para entender como fazer boa parte dos procedimentos que descreverei. O procedimento que faremos pode ser ilustrado pela seguinte imagem:

principio-de-funcionamento-reconhecimento-de-voz-nodemcu

Sem mais enrolação, vamos ao passo a passo:

Página HTML

O primeiro passo é criar a página com as funcionalidades do reconhecimento de voz. Para isso, farei a página da seguinte forma:

Terá um campo de texto onde irá aparecer o que está sendo reconhecido; um botão para ativar/desativar o reconhecimento de voz; um form para enviar os dados para o NodeMcu; elementos adicionais para enfeitar a página (ex: título, subtítulo, cores); por fim, o script.

Então, vamos analisar a criação dos elementos separadamente, um em cada tópico:

Campo de texto

O campo de texto irá apenas receber o texto do script contendo o reconhecimento de voz. O que fiz foi criar um <div>:

1
<div id="result" value=""></div>

Repare que dei a ele o nome de “result”. Além disso, no ínicio do código, editei o estilo do div:

1
2
3
4
5
6
7
8
9
10
11
12
<style>
#result{
    height: 30px;
    border: 2px inset white;
    border-radius:10px;
    padding: 3px 10px 10px 10px;
    margin-bottom: 30px;
    font-size: 14px;
    line-height: 20px;
    background-color:white;
}
</style>

Botão ativar/desativar reconhecimento

A criação do botão é bem direta:

1
<button style="font-size:35px;position:absolute;left:48.8%;top:200px;border-radius:25px;padding: 3px 10px;border-color:rgb(89, 171, 227);background-color:rgb(228, 241, 254);" id="fala"><i class="fa fa-microphone"></i></button>

Pode parecer muito, mas a maioria é adicionando detalhes visuais ao botão. O importante é o ‘id’, que configurei como “fala”. É por meio dele que faremos as funções do botão.

Form

Para saber pra que serve o form e como ele funciona, veja este post do curso NodeMcu Básico.

Neste caso, o form não precisa ter um botão de submit, pois a submissão será feita pelo script. Será necessário apenas ter um input que fica escondido recebendo o texto da fala reconhecida. Portanto:

1
2
3
<form action="" method="POST" id="dados" target="refresh">
<input type="hidden" name="Reconhecido" id="mandar">
</form>

O parâmetro importante aqui é também o ‘id’, que defini como “dados”. Utilizaremos ele para fazer a submissão dos dados.

Para que a página não dê refresh toda vez que uma informação for submetida, utilizei um truque interessante: defini o ‘target’ (onde a resposta da submissão é exibida) como sendo o elemento “refresh”; e o elemento “refresh” é o seguinte <iframe>:

<iframe name=”refresh” style=”display:none;”></iframe>

Fazer isso faz com que a página permaneça intacta sem dar refresh. Assim, poderemos criar uma lista dos comandos já ditos:

<ol id=”lista”></ol>

Para a lista, definimos o ‘id’ como sendo “lista”. Bem intuitivo.

Script do reconhecimento

Abordarei os pontos mais importantes do script. O código completo dele pode ser visto abaixo do tópico dos resultados. Fique atento ao uso dos ‘ids’ citados nos tópicos anteriores.

Primeiro é necessário criar a instância do reconhecimento de voz com o seguinte comando:

1
var speechRecognizer = new webkitSpeechRecognition();

Essa instância possui duas principais funções, que são: 

1
2
speechRecognizer.onresult = function(event){}
speechRecognizer.onerror = function (event) {}

A primeira é executada toda vez que o reconhecimento de voz retorna uma transcrição (mesmo que de uma letra/palavra). Já a segunda, é executada quando ocorre algum erro no processo de reconhecimento (falta de microfone, erro de conexão etc). Dentro da primeira função, ficamos gravando constantemente o resultado traduzido em tempo real e executamos alguns comandos importantes quando a transcrição termina (quando a pessoa para de falar).

Para detectar o fim da transcrição usamos: event.results[indiceDoEvento].isFinal. Quando isso ocorre, nós definimos o valor do ‘form’ como sendo o texto reconhecido (finalTranscripts) e submetemos este valor:

1
2
document.getElementById('mandar').value = finalTranscripts;
document.getElementById('dados').submit();

É claro que tem muitos outros aspectos que estão no código completo que não estou abordando. Porém, estou tentando dar uma visão simplificada do procedimento. O restante dos comandos dá para interpretar sem muita dificuldade.

Script do botão

Por fim, explicarei o script do botão. É possível detectar se ele foi clicado com a seguinte função:

1
document.getElementById('fala').onclick = function() {

Como o botão serve para ativar/desativar o reconhecimento de voz, preciso de uma variável para informar qual é o estado atual do botão (ativo ou desativo):

var pres = false;

Considerando que ela começa em false (botão desativado), ao apertar o botão, o reconhecimento de voz deve ser iniciado e o valor da variável deve ser invertido. Se o botão já está ativo e eu apertá-lo novamente, o reconhecimento deve parar e a variável deve ser invertida:

1
2
3
4
5
6
7
if (pres == true){
    speechRecognizer.stop();
    pres = !pres;
}else{
    pres = !pres;
    speechRecognizer.start();
}

Novamente, no código completo existem mais comandos, mas foquei no principal. Por exemplo, se você clicar no botão para desativar o reconhecimento antes da transcrição terminar, o programa força a submissão do texto que foi reconhecido.

Resultado

Adicionando algumas configurações de cor e estilo, o resultado ficou o seguinte:

reconhecimento de voz página na web funcional

Quando colocamos está página no NodeMcu e clicamos no botão, o seguinte problema aparece:

problema da página de reconhecimento de voz

Este é o problema que comentei no início do post. O Google Chrome não deixa habilitar de forma alguma o microfone (mesmo clicando em ‘perguntar se deseja acessar’). Na primeira imagem deu certo, pois abri a página como um arquivo local e não por um web server.

Código HTML completo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<style>
#result{
    height: 30px;
    border: 2px inset white;
    border-radius:10px;
    padding: 3px 10px 10px 10px;
    margin-bottom: 30px;
    font-size: 14px;
    line-height: 20px;
    background-color:white;
}
</style>
</head>
<body style="background-color:rgb(103, 128,159)">
<p style="color:white;text-align:center;font-family: Segoe UI Black;font-size:32px;font-weight: bold;">Sistema Controlado por Voz</p>
<div style="text-align:center;">Clique no botão e comece a falar</div>
<iframe name="refresh" style="display:none;"></iframe>
<form action="" method="POST" id="dados" target="refresh">
<input type="hidden" name="Reconhecido" id="mandar">
</form>
<div id="result" value=""></div>
<button style="font-size:35px;position:absolute;left:48.8%;top:200px;border-radius:25px;padding: 3px 10px;border-color:rgb(89, 171, 227);background-color:rgb(228, 241, 254);" id="fala"><i class="fa fa-microphone"></i></button>
<ol id="lista"></ol>
<script>
var speechRecognizer = new webkitSpeechRecognition();
var pres = false;
speechRecognizer.continuous = true;
speechRecognizer.interimResults = true;
speechRecognizer.lang = 'pt-BR';
speechRecognizer.onresult = function(event){
    var interimTranscripts = '';
    var finalTranscripts = '';
    for(var i = event.resultIndex; i < event.results.length; i++){
       var transcript = event.results[i][0].transcript;
       transcript.replace("\n", "<br>");
        if(event.results[i].isFinal){
            finalTranscripts = transcript;
            document.getElementById('mandar').value = finalTranscripts;
            document.getElementById('dados').submit();

            speechRecognizer.stop();
            document.getElementById('fala').style.backgroundColor = 'rgb(228, 241, 254)';
            pres = !pres;
            document.getElementById('lista').innerHTML += "<li>" + finalTranscripts + "</li>";
            document.getElementById('mandar').value = "nada";
        }else{
            interimTranscripts += transcript;
        }
    }
    document.getElementById('result').innerHTML = finalTranscripts + '<span style="color:#999">' + interimTranscripts + '</span>';
};
speechRecognizer.onerror = function (event) {
    console.log(event.error);
};
document.getElementById('fala').onclick = function() {
    if (pres == true){
        speechRecognizer.stop();
        document.getElementById('fala').style.backgroundColor = 'rgb(228, 241, 254)';
        pres = !pres;

        document.getElementById('dados').submit();
        document.getElementById('mandar').value = "nada";
    }else{
        pres = !pres;
        speechRecognizer.start();
        document.getElementById('result').innerHTML = " ";
        document.getElementById('fala').style.backgroundColor = 'rgb(37, 116, 169)';
    }
}
</script>
</body>
</html>

NodeMcu

Disponibilizarei o código que criei que pega a fala reconhecida e faz o processamento dela para acionar pinos do NodeMcu. Os comandos existentes são: ligar, desligar, horas, criar, apagar. Os dois primeiros servem para ligar/desligar um pino; o comando “horas” faz a página falar as horas (síntese de voz); e os últimos dois criam/apagam comandos.

Antes disso, vou explicar algumas partes importantes do código.

Criação do web server

Não vou explicar como a criação do web server é feita, pois já fiz isso neste post. Quero apenas explicar um ponto novo aqui.

Toda vez que uma submissão for feita no web server, ela chega da seguinte forma:

… Reconhecido=”texto reconhecido” …

Saiba mais sobre, lendo este post.

Portanto, toda vez que recebermos a submissão, precisamos recortar o texto reconhecido para processá-lo. A forma que escolhi de fazer isso foi criar uma variável chamada postparse que recebe o valor do indice da palavra “Reconhecido” com o comando ‘string.find’. Se o índice realmente existir, então extraímos o texto usando o comando string.sub. Por fim, enviados o texto, juntamente com a conexão, para uma função que criamos (chamada processamento). Enviamos o conn, pois essa função precisará enviar alguns dados para o cliente. 

1
2
3
4
5
postparse={string.find(payload,"Reconhecido")} --Pega o indice da palavra 'reconhecido' no payload
if postparse[2]~=nil then
    reco=string.sub(payload,postparse[2]+2,#payload) --extraimos a frase reconhecida
    processamento(conn, reco) --Envia os dados para o processamento dos comandos
end

Processamento dos dados

Explicar passo a passo da função é bem complicado, visto que é possível utilizar uma outra lógica melhor (creio eu). Sugiro ler os comentários para entender. Portanto, vou explicar a lógica de funcionamento do processamento dos dados.

Considere o texto recebido pela função:

“Quero ligar a luz da sala e desligar a luz do quarto e quero saber as horas”

A função do processamento, basicamente, divide o texto por comando. Ela verifica se no texto existe algum comando (dos que foram definidos), e pega aquele que tem o menor índice (o primeiro que aparece). Dado este comando, a função pega todo o texto que está depois dele até o próximo comando. Ou seja, considerando nosso exemplo, a função pegaria o comando “ligar”, com o texto “a luz da sala e “. A palavra “quero” foi ignorada, pois o comando (“ligar”) está depois dela.

Dessa forma, nós obtemos o comando e as características associadas a ele. No nosso caso, a ação é “ligar” e a característica associada a esta ação é “a luz da sala e”. Esta letra “e” no final acaba não tendo importância na segunda parte do processamento. Enfim, quando o programa faz esse reconhecimento do primeiro comando, ele executa a ação do comando (tópico abaixo) e depois corta o texto deixando do segundo comando pra frente.

No exemplo citado, o programa cortaria o texto e deixaria apenas:

“desligar a luz do quarto e quero saber as horas”

Dessa forma, ele pode fazer a verificação toda novamente e identificar qual é o primeiro comando que aparece. E assim se repete até não existir mais comandos.

Observações

Aqui valem ser feitas duas observações:

  • Adicionei um recurso que, se a função detecta a palavra “cancelar”, ela corta o texto todo que está atrás da palavra.
    • Ex: “quero ligar a luz da sala cancelar quero saber as horas” – tudo que está atrás de cancelar é cortado e sobra apenas: “quero saber as horas”.
  • O comando horas pode acabar tendo alguma “característica” associada a ele (texto depois do comando), mas isso não faz diferença no acionamento do comando (tópico seguinte).
    • Ex: “me fale as horas e quero ligar a luz do quarto” – o programa identifica o comando “horas” e pega o texto associado que está antes do próximo comando, que é: “e quero”. Mesmo com esse texto associado, o resultado não é atrapalhado.

Acionamento dos comandos

O acionamento dos comandos é feito logo depois que o processamento detecta qual é o comando e o texto associado da frase dita. Se o comando dito for:

Horas
  • O programa envia um código de síntese de voz com o elemento <script>
  • cliente:send(‘<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance(“‘ .. “19:45″ .. ‘”));</script>’)
  • Coloquei uma hora fixa (19:45), pois quis apenas mostrar a possibilidade da síntese de voz. Para pegar a hora atual é muito simples, veja aqui.
Ligar/Desligar
  • Criei uma tabela com um nome e um número associado a ele. Esta lista serve de referencia para saber quais nomes existem para acionar os dispositivos e quais são os pinos (GPIO) associados a eles.
  • Ex: a tabela que já existe: acionamento = {“luz da sala:1″,”luz do quarto:2″,”luz da cozinha:3”} – se eu falar “ligar luz da sala”, o programa aciona o pino 1.
  • A lógica do programa é passar em todos os elementos da tabela “acionamento” e verificar se existe cada acionamento no texto associado ao comando. Se existe, ele liga/desliga o pino atrelado ao acionamento.
  • É possível fazer dois acionamentos ao mesmo tempo: ‘quero ligar a luz da sala e a luz do quarto’.
Criar
  • Para criar um comando, é preciso dizer “criar ‘nome do comando’ pino ‘pino associado’ “. É possível, ter um mesmo nome acionando mais de um pino. E é possível também, ter nomes diferentes acionando o mesmo pino.
  • A função separa o nome do comando que você quer do pino que você falou e adiciona ele à tabela ‘acionamento’. Além disso, o programa informa, por voz no web server, se deu certo ou não.
  • Ex: “criar luz do escritório pino 6” – adiciona “escritorio:6” à tabela ‘acionamento’. Ou seja, você pode falar “ligar luz do escritório” para ligar o pino 6.
Apagar
  • Para apagar um comando, basta dizer “apagar ‘nome do comando’ “.
  • A função passa por todos os elementos da tabela “acionamento” e verifica se existe o acionamento que a pessoa falou. Se sim, ele é removido da tabela. Além disso, o programa informa, por voz no web server, se deu certo.
  • Ex: “apagar luz da sala” – o acionamento padrão “luz da sala” não existirá mais.

Código completo

Lembrando, ler os outros posts que referenciei ajuda bastante a entender o código completo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
wifi.setmode(wifi.STATION)
wifi.sta.config {ssid="NOME_DO_SEU_WI-FI", pwd="SENHA_DO_WI-FI"}

comandos = {"ligar","desligar","horas","criar","apagar"} --Define os comandos existentes
acionamento = {"luz da sala:1","luz do quarto:2","luz da cozinha:3"}

gpio.mode(1,gpio.OUTPUT) --Define 6 pinos como saida (pode ser aumentado até 13)
gpio.mode(2,gpio.OUTPUT)
gpio.mode(3,gpio.OUTPUT)
gpio.mode(4,gpio.OUTPUT)
gpio.mode(5,gpio.OUTPUT)
gpio.mode(6,gpio.OUTPUT)

tmr.alarm(0, 1000, 1, function() --Aguarda conectar na rede
   if wifi.sta.getip() == nil then
      print("Conectando à rede...\n")
   else
      ip=wifi.sta.getip()
      print("IP: ",ip)
      tmr.stop(0)
   end
end)


srv = net.createServer(net.TCP)

srv:listen(80, function(conn)
    conn:on("receive", function(conn, payload)
        tls.cert.verify(enable)
        print(payload) -- Imprime os dados recebidos do servidor
        postparse = 0

        postparse={string.find(payload,"Reconhecido")} --Pega o indice da palavra 'reconhecido' no payload
        if postparse[2]~=nil then
            reco=string.sub(payload,postparse[2]+2,#payload) --extraimos a frase reconhecida
            processamento(conn, reco) --Envia os dados para o processamento dos comandos
        end


        local _line       --Envia a pagina em html para o usuario
        if file.open("index.html","r") then
            repeat
                _line = file.readline()
                if (_line~=nil) then
                   conn:send(string.sub(_line,1,-2))
                end
            until _line==nil
            file.close()
        end
       
        conn:on("sent", function(conn) conn:close() end) --fecha a conexão
    end)
end)


----PROCESSA OS COMANDOS----
function processamento(cliente, base)
    base = string.lower(base)

    repeat
        if string.find(base,"cancelar") ~= nil then --Ignora tudo que foi falado antes da palavra cancelar
           base = string.sub(base,string.find(base,"cancelar")+string.len("cancelar"))
        end
       
        comando = '****'; --Qual o comando reconhecido
        ref = string.len(base) --A referencia inicial
        cont = false --A variável que indica se ha outros comandos na string
             
        for i=#comandos, 1, -1 do
            if string.find(base,"%f[%a]" .. comandos[i] .. "%f[%A]") ~= nil then
                if string.find(base,"%f[%a]" .. comandos[i] .. "%f[%A]")<ref then --Testa o comando com menor indice
                    ref = string.find(base,comandos[i])+string.len(comandos[i]) --Pega o comando com menor indice e define ele como referencia
                    comando = comandos[i] --Pega o valor do comando de menor indice
                end
            end
        end

        x = pegarelementos(base,comando) --Funçao que retorna os atributos do comando(o que o comando deve fazer)
        x = string.gsub(x, "+", " ")

        if comando == "criar" then --Funcao que adiciona um comando nos acionamentos
            if string.find(x, "pino") ~= nil and string.match(x, '%d') then
                y=string.sub(x,2,string.find(x,"pino")-2)
                pino = string.match(x, '%d')--pino=string.sub(x,string.find(x,"pino")+5,-1)
                table.insert(acionamento,1,y .. ":" .. pino)
                cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Comando adicionado" .. '"));</script>')
            else    cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Faltou informar o pino" .. '"));</script>')    end
        elseif comando == "apagar" then --Funcao que apaga um comandos dos acionamentos
            for i=#acionamento,1,-1 do                      -- Funcao que aciona os pinos de acordo com o comando
                y=string.sub(acionamento[i],1,string.find(acionamento[i],":")-1) -- Qual e o acionamento
                if string.find(x,y) ~= nil then
                    table.remove(acionamento,i)
                    cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Comando removido" .. '"));</script>')
                end
            end
        elseif comando == "horas" then
            cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "19:45" .. '"));</script>')
        else
            for i=#acionamento,1,-1 do                      -- Funcao que aciona os pinos de acordo com o comando
                y=string.sub(acionamento[i],1,string.find(acionamento[i],":")-1) -- Qual e o acionamento
                if string.find(x,"%f[%a]" .. y .. "%f[%A]") ~= nil then
                    pino=string.sub(acionamento[i],string.find(acionamento[i],":")+1,-1) --Qual e o pino que deve ser acionado
                    if comando == "ligar" or comando == "Ligar" then
                        gpio.write(pino,gpio.HIGH)
                        cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Ligando".. y .. '"));</script>')
                    end
                    if comando == "desligar" or comando == "desligar" then
                        gpio.write(pino,gpio.LOW)
                        cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Desligando".. y .. '"));</script>')
                    end
                end
            end    
        end

        base = string.sub(base,ref) --Redefine a base, excluindo o comando analisado
       
        for i=#comandos, 1, -1 do --Testa se ainda ha outros comandos para serem analisados
            if string.find(base,comandos[i]) ~= nil then
                cont = true
            end
        end
    until not cont
end

----Funçao auxiliar da de processamento----
function pegarelementos(base,valor)
    ref = string.find(base,valor)+string.len(valor) --Valor referência=A analise começa a partir dele
    lim=string.len(base) --valor maximo da busca
               
    for i=#comandos, 1, -1 do
        if string.find(base,comandos[i],ref) ~= nil then
            if string.find(base,comandos[i],ref)<lim  then --Acha qual o comando que aparece apos o comando de entrada(valor)
                lim = string.find(base,"%f[%a]" .. comandos[i] .. "%f[%A]",ref) --Se achar, pega o valor do indice do comando
            end
        end
    end
   
    return string.sub(base,ref,lim); --retorna a parte da string que fica despois do comando de entrada ate o valor limite
end

 

Espero que, mesmo sendo uma solução que não funciona mais, o post tenha trago algum aprendizado e conhecimento para você. Com toda certeza, posso falar que, para chegar no código final, foi muito conhecimento absorvido (programação web, programação em lua, noções básicas de rede, módulos do NodeMcu etc). E isso tudo tinha que ser partilhado.

Reconhecimento de voz – Extensão Google Chrome