IAs para impostores - I

Estaba yo mirando youtube y me encontré con un vídeo de Nate Gentile en el que hacía un ordenador al que le podía hablar y le contestaba así que pensé ... si él puede, yo también. :-D

Así que en este post vamos a hacer un asistente en plan bricolaje usando un script cutre en bash. No va a hacer tantas cosas como Nate: el asistente puede escuchar nuestra petición y devolvernos una respuesta.

Requisitos de hardware y sistema operativo

Aquí tenemos un problema ... bueno, yo no lo tengo pero tu probablemente si: una tarjeta gráfica de marca Nvidia con nvram suficiente como para que entre un modelo decente.

Normalmente es más importante el como se usa que el tamaño, pero aquí el tamaño importa, y mucho. Necesitas una cantidad mínima de memoria para meter el modelo. Es lo que hay.

Si no hay suficiente memoria, tendrás que usar la CPU y todo el proceso va a tardar muuuuucho más. Hablamos de una diferencia de 10 segundos frente a 10 minutos.

Y por supuesto, voy a usar GNU Linux, en concreto Ubuntu.

Software

Vamos a realizar las siguientes operaciones:

  • grabar el audio con arecord
  • pasar el audio a texto con whisper
  • responder al texto con ollama
  • pasar la respuesta a audio con Mozilla TTS
  • reproducir la respuesta con mplayer

La parte de ollama es un mundo en si mismo. Si tuviera que describirlo de forma sencilla, diría que es como Docker pero con modelos. Vaya desastre de descripción he hecho ... pero de momento vas a realizar un acto de fe y en otro post ampliaré esto.

Para instalar todas las dependencias ejecuta esto:

# Dependencias para grabar y reproducir audio
sudo apt-get update
sudo apt-get -y install mplayer alsa-utils curl sox

# Recomendable: Creamos virtualenv y lo activamos
virtualenv ragvenv || true
source ragvenv/bin/activate

# Instalamos whisper
pip install git+https://github.com/openai/whisper.git

# Instalamos tts
# Por algún motivo esto no funciona: pip install git+https://github.com/mozilla/TTS.git
# Así que usamos esta alternativa
pip install git+https://github.com/coqui-ai/TTS.git

# ollama
curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama2

arecord

Empieza a grabar con este comando y cuando acabes de decir lo que sea dale a Ctrl-C:

arecord tmp_rec.wav

Esto te dejará un fichero. Escucha su contenido para ver si se entiende con mplayer tmp_rec.wav ... si no lo entiendes tu, whisper lo va a entender menos todavía.

whisper

Ahora pasamos el audio a texto:

$ whisper tmp_rec.wav --model tiny --language Spanish
[00:00.000 --> 00:03.200]  ¿De qué color es el caballo blanco de Santiago?

Esto, además de dejarte por la salida standard lo que ves, te dejará varios ficheros:

$ ls
tmp_rec.json  tmp_rec.srt  tmp_rec.tsv  tmp_rec.txt  tmp_rec.vtt  tmp_rec.wav

Pues nos interesa el que se apellida .txt porque ese tiene el texto tal cual y es el que meteremos como entrada a nuestra IA.

ollama

Para este caso de uso, ollama es un api compatible con la de openai (mentira) que sirve para usar modelos de IA (también mentira).

Si haces un curl localhost:11414 te debería saludar con un mensaje como Ollama is running o algo así.

Ahora ejecuta esto:

text=$(cat tmp_rec.txt)

echo """
{
  \"model\": \"llama2\",
  \"messages\": [
    {
      \"role\": \"system\",
      \"content\": \"Eres un asistente que resuelve dudas respondiendo de forma concisa.\"
    },
    {
      \"role\": \"user\",
      \"content\": \"$text\"
    }
  ]
}
""" > payload.json
respuesta=`curl -s -X POST -H "Content-Type: application/json" -d @payload.json  http://localhost:11434/v1/chat/completions | jq -r '.choices[0].message.content'`
echo $respuesta
echo -n $respuesta > respuesta.txt

Bien .... siempre quise saber de que color era ese caballo ... gracias IA.

De texto a audio con TTS (Text To Speech)

Pero no sabemos leer, así que vamos a ver si nos lo puede pasar a audio.

Ejecuta esto:

respuesta=`cat respuesta.txt`
time tts --text "$respuesta" --model_name "tts_models/es/mai/tacotron2-DDC" --vocoder_name "vocoder_models/universal/libri-tts/wavegrad" --out_path respuesta.wav

En mi portátil que tiene un Intel(R) Core(TM) i7-7700HQ tarda 1m36,104s . Con un AMD Ryzen 5 5600 tarda 0m42.216s.

O esto si tienes una nvidia con un poco de memoria:

respuesta=`cat respuesta.txt`
time tts --text "$respuesta" --model_name "tts_models/es/mai/tacotron2-DDC" --vocoder_name "vocoder_models/universal/libri-tts/wavegrad" --out_path respuesta.wav --use_cuda yes

En mi portátil que tiene una NVIDIA GeForce GTX 1050 esto tarda 0m15,374s. Con una RTX 4060ti tarda 0m7.083s.

Y ahora escuchamos el resultado

mplayer respuesta.wav

¿Y sin usar un API?

Pues si, también se puede sin usar un API, pero es que así también te vale para el api de OpenAI y además, así tengo excusa para explicar como instalar ollama en Kubernetes más adelante. Y por el nombre del virtualenv ... ya te haces una idea de hacia donde va esta serie de posts.

Happy path

arecord tmp_rec.wav

whisper tmp_rec.wav --model tiny --language Spanish

