Crea tu propio asistente de voz con la API de OpenAI

0
1022

Índice

  1. Introducción
  2. Creando Android App
  3. Creando reconocimiento de voz
  4. Conexion con ChatGPT
  5. Transformando texto a voz
  6. Conclusiones

1. Introducción

Muchos hemos usado Siri, Alexa, Google (o incluso Cortana con sus chistes malos), pero seguro que siempre te ha parecido que hablas con alguien que tiene predefinida sus respuestas, o que no parece muy humano.

Todo esto lo vamos a cambiar hoy, gracias a la irrupción de las IA en nuestras vidas, vamos a crear una pequeña aplicación Android que a través de nuestra voz pueda comunicarse con la API de OpenAI, los creadores de ChatGPT, (si quieres aprender un poco más sobre esta API, te recomiendo este tutorial) y poder pedirle cualquier cosa, bienvenido al tutorial donde vas a crear tu propio asistente de voz.

2. Creando Android App

Para empezar vamos a crear una aplicación Android sencilla, para ello usaremos Android Studio. Para este tutorial deberás tener conocimientos básicos en la creación de una aplicación Android, si no sabes cómo, te recomiendo pasarte por aquí primero.

En principio queremos que sea capaz de reconocer tu voz, transformarla a texto, enviar ese texto a la API de OpenAI, recibir la respuesta y transformarla a voz para que la escuches.

3. Creando reconocimiento de voz

Para esto, al crear el proyecto, se nos creará con una MainActivity por defecto en la ubicación src/main/MainActivity, en este archivo crearemos el siguiente código:

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val btnVoiceInput = findViewById<Button>(R.id.btn_voice_input)
    btnVoiceInput.setOnClickListener {
        startVoiceInput()
  }

  private fun startVoiceInput() {
    val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
    intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
    intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
    intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
    startActivityForResult(intent, REQUEST_CODE_VOICE_INPUT)
  }

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == REQUEST_CODE_VOICE_INPUT && resultCode == RESULT_OK && data != null) {
        val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
        val spokenText = result?.get(0)

        // Esta función la crearemos más adelante, será la que nos de la respuesta de ChatGPT
        getResponse(spokenText) { response ->
            runOnUiThread {
                // Lo completaremos mas adelante...
            }
        }
    }
  }
}

Vamos a explicar que hemos hecho aquí.

En el método onCreate (que es el que se llama cuando se inicializa la Activity) hemos inicializado el reconocimiento de voz, para ello nos apoyaremos en una función privada que hemos creado y que hemos nombrado como startVoiceInput(), aquí trabajamos con un objeto de tipo Intent y a continuación vamos a explicar como funciona esto:

  1. Primero que todo creamos un objeto de tipo Intent y establecemos la acción del intento como ACTION_RECOGNIZE_SPEECH. Esta acción indica que se desea realizar reconocimiento de voz.
  2. Luego agregamos un extra al Intent con clave EXTRA_LANGUAGE_MODEL y valor LANGUAGE_MODEL_FREE_FORM. Esto indica que se utilizará un modelo de lenguaje de reconocimiento de voz libre.
  3. A continuación agregamos un extra al intento con clave EXTRA_MAX_RESULTS y valor 1. Esto establece que se desea obtener solo un resultado del reconocimiento de voz.
  4. Además se agrega un extra al intento con clave EXTRA_LANGUAGE y se establece el valor como el idioma predeterminado del dispositivo. Esto indica el idioma en el que se espera la entrada de voz.
  5. Y por último se inicia una actividad para realizar el reconocimiento de voz utilizando el intento creado anteriormente. La constante REQUEST_CODE_VOICE_INPUT se utiliza como el código de solicitud para identificar la respuesta de esta actividad más adelante.

Esta última constante la hemos declarado al inicio del código de la siguiente manera:

companion object {
    private const val REQUEST_CODE_VOICE_INPUT = 100
}

Una vez tenemos el código, vamos con el diseño, esto lo haremos dentro del fichero src/res/layout/activity_main.xml, que es el archivo de diseño asociado a nuestra MainActivity (este archivo también se genera automáticamente al crear el proyecto). El diseño será bastante sencillo, un botón para empezar a hablar y 2 campos de texto, uno tendrá la pregunta y otro la respuesta:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_voice_input"
        android:layout_margin="18dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/textButton" />

    <TextView
        android:id="@+id/textview_question"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:padding="8dp" />

    <TextView
        android:id="@+id/textview_response"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textSize="18sp"
        android:padding="8dp" />

