Índice
- Introducción
- Creando Android App
- Creando reconocimiento de voz
- Conexion con ChatGPT
- Transformando texto a voz
- 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:
- Primero que todo creamos un objeto de tipo
Intent
y establecemos la acción del intento comoACTION_RECOGNIZE_SPEECH
. Esta acción indica que se desea realizar reconocimiento de voz. - Luego agregamos un extra al
Intent
con claveEXTRA_LANGUAGE_MODEL
y valorLANGUAGE_MODEL_FREE_FORM
. Esto indica que se utilizará un modelo de lenguaje de reconocimiento de voz libre. - A continuación agregamos un extra al intento con clave
EXTRA_MAX_RESULTS
y valor1
. Esto establece que se desea obtener solo un resultado del reconocimiento de voz. - 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. - 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:
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 🙂