Controlar Arduino con la voz

En esta entrada vamos a ver un ejemplo de cómo se puede utilizar la tecnología de reconocimiento de voz integrada en el navegador (Chrome y Edge principalmente) para controlar un robot a través de comandos de voz en JavaScript, para crear una experiencia de usuario más natural y simplificar el proceso de controlar una placa Arduino.

En nuestro caso controlaremos un brazo robot como ya vimos en post anteriores, donde explicábamos la parte de Arduino y el control desde una pagina web usando el puerto serie. Aprovecharemos el código creado en esos dos posts para añadirle control por voz usando la Web Speech API que permite convertir voz a texto en el navegador. Para facilitar la tarea y por similitud con la plataforma Arduino usaremos p5.js y la librería p5.js-speech. Si quieres ver cómo se usa directamente en el navegador puedes mirar este post.

No hay que olvidar que el reconocimiento de voz en el navegador de Chrome y Edge envían los datos a servidores externos. Firefox trato de resolverlo con software libre en local pero los resultados son bastante decepcionantes, sobre todo si no hablas inglés. Si quieres saber como usarlo

El diagrama de secuencia de como funciona sería el siguiente:

Lo primero es incluir las librerías necesarias en la parte HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js">    </script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/addons/p5.sound.min.js"></script>
    <script src="https://unpkg.com/p5-webserial@0.1.1/build/p5.webserial.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/IDMNYU/p5.js-speech@0.0.3/lib/p5.speech.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8" />
  </head>
  <body>
    <main>
    </main>
    <script src="sketch.js"></script>
  </body>
</html>

Veamos solo la parte del reconocimiento de voz:

let speech;
function setupSpeech() {
    // Crea un objeto de reconocimiento de voz con un callback
    speechRec = new p5.SpeechRec('es-ES', gotSpeech);
    // "Reconocimiento continuo" (en lugar de solo una vez)
    let continuous = true;
    // Si deseas probar el reconocimiento parcial (más rápido, menos preciso)
    let interimResults = false;
    // Esto debe venir después de establecer las propiedades
    speechRec.start(continuous, interimResults);
    // Evento de reconocimiento de voz
    function gotSpeech() {
        if (speechRec.resultValue) {
            let said = speechRec.resultString;
            if (said.indexOf("abre la mano") > -1) {
                send("S4:10");
                posicionMano = 10;
            } else if (said.indexOf("cierra la mano") > -1) {
                send("S4:40");
                posicionMano = 10;
            } else if (said.indexOf("gira a la derecha") > -1) {
                posicionBase -= 15;
                send("S1:" + posicionBase);
            } else if (said.indexOf("gira a la izquierda") > -1) {
                posicionBase += 15;
                send("S1:" + posicionBase);
            } else if (said.indexOf("gira al centro") > -1) {
                posicionBase = 90;
                send("S1:" + posicionBase);
            } else if (said.indexOf("mueve adelante") > -1) {
                posicionHombro -= 10;
                posicionCodo += 10;
                send("S2:" + posicionCodo);
                send("S3:" + posicionHombro);
            } else if (said.indexOf("mueve atras") > -1) {
                posicionHombro += 10;
                posicionCodo -= 10;
                send("S2:" + posicionCodo);
                send("S3:" + posicionHombro);
            }
            console.log(said);
        }
    }
}

El primer paso en el código es declarar una variable speech que se utilizará para acceder a las capacidades de conversión de voz a texto del navegador. A continuación, se define una función llamada setupSpeech(), que crea un objeto de reconocimiento de voz utilizando la biblioteca p5.js. Este objeto se utiliza para capturar el habla del usuario y procesarla en tiempo real.

La función setupSpeech() tiene varias opciones para personalizar el reconocimiento de voz. En primer lugar, se especifica el idioma del usuario con el que se realizará el reconocimiento de voz. Luego se configura el reconocimiento de voz para que sea continuo en lugar de solo una vez, lo que significa que el programa escuchará de manera continua. Además, se puede establecer la opción de reconocimiento parcial, que es más rápida pero menos precisa que el reconocimiento completo. La diferencia es que el completo espera a tener

Una vez que se han establecido las opciones de configuración, la función setupSpeech() comienza a escuchar el habla del usuario. Cada vez que se detecta una entrada de voz, se llama a la función de retorno gotSpeech(). Esta función, primero comprueba si se ha detectado alguna entrada de voz válida. Si se ha detectado una entrada de voz, se convierte en una cadena de texto y se comprueba si contiene algunas de las frases clave que se utilizan para controlar el robot. Si se encuentra una frase clave, se envía un comando al robot correspondiente a la acción deseada. Por ejemplo, si se dice «abre la mano», se envía por el puerto serie el comando «S4:10» al robot para que abra la mano. En este caso usamos un simple indexOf para buscar la frase exacta. Se podrían usar estrategias más elaboradas pero para este caso es suficiente.

El código JS completo seria (sketch.js):

const serial = new p5.WebSerial();
let portButton;
let cmdInput;
let posicionBase = 90;
let posicionHombro = 90;
let posicionCodo = 120;
let posicionMano = 10;