</LinearLayout>

Ahora tenemos un botón que podemos pulsar, hablar y capturar el texto, pero nada más, ahora necesitamos crear la conexión con la API para empezar a recibir respuestas

4. Conexión con ChatGPT

Necesitaremos enviar este texto generado a ChatGPT, para ello usaremos su API y para poder hacer uso de ella necesitaremos generar una API Key desde esta web.

Una vez tengamos la API Key, copiala en algún lugar seguro para que no la pierdas, completaremos la función getResponse(), que dejamos declarada en el código anterior

private fun getResponse(question: String?, callback: (String) -> Unit) {
    val apiKey = "<API KEY>"
    val url = "https://api.openai.com/v1/engines/text-davinci-003/completions"

    val requestBody = """
        {
            "prompt": "$question",
            "max_tokens": 1500,
            "temperature": 0.8
        }
    """.trimIndent()

    val textViewQuestion = findViewById<TextView>(R.id.textview_question)
    textViewQuestion.text = "Pregunta: $question"

    val request = Request.Builder()
        .url(url)
        .addHeader("Content-Type", "application/json")
        .addHeader("Authorization", "Bearer $apiKey")
        .post(requestBody.toRequestBody("application/json".toMediaTypeOrNull()))
        .build()

    client.newCall(request).enqueue(object: Callback {
        override fun onFailure(call: Call, e: IOException) {
            Log.e("error", "API fail", e)
        }

        override fun onResponse(call: Call, response: Response) {
            val body = response.body?.string()
            callback.invoke(body ?: "")
        }
    })
}

 

Dentro del requestBody añadimos el prompt, que es el texto que hemos capturado de la voz, el max_tokens, que es la cantidad máxima de tokens que queremos en una respuesta, si la respuesta tiene más, se truncará a esta cantidad, cada token equivale aproximadamente a 4 caracteres. Y por último el atributo temperature, que es un atributo que se suele usar en modelos de generación de IA, este valor puede estar entre 0 y 1, entre más alto, mas aleatoriedad y creatividad en las respuestas, muestra un buen funcionamiento en 0.7 o 0.8, tú puedes experimentar con diferentes valores y ver que se ajusta más a tu criterio.

Y por último hacemos la petición a la API con la librería de OkHttp, tu puedes usar la librería con la que más cómodo te sientas.

Una vez tenemos la respuesta de la API, si todo ha ido bien, irá por el método onResponse, aquí lo que hacemos es devolver el resultado invocando al callback que hemos pasado por parámetro, ¿y esto desde donde se llama?

Si recuerdas en el primer código, dentro del método onActivityResult teniamos este trozo de código sin completar:

getResponse(spokenText) { 
  response -> runOnUiThread { 
    // Lo completaremos mas adelante... 

    } 
  }

Una vez tenemos la respuesta, ejecutamos el código dentro de un runOnUiThread, esto es así porque todas las operaciones relacionadas con la interfaz de usuario deben realizarse en el sub-proceso de la interfaz de usuario para evitar problemas de rendimiento y bloqueo de la interfaz de usuario.

Bien, ahora ya tenemos el texto y la respuesta de la API, pero aún nos falta convertir esta respuesta a voz.

5. Transformando respuesta a voz

Para esto, bastará con utilizar la clase TextToSpeech de la propia API de Android:

getResponse(spokenText) { response ->
    runOnUiThread {
        val jsonObject = JSONObject(response)
        val textResponse = jsonObject.getJSONArray("choices").getJSONObject(0).getString("text")
        val textViewResponse = findViewById<TextView>(R.id.textview_response)
        textViewResponse.text = "Respuesta: $textResponse"
        textToSpeech.speak(textResponse, TextToSpeech.QUEUE_FLUSH, null, null)
    }
}

Aquí convertimos la respuesta a un JSON para obtener solo la parte que nos interesa, que es la respuesta, en este caso, también puedes crear una clase modelo para no acceder como un array, pero para el caso práctico del tutorial no es necesario, y esto se lo pasamos al método speak para poder emitirlo en forma de voz.

textToSpeech.speak(textResponse, TextToSpeech.QUEUE_FLUSH, null, null)

El primer parámetro es el texto, el segundo parámetro la forma de procesarlo, en este caso le decimos que descarte cualquier texto previo que no se haya procesado aún, y los siguientes parámetros son opcionales y no son necesarios en este caso.

