Build a Speech-to-Text Web Application with Rev AI and PHP (Part 2)

By Vikram Vaswani, Developer Advocate - Sep 08, 2022

Introduction

Rev AI's automatic speech recognition (ASR) APIs enable developers to integrate fast and accurate speech-to-text capabilities into their applications. The first part of this tutorial introduced you to Rev AI's Asynchronous Speech-to-Text API. It explained how to record audio through a Web browser and submit the audio to Rev AI for transcription via the API using the Guzzle PHP HTTP client.

At the end of the first part of this tutorial, the example application was able to submit audio to Rev AI, but you still had to use the Rev AI dashboard to manually retrieve your transcripts. This concluding segment closes the loop, explaining how to retrieve transcripts from Rev AI using a webhook and display them in the example application. It will also explain how to add transcript deletion and search functions to the Web application.

attention

The complete source code for the example application is available on GitHub, so you can download and try it immediately.

Assumptions

This tutorial assumes that:

Any application that uses the Rev AI APIs must also comply with Rev AI's API limits and terms of service. Before proceeding, please review these documents and ensure that you are in agreement with them.

attention

This tutorial uses a Docker-based Apache/PHP/MongoDB development environment. If you already have a properly-configured development environment with Apache 2.x, PHP 8.1.x with the MongoDB extension and Composer, you can use that instead. You may need to replace some Docker commands with equivalents.

Step 1: Receive transcripts from Rev AI using a webhook

Once a job is submitted to Rev AI for asynchronous transcription via the API, there are three ways to know when transcription is complete. You can check job status via the Rev AI Web dashboard; you can repeatedly poll the API for status; or you can let Rev AI notify you automatically via a webhook. Of these three options, using a webhook is the most efficient approach.

attention

If you're not familiar with webhooks, refer to our tutorial on getting started with Rev AI webhooks for more information. You can also learn how to use Rev AI webhooks to automatically send email notifications when a job completes, or how to retrieve final transcripts via the API and save them to a MongoDB database.

To let Rev AI know that it should use a webhook, including the notification_config parameter in the request to the Asynchronous Speech-to-Text API endpoint at https://api.rev.ai/speechtotext/v1/jobs, as shown below:

Copy
Copied
curl -X POST "https://api.rev.ai/speechtotext/v1/jobs" \
     -H "Authorization: Bearer <REVAI_ACCESS_TOKEN>" \
     -H "Content-Type: application/json" \
     -d '{
       "source_config": {"url": "https://www.rev.ai/FTC_Sample_1.mp3"},
       "notification_config": {"url": "https://example.com/my/webhook"}
      }'

Once the job is complete, the API will make an HTTP POST request to the specified URL with a JSON document in the POST request body. Here is an example of the POST request body:

Copy
Copied
{
  "job": {
    "id": "8xBckmk6rAqu",
    "created_on": "2022-08-16T14:26:14.151Z",
    "completed_on": "2022-08-16T14:26:53.601Z",
    "name": "FTC_Sample_1.mp3",
    "notification_config": {"url": "https://example.com/my/webhook"},
    "source_config": {"url": "https://www.rev.ai/FTC_Sample_1.mp3"},
    "status": "transcribed",
    "duration_seconds": 107,
    "type": "async",
    "language": "en"
  }
}

From the above discussion, it should be clear that there are two steps involved when integrating a Rev AI webhook into an application:

  1. At job submission time, include a webhook URL in the notification_config parameter of the job request.
  2. Define a webhook URL handler that is able to receive the HTTP POST request, parse the request body and take further action (such as retrieving the transcript) based on the received data.
attention

In order to receive notifications from Rev AI, the webhook URL must be a publicly-accessible URL. When developing and testing locally, this may not always be possible. For such situations, use ngrok to create a temporary public URL that will serve as the webhook URL.

Make the changes as described below:

  1. Update the config/settings.php file with an additional configuration key for the webhook URL.
    Copy
    Copied
    <?php
    return [
        'rev' => [
            'token' => '<REVAI_ACCESS_TOKEN>',
            'callback' => '<CALLBACK_PREFIX>/hook',
        ],
        'mongo' => [
            'uri' => '<MONGODB_URI>'
        ]
    ];
    • If you are deploying the application at an existing public URL, replace the <CALLBACK_PREFIX> placeholder with the application URL.
    • If you are developing and testing locally without a publicly-accessible URL, first create and obtain a temporary webhook URL with ngrok and then replace the <CALLBACK_PREFIX> placeholder in the config/settings.php file with that temporary URL.
  2. Update the front controller at public\index.php with the following changes:
    • Update the POST route handler for the /add URL endpoint to include the notification_config parameter in the job request.
      Copy
      Copied
      <?php
      
      // ...
      
      // POST request handler for /add page
      $app->post(
        '/add',
        function (Request $request, Response $response) {
            // get MongoDB service
            // insert a record in the database for the audio upload
            // get MongoDB document ID
            $mongoClient = $this->get('mongo');
            try {
                $insertResult = $mongoClient->mydb->notes->insertOne(
                    [
                        'status' => 'JOB_RECORDED',
                        'ts'     => time(),
                        'jid'    => false,
                        'error'  => false,
                        'data'   => false,
                    ]
                );
                $id           = (string) $insertResult->getInsertedId();
                // get uploaded file
                // if no upload errors, change status in database record
                $uploadedFiles = $request->getUploadedFiles();
                $uploadedFile = $uploadedFiles['file'];
                if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
                    $mongoClient->mydb->notes->updateOne(
                        [
                            '_id' => new ObjectID($id),
                        ],
                        [
                            '$set' => ['status' => 'JOB_UPLOADED'],
                        ]
                    );
                    // get Rev AI API client
                    // submit audio to API as POST request
                    $revClient   = $this->get('guzzle');
                    $revResponse = $revClient->request(
                        'POST',
                        'jobs',
                        [
                            'multipart' => [
                                [
                                    'name'     => 'media',
                                    'contents' => fopen($uploadedFile->getFilePath(), 'r'),
                                ],
                                [
                                    'name'     => 'options',
                                    'contents' => json_encode(
                                        [
                                            'metadata'         => $id,
                                            'skip_diarization' => 'true',
                                            'notification_config'     => [
                                                'url' => $this->get('settings')['rev']['callback']
                                            ],
                                        ]
                                    ),
                                ],
                            ],
                        ]
                    )->getBody()->getContents();
                    // get API response
                    // if no API error, update status in database record
                    // send 200 response code to client
                    $json        = json_decode($revResponse);
                    $mongoClient->mydb->notes->updateOne(
                        [
                            '_id' => new ObjectID($id),
                        ],
                        [
                            '$set' => [
                                'status' => 'JOB_TRANSCRIPTION_IN_PROGRESS',
                                'jid'    => $json->id,
                            ],
                        ]
                    );
                    $response->getBody()->write(json_encode(['success' => true]));
                    return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
                }
            } catch (\GuzzleHttp\Exception\RequestException $e) {
                // in case of API error
                // update status in database record
                // send error code to client with error message as payload
                $mongoClient->mydb->notes->updateOne(
                    [
                        '_id' => new ObjectID($id),
                    ],
                    [
                        '$set' => [
                            'status' => 'JOB_TRANSCRIPTION_FAILURE',
                            'error'  => $e->getMessage(),
                        ],
                    ]
                );
                $response->getBody()->write(json_encode(['success' => false]));
                return $response->withHeader('Content-Type', 'application/json')->withStatus($e->getResponse()->getStatusCode());
            }
        }
      );
      
      // ...
    • Add a new handler for POST requests to the /hook webhook URL endpoint, which accepts and processes the POST request received from the Rev AI API.
      Copy
      Copied
      <?php
      
      // ...
      
      // POST request handler for /hook webhook URL
      $app->post(
        '/hook',
        function (Request $request, Response $response) {
            try {
                // get MongoDB service
                $mongoClient = $this->get('mongo');
      
                // decode JSON request body
                // obtain identifiers and status
                $json        = json_decode($request->getBody());
                $jid         = $json->job->id;
                $id          = $json->job->metadata;
      
                // if job successful
                if ($json->job->status === 'transcribed') {
      
                    // update status in database
                    $mongoClient->mydb->notes->updateOne(
                        [
                            '_id' => new ObjectID($id),
                        ],
                        [
                            '$set' => ['status' => 'JOB_TRANSCRIPTION_SUCCESS'],
                        ]
                    );
      
                    // get transcript from API
                    $revClient   = $this->get('guzzle');
                    $revResponse = $revClient->request(
                        'GET',
                        "jobs/$jid/transcript",
                        [
                            'headers' => ['Accept' => 'text/plain'],
                        ]
                    )->getBody()->getContents();
                    $transcript  = explode('    ', $revResponse)[2];
      
                    // save transcript to database
                    $mongoClient->mydb->notes->updateOne(
                        [
                            '_id' => new ObjectID($id),
                        ],
                        [
                            '$set' => ['data' => $transcript],
                        ]
                    );
                // if job unsuccesful
                } else {
      
                    // update status in database
                    // save problem detail error message
                    $mongoClient->mydb->notes->updateOne(
                        [
                            '_id' => new ObjectID($id),
                        ],
                        [
                            '$set' => [
                                'status' => 'JOB_TRANSCRIPTION_FAILURE',
                                'error'  => $json->job->failure_detail,
                            ],
                        ]
                    );
                }
            } catch (\GuzzleHttp\Exception\RequestException $e) {
                $mongoClient->mydb->notes->updateOne(
                    [
                      '_id' => new ObjectID($id),
                    ],
                    [
                      '$set' => [
                          'status' => 'JOB_TRANSCRIPTION_FAILURE',
                          'error'  => $e->getMessage(),
                      ],
                    ]
                );
            }
            return $response->withStatus(200);
        }
      );
      
      // ...

      This route handler contains a lot of code, so let's step through it:

      • When this endpoint is invoked with a HTTP POST request, the handler first inspects the JSON document received and extracts three key pieces of information from it:
        • The Rev AI job identifier, which will be used to retrieve the final transcript;
        • The MongoDB document identifier, which serves to identify the corresponding record in the application database. Recollect that this MongoDB document identifier was included in the metadata parameter when the job was originally submitted;
        • The Rev AI job status, which indicates whether transcription succeeded or failed.
      • If the job was successful, the handler updates the document status to JOB_TRANSCRIPTION_SUCCESS . It then uses the Guzzle Rev AI API client to prepare and send a HTTP GET request to https://api.rev.ai/speechtotext/v1/jobs/<ID>/transcript to retrieve the final transcript in plaintext format. The MongoDB document is then updated, with the transcript content saved to the document's data field.
      • If the job was unsuccessful, the handler updates the document status to JOB_TRANSCRIPTION_FAILURE . In this case, the JSON document also includes a problem description, which is saved to the document's error field in the MongoDB database.
      • On completion of the above actions, the handler returns a 200 response code to the Rev AI API server.

Step 2: List transcripts in the application

Once the webhook is in place, retrieving and saving transcripts to the MongoDB database, the next step is to display them in the application user interface.

  1. Update the GET route handler for the /index endpoint to execute a MongoDB query and return all the records in the application database. Make this change in the front controller at public/index.php .
    Copy
    Copied
    <?php
    // ...
    
    // GET request handler for index page
    $app->get(
        '/[index[/]]',
        function (Request $request, Response $response, $args) {
            $params = $request->getQueryParams();
            $mongoClient = $this->get('mongo');
            return $this->get('view')->render(
                $response,
                'index.twig',
                [
                    'status' => !empty($params['status']) ? $params['status'] : null,
                    'data'   => $mongoClient->mydb->notes->find(
                        [],
                        [
                            'sort' => [
                                'ts' => -1,
                            ],
                        ]
                    )
                ]
            );
        }
    )->setName('index');
    
    // ...

    The MongoDB result set is returned via the data Twig template variable.

  2. Update the public/index.twig page template to loop over the result set and display the MongoDB result set as an HTML table, including transcript content, timestamp, status and Rev AI job identifier. Add a "Refresh" button to the page as part of this update, which provides a way for the user to reload the page.
    Copy
    Copied
    {% extends "layout.twig" %}
    
    {% block content %}
      <header class="d-flex justify-content-center py-3">
        <h1>My Notes</h1>
      </header>
    
      {% if status == 'submitted' %}
      <div class="alert alert-success text-center" role="alert">Audio sent for transcription.</div>
      {% endif %}
    
      {% if status == 'error' %}
      <div class="alert alert-danger text-center" role="alert">Audio transcription failed.</div>
      {% endif %}
    
      <table class="table">
        <thead class="thead-light">
          <tr>
            <th scope="col">#</th>
            <th scope="col">Date</th>
            <th scope="col">Contents</th>
            <th scope="col">Status</th>
            <th scope="col">Rev AI Job ID</th>
            <th scope="col"><a class="btn btn-primary" href="{{ url_for('index') }}" role="button">Refresh</a></th>
          </tr>
        </thead>
        <tbody>
      {% for item in data %}
        <tr>
          <td>{{ loop.index }}</td>
          <td>{{ item.ts|date("d M Y h:i e") }}</td>
          <td class="text-wrap" style="max-width: 150px;">{{ item.data }}</td>
        {% if item.status in ['JOB_UPLOADED', 'JOB_TRANSCRIPTION_IN_PROGRESS'] %}
          <td>In progress</td>
        {% elseif item.status == 'JOB_RECORDED' %}
          <td>Recorded</td>
        {% elseif item.status == 'JOB_TRANSCRIPTION_SUCCESS' %}
          <td>Transcribed</td>
        {% elseif item.status == 'JOB_TRANSCRIPTION_FAILURE' %}
          <td class="text-wrap" style="max-width: 150px;">Failed <br/> {{ item.error }}</td>
        {% endif %}
          <td>{{ item.jid }}</td>
          <td></td>
        </tr>
      {% endfor %}
        </tbody>
      </table>
    {% endblock %}

    This template loops over the data template variable and, for each record, displays the formatted date and time, transcript data, job status and Rev AI job identifier. Note that the template inspects the document's status field, which could be any one of JOB_RECORDED, JOB_UPLOADED, JOB_TRANSCRIPTION_IN_PROGRESS, JOB_TRANSCRIPTION_SUCCESS or JOB_TRANSCRIPTION_FAILURE, and displays a human-readable message for each, including an error message for failed jobs.

Step 3: Search for transcripts in the application

Now that you have the basics in place, it's time to add further functionality to the application. For example, users should be able to quickly search the generated transcripts and find those matching specific terms.

This can be achieved by using a MongoDB regular expression to filter the stored transcripts and return the matching ones, as described below:

  1. Update the index page template at views/index.twig to include a simple search form, as below:
    Copy
    Copied
    {% extends "layout.twig" %}
    
    {% block content %}
    
      <!-- ... -->
    
      <div class="col-sm-4">
        <form class="form-inline">
          <div class="input-group">
            <input class="form-control" name="term" value="{{ term }}" placeholder="Search">
            <div class="input-group-append">
              <button class="btn btn-outline-secondary" type="submit">Go</button>
            </div>
          </div>
        </form>
      </div>
    
      <!-- ... -->
    
    {% endblock %}

    When this search form is submitted, the search term entered by the user is added to the URL as a query parameter.

  2. Update the GET route handler for the /index endpoint in the front controller script public/index.php to read the submitted search term and modify the default MongoDB query with an additional regular expression filter. This change ensures that only results matching the search term are returned and displayed in the index page template.
    Copy
    Copied
    <?php
    // ...
    
    // GET request handler for index page
    $app->get(
        '/[index[/]]',
        function (Request $request, Response $response, $args) {
            $params = $request->getQueryParams();
            $condition = !empty($params['term']) ?
                [
                    'data' => new MongoDB\BSON\Regex(filter_var($params['term'], FILTER_UNSAFE_RAW), 'i')
                ] :
                [];
            $mongoClient = $this->get('mongo');
            return $this->get('view')->render(
                $response,
                'index.twig',
                [
                    'status' => !empty($params['status']) ? $params['status'] : null,
                    'data'   => $mongoClient->mydb->notes->find(
                        $condition,
                        [
                            'sort' => [
                                'ts' => -1,
                            ],
                        ]
                    ),
                    'term' => !empty($params['term']) ? $params['term'] : null,
                ]
            );
        }
    )->setName('index');
    
    // ...

    The route handler uses the request object's getQueryParams() method to retrieve the search term and then adds a regular expression condition to the default MongoDB search query. This condition ensures that only transcripts containing the regular expression are returned by the query. The result set is then interpolated into the index page template for display as usual.

Step 4: Delete transcripts from the application

Users should also have the option to delete voice notes which are no longer relevant through the Web application interface.

Implement this functionality as below:

  1. Update the index page template at views/index.twig to support an additional "Delete" button for each displayed transcript and a new deleted status message, as below.
    Copy
    Copied
    {% extends "layout.twig" %}
    
    {% block content %}
    
      <!-- ... -->
    
      {% if status == 'deleted' %}
      <div class="alert alert-success text-center" role="alert">Note deleted.</div>
      {% endif %}
    
      <!-- ... -->
    
      <table class="table">
    
        <!-- ... -->
    
        <tbody>
      {% for item in data %}
        <tr>
          <td>{{ loop.index }}</td>
          <td>{{ item.ts|date("d M Y h:i e") }}</td>
          <td class="text-wrap" style="max-width: 150px;">{{ item.data }}</td>
        {% if item.status in ['JOB_UPLOADED', 'JOB_TRANSCRIPTION_IN_PROGRESS'] %}
          <td>In progress</td>
        {% elseif item.status == 'JOB_RECORDED' %}
          <td>Recorded</td>
        {% elseif item.status == 'JOB_TRANSCRIPTION_SUCCESS' %}
          <td>Transcribed</td>
        {% elseif item.status == 'JOB_TRANSCRIPTION_FAILURE' %}
          <td class="text-wrap" style="max-width: 150px;">Failed <br/> {{ item.error }}</td>
        {% endif %}
          <td>{{ item.jid }}</td>
          <td><a class="btn btn-danger" href="{{ url_for('delete', { 'id': item._id }) }}" role="button">Delete</a></td>
        </tr>
      {% endfor %}
        </tbody>
      </table>
    {% endblock %}

    Note that the generated hyperlink for each "Delete" button will point to a route named delete and will include the MongoDB document identifier as a URL parameter.

  2. Create a GET route handler for the new /delete endpoint which accepts a MongoDB document identifier as a URL parameter. Add this route to the front controller script public/index.php .
    Copy
    Copied
    <?php
    
    // ...
    
    // GET request handler for /delete page
    $app->get(
        '/delete/{id}',
        function (Request $request, Response $response, $args) use ($app) {
            $id          = filter_var($args['id'], FILTER_UNSAFE_RAW);
            $mongoClient = $this->get('mongo');
            $mongoClient->mydb->notes->deleteOne(
                [
                    '_id' => new ObjectID($id),
                ]
            );
            $routeParser = $app->getRouteCollector()->getRouteParser();
            return $response->withHeader('Location', $routeParser->urlFor('index', [], ['status' => 'deleted']))->withStatus(302);
        }
    )->setName('delete');
    
    // ...

    This route handler reads the MongoDB document identifier, uses the MongoDB client's deleteOne method to delete the corresponding transcript from the application database and then redirects the user back to the /index URL with a success notification.

attention

Deleting a voice note as described in this section removes it from the application database, but does not delete it from Rev AI's servers. To do this, you must either submit a separate DELETE request via the API or include the delete_after_seconds parameter in the initial job request. To learn more, refer to the tutorial on deleting user data from Rev AI.

For reference, here is the final front controller script. Replace the public\index.php file with this version.

Copy
Copied
<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
use Slim\Routing\RouteContext;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7;
use DI\ContainerBuilder;
use MongoDB\BSON\ObjectID;

// load dependencies
require __DIR__ . '/../vendor/autoload.php';

// create DI container
$containerBuilder = new ContainerBuilder();

// define services
$containerBuilder->addDefinitions(
    [
        'settings' => function () {
            return include __DIR__ . '/../config/settings.php';
        },
        'view'     => function () {
            return Twig::create(__DIR__ . '/../views');
        },
        'mongo'    => function ($c) {
            return new MongoDB\Client($c->get('settings')['mongo']['uri']);
        },
        'guzzle'   => function ($c) {
            $token = $c->get('settings')['rev']['token'];
            return new Client(
                [
                    'base_uri' => 'https://api.rev.ai/speechtotext/v1/jobs',
                    'headers'  => ['Authorization' => "Bearer $token"],
                ]
            );
        },
    ]
);

$container = $containerBuilder->build();

AppFactory::setContainer($container);

// create application with DI container
$app = AppFactory::create();

// add Twig middleware
$app->add(TwigMiddleware::createFromContainer($app));

// add error handling middleware
$app->addErrorMiddleware(true, true, true);

// GET request handler for index page
$app->get(
  '/[index[/]]',
  function (Request $request, Response $response, $args) {
      $params = $request->getQueryParams();
      $condition = !empty($params['term']) ?
          [
              'data' => new MongoDB\BSON\Regex(filter_var($params['term'], FILTER_UNSAFE_RAW), 'i')
          ] :
          [];
      $mongoClient = $this->get('mongo');
      return $this->get('view')->render(
          $response,
          'index.twig',
          [
              'status' => !empty($params['status']) ? $params['status'] : null,
              'data'   => $mongoClient->mydb->notes->find(
                  $condition,
                  [
                      'sort' => [
                          'ts' => -1,
                      ],
                  ]
              ),
              'term' => !empty($params['term']) ? $params['term'] : null,
          ]
      );
  }
)->setName('index');

// GET request handler for /add page
$app->get(
    '/add',
    function (Request $request, Response $response, $args) {
        return $this->get('view')->render(
            $response,
            'add.twig',
            []
        );
    }
)->setName('add');

// POST request handler for /add page
$app->post(
    '/add',
    function (Request $request, Response $response) {
        // get MongoDB service
        // insert a record in the database for the audio upload
        // get MongoDB document ID
        $mongoClient = $this->get('mongo');
        try {
            $insertResult = $mongoClient->mydb->notes->insertOne(
                [
                    'status' => 'JOB_RECORDED',
                    'ts'     => time(),
                    'jid'    => false,
                    'error'  => false,
                    'data'   => false,
                ]
            );
            $id           = (string) $insertResult->getInsertedId();

            // get uploaded file
            // if no upload errors, change status in database record
            $uploadedFiles = $request->getUploadedFiles();
            $uploadedFile = $uploadedFiles['file'];

            if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
                $mongoClient->mydb->notes->updateOne(
                    [
                        '_id' => new ObjectID($id),
                    ],
                    [
                        '$set' => ['status' => 'JOB_UPLOADED'],
                    ]
                );

                // get Rev AI API client
                // submit audio to API as POST request
                $revClient   = $this->get('guzzle');
                $revResponse = $revClient->request(
                    'POST',
                    'jobs',
                    [
                        'multipart' => [
                            [
                                'name'     => 'media',
                                'contents' => fopen($uploadedFile->getFilePath(), 'r'),
                            ],
                            [
                                'name'     => 'options',
                                'contents' => json_encode(
                                    [
                                        'metadata'         => $id,
                                        'notification_config'     => [
                                            'url' => $this->get('settings')['rev']['callback']
                                        ],
                                        'skip_diarization' => 'true',
                                    ]
                                ),
                            ],
                        ],
                    ]
                )->getBody()->getContents();

                // get API response
                // if no API error, update status in database record
                // send 200 response code to client
                $json        = json_decode($revResponse);
                $mongoClient->mydb->notes->updateOne(
                    [
                        '_id' => new ObjectID($id),
                    ],
                    [
                        '$set' => [
                            'status' => 'JOB_TRANSCRIPTION_IN_PROGRESS',
                            'jid'    => $json->id,
                        ],
                    ]
                );
                $response->getBody()->write(json_encode(['success' => true]));
                return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
            }
        } catch (\GuzzleHttp\Exception\RequestException $e) {
            // in case of API error
            // update status in database record
            // send error code to client with error message as payload
            $mongoClient->mydb->notes->updateOne(
                [
                    '_id' => new ObjectID($id),
                ],
                [
                    '$set' => [
                        'status' => 'JOB_TRANSCRIPTION_FAILURE',
                        'error'  => $e->getMessage(),
                    ],
                ]
            );
            $response->getBody()->write(json_encode(['success' => false]));
            return $response->withHeader('Content-Type', 'application/json')->withStatus($e->getResponse()->getStatusCode());
        }
    }
);

// GET request handler for /delete page
$app->get(
    '/delete/{id}',
    function (Request $request, Response $response, $args) use ($app) {
        $id          = filter_var($args['id'], FILTER_UNSAFE_RAW);
        $mongoClient = $this->get('mongo');
        $mongoClient->mydb->notes->deleteOne(
            [
                '_id' => new ObjectID($id),
            ]
        );
        $routeParser = $app->getRouteCollector()->getRouteParser();
        return $response->withHeader('Location', $routeParser->urlFor('index', [], ['status' => 'deleted']))->withStatus(302);
    }
)->setName('delete');

// POST request handler for /hook webhook URL
$app->post(
    '/hook',
    function (Request $request, Response $response) {
        try {
            // get MongoDB service
            $mongoClient = $this->get('mongo');

            // decode JSON request body
            // obtain identifiers and status
            $json        = json_decode($request->getBody());
            $jid         = $json->job->id;
            $id          = $json->job->metadata;

            // if job successful
            if ($json->job->status === 'transcribed') {

                // update status in database
                $mongoClient->mydb->notes->updateOne(
                    [
                        '_id' => new ObjectID($id),
                    ],
                    [
                        '$set' => ['status' => 'JOB_TRANSCRIPTION_SUCCESS'],
                    ]
                );

                // get transcript from API
                $revClient   = $this->get('guzzle');
                $revResponse = $revClient->request(
                    'GET',
                    "jobs/$jid/transcript",
                    [
                        'headers' => ['Accept' => 'text/plain'],
                    ]
                )->getBody()->getContents();
                $transcript  = explode('    ', $revResponse)[2];

                // save transcript to database
                $mongoClient->mydb->notes->updateOne(
                    [
                        '_id' => new ObjectID($id),
                    ],
                    [
                        '$set' => ['data' => $transcript],
                    ]
                );
            // if job unsuccesful
            } else {

                // update status in database
                // save problem detail error message
                $mongoClient->mydb->notes->updateOne(
                    [
                        '_id' => new ObjectID($id),
                    ],
                    [
                        '$set' => [
                            'status' => 'JOB_TRANSCRIPTION_FAILURE',
                            'error'  => $json->job->failure_detail,
                        ],
                    ]
                );
            }
        } catch (\GuzzleHttp\Exception\RequestException $e) {
            $mongoClient->mydb->notes->updateOne(
                [
                  '_id' => new ObjectID($id),
                ],
                [
                  '$set' => [
                      'status' => 'JOB_TRANSCRIPTION_FAILURE',
                      'error'  => $e->getMessage(),
                  ],
                ]
            );
        }
        return $response->withStatus(200);
    }
);

$app->run();

Step 5: Test the example application

Test the example application by browsing to http://<DOCKER_HOST>. You should see the page below.

List page

Click the "Add" button in the top right corner. You will be redirected to a new page and the browser will prompt for access to the system microphone. Grant this permission, then click the "Start recording" button. Speak and click "Stop recording" once done. Your audio will be uploaded and you should be redirected back to the index page, where you should see your voice note listed with status "In progress".

List page with job in progress

When the transcript is ready, Rev AI will notify the webhook URL. If you are using a public URL, you will be able to see this activity in the Web server logs. Alternatively, if you are using ngrok, you will be able to see this activity in the ngrok dashboard, as shown below:

ngrok monitor

When you see the incoming webhook request in the Web server logs or ngrok monitor, or after approximately 1 minute if you do not have access to monitoring, click the "Refresh" button. The list of voice notes should now reflect the updated status, together with the transcript, as shown below:

List page with successful job

In case the transcript could not be generated due to an error, this too should be visible in the list:

List page with failed job

Enter a search term in the search box and click the "Go" button. The list of voice notes should now be filtered to only display those containing your search term, as shown below:

List page with search results

Click the "Delete" button next to a voice note. The note will be deleted from the database and will vanish from the list of available notes.

Next steps

In this concluding segment, you learnt how to retrieve the final transcript from Rev AI using a webhook and display it in the Web application. You also improved the Web application with basic search and deletion features.

With this, the example speech-to-text Web application is complete. Audio is received and recorded through the browser and transcripts are generated, returned and integrated into the application with Rev AI and PHP.

Learn more about developing speech-to-text applications with Rev AI and PHP by visiting the following links: