Amazon Polly Service with Zend Framework

Amazon Polly Service with Zend Framework

How to integrate Amazon Polly Text to Speech service into my Zend Framework project.

aws_logo.jpg

This is how Amazon introduced the brand new Polly Service:

"Amazon Polly is a service that turns text into lifelike speech. Polly lets you create applications that talk, enabling you to build entirely new categories of speech-enabled products. Polly is an Amazon AI service that uses advanced deep learning technologies to synthesize speech that sounds like a human voice. Polly includes 47 lifelike voices spread across 24 languages, so you can select the ideal voice and build speech-enabled applications that work in many different countries."

So Polly will help me to sound my application to say "Welcome, Suat!" after a successful login, fantastic!

I followed the steps in the "Amazon Polly Developer Guide" at Developers page and signed up for an AWS account and created an IAM user to be used with my service calls. By creating the IAM user, I have credentials consist of Access key ID and Secret access key that I will use to authenticate my connection to the AWS.

Now I need to install AWS SDK. In the project root folder, I run the following composer command:


[smozgur@local Skeleton]$ composer require aws/aws-sdk-php

Ok, I have the SDK installed and it looks I am ready to build an application that will work with Polly.

Let's think about it: I will have an Ajax module that is asking for customer's phone number and once you type the phone number in the text box and hit the Enter key, it will post the phone number to the server and if there is a match then will receive a success return with "Welcome, {customer_name}!" salutation as text. Polly is not involved so far.

I am working on a clean Zend Framework Skeleton Application. Since I will be returning Json from the controller, first thing is adding ViewJsonStrategy in the module configuration.

# /module/Application/src/config/module.config.php

<?php
namespace Application;

return [
    // ...

    'view_manager' => [
        'display_not_found_reason' => true,
        'display_exceptions'       => true,
        'doctype'                  => 'HTML5',
        'not_found_template'       => 'error/404',
        'exception_template'       => 'error/index',
        'template_map' => [
            'layout/layout'           => __DIR__ . '/../view/layout/layout.phtml',
            'application/index/index' => __DIR__ . '/../view/application/index/index.phtml',
            'error/404'               => __DIR__ . '/../view/error/404.phtml',
            'error/index'             => __DIR__ . '/../view/error/index.phtml',
        ],
        'template_path_stack' => [
            __DIR__ . '/../view',
        ],
        'strategies' => [
            'ViewJsonStrategy',
        ],
    ],
    
    // ...
];

In fact, I also need zend-json module which is not installed with Skeleton as default.


[smozgur@local Skeleton]$ composer require zendframework/zend-json

Ok, we can dive into Controller now. I will do the demonstration by adding the necessary actions into the default IndexController.

# /module/Application/src/Controller/IndexController.php

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;


class IndexController extends AbstractActionController
{

    public function indexAction()
    {
        return new ViewModel();
    }
    
    public function checkInAction()
    {
        return new ViewModel();
    }
}

Nothing fancy here, added checkInAction function to catch /application/check-in route. And following is the simple view for this action:

# /module/Application/view/application/index/check-in.phtml

<div>
	<input id="txt-phone" type="text" placeholder="Enter phone number">
	<button id="btn-submit" class="btn btn-sm btn-default">Submit</button>
</div>

This is how the page looks.

Phone lookup page
Phone lookup page

I am ready to add Ajax script into the view template and action function into the IndexController which will send response to this Ajax call. New check-in.phtml:

# /module/Application/view/application/index/check-in.phtml

<div>
	<input id="txt-phone" type="text" placeholder="Enter phone number">
	<button id="btn-submit" class="btn btn-sm btn-default">Submit</button>
</div>

<script type="text/javascript">
	$('#btn-submit').click(function() {
		$.ajax({
	        method: 'POST',
	        url: '/application/ajax-checkin',
	        dataType: 'json',
	        data: {
		        phone: $('#txt-phone').val(),
		    },
		    success: function(response){
			    if (response.success) {
				    // Doing something for the matched phone number
			    } else {
			    	// No match. 
			    }
			    alert(response.message);
	        },
	    });
	});
</script>

And ajaxCheckInAction in the controller:

# /module/Application/src/Controller/IndexController.php

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\View\Model\JsonModel;

class IndexController extends AbstractActionController
{

    public function indexAction()
    {
        return new ViewModel();
    }
    
    public function checkInAction()
    {
        return new ViewModel();
    }
    
    public function ajaxCheckInAction() 
    {
        /**
         * Return value will be a JSON encoded string
         * Using JsonModel() for Ajax returns
         * 
         * @var \Zend\View\Model\JsonModel $response
         */
        $response = new JsonModel();
        
        /*
         * Return nothing if it is not an Ajax request
         */
        if (!$this->getRequest()->isXmlHttpRequest()) {
            return $response;
        }
        
        /**
         * Getting requested phone number from the posted data 
         *
         * @var string $phone
         */
        $phone = (string) $this->params()->fromPost('phone', false);
        
        /**
         * Return error if phone is not given
         */
        if (!$phone) {
            $response->setVariables([
                'success'   => false,
                'message'   => 'Please provide your phone number' 
            ]);
            return $response;
        }
        
        /**
         * Skipping model processing here and 
         * assuming we found the customer
         * and returning the expected salutation.
         */
        $customerName = 'Suat';
        
        $response->setVariables([
            'success' => true,
            'message' => sprintf('Welcome, %s', $customerName),
        ]);
        return $response;
    }
}

This is what I get when I enter a phone number and click on the button.

AJAX response
AJAX response

Almost done. It is time to get Polly involved.

Adding my second Ajax call that will request for the audio file from the controller, by giving the customer id which will be returned by the first Ajax call.

# /module/Application/view/application/index/check-in.phtml

<div>
	<input id="txt-phone" type="text" placeholder="Enter phone number">
	<button id="btn-submit" class="btn btn-sm btn-default">Submit</button>
</div>

<script type="text/javascript">
	$('#btn-submit').click(function() {
		$.ajax({
	        method: 'POST',
	        url: '/application/ajax-checkin',
	        dataType: 'json',
	        data: {
		        phone: $('#txt-phone').val()
		    },
		    success: function(response){
			    if (response.success) {
				    // Doing something for the matched phone number

			    	// Make the second Ajax call to make it say something!
			    	sayIt(response.customerId)
			    } else {
			    	// No match. 
			    }
			    alert(response.message);
	        },
	    });
	});

	function sayIt(customerId) {
		window.AudioContext = window.AudioContext || window.webkitAudioContext;
		var context = new AudioContext();
		
		$.ajax({
	        method: 'POST',
	        url: '/application/ajax-polly',
	        dataType: 'binary',
			xhrFields : {
				responseType : 'arraybuffer'
			},
	        data: {
		        id: customerId
		    },
		    success: function(response){
		        context.decodeAudioData(response, function onSuccess(buffer) {
		        	var source = context.createBufferSource();
		        	source.buffer = buffer;
		        	source.connect(context.destination);
		        	source.start(0); 
		        }, function onError (error) {
		            alert('Error decoding file data.');
		        });
	        }
	    });
	}
</script>

Here is a very good article about Web Audio API what I am using for playing the returned binary data by the controller action in this code.

And the revised IndexController that now includes ajaxPollyAction where the Polly magic happens:

# /module/Application/src/Controller/IndexController.php

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\View\Model\JsonModel;

use Aws\Sdk;

class IndexController extends AbstractActionController
{

    public function indexAction()
    {
        return new ViewModel();
    }
    
    public function checkInAction()
    {
        return new ViewModel();
    }
    
    public function ajaxCheckInAction() 
    {
        /**
         * Return value will be a JSON encoded string
         * Using JsonModel() for Ajax returns
         * 
         * @var \Zend\View\Model\JsonModel $response
         */
        $response = new JsonModel();
        
        /*
         * Return nothing if it is not an Ajax request
         */
        if (!$this->getRequest()->isXmlHttpRequest()) {
            return $response;
        }
        
        /**
         * Getting requested phone number from the posted data  
         *
         * @var string $phone
         */
        $phone = (string) $this->params()->fromPost('phone', false);
        
        /**
         * Return error if phone is not given
         */
        if (!$phone) {
            $response->setVariables([
                'success'   => false,
                'message'   => 'Please provide your phone number' 
            ]);
            return $response;
        }
        
        /**
         * Skipping model processing here and 
         * assuming we found the customer by the phone number
         * and returning the expected salutation.
         */
        $customerId = 123;
        $customerName = 'Suat';
        
        $response->setVariables([
            'success' => true,
            'customerId' => $customerId,
            'message' => sprintf('Welcome, %s', $customerName),
        ]);
        return $response;
    }
    
    public function ajaxPollyAction()
    {
        $response = $this->getResponse();
    
        /*
         * Return nothing if it is not an Ajax request
         */
        if (!$this->getRequest()->isXmlHttpRequest()) {
            return $response;
        }
       
        $id = $this->params()->fromPost('id', false);
        
        if ($id) {
            
            /**
             * Skipping model processing here and
             * assuming we found the customer by the provided id
             */
            $customerName = 'Suat';
            
            /**
             * AWS IAM credentials
             * 
             * @var array $config
             */
            $config = [
                'version'     => 'latest',
                'region'      => 'us-east-1',
                'credentials' => [
                    'key'    => '***IAMKEY***',
                    'secret' => '***IAMSECRET***',
            ]];
            
            /**
             * Create AWS SDK by provinding the $config for authentication
             * 
             * @var \Aws\Sdk $sdk
             */
            $sdk = new Sdk($config);
            $pollyClient = $sdk->createPolly();
    
            /**
             * Preparing required parameters in this array
             * OutputFormat for desired file type
             * Text to Speech 
             * VoiceId one of the 47 lifelike voices in Polly
             * 
             * @var array $args
             */
            $args = [
                'OutputFormat' => 'mp3',
                'Text' => sprintf('Welcome, %s!', $customerName) ,
                'VoiceId' => 'Joanna',
            ];
    
            /**
             * Polly client's synthesizeSpeech method
             * returns the array contains AudioStream 
             * that will be used as the audio file binary content
             * 
             */
            $pollyResponse = $pollyClient->synthesizeSpeech($args);
            $audioContent = $pollyResponse['AudioStream'];
    
            /**
             * Return audio content by setting necessary headers
             */
            $response->setContent($audioContent);
            $response
                ->getHeaders()
                ->addHeaderLine('Content-Transfer-Encoding', 'chunked')
                ->addHeaderLine('Content-Type', 'audio/mpeg');
             
            return $response;
        }
    }
    
}

Finally, I have an application speaking with me!



Published on Dec 04, 2016