Hace no mucho los desarrolladores construíamos aplicaciones monolíticas, que más tarde fueron divididas en código cliente y servidor usando APIs. Después refactorizamos los back-ends construyendo microservicios para tener alta disponibilidad y ser más escalable.

Recientemente estaba en una charla y el ponente habló de las Olas Tecnológicas. Ahora ya puedes pensar cuantas de ellas has vivido a lo largo de tu experiencia profesional.

Hace unos meses me uní a un equipo nuevo donde construimos prototipos rápidos para probar conceptos y aprender para aplicarlo a un producto. A veces acabamos testeando estos conceptos en dispositivos para tener un feeling más exacto de como será la experiencia final.

La Ola Tecnológica en que ahora mismo vivimos me atrevo a decir que trata de explotar los términos Inteligencia Artificial y Bots y desde nuestro equipo no queríamos quedarnos atrás. Empezamos a pensar sobre como enganchar esa inteligencia en diversos dispositivos.

Una prueba de concepto simple, flexible y portable que pudiera ser usado para diferentes propósitos con un mínimo de adaptación.

La idead de usar un Procesador de Lenguaje Natural ( NLP ), que pudiera ser llamado via HTTP API desde una aplicación cliente. La idea sería algo así como, el servicio NLP contestaría nuestras peticiones; la aplicación cliente necesitará un reconocedor de voz para el usuario, transformar esa voz a texto y realizar una petición al servicio NLP, que nos responderá y convertiremos esa repuesta a voz.

Este experimento nos permitirá ver como es la interacción de nuestros usuarios mediante voz y tener feedback rápido de nuestros usuarios en aspectos como: problemas en el reconocimiento, ruido en la comunicación, expectativas con esta nueva tecnología y algunos otros más datos relevantes para el equipo.

Veamos las piezas de puzzle en detalle.

1. El servicio NLP

En esta ocasión probaremos DialogFlow, que es un servicio NLP de Google, tiene muchas integraciones disponibles, SDK para muchos lenguajes y agentes ya cargados que nos facilitan la prueba. Se integra especialmente bien con Google Assistant.

Antes de seguir me gustaría revisar algunos conceptos:

  • Intent: Es un patrón detectado por el servicio NLP mediante el uso de utterances que son un conjunto de sentencias que definen un modelo de comportamiento. Normalmente esos intents son palabras clave que tu puedes acomodar a tu sistema.
  • El servicio NLP define algunos intents y algunas utterances que son usadas por el servicio para responder a nuestras peticiones. El servicio NLP permite peticiones via HTTP API, y eso is lo que necesitamos para la prueba.

Intent Menu

Se pueden añadir nuevos intents.

Intent creation Menu.

Hay una consola en un lateral desde la que puedes testear tus intents y utterances manualmente y siguiendo la documentación se puede encontrar como hacer las peticiones HTTP:

Dialogflow SDKs | Dialogflow

Voice interface is cross-platform. Your agent will understand your users no matter what device you’re using. You design…

dialogflow.com

dialogflow/dialogflow-javascript-client

dialogflow-javascript-client – JavaScript Web SDK for Dialogflow

github.com

2. El Cliente

Como hemos mencionado anteriormente a veces usamos dispositivos para test finales. Ser portable es siempre bueno para construir prototipos que demuestran ciertas hipótesis y React-Native cubre nuestras necesidades. En este caso testearemos sobre una plataforma android por disponibilidad.

Asumimos que has instalado ya react-native-cli y has comenzado un proyecto Android. Si no puedes seguir esta documentación que te muestra los pasos:

Getting Started – React Native

This page will help you install and build your first React Native app. If you already have React Native installed, you…

facebook.github.io

Primero vamos a ensamblar los módulos:

Lo que acaba reflejado en unos cuantos cambios en la configuración de proyecto:

apply plugin: "com.android.application"    import com.android.build.OutputFile    apply from: "../../node_modules/react-native/react.gradle"    def enableSeparateBuildPerCPUArchitecture = false  def enableProguardInReleaseBuilds = false    android {      compileSdkVersion 23      buildToolsVersion "23.0.1"        defaultConfig {          applicationId "com.rnauratest"          minSdkVersion 16          targetSdkVersion 22          versionCode 1          versionName "1.0"          ndk {              abiFilters "armeabi-v7a", "x86"          }      }      splits {          abi {              reset()              enable enableSeparateBuildPerCPUArchitecture              universalApk false  // If true, also generate a universal APK              include "armeabi-v7a", "x86"          }      }      buildTypes {          release {              minifyEnabled enableProguardInReleaseBuilds              proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"          }      }      // applicationVariants are e.g. debug, release      applicationVariants.all { variant ->          variant.outputs.each { output ->              // For each separate APK per architecture, set a unique version code as described here:              // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits              def versionCodes = ["armeabi-v7a":1, "x86":2]              def abi = output.getFilter(OutputFile.ABI)              if (abi != null) {  // null for the universal-debug, universal-release variants                  output.versionCodeOverride =                          versionCodes.get(abi) * 1048576 + defaultConfig.versionCode              }          }      }  }    dependencies {      compile project(':react-native-tts')      compile fileTree(dir: "libs", include: ["*.jar"])      compile "com.android.support:appcompat-v7:23.0.1"      compile "com.facebook.react:react-native:+"  // From node_modules      compile project(':VoiceModule') // Add voice module package  }    // Run this once to be able to run the application with BUCK  // puts all compile dependencies into folder libs for BUCK to use  task copyDownloadableDepsToLibs(type: Copy) {      from configurations.compile      into 'libs'  }
rootProject.name = 'popiggTest'  include ':react-native-tts'  project(':react-native-tts').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-tts/android')    include ':VoiceModule', ':app'  project(':VoiceModule').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-voice')
package com.popiggtest;    import android.app.Application;    import com.facebook.react.ReactApplication;  import com.facebook.react.ReactNativeHost;  import com.facebook.react.ReactPackage;  import com.facebook.react.shell.MainReactPackage;  import com.facebook.soloader.SoLoader;  import com.wmjmc.reactspeech.VoicePackage; // voice recognition package  import net.no_mad.tts.TextToSpeechPackage; // test to speech package    import java.util.Arrays;  import java.util.List;    public class MainApplication extends Application implements ReactApplication {      private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {      @Override      public boolean getUseDeveloperSupport() {        return BuildConfig.DEBUG;      }        @Override      protected List<ReactPackage> getPackages() {        return Arrays.<ReactPackage>asList(            new MainReactPackage(),            new VoicePackage(), // voice recognition package            new TextToSpeechPackage() // test to speech package          );      }    };      @Override    public ReactNativeHost getReactNativeHost() {      return mReactNativeHost;    }      @Override    public void onCreate() {      super.onCreate();      SoLoader.init(this, /* native exopackage */ false);    }  }

3. Último paso

Una vez que todos los módulos están listos veamos como integrarlo dentro de nuestra app:

import React, { Component } from 'react';  import {    StyleSheet,    Text,    Button,    ToastAndroid,    View  } from 'react-native';    import SpeechAndroid from 'react-native-android-voice';  import Tts from 'react-native-tts';    export default class App extends Component<{}> {    constructor(props) {      super(props);      this.onSpeak = this.onSpeak.bind(this);      this.getDialogFlow = this.getDialogFlow.bind(this);      this.state = { showText: null };    }      async getDialogFlow(msg) {      const ACCESS_TOKEN = '<ACCESS-TOKEN>';        try {         const response = await fetch(`https://api.dialogflow.com/v1/query?v=20170712`, {          method: 'POST',          headers: {            'Accept': 'application/json',            'Content-Type': 'application/json; charset=utf-8',            'Authorization': `Bearer ${ACCESS_TOKEN}`,          },          body: JSON.stringify({            query: msg,            lang: 'EN',            sessionId: 'somerandomthing'          })        })        let responseJson = await response.json();        this.setState({          showText: responseJson.result.fulfillment.speech,        });        return responseJson;      } catch(error) {        console.error(error);      }    }      async onSpeak() {      try {        const spokenText = await SpeechAndroid.startSpeech("talk to popiggBot", SpeechAndroid.ENGLISH);          const dialogflowResponse = await this.getDialogFlow(spokenText);        if (this.state.showText) {          Tts.speak(dialogflowResponse.result.fulfillment.speech);          ToastAndroid.show(dialogflowResponse.result.fulfillment.speech, ToastAndroid.LONG);        }      } catch(error) {        switch(error){          case SpeechAndroid.E_VOICE_CANCELLED:            ToastAndroid.show("Voice Recognizer cancelled" , ToastAndroid.LONG);            break;          case SpeechAndroid.E_NO_MATCH:            ToastAndroid.show("No match for what you said" , ToastAndroid.LONG);            break;          case SpeechAndroid.E_SERVER_ERROR:            ToastAndroid.show("Google Server Error" , ToastAndroid.LONG);            break;        }      }    }      render() {      return (        <View style={styles.container}>          <Button          onPress={this.onSpeak}          title="Press to talk"          color="#37B6DF"          accessibilityLabel="Press to talk"        />          </View>      );    }  }    const styles = StyleSheet.create({    container: {      flex: 1,      justifyContent: 'center',      alignItems: 'center',      backgroundColor: '#F5FCFF',    },  });

Con no muchas líneas hemos construido nuestro propio asistente de voz en Android. Pero no todo funciona siempre a la primera.

4. Problemas potenciales y puntos fuertes

Ahora que acabamos de construir nuestro asistente en Android, flexible y ajustable en términos de intents y respuestas, con una interfaz es tiempo de la revisión.

  • Puede ser que la cache juegue en tu contra cuando haces debug de tu aplicación. Las dependencias externas se pueden complicar sumado a la caché de NPM. Atención a esto, borra y reinstala las dependencias, eso suele funcionar.
  • DialogFlow SDK: Finalmente decidimos realizar las peticiones usando Fetch de React-Native porque el SDK no funcionaba fácilmente. La documentación no es la mejor para este caso puntual pero la parte que hay de HTML + JS está muy bien para nuestra demo.
  • El punto clave es el debugger remoto de React-Native. Con un simple console.log, console.warn y console.error, se pueden trazar las acciones y lo que sucede entre bambalinas en tu aplicación con el navegador. Por ejemplo en esta demo es importante saber como van las peticiones y respuestas en la aplicación. También como se está reconociendo la voz.

React-Native remote debug

  • Dialogflow es impresionante, tiene una interfaz sencilla, es fácil de usar, está disponible en varios lenguages y es suficientemente flexible para extender las respuestas del servicio NLP con tus propias respuestas.

Si te ha gustado y quieres ver un poco más profundo como está montado aquí está el código de la demo.

popigg/DialogflowReactNative

DialogflowReactNative – Your voice assistant in ReactNative & DialogFlow

github.com

Espero que te guste, si así es, puedes compartirlo.

Gracias por leerlo.

Por Pablo Gómez Guerrero

Lector, a veces escritor, todos estamos aprendiendo. Prototipos de IA y software en Telefónica.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *