使用 Laravel 和 Nexmo 进行语音转文字通话
发布日期:作者: Michael Heap
这是由 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
,并在 recipient
和 submit
表单组之间添加以下 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_url
和 event_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_key
和 application_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_url
和 event_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 服务”,选择“语音转文本”,然后点击右侧的“添加服务”。为您的项目命名,然后创建项目。
在出现的页面底部,将有一个“凭据”部分。点击右侧的“显示”,并记下您的 username
和 password
。
IBM 没有官方的 PHP 客户端库,因此我们将使用 Guzzle
并直接向他们的 API 发起 HTTP 请求。我们使用刚刚从 Nexmo 请求的音频数据发出 POST
请求,并将得到包含转录文本的 JSON 文档作为响应。
将以下内容添加到 transcribeRecording
方法中,以调用 IBM 转录 API,并将 username
和 password
替换为您的 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_url
的 if
语句还会创建一个新的 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 是 Nexmo 的 PHP 开发者倡导者。他使用各种语言和工具,在用户组和会议上向世界各地的受众分享他的技术专长。当他抽出时间编写代码时,他喜欢降低系统的复杂性,并使其更具可预测性。