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:
- You have a Rev AI account and access token. If not, sign up for a free account and generate an access token .
- You have Docker installed. If not, download and install Docker for your operating system.
- You have Docker Compose installed. If not, install Docker Compose for your operating system.
-
You will deploy the application's webhook at a public URL. If not, or if you prefer to develop and test locally,
download and install
ngrok
and obtain anngrok
authentication token . You will need this to generate a temporary public URL for the webhook.
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:
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:
{
"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:
-
At job submission time, include a webhook URL in the
notification_config
parameter of the job request. - 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:
-
Update the
config/settings.php
file with an additional configuration key for the webhook URL.<?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 theconfig/settings.php
file with that temporary URL.
-
If you are deploying the application at an existing public URL, replace the
-
Update the front controller at
public\index.php
with the following changes:-
Update the POST route handler for the
/add
URL endpoint to include thenotification_config
parameter in the job request.<?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.<?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
toJOB_TRANSCRIPTION_SUCCESS
. It then uses the Guzzle Rev AI API client to prepare and send a HTTP GET request tohttps://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'sdata
field. -
If the job was unsuccessful, the handler updates the document
status
toJOB_TRANSCRIPTION_FAILURE
. In this case, the JSON document also includes a problem description, which is saved to the document'serror
field in the MongoDB database. -
On completion of the above actions, the handler returns a
200
response code to the Rev AI API server.
-
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:
-
Update the POST route handler for the
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.
-
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 atpublic/index.php
.<?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. -
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.{% 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'sstatus
field, which could be any one ofJOB_RECORDED
,JOB_UPLOADED
,JOB_TRANSCRIPTION_IN_PROGRESS
,JOB_TRANSCRIPTION_SUCCESS
orJOB_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:
-
Update the index page template at
views/index.twig
to include a simple search form, as below:{% 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.
-
Update the GET route handler for the
/index
endpoint in the front controller scriptpublic/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.<?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:
-
Update the index page template at
views/index.twig
to support an additional "Delete" button for each displayed transcript and a newdeleted
status message, as below.{% 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. -
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 scriptpublic/index.php
.<?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.
<?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.
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".
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:
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:
In case the transcript could not be generated due to an error, this too should be visible in the list:
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:
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:
- Documentation: Asynchronous Speech-To-Text API job submission , transcript retrieval and job deletion
- Code samples: Asynchronous Speech-To-Text API
- Tutorial: Get Started with Rev AI API Webhooks
- Tutorial: Use Webhooks to Trigger Job Email Notifications with Node.js, SendGrid and Express
- Tutorial: Save Transcripts to MongoDB with a Node.js Webhook
- Tutorial: Delete User Files from Rev AI
- Tutorial: Asynchronous Speech-To-Text API best practices
- Documentation: Slim framework
- Documentation: Guzzle PHP HTTP client
- Documentation: MongoDB PHP driver