使用 Laravel 和 Nexmo 进行语音转文字通话

发布日期:作者:

Text-To-Speech calls with Laravel and Nexmo image

这是由 Michael Heap 撰写的一系列教程的第二部分,涵盖了如何使用 Laravel 构建多渠道帮助台系统。

在上一篇文章中,我们介绍了 使用 Nexmo 发送和接收短信 Laravel 通知,我建议您在完成本篇文章的任务之前完成上一篇文章中的任务。或者,您可以查看 Github 上的预构建版本

今天,我们将更进一步,添加在每次向客户的工单添加新回复时,向客户进行语音转文字通话的功能。

先决条件

要完成本篇文章,您需要一个 Nexmo 帐户和已安装并配置的 Nexmo 命令行工具,以及您通常的 Laravel 先决条件。您还需要本系列教程第一部分中的 Deskmo 应用程序。

在开始之前,您需要再次将本地服务器暴露到互联网。在一个终端中运行 php artisan serve,在另一个终端中运行 ngrok http 8000。如果您使用的是自动生成的 ngrok URL,则需要在 Nexmo 仪表板 上更新您的短信 webhook URL。

选择我们的通知方法

首先要做的是使工单回复的通知机制可配置。我们希望帮助台代理能够在添加回复时选择反馈方法,而不是自动发送短信。

打开 resources/views/ticket/create.blade.php,并在 recipientsubmit 表单组之间添加以下 HTML 代码,以添加单选按钮来选择通知方法

<div class="form-group">
<div class="radio">
<label>
<input type="radio" name="notification_method" value="sms">
SMS
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="notification_method" value="voice">
Voice
</label>
</div>
</div>

除了添加 HTML 代码之外,我们还需要更新 TicketController 以使用传入的值。打开 app/Http/Controllers/TicketController.php 并编辑 store 方法。我们需要在该方法顶部的验证器中将 notification_method 标记为必填字段

$data = $request->validate([
'title' => 'required',
'content' => 'required',
'recipient' => 'required|exists:users,id',
'channel' => 'required',
'notification_method' => 'required',
]);

最后,在 store 方法的末尾,找到发送通知的位置,将其用 if 语句包装,并添加当首选通知方法为语音通话时和当提供无效通知方法时的条件。

if ($data['notification_method'] === 'sms') {
Notification::send($ticket->subscribedUsers()->get(), new TicketCreated($entry));
} elseif ($data['notification_method'] === 'voice') {
// Make a voice call here
} else {
throw new \Exception('Invalid notification method provided');
}

代理现在可以选择语音通话作为通知方法,但不会向客户发送通知。我们需要为应用程序添加对语音通话的支持。

在处理语音通话时,Nexmo 将以两种不同的方式与我们的应用程序进行交互 - 当接听电话时,以及当通话状态发生变化时。

接听电话

当接听电话时,Nexmo 会向 answer_url 发起 GET 请求。我们应用程序中的此端点将返回一个 JSON 文档,解释通话期间要执行的操作。此响应被称为 Nexmo 通话控制对象 (NCCO)。

首先,创建一个 WebhookController 来处理此传入请求

php artisan make:controller WebhookController

要让通话向客户说出消息,我们需要使用 talk 操作。当以 JSON 表示时,它采用以下形式

[
{
"action": "talk",
"text": "This is an example talk action"
}
]

让我们采用这种格式并实现一个 answer 方法,该方法将返回一个 NCCO。我们的文本应包含工单条目存在时的消息内容,以及不存在时的错误消息。将以下内容添加到新的 WebhookController

public function answer(TicketEntry $ticket) {
if (!$ticket->exists) {
return response()->json([
[
'action' => 'talk',
'text' => 'Sorry, there has been an error fetching your ticket information'
]
]);
}
 
return response()->json([
[
'action' => 'talk',
'text' => $ticket->content
]
]);
}

我们需要在控制器顶部添加 use App\TicketEntry;,以便它可以在我们的方法中用作类型提示。

除了实现控制器之外,我们还需要告诉我们的应用程序如何路由传入请求。打开 routes/web.php 并将以下路由添加到底部

Route::get('/webhook/answer/{ticket?}', 'WebhookController@answer');

您现在应该能够访问此 webhook 端点,并查看 成功响应错误响应

注意:这将适用于演示目的,但它非常不安全,因为攻击者可以在 URL 中放入任何 ID 来查看工单响应。如果您在应用程序中实现此功能,则需要在应用程序和 Nexmo 之间设置一个共享密钥。

语音通话事件

除了在接听电话时向 answer_url 发起 GET 请求之外,Nexmo 还将通话状态更改发送到 event_url。例如,当以下情况发生时,Nexmo 会向我们的 event_url 发起 POST 请求:

  • 拨打电话
  • 通话接收方接听
  • 通话中所有参与者挂断
  • 通话录音可用

由于这些事件中的信息稍后可能会有用,因此我们将在每次收到事件时在应用程序日志中添加一个条目。

我们需要在 WebhookController 顶部导入 Log 门面

use Log;

接下来,我们需要实现 event 方法。由于我们将接收到的所有内容都作为 上下文信息 编写,因此此方法中只需要两行代码 - 一行是记录数据,另一行是告诉 Nexmo 已收到数据且没有问题

public function event(Request $request) {
Log::info('Call event', $request->all());
return response('', 204);
}

最后,我们需要添加一些路由,以便传入的 Nemxo 请求能够到达此控制器。再次打开 routes/web.php 并添加以下路由

Route::post('/webhook/event', 'WebhookController@event');

由于这是一个 POST 路由,并且数据来自应用程序外部,因此我们需要像上一篇文章中所做的那样打开 app/Middleware/VerifyCsrfToken.php,并将新的事件端点添加到白名单端点列表中。这将防止 Nexmo 从 Laravel 接收 CSRF 错误

protected $except = [
'ticket-entry',
'webhook/event'
];

如果您想尝试一下,请向 http://localhost:8000/webhook/event 发起 POST 请求,并带上您喜欢的任何数据,您将在 storage/logs/laravel.log 中的应用程序日志文件中看到它。

创建 Nexmo 应用程序

现在我们已经创建了 Nexmo 依赖的所有端点,是时候在 Nexmo 上注册我们的应用程序了。为此,我们将使用我们在第一篇文章中安装的 Nexmo CLI 工具。

有一个 app:create 命令,它接受 answer_urlevent_url,并将生成一个私钥,我们使用它来对 Nexmo API 进行身份验证。现在创建一个应用程序,将 ngrok URL 替换为您自己的 URL,方法是在 composer.json 所在的同一目录中运行以下命令

nexmo app:create LaravelNews http://abc123.ngrok.io/webhook/answer http://abc123.ngrok.io/webhook/event --keyfile private.key

创建应用程序后,我们需要将其与 Nexmo 电话号码关联。这样 Nexmo 才能知道在拨打该号码时查询哪个应用程序的 answer_url。我们需要在第一步创建应用程序时返回的应用程序 ID 和我们在第一篇文章中购买的电话号码。有了这些信息后,我们可以使用 Nexmo CLI 将该号码与我们的应用程序关联起来。

nexmo link:app <YOUR_NUMBER> <APPLICATION_ID>

我们已经完成了所有必要的工作,让 Nexmo 能够为我们的应用程序处理语音呼叫。此时,我们可以拨打我们的 Nexmo 号码来收听默认的文本到语音消息。

剩下要做的唯一事情是在添加工单条目时实际向客户发起外呼。

nexmo/laravel

在第一篇博客文章中,我们使用 Composer 安装了 nexmo/client,允许 Laravel 通过通知系统发送 SMS 通知。这个库包含所有与 Nexmo API 交互的逻辑。

我们可以直接使用该库,但 Nexmo 提供了一个 Laravel 服务提供商,它可以为我们简化操作。让我们现在就安装它。

composer require nexmo/laravel

我们之前已经在 .env 文件中填充了 API 密钥和密钥,但在进行语音呼叫时,我们需要使用 JWT 进行身份验证(而不是使用 API 密钥和密钥)。Nexmo PHP 库将负责我们的身份验证,我们只需要提供之前创建的 private.key 的路径和要使用的 application_id 即可。打开 .env 文件并添加以下内容:

NEXMO_APPLICATION_ID=<application_id>
NEXMO_PRIVATE_KEY=./private.key

nexmo/laravel 包通常会发布 config/nexmo.php,它会读取 .env 文件并填充正确的配置值。但是,由于我们在上一篇文章中在 config/services.php 中创建了一个 nexmo 部分,所以在我们的应用程序中不会发生这种情况。

为了添加对语音呼叫的支持,我们需要更新 config/services.php,并在 nexmo 部分添加 private_keyapplication_id

'nexmo' => [
'key' => env('NEXMO_KEY'),
'secret' => env('NEXMO_SECRET'),
'sms_from' => env('NEXMO_NUMBER'),
'private_key' => env('NEXMO_PRIVATE_KEY'),
'application_id' => env('NEXMO_APPLICATION_ID'),
],

进行文本到语音语音呼叫

现在我们已经提供了身份验证信息,是时候进行语音呼叫了。我们需要更新我们的 TicketController,以便在选择“语音”作为沟通方式时触发呼叫。

为了进行外呼,我们需要向 Nexmo 提供 to 号码、from 号码以及有关要用于此呼叫的 answer_urlevent_url 的详细信息。这很重要,因为我们想要根据刚刚创建的 TicketEntry ID 提供特定的 answer_url

打开 TicketController 并向下滚动到我们放置注释 // Make a voice call here 的位置。用以下代码替换该注释,确保将 ngrok URL 替换为您的主机名。

$currentHost = 'http://abc123.ngrok.io';
Nexmo::calls()->create([
'to' => [[
'type' => 'phone',
'number' => $cc->user->phone_number
]],
'from' => [
'type' => 'phone',
'number' => config('services.nexmo.sms_from')
],
'answer_url' => [$currentHost.'/webhook/answer/'.$entry->id],
'event_url' => [$currentHost.'/webhook/event']
]);

此外,我们需要在文件顶部的导入部分添加 use Nexmo;,以便正确解析它。

完成这些更改后,我们可以尝试一下!添加一个新的工单,选择语音呼叫作为通知方式。您的手机应该响铃,最新的工单条目应该使用 Nexmo 文本到语音引擎朗读出来。

捕获用户的回复

这是一个很好的第一步,但就像我们发送短信时一样,沟通是单向的。如果用户可以回话,并将这些话语添加到工单中,那不是很好吗?

Nexmo 允许我们录制呼叫的音频,并在呼叫结束后获取该录音。我们将使用该录音并将其发送到转录服务,将返回的文本添加到我们的工单中作为新条目。

为了让 Nexmo 录制我们的呼叫,我们需要在 NCCO 中添加一些新的操作。我们需要更新 WebhookController,以便 answer 操作读取添加的 TicketEntry,然后开始监听用户的回复。我们希望当用户可以开始讲话时,呼叫发出提示音,并在他们按下 # 键时停止录音。

return response()->json([
[
'action' => 'talk',
'text' => $ticket->content
],
[
'action' => 'talk',
'text' => 'To add a reply, please leave a message after the beep, then press the pound key',
'voiceName' => 'Brian'
],
[
'action' => 'record',
'endOnKey' => '#',
'beepStart' => true
]
]);

现在尝试添加一个新的工单,选择语音作为通知方式。当呼叫接通时,您应该听到我们刚刚添加的新操作,并提示您通过语音添加回复。

获取呼叫录音

呼叫结束后,Nexmo 会将包含呼叫 recording_url 的事件发送到我们的 event_url,该事件看起来像这样。

{
"start_time": "2018-02-11T15:02:28Z",
"recording_url": "https://api.nexmo.com/v1/files/092c732b-19b0-468c-bcd6-3f069650ddaf",
"size": 28350,
"recording_uuid": "8c618cc3-5bf5-42af-91cd-b628857f7fea",
"end_time": "2018-02-11T15:02:35Z",
"conversation_uuid": "CON-9ff341d8-fb45-47c7-aa27-9144c8db0447",
"timestamp": "2018-02-11T15:02:35.889Z"
}

目前,我们的应用程序会接收此事件并将其记录到磁盘。除了记录信息之外,我们还想要获取 recording_url 并使用转录服务获取文本。

再次打开 WebhookController 并更新 event 方法,以检查是否设置了 recording_url。如果设置了,则调用 transcribeRecording 方法(我们将在下一步实现此方法)。

public function event(Request $request) {
$params = $request->all();
Log::info('Call event', $params);
if (isset($params['recording_url'])) {
$voiceResponse = $this->transcribeRecording($params['recording_url']);
}
return response('', 204);
}

接下来,我们需要实现 transcribeRecording 方法。第一步是获取 recording_url 并使用 Nexmo 库获取它。

public function transcribeRecording($recordingUrl) {
$audio = \Nexmo::get($recordingUrl)->getBody();
}

这将提供呼叫的原始音频,我们可以将其输入到转录服务中,以获取语音文本。

转录呼叫录音

要转录呼叫,我们将使用 IBM 的 语音转文本 API。如果您还没有帐户,则需要注册 IBM Bluemix 才能完成接下来的部分。

登录后,访问 项目 页面,并点击右上角的“创建项目”。在模态窗口中,点击“获取 Watson 服务”,选择“语音转文本”,然后点击右侧的“添加服务”。为您的项目命名,然后创建项目。

在出现的页面底部,将有一个“凭据”部分。点击右侧的“显示”,并记下您的 usernamepassword

IBM 没有官方的 PHP 客户端库,因此我们将使用 Guzzle 并直接向他们的 API 发起 HTTP 请求。我们使用刚刚从 Nexmo 请求的音频数据发出 POST 请求,并将得到包含转录文本的 JSON 文档作为响应。

将以下内容添加到 transcribeRecording 方法中,以调用 IBM 转录 API,并将 usernamepassword 替换为您的 IBM 凭据。

$client = new \GuzzleHttp\Client([
'base_uri' => 'https://stream.watsonplatform.net/'
]);
 
$transcriptionResponse = $client->request('POST', 'speech-to-text/api/v1/recognize', [
'auth' => ['username', 'password'],
'headers' => [
'Content-Type' => 'audio/mpeg',
],
'body' => $audio
]);
 
$transcription = json_decode($transcriptionResponse->getBody());

IBM 转录 API 返回 JSON 响应,其结构如下所示。

{
"results": [
{
"alternatives": [
{
"confidence": 0.767,
"transcript": "hello "
}
],
"final": true
},
{
"alternatives": [
{
"confidence": 0.982,
"transcript": "this is a test "
}
],
"final": true
}
],
"result_index": 0
}

我们需要遍历此响应,构建一个字符串并将其作为回复添加到我们的工单中。将以下内容添加到 transcribeRecording 方法的末尾。

$voiceResponse = '';
foreach ($transcription->results as $result) {
$voiceResponse .= $result->alternatives[0]->transcript.' ';
}
 
return $voiceResponse;

现在我们已经有了转录的文本,是时候将其添加到我们的工单中了。不幸的是,我们没有办法将传入的 recording_url 与用户的电话号码关联起来,因此我们不知道音频属于哪个工单。

注意:为了使这篇文章中的内容保持简单,我们将把此回复添加到最近创建的工单中,并将其归因于创建该工单的用户。在现实世界中,您需要在拨打电话时跟踪呼叫的 conversation_uuid,并使用它来查找正确用户(但我将把这个任务留给您作为练习)。

更新 event 方法,以便检查是否有 recording_urlif 语句还会创建一个新的 TicketEntry 并将其添加到最近的工单中。

if (isset($params['recording_url'])) {
$voiceResponse = $this->transcribeRecording($params['recording_url']);
 
$ticket = Ticket::all()->last();
$user = $ticket->subscribedUsers()->first();
 
$entry = new TicketEntry([
'content' => $voiceResponse,
'channel' => 'voice',
]);
 
$entry->user()->associate($user);
$entry->ticket()->associate($ticket);
$entry->save();
}

除了更新 event 方法之外,您还需要在 WebhookController 顶部的导入列表中添加 use App\Ticket;

现在添加一个新的工单,并在接听电话时尝试说“这是一个音频回复,太棒了”,然后按下 # 键。呼叫结束后,打开刚刚添加的工单并刷新,直到您的回复出现(这应该只需要几秒钟)。

结论

恭喜,您已经完成了!我们将现有的应用程序从只能处理双向短信消息升级为能够进行语音呼叫并使用 IBM Watson 转录 API 接收客户的回复。

在本系列的下一篇文章中,我们将添加使用聊天应用程序(而不是短信或语音)向客户发送消息的功能,为他们提供与您的支持人员进行实时对话的方式。

如果您想获得一些 Nexmo 积分来完成这篇文章并测试该平台,请在 [email protected] 处联系我们,并引用 LaravelNews,我们会为您安排好。

如果您有任何想法或问题,请随时联系 Twitter 上的 @mheap 或 [email protected]


非常感谢 Nexmo 本周赞助 Laravel News。

Michael Heap photo

Michael 是 Nexmo 的 PHP 开发者倡导者。他使用各种语言和工具,在用户组和会议上向世界各地的受众分享他的技术专长。当他抽出时间编写代码时,他喜欢降低系统的复杂性,并使其更具可预测性。

归档于
Cube

Laravel 新闻稿

加入 40,000 多位其他开发者,永不错过新的提示、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和对本地和生产环境的即时反馈进行调试。

Tinkerwell
No Compromises logo

没有妥协

Joel 和 Aaron,来自 No Compromises 播客的两位经验丰富的开发者,现在可供您为您的 Laravel 项目聘用。 ⬧ 固定费率为每月 7500 美元。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

没有妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保您的 Web 应用程序成功。

Kirschbaum
Shift logo

Shift

运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,让您的应用程序保持新鲜。

Shift
Bacancy logo

Bacancy

让您的项目充满活力,聘用一位经验丰富的 Laravel 开发人员,拥有 4-6 年的经验,每月仅需 2500 美元。获得 160 小时的专用专业知识和 15 天的无风险试用期。立即安排电话会议!

Bacancy
Lucky Media logo

Lucky Media

现在就获得好运 - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar:Laravel 电子商务

Laravel 的电子商务。一个开源软件包,将现代无头电子商务功能的强大功能带入 Laravel。

Lunar:Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 招聘网站

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit:Laravel SaaS 启动套件

SaaSykit 是一个 Laravel SaaS 启动套件,包含运行现代 SaaS 所需的所有功能。支付、漂亮的结账、管理面板、用户仪表板、身份验证、现成的组件、统计、博客、文档等等。

SaaSykit:Laravel SaaS 启动套件
Rector logo

Rector

您无缝升级 Laravel、降低成本和加速创新的合作伙伴,为成功的公司提供服务

Rector
MongoDB logo

MongoDB

使用 MongoDB 和 Laravel 的强大集成来增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。在使用熟悉的 Eloquent API 的同时,支持事务、搜索、分析和移动用例。了解 MongoDB 如何将灵活、现代的数据库转化您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel 提示构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的站点搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包为您的 Laravel 应用程序添加评论

阅读文章