Note

An online, interactive version of this example is available at Binder: binder

Web service

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 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.

All that is needed to set up the most basic web server in Flask is a single file. We adapt the standard Flask example to accept a sound file, access Parselmouth’s 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 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.

[1]:
%%writefile server.py

from flask import Flask, request, jsonify
import tempfile

app = Flask(__name__)

@app.route('/pitch_track', methods=['POST'])
def pitch_track():
    import parselmouth

    # Save the file that was sent, and read it into a parselmouth.Sound
    with tempfile.NamedTemporaryFile() as tmp:
        tmp.write(request.files['audio'].read())
        sound = parselmouth.Sound(tmp.name)

    # Calculate the pitch track with Parselmouth
    pitch_track = sound.to_pitch().selected_array['frequency']

    # Convert the NumPy array into a list, then encode as JSON to send back
    return jsonify(list(pitch_track))
Writing server.py

Normally, we can then run the server typing FLASK_APP=server.py flask run on the command line, as explained in the Flask documentation. 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.

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.

[2]:
import os
import subprocess
import sys
import time

# Start a subprocess that runs the Flask server
p = subprocess.Popen([sys.executable, "-m", "flask", "run"], env=dict(**os.environ, FLASK_APP="server.py"), stdout=subprocess.PIPE, stderr=subprocess.PIPE)

# Start two subthreads that forward the output from the Flask server to the output of the Jupyter notebook
def forward(i, o):
    while p.poll() is None:
        l = i.readline().decode('utf-8')
        if l:
            o.write("[SERVER] " + l)

import threading
threading.Thread(target=forward, args=(p.stdout, sys.stdout)).start()
threading.Thread(target=forward, args=(p.stderr, sys.stderr)).start()

# Let's give the server a bit of time to make sure it has started
time.sleep(2)
[SERVER]  * Serving Flask app "server.py"
[SERVER]  * Environment: production
[SERVER]    WARNING: This is a development server. Do not use it in a production deployment.
[SERVER]    Use a production WSGI server instead.
[SERVER]  * Debug mode: off
[SERVER]  * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

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, extracted from a Wikipedia Commons audio file.

[3]:
from IPython.display import Audio
Audio(filename="audio/the_north_wind_and_the_sun.wav")
[3]:

To do so, we use the requests library in this example, but we could use any library to send a standard HTTP request.

[4]:
import requests
import json

# Load the file to send
files = {'audio': open("audio/the_north_wind_and_the_sun.wav", 'rb')}
# Send the HTTP request and get the reply
reply = requests.post("http://127.0.0.1:5000/pitch_track", files=files)
# Extract the text from the reply and decode the JSON into a list
pitch_track = json.loads(reply.text)
print(pitch_track)
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 245.46350823831477, 228.46732333120045, 220.229881904913, 217.9494117767135, 212.32120094882643, 208.42371077564596, 213.3210292245136, 219.22164169979897, 225.08564349338334, 232.58018420251648, 243.6102854675347, 267.9586673940531, 283.57192373203253, 293.09087794771966, 303.9716558501677, 314.16812500255537, 320.11744147538917, 326.34395013825196, 333.3632387299925, 340.0277922275489, 345.8240749033839, 348.57743419008335, 346.9665344057159, 346.53179321965666, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 445.1355539937184, 442.99367847432956, 0.0, 0.0, 0.0, 0.0, 0.0, 236.3912949256524, 233.77304383699934, 231.61759183978316, 229.252937317608, 226.5388725505901, 223.6713912521482, 217.56247158178041, 208.75233223541412, 208.36854272051312, 205.1132684638252, 202.99628328370704, 200.74245529822406, 198.379243723561, 195.71387722456126, 192.92640662381228, 189.55087006373063, 186.29856999154498, 182.60612897184708, 178.0172095327713, 171.7286500573546, 164.43397092360505, 163.15047735066148, 190.94898597265222, 180.11404296436555, 177.42215658133307, 176.85852955755865, 175.90234348007218, 172.72381274834703, 165.07291074214982, 170.84308758689093, 173.84326581969435, 175.39817924857263, 174.73813404735137, 171.30666910901442, 167.57344824865035, 165.26925804867895, 164.0488248694515, 163.3665771538607, 162.9182321154844, 164.4049979046003, 164.16734205916592, 160.17875848111373, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 163.57343758482958, 160.63654708070163, 150.27906547408838, 143.6142724404569, 139.70737167424176, 138.15535972924215, 137.401926952887, 137.45520345586323, 136.78723483908712, 135.18334597312617, 132.3066180187801, 136.04747210818914, 138.65745092917942, 139.1335736781387, 140.238485464634, 141.83711308294014, 143.10991285599226, 144.40501561368708, 146.07295382762607, 147.47513524525806, 148.1692013818143, 149.54122031709116, 151.0336292203337]
[SERVER] 127.0.0.1 - - [08/Mar/2021 19:23:31] "POST /pitch_track HTTP/1.1" 200 -

Since we used the standard json library from Python to decode the reply from server, pitch_track is now a normal list of floats and we can for example plot the estimated pitch track:

[5]:
import matplotlib.pyplot as plt
import seaborn as sns
[6]:
sns.set() # Use seaborn's default style to make attractive graphs
plt.rcParams['figure.dpi'] = 100 # Show nicely large images in this notebook
[7]:
plt.figure()
plt.plot([float('nan') if x == 0.0 else x for x in pitch_track], '.')
plt.show()
../_images/examples_web_service_14_0.png

Refer to the examples on plotting for more details on using Parselmouth for plotting.

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:

<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" src="plotly.min.js"></script>
    <script type="text/javascript">
    var update_plot = function() {
        var audio = document.getElementById("audio").files[0];
        var formData = new FormData();
        formData.append("audio", audio);

        $.getJSON({url: "http://127.0.0.1:5000/pitch_track", method: "POST",
                   data: formData, processData: false, contentType: false,
                   success: function(data){
                       Plotly.newPlot("plot", [{ x: [...Array(data.length).keys()],
                                                 y: data.map(function(x) { return x == 0.0 ? undefined : x; }),
                                                 type: "lines" }]);}});
    };
    </script>
</head>
<body>
<form onsubmit="update_plot(); return false;">
    <input type="file" name="audio" id="audio" />
    <input type="submit" value="Get pitch track" />
    <div id="plot" style="width:1000px;height:600px;"></div>
</form>
</body>

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, and we refer to its documentation for further details.

[8]:
# Let's shut down the server
p.kill()
[9]:
# Cleaning up the file that was written to disk
!rm server.py