
Compartir:
Mark era nominalmente responsable de las bibliotecas cliente de Nexmo (aunque sólo escribe las bibliotecas Python y Java). Originalmente era un desarrollador Java, ha sido un desarrollador Python durante 18 años, y está incursionando cada vez más en Go y Rust. Le gusta llevar al límite los lenguajes de programación y luego enseñar estas técnicas a otros programadores. Tiene un sombrero vikingo, pero no es un vikingo, y es Judy2k en Twitter por razones que no discutirá.
Cómo construir un buzón de voz con Python y Flask
Tiempo de lectura: 11 minutos
Era una noche oscura y tormentosa, pero no podía dejar que el tiempo me detuviera. Tenía una misión que cumplir. Tras refugiarme un rato en una cabina telefónica pública, cogí el auricular mugriento. Aunque a cualquier persona normal le sorprendería oír el tono de marcación en una de estas reliquias del pasado, yo ya me lo esperaba. Marqué el número que me sabía de memoria...
"Soy Oleg's Pizza. Deje un mensaje después de la señal".
Eso era todo lo que decía siempre; nunca había una persona de verdad al otro lado de la línea, solo una voz robótica de una empresa que no sospechaba nada.
Oí el pitido y el clic de la grabadora.
«Hola, soy Chuck», dije al teléfono, «me gustaría una pizza de pepperoni y champiñones, por favor».
Colgué y me alejé, de vuelta al viento y la lluvia.
No me llamo Chuck y no me gusta el pepperoni, pero esto dejaría las cosas claras: me habrían descubierto y para el lunes ya me habría ido. Solo un recuerdo que se desvanece en la mente de quienes me conocían...
Fin de la escena.
Me encantan las buenas películas de espionaje, y parece que una de las cosas más difíciles de ser espía es encontrar un punto de entrega secreto donde dejar mensajes para tu contacto. En este tutorial, crearás un punto de entrega oculto mediante el buzón de voz utilizando la Voice API de Vonage, Python y Flask, lo que dará como resultado una aplicación web que responde a las llamadas entrantes con un saludo, graba el mensaje de la persona que llama y te permite reproducirlo en el navegador.
Requisitos previos
Voy a dar por hecho que has leído la fantástica entrada de Aaron en la que describe cómo utilizar Ngrok para desarrollar webooks. Si no lo has hecho, ve a leerlo ahora mismo: merece la pena y lo vas a necesitar para este tutorial.
También voy a dar por hecho que tienes conocimientos prácticos de Python y Flask.
Te recomiendo que instales la herramienta CLI de Vonage y leer la breve entrada del blog sobre cómo instalarla. Algunas de las instrucciones que aparecen a continuación la utilizarán, aunque puedes realizar estas acciones en el panel de control de Vonage si lo prefieres.
Qué vas a construir
Te voy a enseñar cómo crear un servicio básico de buzón de voz que permita a los usuarios llamar a tu número de Vonage y dejar un mensaje utilizando la API de voz de Vonage Voice API.
El mensaje grabado se copiará en tu servidor y crearás una página web básica que muestre la lista de grabaciones y te permita reproducirlas en el navegador, a modo de tu propio punto de entrega secreto.
Inicio del proyecto
Si prefiere seguir con mi código existente, puede encontrarlo aquípero te recomiendo que sigas este post y lo construyas tú mismo.
La estructura de la carpeta de nuestro proyecto es la siguiente:
demo-dead-drop-voice-python-flask/
│
├── answerphone/ # Main Flask application package
│ ├── __init__.py # App factory, routes, and webhook handlers
│ └── templates/
│ └── index.html.j2 # Jinja2 template listing all recordings
│
├── recordings/ # Downloaded MP3 recordings (gitignored)
│
├── .env.template # Template for environment variables
├── .gitignore # Git ignore rules
├── Makefile # Convenience commands (e.g. `make run`)
├── README.md # Project documentation
└── requirements.txt # Python dependenciesComo este es un proyecto pequeño, todo tu código Python irá en answerphone/__init__.pypero si fuera más grande, podrías dividirlo en módulos separados bajo el paquete answerphone paquete.
También pondrás nuestros recursos estáticos en static y tus plantillas en templates y Flask sabrá dónde encontrarlos.
He optado por guardar mis grabaciones MP3 en una carpeta a nivel de proyecto recordings fuera del paquete answerphone porque es una buena idea separar los datos (¡especialmente los que se descargan de Internet!) del código ejecutable.
También hay un .env.template archivo en el directorio del proyecto que contiene la plantilla de configuración para el .env archivo que contiene todos mis secretos.
Instalar dependencias
Copia y pega lo siguiente directamente en requirements.txt, crea y activa un entorno virtualy, a continuación, ejecuta pip install -r requirements.txt:
flask
tinydb
vonage
python-dotenvUn rápido resumen de nuestras dependencias:
dotenv se utilizará para cargar la configuración de nuestro
.envarchivo de configuración.matraz es nuestro framework web y servidor web de desarrollo.
tinydb es una base de datos muy ligera que almacena todos tus datos en formato JSON.
Vonage es el SDK de Python de Vonage, y facilita el uso de las API de Vonage en comparación con hacerlo manualmente. El SDK utiliza Pydantic para facilitar la validación de datos.
Definir los webhooks
Abra __init__.py en su answerphone paquete y escriba lo siguiente:
from flask import Flask
@app.route("/answer", methods=["POST"])
def answer():
"""
An NCCO webhook, providing actions that tell Vonage to read a statement
to the user and then record a message.
"""
logger.info("Phone call received ...")
greeting = "You have reached Oleg's pizza. Please leave a message after the beep."
event_url = url_for("new_recording", _external=True, _scheme="https")
logger.info(f"event_url: {event_url}")
ncco: list[NccoAction] = [
Talk(text=greeting, language="en-GB", style=0),
Record(beepStart=True, endOnSilence=3),
]
return [action.model_dump(by_alias=True, exclude_none=True) for action in ncco]Asegúrese de que está ejecutando Ngrok e inicie su servidor de desarrollo con:
Ahora, si visita https://your-random-id.ngrok.io/answer con tu navegador web deberías ver algo como lo siguiente:
[
{
"action": "talk",
"text": "You have reached Oleg's pizza. Please leave a message after the beep.",
"voiceName": "Brian"
},
{
"action": "record",
"beepStart": true,
"eventUrl": [
"https://example.com/recording"
],
"endOnSilence": 3
}
]Ahora vamos a crear una aplicación de Voice y a vincular un número a esta URL. En tu consola, ejecuta la herramienta CLI de Vonage, que te guiará paso a paso por el proceso de creación de tu aplicación:
Imprimirá algo como Application created: 26aa5db4-546a-11e9-8f2d-0f348a273d3ay creará un archivo llamado private.key en su directorio actual.
Tome este ID y péguelo en un nuevo .env así:
VONAGE_APPLICATION_ID=26aa5db4-546a-11e9-8f2d-0f348a273d3a
VONAGE_PRIVATE_KEY=private.keyDeja esto por ahora; te explicaré cómo cargar la configuración dentro de un momento.
Si necesitas comprar un número, te recomiendo que lo hagas en el panel de control de Vonage.
Una vez que hayas comprado un número (asegúrate de que es compatible con Voice), vuelve a la línea de comandos y utiliza el comando vonage para vincular el número a tu aplicación:
vonage apps numbers link your-application-id your-purchased-numberAhora, si llamas a tu número de Vonage, deberías escuchar el mensaje de la talk paso anterior: «Ha llamado a Oleg's Pizza. Por favor, deja un mensaje tras la señal». ¡Perfecto!
Comprueba los registros de Ngrok. Es posible que veas algunos errores 404 en /event. No te preocupes por esto ahora mismo; más adelante en este tutorial añadirás un webhook de eventos.
Lamentablemente, una vez que Vonage ha terminado de grabar tu mensaje, actualmente realiza una solicitud POST a la URL indicada en tu record acción, que está configurada en https://example.com/recording.
Vamos a arreglar eso para que pueda recibir el evento de grabación y descargar el MP3, para que su manejador pueda recoger los mensajes de sus agentes.
En su __init__.pyañada lo siguiente:
# Add to your imports:
import logging
from dotenv import load_dotenv
from flask import Flask, request, render_template, url_for, make_response
from vonage import Auth, Vonage
from vonage_voice import NccoAction, Talk, Record
import os
# After your imports:
load_dotenv()
client = Vonage(
Auth(
application_id=os.environ["VONAGE_APPLICATION_ID"],
private_key=os.environ["VONAGE_PRIVATE_KEY"],
)
)
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.route("/new-recording", methods=["POST", "GET"])
def new_recording():
"""
A Vonage event webhook, only called when a recording is made available.
This webhook downloads the MP3 to the `recordings` directory and adds
metadata to the `recordings` table in the db.
"""
recording_id = request.json["recording_uuid"]
recording_url = request.json["recording_url"]
recording_filepath = f"recordings/{recording_id}.mp3"
logger.info(f"A new message has been recorded with UUID: ===> {recording_id}")
logger.info(f"Downloading recording at: ===> {recording_url}")
# Ideally this would be done as a background process.
# Try using Celery, or using Sanic instead of Flask!
client.voice.download_recording(recording_url, recording_filepath)
recordings.insert(request.json)
return ""y ahora modifique su answer webhook. La segunda acción debe tener este aspecto:
Record(beepStart=True, eventUrl=[event_url], endOnSilence=3)Ahora estás utilizando la función url_for de Flask para obtener una URL que apunte al new_recording que acabas de añadir al archivo.
Asegúrese de que su carpeta recording y reinicie el servidor de desarrollo Flask.
Ahora, cuando llames a tu número de Vonage y dejes un mensaje, deberías encontrar un archivo MP3 en la recording carpeta. ¡Ábrelo en tu reproductor de MP3 favorito para escuchar lo que dice!
Si quisieras, podrías parar ahora. Ya has aprendido todo lo básico sobre cómo hacer que Vonage grabe un mensaje y, a continuación, cómo descargar ese mensaje a tu servidor.
Pero sería una buena idea almacenar algunos metadatos junto con el audio, para saber quién era la persona que llamaba y cuándo lo hizo. De esta forma, puedes añadir una página con una lista de todas las llamadas a tu contestador automático.
Elegí TinyDB para hacerlo. Es un pequeño almacén de datos realmente minimalista que guarda los datos en un archivo JSON. No es muy rápido y no gestiona muy bien grandes cantidades de datos, ¡pero para este proyecto está bien!
Añada lo siguiente a su archivo .env archivo: DATABASE_PATH=answerphone.db.
Le dices a TinyDB que almacene los datos en este archivo con lo siguiente cerca de la parte superior de tu archivo __init__.py archivo:
from tinydb import TinyDB, Query
db = TinyDB(os.environ["DATABASE_PATH"])Ahora añada lo siguiente, para crear dos "tablas" para almacenar los datos de la persona que llama y los datos de la grabación:
calls = db.table('calls')
recordings = db.table('recordings')Ahora tienes que hacer dos cosas: debes responder a los eventos de llamada y registrar los datos de la llamada cuando se conteste a una llamada, y debes añadir un par de líneas a tu recording webhook para que almacene los datos de la grabación en la base de datos.
En primer lugar, añada el event webhook:
@app.route("/event", methods=["POST"])
def event():
if request.json.get('status') == 'answered':
calls.insert(request.json)
return ""La línea calls.insert(request.json) almacena todos los datos JSON de la solicitud en la tabla calls que creó anteriormente.
Ahora añade una línea similar a tu recording webhook después del código para guardar el archivo MP3 en tu recordings carpeta:
recording_id = request.json["recording_uuid"]
recording_url = request.json["recording_url"]
recording_filepath = f"recordings/{recording_id}.mp3"
logger.info(f"A new message has been recorded with UUID: ===> {recording_id}")
logger.info(f"Downloading recording at: ===> {recording_url}")
client.voice.download_recording(recording_url, recording_filepath)
recordings.insert(request.json)
return ""Vuelve a llamar a tu número de Vonage y deja un mensaje. Comprueba que funciona sin errores.
Si echas un vistazo dentro de answerphone.db deberías ver una carga de datos JSON almacenados. ¡Ahora vamos a cargar esos datos en una bonita página web!
En primer lugar, añade una vista que te permita cargar un archivo MP3 en el navegador:
@app.route("/recordings/<uuid>")
def recording(uuid):
response = make_response(open(f'recordings/{uuid}.mp3', 'rb').read())
response.headers['Content-Type'] = 'audio/mpeg'
return responseEl código anterior abre el archivo MP3 binario, genera una respuesta a partir de los bytes y, a continuación, establece el encabezado «Content-Type» en audio/mpeg que es el tipo correcto para los datos MP3.
Puedes probarlo cargando la URL "/recordings/you-uuid-goes-here" con el id de uno de los archivos MP3 de tu carpeta de grabaciones.
Ahora debe añadir una vista que enumere todas las grabaciones, junto con algunos de los datos de llamada asociados a cada grabación.
Esto puede facilitarse con una pequeña clase de ayudantes.
Ponga este código cerca de la parte superior de su __init__.py archivo:
class Recording:
def __init__(self, data):
self.uuid = data['recording_uuid']
related_calls = calls.search(Query().conversation_uuid == data['conversation_uuid'])
if related_calls:
self.related_call = related_calls[0]
else:
self.related_call = NoneEsta clase está diseñada para inicializarse utilizando los datos JSON proporcionados al recording y almacenados en la tabla recordings de nuestra base de datos.
Busca automáticamente los datos de llamada asociados en la tabla calls y los añade al objeto Recording como el atributo related_call atributo.
Ahora escribe el siguiente código de vista, que pasa una Recording a la vista por cada grabación almacenada en la base de datos:
@app.route("/")
def index():
"""
A view which lists all stored recordings.
"""
return render_template("index.html.j2", recordings=[Recording(r) for r in recordings])Esto no funcionará por ahora, ya que aún no has creado un archivo de plantilla.
Cree un archivo en answerphone/templates/index.html.j2 y pon dentro algo como lo siguiente
<!doctype html>
<html>
<head>
<title>Oleg's Pizza</title>
</head>
<body>
<h1><i>"Oleg's Pizza"</i><br>Dead Drop Recordings</h1>
{% for recording in recordings -%}
<h2>Call From: <em>{{ recording.related_call.from }}</em></h2>
<p><strong>When:</strong> {{ recording.related_call.timestamp }}</p>
<a href="/recordings/{{ recording.uuid }}">Listen</a>
{% endfor -%}
</body>
</html>Ahora, si visita su https://localhost:5000/ debería ver algo como lo siguiente:
¡Ahora eres un maestro de los servicios secretos!
Resumiré lo que acabas de hacer:
Has respondido a una llamada entrante con algunas acciones de NCCO
Usted ordenó a Vonage que grabara parte de una llamada telefónica
Has utilizado el evento de grabación para descargar el archivo MP3 creado
¡Has guardado los datos de las llamadas en una base de datos y has creado una lista de reproducción en la web!
Lecturas complementarias
Si quieres profundizar un poco más en lo que acabas de aprender, lo siguiente puede resultarte útil:
El repositorio de GitHub de este proyecto
Echa un vistazo a las itinerarios de aprendizaje de Voice en el Centro de formación de Vonage
Próximos pasos
Hay varias formas de seguir desarrollando este proyecto. Podrías utilizar un websocket para notificar al navegador cuando aparece una nueva grabación, de modo que el operador no tenga que recargar el navegador para recibir mensajes de su agente.
También podrías utilizar la Messages API de Vonage para enviar al gestor un mensaje SMS cuando haya una nueva grabación disponible.
¿Tiene alguna pregunta o quiere compartir lo que está construyendo?
Suscríbase al Boletín para desarrolladores
Síguenos en X (antes Twitter) para estar al día
Ver tutoriales en nuestro canal de YouTube
Conéctese con nosotros en la página Vonage Developer en LinkedIn
Ayúdanos a mejorar nuestra experiencia como desarrolladores rellenando nuestro cuestionario «La voz del desarrollador»
Mantente conectado y entérate de las últimas noticias, consejos y eventos para desarrolladores.
Compartir:
Mark era nominalmente responsable de las bibliotecas cliente de Nexmo (aunque sólo escribe las bibliotecas Python y Java). Originalmente era un desarrollador Java, ha sido un desarrollador Python durante 18 años, y está incursionando cada vez más en Go y Rust. Le gusta llevar al límite los lenguajes de programación y luego enseñar estas técnicas a otros programadores. Tiene un sombrero vikingo, pero no es un vikingo, y es Judy2k en Twitter por razones que no discutirá.