https://d226lax1qjow5r.cloudfront.net/blog/blogposts/serverless-paging-amazon-transcribe-dr/Pager-Hack-the-Planet.png

Creación de una aplicación de paginación sin servidor con Amazon Transcribe

Publicado el May 4, 2021

Tiempo de lectura: 9 minutos

Los años 90 tuvieron grandes tecnologías: MiniDisc, Tamagotchi y PAGERS.

De acuerdo, para la mayoría de las cosas nuestros smartphones han avanzado bastante en los últimos 20 años, pero hay cierta nostalgia en los buscapersonas.

Y como cualquiera que haya estado de guardia y se haya despertado a las 2 de la mañana con una llamada pidiendo ayuda frenéticamente, los buscapersonas tenían una ventaja que hemos perdido con los teléfonos: recibías el mensaje y luego devolvías la llamada a la persona.

Para los que no se acuerden de los buscapersonas, llamabas al número de una persona y la llamada era atendida por un operador que tomaba un mensaje, lo tecleaba en su consola y el mensaje aparecía en la pantalla de tu buscapersonas instantes después.

En este tutorial te voy a mostrar cómo recrear ese servicio de mensajería usando Nexmo y AWS.

La IA ha recorrido un largo camino recientemente y ahora podemos reemplazar el costoso operador de mensajería con una API; para este ejemplo voy a utilizar Amazon Transcribe. Usaremos Nexmo para recibir la llamada entrante, contestarla y grabar un mensaje, luego pasaremos esa grabación a Transcribe y cuando recibamos el texto lo enviaremos a tu teléfono usando Nexmo SMS.

Para unir las partes, crearemos una aplicación python con el framework Chalice, que nos permitirá implementar y ejecutar todo en AWS Lambda y S3.

Las ventajas de esta arquitectura son que nuestros costes de alojamiento serán muy reducidos. De hecho, si estás ejecutando esto solo para tu uso personal, es probable que te mantengas dentro de los niveles gratuitos de AWS y si quieres hacerlo crecer, entonces la pila sin servidor escalará a volúmenes masivos.

Requisitos previos

Para este tutorial necesitarás lo siguiente:

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Configurar

Antes de implementar nuestra funcionalidad necesitamos hacer algunas configuraciones.

Aplicación Nexmo Voice

Tenemos que crear una nueva aplicación de voz, ya sea dentro del tablero de instrumentos nexmo o mediante la herramienta de línea de comandos

nexmo app:create “Paging Service” http://example.com/answer http://example.com/event --keyfile private.key

Anote el ID de la aplicación que se devuelve; esto también guardará la clave privada en un archivo.

Por ahora usaremos valores ficticios para los webhooks, o si quieres probar localmente puedes usar ngrok para tu host.

Cubo S3

También necesitamos crear un bucket de S3 en el que colocar las grabaciones para su transcripción; podemos hacerlo utilizando la CLI de AWS. Crearemos nuestros recursos en la región de AWS us-east-1.

aws s3api create-bucket --bucket pagingservice --region us-east-1

Aplicación Cáliz

En primer lugar vamos a crear un nuevo proyecto de cáliz:

chalice new-project

Cuando se le solicite, dé un nombre a su proyecto, por ejemplo paging-service. Ahora tendrá una plantilla básica para una aplicación de cáliz creada en la carpeta con el nombre de su proyecto.

En esa carpeta hay tres archivos importantes:

  • app.py: el código principal de la aplicación

  • requirements.txt: enumera los módulos python que está utilizando

  • .chalice\config.jsoncontiene varios ajustes relacionados con su proyecto

Módulos de importación

La plantilla ya habrá importado el módulo chalice. También necesitamos el módulo boto3 para conectar con S3 y Transcribe, y el módulo Nexmo para obtener la grabación y enviar el SMS. También importamos el módulo os para acceder a las variables de entorno.

import boto3
import nexmo
import json
import os

También necesitamos añadir boto3 y nexmo al archivo requirements.txt para que lambda sepa que debe instalarlos cuando se despliegue la aplicación.

Variables de configuración

APPLICATION_ID = os.environ['APPLICATION_ID']
API_KEY = os.environ['API_KEY']
API_SECRET = os.environ['API_SECRET']
NAME = os.environ['NAME']
NUMBER = os.environ['NUMBER']
NEXMO_NUMBER = os.environ['NEXMO_NUMBER']
S3_BUCKET = 'pagingdemo'

Estamos estableciendo la mayoría de nuestras variables de entorno varibles, se puede ver cómo configurar estos con cáliz de la README en github alternativly sólo puede establecer sus propios valores aquí.

Inicializar clientes

Vamos a conectarnos a tres servicios externos como parte de nuestra aplicación: AWS S3, Amazon Transcribe y Nexmo. Vamos a crear esas conexiones aquí:

S3 = boto3.client('s3')
TRANSCRIBE = boto3.client('transcribe')
NEXMO = nexmo.Client(
    key=API_KEY,
    secret=API_SECRET,
    application_id=APPLICATION_ID,
    private_key='chalicelib/private.key',
)

No necesitamos suministrar credenciales para los servicios de Amazon ya que boto y chalice lo harán automáticamente cuando despleguemos a Lambda.

Cómo escribir el código del manipulador

Para esta aplicación hay tres etapas por las que tenemos que pasar para transcribir un mensaje de voz; éstas se alinearán bien con tres manejadores separados en el código de nuestra aplicación.

El primero es un manejador webhook para responder a la solicitud de llamada entrante de Nexmo y devolver un NCCO (Nexmo Call Control Object), que es una lista de acciones a realizar en la llamada, representada como un objeto JSON.

@app.route('/answer')
def answer():
    req = app.current_request.to_dict()
    ncco =[
            {
                'action': 'talk',
                'text': "Welcome to {}s messaging service, please leave a short message after the tone".format(NAME),
            },
            {
                'action': 'record',
                'endOnSilence': 3,
                'endOnKey': '#',
                'beepStart' : True,
                'eventUrl' : [req['headers']['x-forwarded-proto'] + "://" + req['headers']['host'] + "/api/recording?from=" +req['query_params']['from']]
            },
            {
                'action': 'talk',
                'text': "thankyou, your message has been forwarded"
            }
        ]
    return ncco

El decorador @app.route define la ruta a la que responderá este manejador. Convertimos los parámetros de la petición entrante en un objeto req ya que querremos utilizar algunos de esos datos más adelante. Luego creamos nuestra respuesta NCCO.

La primera acción es el talk que es el saludo inicial que oyen las personas que llaman, aquí estamos introduciendo el parámetro NOMBRE para personalizar el saludo.

Luego tenemos una record que es donde capturamos el mensaje de la persona que llama. He puesto endOnSilence a tres segundos para que cuando terminen de hablar siga adelante o puedan usar el botón endonKey para pulsar #. beepStart es True para que la persona que llama sepa cuando empezar a hablar.

El parámetro eventUrl parece un poco complicado, pero todo lo que estoy haciendo aquí es construir la URL que será el mismo host y protocolo que el webhook entrante utilizado en la puerta de enlace de la API, por lo que no necesitamos codificarlo. La ruta es /api/recordingdonde api es la predeterminada para la pasarela API. Por último estamos etiquetando en el from como parámetro de consulta para que cuando el webhook de grabación entre sepamos el ID original de la llamada (ya que nexmo no pasa esto como estándar en los eventos de grabación).

Terminamos nuestro NCCO con una simple talk para que la persona que llama sepa que su mensaje ha sido capturado y pueda colgar. Si cuelgan mientras la grabación sigue activa, el mensaje seguirá transmitiéndose.

En nuestro siguiente manejador recibiremos el evento de grabación entrante de Nexmo, buscaremos y almacenaremos la grabación en S3, y luego iniciaremos la acción de transcripción.

@app.route('/recording', methods=['POST'])
def recording():
  qparams=  app.current_request.query_params
  data =  app.current_request.json_body
  recfile = NEXMO.get_recording(data['recording_url'])
  S3.put_object(
      Bucket=S3_BUCKET,
      Key=data['conversation_uuid']+".mp3",
      Body=recfile,
      ContentType='audio/mp3',
      Metadata={
        'callerid': qparams['from'],
        'time' : data['end_time']
      }
  )
  response = TRANSCRIBE.start_transcription_job(
      TranscriptionJobName=data['conversation_uuid'],
      LanguageCode='en-GB',
      MediaFormat='mp3',
      Media={
          'MediaFileUri': 'https://s3.amazonaws.com/{}/{}'.format(S3_BUCKET, data['conversation_uuid']+".mp3")
      },
      OutputBucketName=S3_BUCKET,
  )
  return "ok"

Tenemos el mismo decorador @app.route sin embargo, también estamos especificando que esto va a manejar una solicitud POST, (cáliz por defecto a GET).

Estamos agarrando los parámetros de la cadena de consulta donde pasamos los from a un diccionario llamado qparams y luego estamos poniendo el cuerpo JSON del webhook en un objeto llamado data.

Usaremos el objeto NEXMO que hemos creado como conexión a Nexmo para obtener la grabación y almacenarla en el bucket de S3 utilizando el UUID de la conversación como clave. También establecemos un par de metadatos contra el objeto, a saber, el ID original de la persona que llama (from) y la hora de la grabación.

Por último, iniciamos un trabajo de transcripción que apunta a nuestra nueva grabación en S3. Tenemos que proporcionar un nombre para el trabajo (de nuevo vamos a utilizar el UUID de la conversación), el idioma en que está el audio (en este caso Inglés - Británico), el formato de los medios de comunicación, y la URI para el archivo en S3. Este formato dependerá de la región en la que hayas creado tu bucket, el ejemplo aquí es para us-east-1. Por último, especificamos el bucket de salida en el que se escribirá la transcripción resultante: utilizaremos el mismo bucket que las grabaciones.

Para el controlador final estamos disparando un poco diferente; esta vez no es un webhook, pero la llegada del resultado de la transcripción en nuestro cubo de S3 que invoca nuestro código.

@app.on_s3_event(bucket=S3_BUCKET, events=['s3:ObjectCreated:*'], suffix='.json')
def transcribed(event):
  # Get transcription from S3
  obj = S3.get_object( Bucket=S3_BUCKET, Key=event.key)
  data = json.loads(obj['Body'].read())
  # Make recording public
  S3.put_object_acl(ACL='public-read', Bucket=S3_BUCKET, Key= data['jobName']+".mp3")
  #Build SMS
  text = data['results']['transcripts'][0]['transcript'].upper()
  obj = S3.get_object( Bucket=S3_BUCKET, Key=data['jobName']+".mp3")
  callerid = obj['ResponseMetadata']['HTTPHeaders']['x-amz-meta-callerid']
  url = 'https://s3.amazonaws.com/{}/{}'.format(S3_BUCKET, data['jobName']+".mp3")
  message = "[From: +{}]\n\n{}\n\n{}".format(callerid, text, url)
  #Send SMS
  NEXMO.send_message({'from': NEXMO_NUMBER, 'to': NUMBER, 'text': message})

Notarás que el decorador tiene un formato diferente: on_s3_event. También estamos especificando el bucket que nos interesa, el tipo de evento, cuándo se crea un nuevo objeto y el sufijo de esos objetos como JSON para que no se dispare cuando la grabación .mp3 se añadan al bucket.

A continuación, obtenemos el nuevo objeto, que es una respuesta JSON de nuestra transcripción, y lo guardamos en el archivo data. Hacemos el archivo de grabación mp3 públicamente legible y luego empezamos a construir nuestro mensaje de notificación. Me gusta ver el texto de la transcripción en MAYÚSCULAS ya que se siente un poco más como el servicio de buscapersonas retro. También añadimos el identificador original de la persona que llama al principio del mensaje y, por último, añadimos la URL de la grabación de audio al final del mensaje, por si la transcripción no es perfecta y quieres oír lo que dijo originalmente la persona que llamó.

Por último, enviamos el SMS utilizando el objeto cliente NEXMO creado anteriormente.

Despliegue

Ahora que hemos creado la aplicación, solo tenemos que desplegarla en AWS ejecutando:

chalice deploy

Esto crea una función Lambda y configura las reglas de la pasarela API. También crea los eventos del bucket de S3 y configura las políticas de seguridad de IAM asociadas para nosotros automáticamente.

A continuación, debería obtener una salida que contenga la URL de la puerta de enlace de la API, por ejemplo:

Rest API URL: https://3u9ucalu05.execute-api.us-east-1.amazonaws.com/api/

Usando esta URL como base, actualice su aplicación Nexmo para configurar el webhook de respuesta para que apunte a su aplicación desplegada. Para ello, necesitará el ID de su aplicación:

nexmo app:update [APPLICATION UUID] “Paging Service” `chalice url`answer `chalice url`event

Por último, asegúrese de que su número Nexmo está vinculado a la aplicación y llame. Deberías escuchar tu saludo y poder dejar un mensaje. Poco después recibirá un mensaje de texto con su transcripción y un enlace al archivo de audio:

text message screenshottext message

Próximos pasos

Hay varias formas de ampliar esta aplicación. Algunas ideas que podrías considerar son añadir soporte para múltiples usuarios creando un mapeo de número entrante a número de notificación y saludo, o cambiar la notificación SMS por correo electrónico si eso se adapta mejor a tu caso de uso.

Además, los archivos MP3 y de transcripción se almacenarán de forma permanente en tu bucket de S3, por lo que es posible que desees buscar una forma de eliminarlos o caducar después de un cierto período.

También vale la pena mencionar que el servicio Amazon Transcribe no es el más rápido, especialmente para clips de audio cortos. En mis pruebas he visto un retraso de 1-2 minutos en la transcripción de un mensaje de voz corto, así que tenlo en cuenta si estás pensando en utilizarlo para notificaciones en las que el tiempo es un factor crítico.

Puede encontrar todo el código fuente de la aplicación junto con la configuración de chalice en el Repositorio GitHub.

Compartir:

https://a.storyblok.com/f/270183/384x384/7fbbc7293b/sammachin.png
Sam MachinAntiguos alumnos de Vonage