Johnny5 - XMPP Whisper bot
"Attractive. Nice software!"
Jhonny5 nos da una mano transliterando en Lost in Cyberspace, es un proyecto que demuestra lo rápido y simple que puede ser desarrollar en Python un bot para XMPP, de hecho, ue un desarrollo que nos tomo un par de días y pudimos publicarlo a tiempo para homenajear a ley de lenguaje de señas argentino.
Con este post vamos a iniciar una serie de posts relacionados a desarrollo de herramientas y soluciones basadas en XMPP, la idea es empezar con Python y Slixmpp para luego ir explorando otras opciones, dependiendo de lo que vayamos desarrollando para Cyberdelia. Para arrancar es ideal tener una idea de lo que es XMPP, el funcionamiento básico del protocolo, que son y que tipos de stanzas existen, como así también tener conocimientos de Python (al menos mientras estemos hablando de librerías o frameworks basados en esta tecnología).
Bots en XMPP
Existen varias formas de hacer bots en XMPP, se pueden usar frameworks orientados a chatops como errBot o librerías como slixmpp, incluso podríamos incluir Spade como framework, ya que al fin y al cabo un agente no es ni mas ni menos que una porción de software que hace algo en base a mensajes, estos podrían ser tanto mensajes estándar como mensajera pub/sub. En este post nos vamos a concentrar en mensajería instantánea, pero más adelante veremos como podemos utilizar los mecanismos pub/sub de XMPP.
Elegir SliXMPP para desarrollo de bots no es casual, decidimos usarla por que tanto el lenguaje como la librería son una excelente opción para iniciarse en el desarrollo de XMPP por la simpleza y agilidad que adquirimos desarrollando en Python, sumado a la posibilidad de acompañar el uso de la librería SliXMPP con el libro "XMPP:The Definitive Guide publicado por O'Reilly junto a otros detalles que seria mejor detallarlos a continuación.
Slixmpp
Esta librería es un fork de SleekXMPP, que fue utilizada para los ejemplos del libro que mencionamos anteriormente. Pero con la obsolescencia de Python 2.6 el equipo de Poezio decidió tomar el toro por las astas y forkear el proyecto para migarlo a asyncio y darle soporte a Python 3.7+ (al momento de escribir el post).
Bajo la misma filosofía y los mismos principios de diseño de SleekXMPP, Slixmpp esta diseñado para tener un bajo numero de dependencias, dar soporte a XEPs bajo un esquema de plugins y abstraer lo suficiente al protocolo como para no tener que manejar XML, haciéndolo muy sencillo de usar.
En serio... es sencillo y su abstracción lo hace notablemente simple de utilizar, el siguiente código boilerplate nos permite procesar mensajes individuales y grupales.
Slixmpp Boilerplate code
#!/usr/bin/env python3
import slixmpp
class XMPPBot(slixmpp.ClientXMPP):
def __init__(self, jid, password, room, nick):
slixmpp.ClientXMPP.__init__(self, jid, password)
self.room = room
self.nick = nick
self.add_event_handler("session_start", self.start)
self.add_event_handler("message", self.message_handler)
self.add_event_handler("groupchat_message", self.muc_message_handler)
self.add_event_handler("disconnected",self.disconnected)
async def start(self, event):
await self.get_roster()
self.send_presence()
self.plugin['xep_0045'].join_muc(self.room,
self.nick)
def disconnected(self):
self.reconnect()
def message_handler(self, msg):
"""
Message handler
"""
def muc_message_handler(self, msg):
"""
MUC Message handler
"""
if __name__ == '__main__':
jid="user@server.ltd"
password="somerandomfakepassword"
room="arandom@room.server.ltd"
nick="xmppbotnick"
xmpp = XMPPBot(jid, password, room, nick)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0045') # Multi-User Chat
xmpp.connect()
asyncio.get_event_loop().run_forever()
Bien, vayamos por partes dijo Jack... Lo primero que nos debe interesar es la
clase que estamos heredando cuando definimos XMPPBot
, slixmpp.ClientXMPP
:
ClientXMPP
hereda de BaseXMPP
que a su vez hereda de XMLStream
, una
subclase de asyncio. BaseProtocol
. XMLStream
tiene la responsabilidad de
gestionar la conexión y despachar eventos. XMLStream
es una clase genérica
que abstrae los detalles de establecer la conexión con el servidor como así
también el envío y recepción de stanzas XML
. Por su lado, BaseXMPP
extiende
XMLStream
para usarla con XMPP proveyendo el mecanismo de plugins para extender
y agregar soporte a nuevas características del protocolo XMPP. ClientXMPP
, como
decíamos antes hereda de BaseXMPP
y agrega la funciones especificas para
gestionar conexiones de clientes y abstraer el concepto cliente, como decíamos,
para encapsular el protocolo facilitándonos mucho la interacción con XMPP.
Sabiendo cual es la cadena de responsabilidades que termina recibiendo nuestra
clase es tan importante como conocer los mecanismos básicos para que nuestro bot
funcione. Del código anterior podemos ver que tenemos dos secciones importantes.
La primera es la definición de la clase XMPPBot
y la segunda es el condicional
de ejecución del hilo principal, donde instanciamos la clase y hacemos un par de
cosas más que vamos a detallar a continuación, pero primero veamos los
principales bloques de nuestra clase.
El método constructor del bot es donde ocurre la mayor parte de la magia de
slixmpp
, aquí inicializamos una instancia pasándole al constructor de la clase
heredada los datos de la cuenta que vamos a utilizar, es decir el jid
y sus
credenciales para luego empezar a inicializar las propiedades de nuestra clase:
def __init__(self, jid, password, room, nick):
slixmpp.ClientXMPP.__init__(self, jid, password)
self.room = room
self.nick = nick
self.add_event_handler("session_start", self.start)
self.add_event_handler("message", self.message_handler)
self.add_event_handler("groupchat_message", self.muc_message_handler)
self.add_event_handler("disconnected",self.disconnected)
Por ultimo el constructor agrega los manejadores de eventos, es decir, los métodos que serán llamados cuando ocurra determinado evento. Aquí vemos solo cuatro eventos:
session_start
message
groupchat_message
disconnected
A los cuales se les asignan los métodos definidos en la clase según la tabla debajo:
Evento | Método |
---|---|
session_start |
start(event) |
message |
message_handler(xmpp_msg ) |
groupchat_message |
muc_message_handler(xmpp_msg) |
disconnected |
disconnected() |
El método start(event)
es llamado cuando obtenemos el evento de inicio de
sesión, es decir tenemos una conexión exitosa. Este método es bastante sencillo
para un caso de uso simple, pero deberá modificarse para atender los
requerimientos del caso de uso que estemos satisfaciendo:
async def start(self, event):
await self.get_roster()
self.send_presence()
self.plugin['xep_0045'].join_muc(self.room,
self.nick)
Lo primero que hacemos es obtener el roster del jid
usado y luego enviamos
nuestra presencia al servidor para que la propague a todos los usuarios de
nuestro roster. En este caso, como queremos ingresar a una sala de chat, usando
el plugin correspondiente al estándar xep_0045
ingresamos a la sala definida
en el atributo room
con el nick definido en nick
.
Manejar desconexiones es muy importante, existen varias razonas por las cuales
nuestro programa puede perder la conexión y habrá que analizar cada caso, en
principio vamos a atacar solo el evento disconnected
intentando reconectarnos
como lo hicimos en el método:
def disconnected(self):
self.reconnect()
Por ultimo, los métodos que suelen interesarnos más porque es por donde va a iniciarse todas nuestras interacciones, son los métodos asociados a los eventos de mensajes:
def message_handler(self, msg):
"""
Message handler
"""
def muc_message_handler(self, msg):
"""
MUC Message handler
"""
Como podemos ver ambos necesitan como parámetro el mensaje pero vamos a necesitar tener en cuenta varias cosas para evitar algunos problemas que se dan cuando nuestro bot envía respuestas, sobre todo en salas de chat. Antes de ponernos a ver estos detalles veamos la segunda parte de nuestro bot y luego analicemos un poco mas en profundidad nuestro caso de uso.
if __name__ == '__main__':
jid="user@server.ltd"
password="somerandomfakepassword"
room="arandom@room.server.ltd"
nick="xmppbotnick"
xmpp = XMPPBot(jid, password, room, nick)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0045') # Multi-User Chat
xmpp.connect()
asyncio.get_event_loop().run_forever()
En nuestro bloque de ejecución principal, simplemente creamos una instancia de
nuestra subclase de XMPPClient
, en este ejemplo sería XMPPBot
, pasándole las
credenciales del usuario que estará usando nuestro bot y el resto de opciones
que puede variar dependiendo de nuestras necesidades.
Con el objeto instanciado, luego registramos los plugins que vamos a estar
usando, en XMPPBot
solo usamos XEP-0030
que provee descubrimiento de
servicios y XEP-0045
para chat multiusuario. Por ultimo llamamos al método
connect()
e instruimos a ayncio para que ejecute el loop de eventos para
siempre, esto implica que nuestro bot estará manejando eventos hasta que
cancelemos la ejecución del bot. Vamos a detenernos un poco en el tema del
event loop
, entender el funcionamiento de asyncio y el event loop es de vital
importancia para comprender como funciona nuestro bot.
Async.io
asyncio
es un modulo Python que nos brinda un marco de trabajo para
desarrollar programas asíncronos mediante el uso de coroutines
, event loops
y tasks
. Permitiéndonos escribir código concurrente que es más eficiente
y responsivo, especialmente cuando estamos hablando de operaciones de I/O.
El concepto de corrutinas (coroutines) es el corazón de asyncio
, estas son
funciones que pueden ser detenidas y resumidas. Cuando una corrutina se
encuentra con un objeto awaitable
(como una operación I/O), esta suspende su
ejecución y permite la ejecución de la otra tarea. Esto permite que puedan
planificarse y ejecutarse multiples tareas de forma concurrente, sin bloquear la
ejecución del programa.
El coordinador central, que ejecuta estas corrutinas es el event loop
, quien
tiene la responsabilidad de gestionar y ejecutar operaciones asíncronas. El loop
de eventos corre continuamente y planifica las corrutinas, realiza operaciones
I/O y maneja los callbacks cuando algun objeto awaitable
esta listo.
Algunos casos de uso que podemos resolver mediante el uso de asyncio son operaciones que requieran entra/salida asincronica, ejecución de callbacks o aplicaciones que requieran timers o deplays.
Transliteración de audio en salas de chat de múltiples usuarios (a.k.a: MUC)
Desde hace algunos años a esta parte, estamos viendo gran cantidad de usuarios que, comportándose como usuarios de NexTel, les encanta enviar mensajes de audio tanto en salas de chat como en chat uno a uno.
Esto implica que dejan afuera a gran cantidad de usuarios que carecen de los medios necesarios para escuchar mensajes de audio. Sea por una imposibilidad física, por que no cuentan con dispositivos, no pueden escuchar audio por el entorno donde están o simplemente prefieren leer a escuchar mensajes de audio... muchas veces interminables.
Con el surgimiento de los modelos de lenguaje masivos, han salido a la luz varias herramientas que resuelven muchos problemas que antes requerían largas horas de esfuerzo de desarrollo para tener resultados al menos pobres, siendo generoso.
Whisper AI
Dentro de estos modelos Whisper AI resuelve justamente el caso de uso de transliteración. Su uso es muy simple, si queremos que sea simple ... sino, como todo, podemos hacerlo tan complejo como queramos. Pero, manteniendo la filosofía KISS del desarrollo de software (y diseño de sistemas de información en general) vamos a tratar de simplificar al máximo el uso de Whisper.
Si bien el modelo, como decíamos antes, nos brinda herramientas para complejizar un poco mas nuestro código. Por ejemplo, configurar de antemano el idioma o acortar el audio a una duración determinada. No vamos a usar estas características del modelo ya que como decíamos antes, nuestro objetivo es que sea lo mas simple posible. El siguiente bloque de código no es, ni más ni menos, que todo lo requerido para transliterar un audio:
import whisper
async def transcribe(self, audio_file):
"""
Returns audio transcription text
"""
model = whisper.load_model(self.model)
result = model.transcribe(audio_file)
logging.info('File %s transliterated' % audio_file)
return result["text"]
Habiendo importado el modulo whisper
, lo primero que hacemos en nuestro método
es cargar el modelo en memoria. Si el modelo no esta disponible, va a intentar
descargarlo de internet, así que nos tenemos que asegurar que nuestro bot pueda
alcanzar internet.
Luego, con el modelo cargado en memoria, llamamos al método transcribe que toma como parámetro el archivo de audio a transliterar y nos retorna un vector que tiene el texto resultante de la transliteración, entre otras cosas como el idioma detectado.
Ya prácticamente sabemos todo lo necesario para construir nuestro bot.
Johnny5
Primero veamos el código completo para luego analizarlo por partes:
#!/usr/bin/env python3
import logging
from getpass import getpass
from argparse import ArgumentParser
from slixmpp import ClientXMPP
from slixmpp.exceptions import IqError, IqTimeout
import asyncio
import whisper
import re
import os
import urllib.request
import time
import random
class WhisperBot(ClientXMPP):
"""
A simple XMPP that transliterates audios with Whisper
"""
def __init__(self, jid, password, room, nick, quotes, about, model):
ClientXMPP.__init__(self, jid, password)
self.room = room
self.nick = nick
self.quotes = quotes
self.about = about
self.model = model
self.add_event_handler("session_start", self.start)
self.add_event_handler("groupchat_message", self.muc_message)
self.add_event_handler("disconnected",self.disconnected)
async def start(self, event):
await self.get_roster()
self.send_presence()
self.plugin['xep_0045'].join_muc(self.room,
self.nick,
)
def disconnected(self):
logging.info("reconnecting...")
self.reconnect()
def muc_message(self, msg):
print("Got a message")
if self.is_audio_msg(msg):
logging.info('Got an audio from %s' % msg['mucnick'])
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
print('Async event loop already running. Adding coroutine to '\
'the event loop.')
tsk = loop.create_task(self.sendtranscription(msg))
tsk.add_done_callback(
lambda t: print(f'Task done with result={t.result()}'))
elif msg['mucnick'] != self.nick and self.nick in msg['body'] \
and "about" in msg['body']:
self.send_message(mto=msg['from'].bare,
mbody="%s" % self.abouttext(self.about),
mtype='groupchat')
elif msg['mucnick'] != self.nick and self.nick in msg['body']:
self.send_message(mto=msg['from'].bare,
mbody="%s" % self.randomtext(self.quotes),
mtype='groupchat')
print("Done")
def abouttext(self, file_name):
retval = "Number 5... is alive! and here to help with audio transliteration 🤖️"
with open(file_name, 'r') as file:
retval = file.readlines()
return "🤖️ %s" % ' '.join(retval)
def randomtext(self, file_name):
retval = "Ops!"
with open(file_name, 'r') as file:
textlines = file.readlines()
retval = random.choice(textlines)
return "🤖️ %s" % retval
async def sendtranscription(self, msg):
rcv_time = time.strftime("%H:%M", time.localtime())
audio_msg_file = self.get_audio(msg['body'])
transcript = await self.transcribe(audio_msg_file)
self.send_message(mto=msg['from'].bare,
mbody="💬 A las %s hrs., %s dijo: %s" \
% (rcv_time, msg['mucnick'], transcript),
mtype='groupchat')
self.rm_audio(audio_msg_file)
logging.info('Transcription sent')
def is_audio_msg(self, msg):
body = msg['body']
match = re.match(r'^https?://.+\.(m4a|mp3|ogg|opus)$', body)
if not match:
return False
audio_url = match.group(0)
# Check if the URL points to a valid audio file
if audio_url.endswith(('.mp3', '.m4a', 'ogg', 'opus')):
logging.info('Got an audio url!')
return True
return False
def get_audio(self, url):
save_directory = "/srv/downloads"
if not os.path.exists(save_directory):
os.makedirs(save_directory)
filename = os.path.basename(url)
file_path = os.path.join(save_directory, filename)
urllib.request.urlretrieve(url, file_path)
logging.info('File %s downloaded' % file_path)
return file_path
def rm_audio(self, file_path):
try:
os.remove(file_path)
print(f"{file_path} has been deleted successfully!")
except OSError as e:
print(f"Error: {file_path} - {e.strerror}.")
async def transcribe(self, audio_file):
model = whisper.load_model(self.model)
result = model.transcribe(audio_file)
logging.info('File %s transliterated' % audio_file)
return result["text"]
if __name__ == '__main__':
parser = ArgumentParser()
parser.add_argument("-q", "--quiet", help="set logging to ERROR",
action="store_const", dest="loglevel",
const=logging.ERROR, default=logging.INFO)
parser.add_argument("-d", "--debug", help="set logging to DEBUG",
action="store_const", dest="loglevel",
const=logging.DEBUG, default=logging.INFO)
parser.add_argument("-j", "--jid", dest="jid",
help="JID to use")
parser.add_argument("-p", "--password", dest="password",
help="password to use")
parser.add_argument("-r", "--room", dest="room",
help="MUC room to join")
parser.add_argument("-n", "--nick", dest="nick",
help="MUC nickname")
parser.add_argument("-u", "--quotes", dest="quotes",
help="quotes text file")
parser.add_argument("-a", "--about", dest="about",
help="about text file")
parser.add_argument("-m", "--model", dest="model",
help="Whisper model")
args = parser.parse_args()
logging.basicConfig(level=args.loglevel,
format='%(levelname)-8s %(message)s')
if args.jid is None:
args.jid = input("Username: ")
if args.password is None:
args.password = getpass("Password: ")
if args.room is None:
args.room = input("MUC room: ")
if args.nick is None:
args.nick = input("MUC nickname: ")
xmpp = WhisperBot(args.jid, args.password, args.room,
args.nick, args.quotes, args.about, args.model)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0045') # Multi-User Chat
xmpp.register_plugin('xep_0199') # XMPP Ping
xmpp.connect()
asyncio.get_event_loop().run_forever()
Esta vez vamos a empezar analizando nuestro bloque principal de ejecución:
if __name__ == '__main__':
parser = ArgumentParser()
parser.add_argument("-q", "--quiet", help="set logging to ERROR",
action="store_const", dest="loglevel",
const=logging.ERROR, default=logging.INFO)
parser.add_argument("-d", "--debug", help="set logging to DEBUG",
action="store_const", dest="loglevel",
const=logging.DEBUG, default=logging.INFO)
parser.add_argument("-j", "--jid", dest="jid",
help="JID to use")
parser.add_argument("-p", "--password", dest="password",
help="password to use")
parser.add_argument("-r", "--room", dest="room",
help="MUC room to join")
parser.add_argument("-n", "--nick", dest="nick",
help="MUC nickname")
parser.add_argument("-u", "--quotes", dest="quotes",
help="quotes text file")
parser.add_argument("-a", "--about", dest="about",
help="about text file")
parser.add_argument("-m", "--model", dest="model",
help="Whisper model")
args = parser.parse_args()
logging.basicConfig(level=args.loglevel,
format='%(levelname)-8s %(message)s')
if args.jid is None:
args.jid = input("Username: ")
if args.password is None:
args.password = getpass("Password: ")
if args.room is None:
args.room = input("MUC room: ")
if args.nick is None:
args.nick = input("MUC nickname: ")
Agregamos al ejemplo anterior el procesamiento de argumentos enviados por linea
de comandos y en vez de hardcodear
los valores de las variables, asignamos lo
que nos pasen como argumentos.
Lo próximo es instanciar nuestro bot pasándole como parámetros del constructor las variables que corresponden a nuestra clase, tal como lo hicimos en el ejemplo anterior:
xmpp = WhisperBot(args.jid, args.password, args.room,
args.nick, args.quotes, args.about, args.model)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0045') # Multi-User Chat
xmpp.connect()
asyncio.get_event_loop().run_forever()
Ahora a lo importante ... a nuestra clase WhisperBot
, la cual expone la
siguiente interface:
Empecemos viendo primero el constructor de la clase, así lo usamos de hilo conductor de nuestro análisis:
def __init__(self, jid, password, room, nick, quotes, about, model):
ClientXMPP.__init__(self, jid, password)
self.room = room
self.nick = nick
self.quotes = quotes
self.about = about
self.model = model
self.add_event_handler("session_start", self.start)
self.add_event_handler("groupchat_message", self.muc_message)
self.add_event_handler("disconnected",self.disconnected)
Esta vez, a diferencia del código boilerplate que usamos para introducirnos en el tema, nuestro constructor requiere mas parámetros, estos son:
- jid y password:Credenciales de nuestro boot
- room: jid de la sala MUC donde entrará nuestro bot
- nick: Nickname que usará nuestro bot en la sala MUC
- quotes: Path al archivo de texto donde obtener frases de Johnny 5
- about: Path al archivo de texto que contiene la respuesta al comando about.
- model: Modelo Whisper a ser usado (
tiny
,base
,small
,medium
,large
).
Asignamos estos parámetros a los atributos de la instancia y luego comenzamos
a agregar los callbacks a los eventos que nos interesa manejar. El primero es el
callback del evento session_start
, que es idéntico al visto anteriormente, no
agregamos ninguna lógica extra, lo mismo para el evento disconnected
:
async def start(self, event):
await self.get_roster()
self.send_presence()
self.plugin['xep_0045'].join_muc(self.room,
self.nick,)
def disconnected(self):
logging.info("reconnecting...")
self.reconnect()
La mágia ocurre en el método muc_message
, que maneja los eventos
groupchat_message
de la siguiente forma:
def muc_message(self, msg):
print("Got a message")
if self.is_audio_msg(msg):
logging.info('Got an audio from %s' % msg['mucnick'])
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
print('Async event loop already running. Adding coroutine to '\
'the event loop.')
tsk = loop.create_task(self.sendtranscription(msg))
tsk.add_done_callback(
lambda t: print(f'Task done with result={t.result()}'))
elif msg['mucnick'] != self.nick and self.nick in msg['body'] \
and "about" in msg['body']:
self.send_message(mto=msg['from'].bare,
mbody="%s" % self.abouttext(self.about),
mtype='groupchat')
elif msg['mucnick'] != self.nick and self.nick in msg['body']:
self.send_message(mto=msg['from'].bare,
mbody="%s" % self.randomtext(self.quotes),
mtype='groupchat')
print("Done")
Lo primero que hace es verificar si el mensaje es un mensaje de audio, en caso de no serlo y de que el mensaje contenga al nick del bot, es decir, es nombrado dentro de la sala de chat pueden pasar dos cosas:
- Si el mensaje contiene about, llama al metodo abouttext, que retorna el contenido del archivo que contiene el texto sobre el robot.
- Si el mensaje no contiene about, llama al método randomtext que retorna una frase al azar del archivo que contiene las frases.
Podemos ver que son métodos muy simples:
def abouttext(self, file_name):
retval = "Number 5... is alive! and here to help with audio transliteration 🤖️"
with open(file_name, 'r') as file:
retval = file.readlines()
return "🤖️ %s" % ' '.join(retval)
def randomtext(self, file_name):
retval = "Ops!"
with open(file_name, 'r') as file:
textlines = file.readlines()
retval = random.choice(textlines)
return "🤖️ %s" % retval
Verificar si un mensaje es de audio o no, lo resolvemos usando expresiones
regulares y verificando que la url
del audio termine en las extensiones
necesarias.
def is_audio_msg(self, msg):
body = msg['body']
match = re.match(r'^https?://.+\.(m4a|mp3|ogg|opus)$', body)
if not match:
return False
audio_url = match.group(0)
# Check if the URL points to a valid audio file
if audio_url.endswith(('.mp3', '.m4a', 'ogg', 'opus')):
logging.info('Got an audio url!')
return True
return False
Ahora, en el caso de que si sea un mensaje de audio nos topamos con el primer problema, si nosotros obtenemos el archivo y he iniciamos el proceso de transcripción, nuestro bot dejara de responder y no le retornara el control a nuestro blucle de eventos. Perdemos la capacidad de atajar nuevos eventos, un fallo catastrófico cuando estamos hablando de bots.
Para eso vamos a tener que bucear dentro del bucle de eventos para que nuestra
tarea de transcripción pueda ser gestionada por el event loop
. En caso de
estar en el ámbito correcto y que tengamos un event loop
ejecutándose, creamos
una tarea asíncrona desde el mismo bucle en ejecución:
tsk = loop.create_task(self.sendtranscription(msg))
tsk.add_done_callback(
lambda t: print(f'Task done with result={t.result()}'))
El método sendtranscripiton
es un método asíncrono y es definido de la
siguiente forma:
async def sendtranscription(self, msg):
rcv_time = time.strftime("%H:%M", time.localtime())
audio_msg_file = self.get_audio(msg['body'])
transcript = await self.transcribe(audio_msg_file)
self.send_message(mto=msg['from'].bare,
mbody="💬 A las %s hrs., %s dijo: %s" \
% (rcv_time, msg['mucnick'], transcript),
mtype='groupchat')
self.rm_audio(audio_msg_file)
logging.info('Transcription sent')
Primero obtenemos la hora y minuto que fue enviado el mensaje para poder darle algo de temporalidad al mensaje de respuesta ya que dependiendo del tamaño del audio enviado el bot tardara mas o menos tiempo y es una forma de poder organizar los mensajes cuando hay muchos mensajes de audio.
Luego descargamos el archivo de audio y llamamos al método transcribe
que es
bloqueante y es donde efectivamente ocurre la transliteración usando whisper
:
async def transcribe(self, audio_file):
model = whisper.load_model(self.model)
result = model.transcribe(audio_file)
logging.info('File %s transliterated' % audio_file)
return result["text"]
Una ves devuelta la transcripción del audio, el método sendtranscription
envía, de manera completamente asíncrona, el mensaje al canal MUC donde el bot
esta participando.
Con esto completamos nuestra primer aproximación al desarrollo de aplicaciones que utilicen XMPP. En el próximo post vamos a ver como solucionar un problema de conexión que se da en algunos escenarios, mientras tanto pueden seguir el avance de este proyecto en nuestro repositorio en Codeberg.