Drupal article to Audio using AWS Polly and S3

Deeps
3 min readMay 12, 2022

Recently we have a requirement to convert news article content to audio. After lots of reading and exploration. I came to a decision of using AWS polly as audio coverter and S3 to store audio file.

It works by sending article node data to AWS polly when node is saved, AWS polly converts data into audio file which will eventually be stored in S3 bucket. Most the thing I’ve explained here is really specific to our requirements.

Drupal

I have decided to trigger the text to audio in hook_node_presave. It will also be fine to work on hook_node_update. The reason for doing in presave is to populate Drupal field programmatically with a link to a audio before it is saved.

So I have created a service.

services:
<module_name>.manager:
class: Drupal\<module_name>\Service\TextToAudio
arguments: ['@config.factory']

Service file

<?php

namespace Drupal\<module_name>\Service;

use Aws\Credentials\Credentials;
use Aws\Polly\PollyClient;
use Aws\S3\S3Client;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\<module_name>\DataTransformation;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Class to covert text to audio.
*/
class TextToAudio implements ContainerInjectionInterface {

/**
* The config factory.
*
*
@var ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;

/**
* The aws polly configuration object.
*
*
@var config
*/
public $config;

/**
* The aws polly client object.
*
*
@var Aws\Polly\PollyClient
*/
public $pollyClient;

/**
* The aws s3 client object.
*
*
@var Aws\Polly\s3client
*/
public $s3Client;

/**
* Creates a config of aws_polly.settings.
*
*
@param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
$this->config = $this->configFactory->get('<module_name>'.settings');
$this->s3Client = new S3Client($this->getClientDetails());
$this->pollyClient = new PollyClient($this->getClientDetails());
}

/**
* {
@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
);
}

/**
* Return aws clients detail.
*
*
@return array
* Returns array.
*/
public function getClientDetails() {
$credentials = new Credentials(<AWS key>, <AWS secret>);
return [
'version' => latest,
'region' => eu-west-1,
'credentials' => $credentials,
];
}

/**
* Delete file(s) from s3 bucket.
*
*
@param int $eid
* Entity id.
*/
public function deleteCurrentFile($eid) {
$this->s3Client->deleteMatchingObjects('<s3_bucket_name>', $eid . '---');
}

/**
* Return args for polly.
*
*
@param int $eid
* Entity id.
*
*
@return array
* Returns array.
*
*
@throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*
@throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function getS3Args($eid) {
$args = [
'OutputFormat' => 'mp3',
'LanguageCode' => GB-EN,
'Text' => <ssml_data>,
'OutputS3BucketName' => <S3_bucket_name>,
'OutputS3KeyPrefix' => $eid . '---',
'TextType' => 'ssml',
'VoiceId' => 'Emma',
'Engine' => 'Neural',
];

return $args;
}

/**
* Transform data to audio and add to S3.
*
*
@param int $eid
* Entity id.
*
*
@return string
* Returns ssml data.
*
*
@throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*
@throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function getData($eid) {
$data = new DataTransformation();
$ssml_data = $data->tranformData($eid);

return $ssml_data;
}

/**
* Get audio filename from polly.
*
*
@param object $audioData
* Audio object from polly.
*
*
@return mixed
* Returns filename.
*/
public function getAudioFilename($audioData) {
return $audioData->get('SynthesisTask')['TaskId'];
}

}

Also created data transformation class from Drupal node to ssml format.

And finally made called to the service within <module_name>.module file

/**
* Implements hook_node_presave().
*/
function <module_name>_node_presave(EntityInterface $entity) {
$eid = $entity->id();
$bundle = $entity->bundle();
$config = \Drupal::configFactory()->getEditable('<module_name>.settings');

$textToAudio = \Drupal::service('<module_name>.manager');
if ($bundle == 'article') {
try {
// Delete existing audio file(s).
$textToAudio->deleteCurrentFile($eid);
}
catch (Exception $e) {
\Drupal::logger('<module_name>')
->error('Cannot delete item from s3 bucket. <br /><br />' . $e->getMessage());
}

try {
// Creates new audio file and add it to s3 bucket.
$audioData = $textToAudio->pollyClient->startSpeechSynthesisTask($textToAudio->getS3Args($eid));

// Get mp3 filename and add it to the node audio filename field.
$filename = $textToAudio->getAudioFilename($audioData);
if (!empty($filename)) {
$audio_full_path = $config->get('aws_url') . $eid . '---.' . $filename . '.mp3';
$entity->set('audio_filename', $audio_full_path);
}
}
catch (Exception $e) {
// Log the exception to watchdog.
\Drupal::logger('<module_name>')
->error('There is an issue in getting mp3 audio filename for nid ' . $eid . ' <br /><br />' . $e->getMessage());
}
}
}

Make sure to add aws package in your composer.json file.

"aws/aws-sdk-php": "^3.222",

That’s all. This will convert your ssml structured data to audio using AWS polly and store in the S3 bucket.

--

--