function setup() {
    createCanvas(400, 400);
    portButton = createButton("Elegir puerto");
    portButton.position(10, 10);
    portButton.mousePressed(choosePort);
    inicializarSerial();
    // Grupo Base
    crearGrupoBotones("Base", 1, 50, 50, () => {
        return posicionBase += 5; }, () => { return posicionBase -= 5; });
    // Grupo Hombro
    crearGrupoBotones("Hombro", 3, 50, 120, () => { 
        return posicionHombro += 5; }, () => { return posicionHombro -= 5; });
    // Grupo Codo
    crearGrupoBotones("Codo", 2, 50, 190, () => { 
        return posicionCodo += 5; }, () => { return posicionCodo -= 5; });
    // Grupo Mano
    crearGrupoBotones("Mano", 4, 50, 260, () => { 
        return posicionMano += 5; }, () => { return posicionMano -= 5; });
    let botonApagar = createButton("Pos. Inicial");
    botonApagar.position(175, 300);
    botonApagar.mousePressed(() => {
        send("Q");
        posicionBase = 90;
        posicionHombro = 90;
        posicionCodo = 120;
        posicionMano = 10;
    });
    let inputCmd = createInput();
    inputCmd.position(170, 360);
    inputCmd.size(60);
    botonCmd = createButton('->');
    botonCmd.position(240, 360);
    botonCmd.mousePressed(() => { send(inputCmd.value()) });
    setupSpeech();
}

let speech;

function setupSpeech() {
    // Crea un objeto de reconocimiento de voz con una función de retorno
    speechRec = new p5.SpeechRec('es-ES', gotSpeech);
    // "Reconocimiento continuo" (en lugar de solo una vez)
    let continuous = true;
    // Si deseas probar el reconocimiento parcial (más rápido, menos preciso)
    let interimResults = false;
    // Esto debe venir después de establecer las propiedades
    speechRec.start(continuous, interimResults);
    // Evento de reconocimiento de voz
    function gotSpeech() {
        if (speechRec.resultValue) {
            let said = speechRec.resultString;
            if (said.indexOf("abre la mano") > -1) {
                send("S4:10");
                posicionMano = 10;
            } else if (said.indexOf("cierra la mano") > -1) {
                send("S4:40");
                posicionMano = 10;
            } else if (said.indexOf("gira a la derecha") > -1) {
                posicionBase -= 15;
                send("S1:" + posicionBase);
            } else if (said.indexOf("gira a la izquierda") > -1) {
                posicionBase += 15;
                send("S1:" + posicionBase);
            } else if (said.indexOf("gira al centro") > -1) {
                posicionBase = 90;
                send("S1:" + posicionBase);
            } else if (said.indexOf("mueve adelante") > -1) {
                posicionHombro -= 10;
                posicionCodo += 10;
                send("S2:" + posicionCodo);
                send("S3:" + posicionHombro);
            } else if (said.indexOf("mueve atras") > -1) {
                posicionHombro += 10;
                posicionCodo -= 10;
                send("S2:" + posicionCodo);
                send("S3:" + posicionHombro);
            }
            console.log(said);
        }
    }
}

function draw() {
    background(220);
    // Mostrar los valores actuales de las posiciones
    textSize(20);
    text("Base: " + posicionBase, 150, 60);
    text("Hombro: " + posicionHombro, 150, 130);
    text("Codo: " + posicionCodo, 150, 200);
    text("Mano: " + posicionMano, 150, 270);
}

function crearGrupoBotones(nombre, servo, x, y, funcSumar, funcRestar) {
    textSize(20);
    text(nombre, x, y);
    //Sumar
    let botonSumar = createButton("+");
    botonSumar.position(x + 75, y);
    botonSumar.mousePressed(() => {
        send("S" + servo + ":" + funcSumar());
    });
    //Restar
    let botonRestar = createButton("-");
    botonRestar.position(x + 225, y);
    botonRestar.mousePressed(() => {
        send("S" + servo + ":" + funcRestar());
    });
}

//enviar datos al puerto serie
function send(cmd) {
    console.log(cmd);
    serial.write(cmd+"\n");
}

//leer datos del puerto serie
function serialEvent() {
    let readSerialStr = serial.readLine();
    trim(readSerialStr);
    if (readSerialStr) {
        console.log(readSerialStr);
    }
}

//incializar la conexion serie
function inicializarSerial() {
    if (!navigator.serial) {
        alert("WebSerial no esta sorportado en este navegador. Prueba en Chrome o Edge.");
    }
    serial.getPorts();
    serial.on("noport", showPortButton);
    serial.on("portavailable", openPort);
    serial.on("requesterror", portError);
    serial.on("data", serialEvent);
    serial.on("close", closePort);
    navigator.serial.addEventListener("connect", portConnect);
    navigator.serial.addEventListener("disconnect", portDisconnect);
}

// Muestra la ventana de seldccion de puerto
function choosePort() {
    showPortButton();
    serial.requestPort();
}

//abrir conexion con puerto serie
function openPort() {
    console.log("Abriendo puerto serie");
    serial.open().then(initiateSerial);
    function initiateSerial() {
        console.log("Puerto serie abierto");
    }
    hidePortButton();
}

//Cerrar conexion con puerto serie
function closePort() {
    console.log("Puerto serie cerrado");
    serial.close();
    showPortButton();
}

//Error con el puerto serie
function portError(err) {
    alert("Serial port error: " + err);
    showPortButton();
}

//Evento puerto serie conectado
function portConnect() {
    console.log("Puerto serie conectado");
    serial.getPorts();
    hidePortButton()
}

//Evento puerto serie desconectado
function portDisconnect() {
    serial.close();
    console.log("Puerto serie desconectado");
    showPortButton();
}

function showPortButton() {
    portButton.show();
}

function hidePortButton() {
    portButton.hide();
}

Puedes ver el siguiente vídeo sobre este tema en mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Y si estas interesado en el tema puedes ver la lista de vídeos.