Esta variable la definimos al comienzo de la clase:

private lateinit var textToSpeech: TextToSpeech

Y la inicializamos dentro del método onCreate:

textToSpeech = TextToSpeech(this, this)

Para esto también debemos implementar la interfaz OnInitListener de la clase TextToSpeech:

class MainActivity : AppCompatActivity(), TextToSpeech.OnInitListener {
override fun onInit(status: Int) { 
    if (status == TextToSpeech.SUCCESS) {
        val result = textToSpeech.setLanguage(Locale.getDefault())
        if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
            Log.e("MyActivity", "Language not supported")
        }
    } else {
        Log.e("MyActivity", "Initialization failed")
    }
}

En este código, estamos inicializando nuestro procesador de texto a voz. Con esto ya tendremos nuestra app funcionando.

Aquí os dejo una captura de un pequeño ejemplo:

Captura de pantalla de la aplicacion

Como último apunte, deberás añadir el permiso de acceso a Internet en el AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Además, te dejo el código completo de la MainActivity para que puedas utilizarlo:

package com.juanmaperez.aivoiceassistant

import android.content.Intent
import android.os.Bundle
import android.speech.RecognizerIntent
import android.speech.tts.TextToSpeech
import android.util.Log
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Callback
import okhttp3.Call
import okhttp3.Response
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
import java.util.*

class MainActivity : AppCompatActivity(), TextToSpeech.OnInitListener {

    private lateinit var textToSpeech: TextToSpeech
    private val client = OkHttpClient()

    companion object {
        private const val REQUEST_CODE_VOICE_INPUT = 100
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        textToSpeech = TextToSpeech(this, this)

        val btnVoiceInput = findViewById<Button>(R.id.btn_voice_input)
        btnVoiceInput.setOnClickListener {
            startVoiceInput()
        }

    }

    private fun startVoiceInput() {
        val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
        intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
        startActivityForResult(intent, REQUEST_CODE_VOICE_INPUT)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == REQUEST_CODE_VOICE_INPUT && resultCode == RESULT_OK && data != null) {
            val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
            val spokenText = result?.get(0)

            getResponse(spokenText) { response ->
                runOnUiThread {
                    val jsonObject = JSONObject(response)
                    val textResponse = jsonObject.getJSONArray("choices").getJSONObject(0).getString("text")
                    val textViewResponse = findViewById<TextView>(R.id.textview_response)
                    textViewResponse.text = "Respuesta: $textResponse"
                    textToSpeech.speak(textResponse, TextToSpeech.QUEUE_FLUSH, null, null)
                }
            }
        }
    }

    private fun getResponse(question: String?, callback: (String) -> Unit) {
        val apiKey = "<API_KEY>"
        val url = "https://api.openai.com/v1/engines/text-davinci-003/completions"

        val requestBody = """
            {
                "prompt": "$question",
                "max_tokens": 1500,
                "temperature": 0.8
            }
        """.trimIndent()

        val textViewQuestion = findViewById<TextView>(R.id.textview_question)
        textViewQuestion.text = "Pregunta: $question"

        val request = Request.Builder()
            .url(url)
            .addHeader("Content-Type", "application/json")
            .addHeader("Authorization", "Bearer $apiKey")
            .post(requestBody.toRequestBody("application/json".toMediaTypeOrNull()))
            .build()

        client.newCall(request).enqueue(object: Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.e("error", "API fail", e)
            }

            override fun onResponse(call: Call, response: Response) {
                val body = response.body?.string()
                callback.invoke(body ?: "")
            }
        })
    }

    override fun onInit(status: Int) {
        if (status == TextToSpeech.SUCCESS) {
            val result = textToSpeech.setLanguage(Locale.getDefault())
            if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                Log.e("MyActivity", "Language not supported")
            }
        } else {
            Log.e("MyActivity", "Initialization failed")
        }
    }
}

6. Conclusiones

Espero que este tutorial te haya gustado, y como puedes observar no es muy complicado crear tu propio asistente de voz, a partir de aquí puedes investigar y hacer que se active automáticamente con comandos de voz, como «Hola ChatGPT», o por ejemplo conectarlo con alguna interfaz de sonido en tu casa y tendrías tu propia casa parlante. La imaginación es el límite, nos vemos en el próximo tutorial 🙂

 

 

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad