{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Web service" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since Parselmouth is a normal Python library, it can also easily be used within the context of a web server. There are several Python frameworks that allow to quickly set up a web server or web service. In this examples, we will use [Flask](https://flask.palletsprojects.com/) to show how easily one can set up a web service that uses Parselmouth to access Praat functionality such as the pitch track estimation algorithms. This functionality can then be accessed by clients without requiring either Praat, Parselmouth, or even Python to be installed, for example within the context of an online experiment." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All that is needed to set up the most basic web server in Flask is a single file. We adapt [the standard Flask example](https://flask.palletsprojects.com/quickstart/) to accept a sound file, access Parselmouth's [Sound.to_pitch](../api_reference.rst#parselmouth.Sound.to_pitch), and then send back the list of pitch track frequencies. Note that apart from [saving the file that was sent in the HTTP request](https://flask.palletsprojects.com/quickstart/#file-uploads) and encoding the resulting list of frequencies in JSON, the Python code of the `pitch_track` function is the same as one would write in a normal Python script using Parselmouth." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile server.py\n", "\n", "from flask import Flask, request, jsonify\n", "import tempfile\n", "\n", "app = Flask(__name__)\n", "\n", "@app.route('/pitch_track', methods=['POST'])\n", "def pitch_track():\n", " import parselmouth\n", "\n", " # Save the file that was sent, and read it into a parselmouth.Sound\n", " with tempfile.NamedTemporaryFile() as tmp:\n", " tmp.write(request.files['audio'].read())\n", " sound = parselmouth.Sound(tmp.name)\n", " \n", " # Calculate the pitch track with Parselmouth\n", " pitch_track = sound.to_pitch().selected_array['frequency']\n", " \n", " # Convert the NumPy array into a list, then encode as JSON to send back\n", " return jsonify(list(pitch_track))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Normally, we can then run the server typing `FLASK_APP=server.py flask run` on the command line, as explained in the [Flask documentation](https://flask.palletsprojects.com/quickstart/). Please do note that to run this server publicly, in a secure way and as part of a bigger setup, other options are available to deploy! Refer to the [Flask deployment documentation](https://flask.palletsprojects.com/deploying/)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, to run the server from this Jupyter notebook and still be able to run the other cells that access the functionality on the client side, the following code will start the server in a separate thread and print the output of the running server." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import subprocess\n", "import sys\n", "import time\n", "\n", "# Start a subprocess that runs the Flask server\n", "p = subprocess.Popen([sys.executable, \"-m\", \"flask\", \"run\"], env=dict(**os.environ, FLASK_APP=\"server.py\"), stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n", "\n", "# Start two subthreads that forward the output from the Flask server to the output of the Jupyter notebook\n", "def forward(i, o):\n", " while p.poll() is None:\n", " l = i.readline().decode('utf-8')\n", " if l:\n", " o.write(\"[SERVER] \" + l)\n", "\n", "import threading\n", "threading.Thread(target=forward, args=(p.stdout, sys.stdout)).start()\n", "threading.Thread(target=forward, args=(p.stderr, sys.stderr)).start()\n", "\n", "# Let's give the server a bit of time to make sure it has started\n", "time.sleep(2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that the server is up and running, we can make a standard HTTP request to this web service. For example, we can send a Wave file with an audio recording of someone saying *\"The north wind and the sun [...]\"*: [the_north_wind_and_the_sun.wav](audio/the_north_wind_and_the_sun.wav), extracted from a [Wikipedia Commons audio file](https://commons.wikimedia.org/wiki/File:Recording_of_speaker_of_British_English_%28Received_Pronunciation%29.ogg)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from IPython.display import Audio\n", "Audio(filename=\"audio/the_north_wind_and_the_sun.wav\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To do so, we use the [requests library](https://requests.readthedocs.io/) in this example, but we could use any library to send a standard HTTP request." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import requests\n", "import json\n", "\n", "# Load the file to send\n", "files = {'audio': open(\"audio/the_north_wind_and_the_sun.wav\", 'rb')}\n", "# Send the HTTP request and get the reply\n", "reply = requests.post(\"http://127.0.0.1:5000/pitch_track\", files=files)\n", "# Extract the text from the reply and decode the JSON into a list\n", "pitch_track = json.loads(reply.text)\n", "print(pitch_track)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since we used the standard `json` library from Python to decode the reply from server, `pitch_track` is now a normal list of `float`s and we can for example plot the estimated pitch track:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import seaborn as sns" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sns.set() # Use seaborn's default style to make attractive graphs\n", "plt.rcParams['figure.dpi'] = 100 # Show nicely large images in this notebook" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure()\n", "plt.plot([float('nan') if x == 0.0 else x for x in pitch_track], '.')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Refer to the [examples on plotting](plotting.rst) for more details on using Parselmouth for plotting." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Importantly, Parselmouth is thus only needed by the server; the client only needs to be able to send a request and read the reply. Consequently, we could even use a different programming language on the client's side. For example, one could make build a HTML page with JavaScript to make the request and do something with the reply:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```html\n", "\n", " \n", " \n", " \n", " \n", "\n", "\n", "
\n", " \n", " \n", "
\n", "
\n", "\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Again, one thing to take into account is the security of running such a web server. However, apart from deploying the flask server in a secure and performant way, we also need one extra thing to circumvent a standard security feature of the browser. Without handling Cross Origin Resource Sharing (CORS) on the server, the JavaScript code on the client side will not be able to access the web service's reply. A Flask extension exists however, [Flask-CORS](https://flask-cors.readthedocs.io/), and we refer to its documentation for further details." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's shut down the server\n", "p.kill()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Cleaning up the file that was written to disk\n", "!rm server.py" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.7" } }, "nbformat": 4, "nbformat_minor": 4 }