text=$(cat tmp_rec.txt)
echo """
{
  \"model\": \"llama2\",
  \"messages\": [
    {
      \"role\": \"system\",
      \"content\": \"Eres un asistente que resuelve dudas respondiendo de forma concisa.\"
    },
    {
      \"role\": \"user\",
      \"content\": \"$text\"
    }
  ]
}
""" > payload.json
respuesta=`curl -s -X POST -H "Content-Type: application/json" -d @payload.json  http://localhost:11434/v1/chat/completions | jq -r '.choices[0].message.content'`

tts --text "$respuesta" --model_name "tts_models/es/mai/tacotron2-DDC" --vocoder_name "vocoder_models/universal/libri-tts/wavegrad" --out_path respuesta.wav

mplayer respuesta.wav

El script completo

Ha sido un dolor hacer esto en bash, pero el reto incluía hacerlo en bash así que ... allá va:

#arecord_params="-D hw:1,0 -f S16_LE"
arecord_params=""
use_cuda="--use_cuda yes"
recording_flag=0
pid=0

tmp_path=$HOME/tmp/
mkdir -p $tmp_path

while true; do
  # Esto no lo he explicado arriba, pero graba un segundo de sonido y guarda el RMS delta ... que viene a ser un indicador de como cuanto volumen tiene el sonido se está recibiendo.
  # Como es un fichero pequeño y no quiero machacar el disco, lo guardo en /dev/shm, que almacena en memoria ram.
  RMSdelta=$(arecord $arecord_params -d 1 /dev/shm/tmp_rec_check.wav ; sox -t .wav /dev/shm/tmp_rec_check.wav -n stat 2>&1 | sed -e 's/ //g' | grep "RMSdelta" | cut -d ':' -f 2)
  if [ "$recording_flag" -eq 0 ]; then
    # Si el sonido supera un umbral, empieza a grabar
    if (( $(echo "$RMSdelta > 0.007" | bc -l) )); then
      echo "Comenzando grabación..."
      arecord $arecord_params /dev/shm/tmp_rec.wav &
      pid=$!
      recording_flag=1
    fi
  else
    if (( $(echo "$RMSdelta <= 0.007" | bc -l) )); then
      echo "Deteniendo grabación..."
      kill $pid
      recording_flag=0
      tmp_date=`date +%y%m%d%H%M%S`
      # Aquí puedes colocar el comando para procesar el archivo tmp_rec.wav, por ejemplo, lanzar Whisper
      echo "Procesando grabación..."
      cp /dev/shm/tmp_rec.wav $tmp_path/tmp_rec-$tmp_date.wav

      ## Sonido a texto
      # A whisper no se le puede pasar un path para sus salidas así que creamos un directorio, nos metemos en él y nos volvemos aquí
      local_path=`pwd`
      mkdir -p $tmp_path/tmp_rec-$tmp_date
      cd $tmp_path/tmp_rec-$tmp_date
      echo "whisper $tmp_path/tmp_rec-$tmp_date.wav --model tiny --language Spanish"
      whisper $tmp_path/tmp_rec-$tmp_date.wav --model tiny --language Spanish
      #whisper $tmp_path/tmp_rec-$tmp_date.wav --model medium --language Spanish
      cd $local_path

      text=$(cat $tmp_path/tmp_rec-$tmp_date/tmp_rec-$tmp_date.txt)
      echo Texto a pasar al api: $text

      echo """
      {
        \"model\": \"llama2\",
        \"messages\": [
          {
            \"role\": \"system\",
            \"content\": \"Eres un asistente que resuelve dudas respondiendo de forma concisa.\"
          },
          {
            \"role\": \"user\",
            \"content\": \"$text\"
          }
        ]
      }
      """ > $tmp_path/payload.json
      echo payload:
      cat $tmp_path/payload.json
      output=`curl -X POST -H "Authorization: Bearer $token" -H "Content-Type: application/json" -d @$tmp_path/payload.json  http://localhost:11434/v1/chat/completions | jq -r '.choices[0].message.content'`

      echo -n $output > $tmp_path/tmp_rec-$tmp_date/response-$tmp_date.txt
      echo "----------------------------------------"
      echo $output
      echo "----------------------------------------"

      # Ahora que tenemos la respuesta en $output, se la pasamos a TTS para que la pase a voz y parezca que nos está contestando una persona
      time tts --text "$output" --model_name "tts_models/es/mai/tacotron2-DDC" --vocoder_name "vocoder_models/universal/libri-tts/wavegrad" --out_path $tmp_path/tmp_rec-$tmp_date/tmp_rec-$tmp_date.wav $use_cuda
      sleep 1s
      mplayer $tmp_path/tmp_rec-$tmp_date/tmp_rec-$tmp_date.wav

      sleep 10s
    else
      # Continuar grabando, omitir este ciclo
      echo "Continuando grabación..."
    fi
  fi
done

Alternativas

A poco que te pongas a buscar vas a encontrar alternativas a ollama, pero ollama tiene un par de cosas que lo hacen especial: los Modelfiles y la facilidad para ser ejecutado dentro de un contenedor.

En otro post entraré más en detalle en este tema, pero esa es mi opinión.

Hay otra alternativa que también me gusta mucho y que si activas un plugin que viene en la pestaña de plugins ... pues ya tienes el api de openai para usarla con tus proyectos: text-generation-webui. Este proyecto pretende ser como la interfaz web de Stable Diffusion pero para LLMs y me gusta mucho.

Y luego, si quieres probar más cosas, tienes LM Studio, gpt4all y lo que vaya saliendo. Pero las que veo más interesantes ahora mismo son las que he comentado arriba: ollama y text-generation-webui.

Un poco de música para entender lo que viene

Un mapa de Beat Saber hecho con una IA de un videoclip que Jaime Altozano hizo con una IA: