paint-brush
Cómo construir su propio confesionario de IA: cómo agregar una voz al LLMby@slavasobolev
661
661

Cómo construir su propio confesionario de IA: cómo agregar una voz al LLM

Iaroslav Sobolev11m2024/07/14
Read on Terminal Reader

A finales de febrero, Bali acogió el festival [Lampu](https://lampu.org/), organizado según los principios del famoso Burning Man. Nos inspiramos en la idea de los confesionarios católicos y las capacidades del actual LLM. Construimos nuestro propio confesionario de IA, donde cualquiera podía hablar con una inteligencia artificial.
featured image - Cómo construir su propio confesionario de IA: cómo agregar una voz al LLM
Iaroslav Sobolev HackerNoon profile picture
0-item
1-item

Si bien OpenAI está retrasando el lanzamiento de los modos de voz avanzados para ChatGPT, quiero compartir cómo creamos nuestra aplicación de voz LLM y la integramos en un stand interactivo.

Habla con la IA en la jungla.

A finales de febrero, Bali acogió el festival Lampu , organizado según los principios del famoso Burning Man. Según su tradición, los participantes crean sus propias instalaciones y objetos de arte.


Mis amigos del Campamento 19:19 y yo, inspirados por la idea de los confesionarios católicos y las capacidades de los LLM actuales, se nos ocurrió la idea de construir nuestro propio confesionario de IA, donde cualquiera pudiera hablar con una inteligencia artificial.


Así es como lo imaginamos desde el principio:

  • Cuando el usuario ingresa a una cabina, determinamos que necesitamos iniciar una nueva sesión.


  • El usuario hace una pregunta y la IA escucha y responde. Queríamos crear un entorno privado y de confianza donde todos pudieran discutir abiertamente sus pensamientos y experiencias.


  • Cuando el usuario abandona la sala, el sistema finaliza la sesión y olvida todos los detalles de la conversación. Esto es necesario para mantener todos los diálogos privados.

Prueba de concepto

Para probar el concepto y comenzar a experimentar con un mensaje para el LLM, creé una implementación sencilla en una noche:

  • Escuche un micrófono.
  • Reconozca el habla del usuario utilizando el modelo Speech-to-Text (STT) .
  • Generar una respuesta vía LLM .
  • Sintetiza una respuesta de voz utilizando el modelo Texto a voz (TTS) .
  • Reproduzca la respuesta al usuario.



Para implementar esta demostración, confié completamente en los modelos de nube de OpenAI: Whisper , GPT-4 y TTS . Gracias a la excelente biblioteca Speech_recognition , construí la demostración en solo unas pocas docenas de líneas de código.


 import os import asyncio from dotenv import load_dotenv from io import BytesIO from openai import AsyncOpenAI from soundfile import SoundFile import sounddevice as sd import speech_recognition as sr load_dotenv() aiclient = AsyncOpenAI( api_key=os.environ.get("OPENAI_API_KEY") ) SYSTEM_PROMPT = """ You are helpfull assistant. """ async def listen_mic(recognizer: sr.Recognizer, microphone: sr.Microphone): audio_data = recognizer.listen(microphone) wav_data = BytesIO(audio_data.get_wav_data()) wav_data.name = "SpeechRecognition_audio.wav" return wav_data async def say(text: str): res = await aiclient.audio.speech.create( model="tts-1", voice="alloy", response_format="opus", input=text ) buffer = BytesIO() for chunk in res.iter_bytes(chunk_size=4096): buffer.write(chunk) buffer.seek(0) with SoundFile(buffer, 'r') as sound_file: data = sound_file.read(dtype='int16') sd.play(data, sound_file.samplerate) sd.wait() async def respond(text: str, history): history.append({"role": "user", "content": text}) completion = await aiclient.chat.completions.create( model="gpt-4", temperature=0.5, messages=history, ) response = completion.choices[0].message.content await say(response) history.append({"role": "assistant", "content": response}) async def main() -> None: m = sr.Microphone() r = sr.Recognizer() messages = [{"role": "system", "content": SYSTEM_PROMPT}] with m as source: r.adjust_for_ambient_noise(source) while True: wav_data = await listen_mic(r, source) transcript = await aiclient.audio.transcriptions.create( model="whisper-1", temperature=0.5, file=wav_data, response_format="verbose_json", ) if transcript.text == '' or transcript.text is None: continue await respond(transcript.text, messages) if __name__ == '__main__': asyncio.run(main())


Los problemas que teníamos que resolver se hicieron evidentes inmediatamente después de las primeras pruebas de esta demostración:

  • Retraso en la respuesta . En una implementación ingenua, el retraso entre la pregunta y la respuesta del usuario es de 7 a 8 segundos o más. Esto no es bueno, pero obviamente hay muchas formas de optimizar el tiempo de respuesta.


  • Ruido ambiental . Descubrimos que en ambientes ruidosos, no podemos confiar en que el micrófono detecte cuándo un usuario comenzó y terminó de hablar automáticamente. Reconocer el inicio y el final de una frase ( endpointing ) no es una tarea trivial. Si a esto le sumamos el ambiente ruidoso de un festival de música, queda claro que se necesita un enfoque conceptualmente diferente.


  • Imita una conversación en vivo . Queríamos darle al usuario la posibilidad de interrumpir la IA. Para conseguirlo tendríamos que mantener el micrófono encendido. Pero en este caso, tendríamos que separar la voz del usuario no sólo de los sonidos de fondo sino también de la voz de la IA.


  • Comentario . Debido al retraso en la respuesta, a veces nos parecía que el sistema estaba congelado. Nos dimos cuenta de que necesitamos informar al usuario cuánto tiempo se procesará la respuesta.


Teníamos la opción de resolver estos problemas: buscando una solución de ingeniería o de producto adecuada.

Pensando en la UX del stand

Antes incluso de empezar a codificar, tuvimos que decidir cómo interactuaría el usuario con el stand:

  • Deberíamos decidir cómo detectar un nuevo usuario en la cabina para restablecer el historial de diálogos anteriores.


  • Cómo reconocer el principio y el final del discurso de un usuario y qué hacer si quiere interrumpir la IA.


  • Cómo implementar retroalimentación cuando hay una respuesta retrasada por parte de la IA.


Para detectar un nuevo usuario en el stand, consideramos varias opciones: sensores de apertura de puertas, sensores de peso del piso, sensores de distancia y un modelo cámara + YOLO. El sensor de distancia detrás de la espalda nos pareció el más fiable, ya que excluía activaciones accidentales, como cuando la puerta no estaba lo suficientemente cerrada, y no requería una instalación complicada, a diferencia del sensor de peso.


Para evitar el desafío de reconocer el principio y el final de un diálogo, decidimos agregar un gran botón rojo para controlar el micrófono. Esta solución también permitió al usuario interrumpir la IA en cualquier momento.


Teníamos muchas ideas diferentes sobre cómo implementar comentarios al procesar una solicitud. Nos decidimos por una opción con una pantalla que muestra lo que está haciendo el sistema: escuchar el micrófono, procesar una pregunta o responder.


También consideramos una opción bastante inteligente con un teléfono fijo antiguo. La sesión comenzaría cuando el usuario levantara el teléfono y el sistema escucharía al usuario hasta que colgara. Sin embargo, decidimos que es más auténtico cuando el usuario es "respondido" por la cabina en lugar de por una voz desde el teléfono.


Durante la instalación y en el festival.


Al final, el flujo de usuarios final quedó así:

  • Un usuario entra a una cabina. Un sensor de distancia se activa a sus espaldas y lo saludamos.


  • El usuario presiona un botón rojo para iniciar un diálogo. Escuchamos el micrófono mientras se pulsa el botón. Cuando el usuario suelta el botón, comenzamos a procesar la solicitud y lo indicamos en pantalla.


  • Si el usuario quiere hacer una nueva pregunta mientras la IA responde, puede presionar el botón nuevamente y la IA dejará de responder inmediatamente.


  • Cuando el usuario sale de la cabina, el sensor de distancia se activa nuevamente y borramos el historial de diálogo.

Arquitectura


Arduino monitorea el estado del sensor de distancia y del botón rojo. Envía todos los cambios a nuestro backend vía HTTP API, lo que permite al sistema determinar si el usuario ha entrado o salido de la cabina y si es necesario activar la escucha del micrófono o comenzar a generar una respuesta.


La interfaz de usuario web es solo una página web abierta en un navegador que recibe continuamente el estado actual del sistema desde el backend y se lo muestra al usuario.


El backend controla el micrófono, interactúa con todos los modelos de IA necesarios y expresa las respuestas del LLM. Contiene la lógica central de la aplicación.

Hardware

Cómo codificar un boceto para Arduino, conectar correctamente el sensor de distancia y el botón y ensamblarlo todo en la cabina es un tema para un artículo aparte. Repasemos brevemente lo que obtuvimos sin entrar en detalles técnicos.


Usamos un Arduino, más precisamente, el modelo ESP32 con un módulo Wi-Fi incorporado. El microcontrolador estaba conectado a la misma red Wi-Fi que la computadora portátil, que ejecutaba el backend.



Lista completa de hardware que utilizamos:

backend

Los componentes principales del canal son Speech-To-Text (STT), LLM y Text-To-Speech (TTS). Para cada tarea, hay muchos modelos diferentes disponibles tanto localmente como a través de la nube.



Como no teníamos una GPU potente a mano, decidimos optar por versiones de los modelos basadas en la nube. La debilidad de este enfoque es la necesidad de una buena conexión a Internet. Sin embargo, la velocidad de interacción después de todas las optimizaciones fue aceptable, incluso con el Internet móvil que teníamos en el festival.


Ahora, echemos un vistazo más de cerca a cada componente de la tubería.

Reconocimiento de voz

Muchos dispositivos modernos admiten desde hace mucho tiempo el reconocimiento de voz. Por ejemplo, Apple Speech API está disponible para iOS y macOS, y Web Speech API está disponible para navegadores.


Desafortunadamente, son de calidad muy inferior a Whisper o Deepgram y no pueden detectar automáticamente el idioma.


Para reducir el tiempo de procesamiento, la mejor opción es reconocer el habla en tiempo real mientras el usuario habla. A continuación se muestran algunos proyectos con ejemplos de cómo implementarlos: susurro_streaming , susurro.cpp


Con nuestra computadora portátil, la velocidad del reconocimiento de voz utilizando este enfoque resultó estar lejos del tiempo real. Después de varios experimentos, nos decidimos por el modelo Whisper de OpenAI basado en la nube.

LLM e ingeniería rápida

El resultado del modelo Speech To Text del paso anterior es el texto que enviamos al LLM con el historial de diálogo.


Al elegir un LLM, comparamos GPT-3.5. GPT-4 y Claude. Resultó que el factor clave no era tanto el modelo específico sino su configuración. Al final, nos decidimos por GPT-4, cuyas respuestas nos gustaron más que las demás.


La personalización del mensaje para los modelos LLM se ha convertido en una forma de arte separada. Hay muchas guías en Internet sobre cómo ajustar su modelo según sus necesidades:



Tuvimos que experimentar extensamente con los ajustes de temperatura y avisos para que el modelo respondiera de manera atractiva, concisa y con humor.

Texto a voz

Expresamos la respuesta recibida del LLM utilizando el modelo Text-To-Speech y se la reproducimos al usuario. Este paso fue la principal fuente de retrasos en nuestra demostración.


Los LLM tardan bastante en responder. Sin embargo, admiten la generación de respuestas en modo streaming, token por token. Podemos utilizar esta función para optimizar el tiempo de espera expresando frases individuales a medida que se reciben sin esperar una respuesta completa del LLM.


Expresar oraciones individuales


  • Realizar una consulta al LLM.


  • Acumulamos la respuesta en el buffer token por token hasta tener una frase completa de longitud mínima . El parámetro de longitud mínima es importante porque afecta tanto a la entonación de la voz como al tiempo de retardo inicial.


  • Envíe la oración generada al modelo TTS y reproduzca el resultado al usuario. En este paso, es necesario asegurarse de que no exista ninguna condición de carrera en el orden de reproducción.


  • Repita el paso anterior hasta el final de la respuesta LLM.


Usamos el tiempo mientras el usuario escucha el fragmento inicial para ocultar el retraso en el procesamiento de las partes restantes de la respuesta del LLM. Gracias a este enfoque, el retraso de respuesta se produce sólo al principio y es de ~3 segundos.


 async generateResponse(history) { const completion = await this.ai.completion(history); const chunks = new DialogChunks(); for await (const chunk of completion) { const delta = chunk.choices[0]?.delta?.content; if (delta) { chunks.push(delta); if (chunks.hasCompleteSentence()) { const sentence = chunks.popSentence(); this.voice.ttsAndPlay(sentence); } } } const sentence = chunks.popSentence(); if (sentence) { this.voice.say(sentence); } return chunks.text; }


Toques finales

Incluso con todas nuestras optimizaciones, un retraso de 3 a 4 segundos sigue siendo significativo. Decidimos cuidar la interfaz de usuario con comentarios para evitar que el usuario tenga la sensación de que la respuesta está bloqueada. Analizamos varios enfoques:


  • Indicadores LED . Necesitábamos mostrar cinco estados: inactivo, esperando, escuchando, pensando y hablando. Pero no pudimos descubrir cómo hacerlo de una manera que fuera fácil de entender con LED.


  • Las palabras de relleno , como "Déjame pensar", "Hmm", etc., imitan el habla de la vida real. Rechazamos esta opción porque los rellenos a menudo no coincidían con el tono de las respuestas del modelo.


  • Pon una mampara en la cabina. Y muestra diferentes estados con animaciones.


Nos decidimos por la última opción con una página web simple que sondea el backend y muestra animaciones según el estado actual.


Los resultados

Nuestra sala de confesión de IA funcionó durante cuatro días y atrajo a cientos de asistentes. Gastamos alrededor de $50 en API de OpenAI. A cambio recibimos numerosos comentarios positivos y valiosas impresiones.


Este pequeño experimento demostró que es posible agregar una interfaz de voz intuitiva y eficiente a un LLM incluso con recursos limitados y condiciones externas desafiantes.


Por cierto, las fuentes backend disponibles en GitHub