diff --git a/README.md b/README.md index be24b38a5..5905eb70f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ * [Set Up OpenAI Embeddings Language Processing](#set-up-classification-via-openai-embeddings) * [Set Up OpenAI Whisper Language Processing](#set-up-audio-transcripts-generation-via-openai-whisper) * [Set Up Azure AI Language Processing](#set-up-text-to-speech-via-microsoft-azure) +* [Set Up AWS Language Processing](#set-up-text-to-speech-via-amazon-polly) * [Set Up Azure AI Vision Image Processing](#set-up-image-processing-features-via-microsoft-azure) * [Set Up OpenAI DALL·E Image Processing](#set-up-image-generation-via-openai) * [Set Up OpenAI Moderation Language Processing](#set-up-comment-moderation-via-openai-moderation) @@ -45,7 +46,7 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro * Generate new images on demand to use in-content or as a featured image using [OpenAI's DALL·E 3 API](https://platform.openai.com/docs/guides/images) * Generate transcripts of audio files using [OpenAI's Whisper API](https://platform.openai.com/docs/guides/speech-to-text) * Moderate incoming comments for sensitive content using [OpenAI's Moderation API](https://platform.openai.com/docs/guides/moderation) -* Convert text content into audio and output a "read-to-me" feature on the front-end to play this audio using [Microsoft Azure's Text to Speech API](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/text-to-speech) +* Convert text content into audio and output a "read-to-me" feature on the front-end to play this audio using [Microsoft Azure's Text to Speech API](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/text-to-speech) or [Amazon Polly](https://aws.amazon.com/polly/) * Classify post content using [IBM Watson's Natural Language Understanding API](https://www.ibm.com/watson/services/natural-language-understanding/) and [OpenAI's Embedding API](https://platform.openai.com/docs/guides/embeddings) * BETA: Recommend content based on overall site traffic via [Microsoft Azure's AI Personalizer API](https://azure.microsoft.com/en-us/services/cognitive-services/personalizer/) *(note that this service has been [deprecated by Microsoft](https://learn.microsoft.com/en-us/azure/ai-services/personalizer/) and as such, will no longer work. We are looking to replace this with a new provider to maintain the same functionality (see [issue#392](https://github.com/10up/classifai/issues/392))* * Generate image alt text, image tags, and smartly crop images using [Microsoft Azure's AI Vision API](https://azure.microsoft.com/en-us/services/cognitive-services/computer-vision/) @@ -77,6 +78,7 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro * To utilize the Azure AI Vision Image Processing functionality or Text to Speech Language Processing functionality, you will need an active [Microsoft Azure](https://signup.azure.com/signup) account. * To utilize the Azure OpenAI Language Processing functionality, you will need an active [Microsoft Azure](https://signup.azure.com/signup) account and you will need to [apply](https://aka.ms/oai/access) for OpenAI access. * To utilize the Google Gemini Language Processing functionality, you will need an active [Google Gemini](https://ai.google.dev/tutorials/setup) account. +* To utilize the AWS Language Processing functionality, you will need an active [AWS](https://console.aws.amazon.com/) account. ## Pricing @@ -399,6 +401,47 @@ Note that [OpenAI](https://platform.openai.com/docs/guides/speech-to-text) can c * Click the button to preview the generated speech audio for the post. * View the post on the front-end and see a read-to-me feature has been added +## Set Up Text to Speech (via Amazon Polly) + +### 1. Sign up for AWS (Amazon Web Services) + +* [Register for a AWS account](https://aws.amazon.com/free/) or sign into your existing one. +* Sign in to the AWS Management Console and open the IAM console at [https://console.aws.amazon.com/iam/](https://console.aws.amazon.com/iam/) +* Create IAM User (If you don't have any IAM user) + * In the navigation pane, choose **Users** and then click **Create user** + * On the **Specify user details** page, under User details, in User name, enter the name for the new user. + * Click **Next** + * On the **Set permissions** page, under Permissions options, select **Attach policies directly** + * Under **Permissions policies**, search for the policy **polly** and select **AmazonPollyFullAccess** Policy + * Click **Next** + * On the **Review and create** page, Review all of the choices you made up to this point. When you are ready to proceed, Click **Create user**. +* In the navigation pane, choose **Users** +* Choose the name of the user for which you want to create access keys, and then choose the **Security credentials** tab. +* In the **Access keys** section, click **Create access key**. +* On the **Access key best practices & alternatives** page, select **Application running outside AWS** +* Click **Next** +* On the **Retrieve access key** page, choose **Show** to reveal the value of your user's secret access key. +* Copy and save the credentials in a secure location on your computer or click "Download .csv file" to save the access key ID and secret access key to a `.csv` file. + +### 2. Configure AWS credentials under Tools > ClassifAI > Language Processing > Text to Speech + +* Select **Amazon Polly** in the provider dropdown. +* In the `AWS access key` field, enter the `Access key +` copied from above. +* In the `AWS secret access key` field, enter your `Secret access key` copied from above. +* In the `AWS Region` field, enter your AWS region value eg: `us-east-1` +* Click **Save Changes** (the page will reload). +* If connected successfully, a new dropdown with the label "Voices" will be displayed. +* Select a voice and voice engine as per your choice. +* Select a post type that should use this service. + +### 3. Using the Text to Speech service + +* Assuming the post type selected is "post", create a new post and publish it. +* After a few seconds, a "Preview" button will appear under the ClassifAI settings panel. +* Click the button to preview the generated speech audio for the post. +* View the post on the front-end and see a read-to-me feature has been added + ## Set Up Image Processing features (via Microsoft Azure) Note that [Azure AI Vision](https://docs.microsoft.com/en-us/azure/cognitive-services/computer-vision/home#image-requirements) can analyze and crop images that meet the following requirements: diff --git a/composer.json b/composer.json index 40a11b254..b6cca2bfc 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "require": { "php": ">=7.4", "yahnis-elsts/plugin-update-checker": "5.1", - "ua-parser/uap-php": "dev-master" + "ua-parser/uap-php": "dev-master", + "aws/aws-sdk-php": "^3.300" }, "autoload": { "psr-4": { @@ -30,7 +31,8 @@ }, "scripts": { "lint": "phpcs -s . --runtime-set testVersion 7.4-", - "lint-fix": "phpcbf ." + "lint-fix": "phpcbf .", + "pre-autoload-dump": "Aws\\Script\\Composer\\Composer::removeUnusedServices" }, "minimum-stability": "dev", "config": { @@ -42,5 +44,10 @@ "exclude": [ "!/vendor/" ] + }, + "extra": { + "aws/aws-sdk-php": [ + "Polly" + ] } } diff --git a/composer.lock b/composer.lock index fa4caee7d..9cf59b1f2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,43 +4,599 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7a9a2237236b5a380f663428adcb1357", + "content-hash": "a9f9528d1fa7e7d07331ef0f5052c7c1", "packages": [ + { + "name": "aws/aws-crt-php", + "version": "v1.2.4", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/eb0c6e4e142224a10b08f49ebf87f32611d162b2", + "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.4" + }, + "time": "2023-11-08T00:42:13+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.300.13", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "b1eb7307d30ebcfa4e156971f658c2d177434db3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b1eb7307d30ebcfa4e156971f658c2d177434db3", + "reference": "b1eb7307d30ebcfa4e156971f658c2d177434db3", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "guzzlehttp/promises": "^1.4.0 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "mtdowling/jmespath.php": "^2.6", + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.300.13" + }, + "time": "2024-03-07T19:14:04+00:00" + }, { "name": "composer/ca-bundle", "version": "dev-main", "source": { "type": "git", - "url": "https://github.com/composer/ca-bundle.git", - "reference": "b66d11b7479109ab547f9405b97205640b17d385" + "url": "https://github.com/composer/ca-bundle.git", + "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", + "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.4.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-02-23T10:16:52+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.x-dev", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:35:24+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "default-branch": true, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:19:20+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.x-dev", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "default-branch": true, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "b243cacd2a9803b4cbc259246aa5081208238c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385", - "reference": "b66d11b7479109ab547f9405b97205640b17d385", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/b243cacd2a9803b4cbc259246aa5081208238c10", + "reference": "b243cacd2a9803b4cbc259246aa5081208238c10", "shasum": "" }, "require": { - "ext-openssl": "*", - "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" }, "default-branch": true, + "bin": [ + "bin/jp.php" + ], "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-master": "2.7-dev" } }, "autoload": { + "files": [ + "src/JmesPath.php" + ], "psr-4": { - "Composer\\CaBundle\\": "src" + "JmesPath\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -49,39 +605,381 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" } ], - "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "description": "Declaratively specify how to extract elements from a JSON document", "keywords": [ - "cabundle", - "cacert", - "certificate", - "ssl", - "tls" + "json", + "jsonpath" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.4.0" + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/master" + }, + "time": "2023-11-30T16:26:47+00:00" + }, + { + "name": "psr/http-client", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/7037f4b0950474e9d1350e8df89b15f1842085f6", + "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2023-09-22T11:16:44+00:00" + }, + { + "name": "psr/http-message", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "2.5.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "80d075412b557d41002320b96a096ca65aa2c98d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", + "reference": "80d075412b557d41002320b96a096ca65aa2c98d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/2.5" }, "funding": [ { - "url": "https://packagist.com", + "url": "https://symfony.com/sponsor", "type": "custom" }, { - "url": "https://github.com/composer", + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-24T14:02:46+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2023-12-18T12:05:55+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "ua-parser/uap-php", @@ -447,12 +1345,12 @@ "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "202aaf6b7c2e1e0a622b0298e9f3f537e4d84018" + "reference": "2f5294676c802a62b0549f6bc8983f14294ce369" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/202aaf6b7c2e1e0a622b0298e9f3f537e4d84018", - "reference": "202aaf6b7c2e1e0a622b0298e9f3f537e4d84018", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/2f5294676c802a62b0549f6bc8983f14294ce369", + "reference": "2f5294676c802a62b0549f6bc8983f14294ce369", "shasum": "" }, "require": { @@ -500,7 +1398,7 @@ "type": "tidelift" } ], - "time": "2023-11-01T08:01:43+00:00" + "time": "2024-02-10T11:10:03+00:00" }, { "name": "nikic/php-parser", @@ -508,12 +1406,12 @@ "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ce019e9ad711e31ee87c2c4c72e538b5240970c3" + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ce019e9ad711e31ee87c2c4c72e538b5240970c3", - "reference": "ce019e9ad711e31ee87c2c4c72e538b5240970c3", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", "shasum": "" }, "require": { @@ -557,9 +1455,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/master" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" }, - "time": "2024-01-14T09:02:54+00:00" + "time": "2024-03-05T20:51:40+00:00" }, { "name": "phar-io/manifest", @@ -567,12 +1465,12 @@ "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "67729272c564ab9f953c81f48db44e8b1cb1e1c3" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/67729272c564ab9f953c81f48db44e8b1cb1e1c3", - "reference": "67729272c564ab9f953c81f48db44e8b1cb1e1c3", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { @@ -581,7 +1479,7 @@ "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", - "php": "^7.3 || ^8.0" + "php": "^7.2 || ^8.0" }, "default-branch": true, "type": "library", @@ -619,7 +1517,7 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, "funding": [ { @@ -627,7 +1525,7 @@ "type": "github" } ], - "time": "2023-06-01T14:19:47+00:00" + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -860,12 +1758,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489" + "reference": "7384703f57a65879dc6fb5a0fc0dbe60fe2c1d8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", - "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/7384703f57a65879dc6fb5a0fc0dbe60fe2c1d8d", + "reference": "7384703f57a65879dc6fb5a0fc0dbe60fe2c1d8d", "shasum": "" }, "require": { @@ -931,7 +1829,7 @@ "type": "open_collective" } ], - "time": "2023-12-08T16:49:07+00:00" + "time": "2024-03-04T02:11:33+00:00" }, { "name": "phpcsstandards/phpcsutils", @@ -939,18 +1837,18 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "26dcb893d86fbe90ab2a8abd7b08a3fda3602237" + "reference": "7883bd854d3a7594a1504fca79aacd3595dacd2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/26dcb893d86fbe90ab2a8abd7b08a3fda3602237", - "reference": "26dcb893d86fbe90ab2a8abd7b08a3fda3602237", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/7883bd854d3a7594a1504fca79aacd3595dacd2d", + "reference": "7883bd854d3a7594a1504fca79aacd3595dacd2d", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.8.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.9.0 || 4.0.x-dev@dev" }, "require-dev": { "ext-filter": "*", @@ -1020,7 +1918,7 @@ "type": "open_collective" } ], - "time": "2024-01-15T05:03:54+00:00" + "time": "2024-03-04T08:00:16+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1028,12 +1926,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { @@ -1090,7 +1988,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, "funding": [ { @@ -1098,7 +1996,7 @@ "type": "github" } ], - "time": "2023-12-22T06:47:57+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1347,12 +2245,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4c1997c21fb0e29198b7b83be49d460df2571d79" + "reference": "5b92d2809fe6c9dbf892e8016df656f16ef157e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4c1997c21fb0e29198b7b83be49d460df2571d79", - "reference": "4c1997c21fb0e29198b7b83be49d460df2571d79", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5b92d2809fe6c9dbf892e8016df656f16ef157e1", + "reference": "5b92d2809fe6c9dbf892e8016df656f16ef157e1", "shasum": "" }, "require": { @@ -1442,20 +2340,20 @@ "type": "tidelift" } ], - "time": "2024-01-21T09:34:47+00:00" + "time": "2024-03-06T06:47:12+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -1490,7 +2388,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -1498,7 +2396,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -1748,12 +2646,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -1798,7 +2696,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -1806,7 +2704,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -1877,12 +2775,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -1938,7 +2836,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -1946,7 +2844,7 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", @@ -1954,12 +2852,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -2002,7 +2900,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -2010,7 +2908,7 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", @@ -2414,12 +3312,12 @@ "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "02703669a3780f6c9b293bfe6294cfb359264b10" + "reference": "b52d51ca3f224c4459a6ae686a0104e80fbfb7df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/02703669a3780f6c9b293bfe6294cfb359264b10", - "reference": "02703669a3780f6c9b293bfe6294cfb359264b10", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/b52d51ca3f224c4459a6ae686a0104e80fbfb7df", + "reference": "b52d51ca3f224c4459a6ae686a0104e80fbfb7df", "shasum": "" }, "require": { @@ -2465,7 +3363,7 @@ "source": "https://github.com/sirbrillig/phpcs-variable-analysis", "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "time": "2023-12-07T16:24:19+00:00" + "time": "2024-03-04T15:42:00+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -2473,12 +3371,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "b03d10fc5a68504e3ea28fc84651b92cb0252fd9" + "reference": "e72c99b4785937d05f9790a95e41259dd8e9777c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/b03d10fc5a68504e3ea28fc84651b92cb0252fd9", - "reference": "b03d10fc5a68504e3ea28fc84651b92cb0252fd9", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/e72c99b4785937d05f9790a95e41259dd8e9777c", + "reference": "e72c99b4785937d05f9790a95e41259dd8e9777c", "shasum": "" }, "require": { @@ -2546,20 +3444,20 @@ "type": "open_collective" } ], - "time": "2024-01-22T02:36:17+00:00" + "time": "2024-03-07T21:48:16+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -2588,7 +3486,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -2596,7 +3494,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -2670,12 +3568,12 @@ "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "f07cf7b2ea73c3de1f72cf115e3cd446c8ad2713" + "reference": "e3a5bad2c69c147fbde4b8ad83f422bac3b36153" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/f07cf7b2ea73c3de1f72cf115e3cd446c8ad2713", - "reference": "f07cf7b2ea73c3de1f72cf115e3cd446c8ad2713", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/e3a5bad2c69c147fbde4b8ad83f422bac3b36153", + "reference": "e3a5bad2c69c147fbde4b8ad83f422bac3b36153", "shasum": "" }, "require": { @@ -2725,7 +3623,7 @@ "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2023-12-14T21:29:51+00:00" + "time": "2024-03-04T17:15:33+00:00" } ], "aliases": [], @@ -2739,5 +3637,5 @@ "php": ">=7.4" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/includes/Classifai/Features/TextToSpeech.php b/includes/Classifai/Features/TextToSpeech.php index bd530006c..afb366d2f 100644 --- a/includes/Classifai/Features/TextToSpeech.php +++ b/includes/Classifai/Features/TextToSpeech.php @@ -4,6 +4,7 @@ use Classifai\Services\LanguageProcessing; use Classifai\Providers\Azure\Speech; +use Classifai\Providers\AWS\AmazonPolly; use WP_REST_Server; use WP_REST_Request; use WP_Error; @@ -54,7 +55,8 @@ public function __construct() { // Contains just the providers this feature supports. $this->supported_providers = [ - Speech::ID => __( 'Microsoft Azure AI Speech', 'classifai' ), + Speech::ID => __( 'Microsoft Azure AI Speech', 'classifai' ), + AmazonPolly::ID => __( 'Amazon Polly', 'classifai' ), ]; } @@ -84,6 +86,7 @@ public function feature_setup() { } add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ] ); + add_action( 'admin_notices', [ $this, 'show_error_if' ] ); add_action( 'save_post', [ $this, 'save_post_metadata' ], 5 ); } @@ -225,6 +228,18 @@ public function rest_handle_audio( \WP_Post $post, WP_REST_Request $request ) { if ( $results && ! is_wp_error( $results ) ) { $this->save( $results, $request->get_param( 'id' ) ); + delete_post_meta( $post->ID, '_classifai_text_to_speech_error' ); + } elseif ( is_wp_error( $results ) ) { + update_post_meta( + $post->ID, + '_classifai_text_to_speech_error', + wp_json_encode( + [ + 'code' => $results->get_error_code(), + 'message' => $results->get_error_message(), + ] + ) + ); } } } @@ -233,6 +248,19 @@ public function rest_handle_audio( \WP_Post $post, WP_REST_Request $request ) { * Register any needed endpoints. */ public function register_endpoints() { + $post_types = $this->get_supported_post_types(); + foreach ( $post_types as $post_type ) { + register_meta( + $post_type, + '_classifai_text_to_speech_error', + [ + 'show_in_rest' => true, + 'single' => true, + 'auth_callback' => '__return_true', + ] + ); + } + register_rest_route( 'classifai/v1', 'synthesize-speech/(?P\d+)', @@ -460,6 +488,18 @@ public function save_post_metadata( int $post_id ) { if ( $results && ! is_wp_error( $results ) ) { $this->save( $results, $post_id ); + delete_post_meta( $post_id, '_classifai_text_to_speech_error' ); + } elseif ( is_wp_error( $results ) ) { + update_post_meta( + $post_id, + '_classifai_text_to_speech_error', + wp_json_encode( + [ + 'code' => $results->get_error_code(), + 'message' => $results->get_error_message(), + ] + ) + ); } } } @@ -671,7 +711,7 @@ public function render_post_audio_controls( string $content ): string { * @return string */ public function get_enable_description(): string { - return esc_html__( 'A button will be added to the status panel that can be used to generate titles.', 'classifai' ); + return esc_html__( 'Enables speech generation for post content.', 'classifai' ); } /** @@ -854,4 +894,43 @@ public function migrate_settings() { return $new_settings; } + + /** + * Outputs an admin notice with the error message if needed. + */ + public function show_error_if() { + global $post; + + if ( empty( $post ) ) { + return; + } + + $post_id = $post->ID; + + if ( empty( $post_id ) ) { + return; + } + + $error = get_post_meta( $post_id, '_classifai_text_to_speech_error', true ); + + if ( ! empty( $error ) ) { + delete_post_meta( $post_id, '_classifai_text_to_speech_error' ); + $error = (array) json_decode( $error ); + $code = ! empty( $error['code'] ) ? $error['code'] : 500; + $message = ! empty( $error['message'] ) ? $error['message'] : 'Unknown API error'; + + ?> +
+

+ +

+

+ + - + +

+
+ feature_instance = $feature_instance; + + do_action( 'classifai_' . static::ID . '_init', $this ); + add_action( 'wp_ajax_classifai_get_voice_dropdown', [ $this, 'get_voice_dropdown' ] ); + } + + /** + * Render the provider fields. + */ + public function render_provider_fields() { + $settings = $this->feature_instance->get_settings( static::ID ); + + add_settings_field( + 'access_key_id', + esc_html__( 'Access key', 'classifai' ), + [ $this->feature_instance, 'render_input' ], + $this->feature_instance->get_option_name(), + $this->feature_instance->get_option_name() . '_section', + [ + 'option_index' => static::ID, + 'label_for' => 'access_key_id', + 'input_type' => 'text', + 'default_value' => $settings['access_key_id'], + 'class' => 'large-text classifai-provider-field hidden provider-scope-' . static::ID, + 'description' => sprintf( + wp_kses( + /* translators: %1$s is replaced with the OpenAI sign up URL */ + __( 'Enter the AWS access key. Please follow the steps given here to generate AWS credentials.', 'classifai' ), + [ + 'a' => [ + 'href' => [], + 'title' => [], + ], + ] + ), + esc_url( 'https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey' ) + ), + ] + ); + + add_settings_field( + 'secret_access_key', + esc_html__( 'Secret access key', 'classifai' ), + [ $this->feature_instance, 'render_input' ], + $this->feature_instance->get_option_name(), + $this->feature_instance->get_option_name() . '_section', + [ + 'option_index' => static::ID, + 'label_for' => 'secret_access_key', + 'input_type' => 'password', + 'default_value' => $settings['secret_access_key'], + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, + 'description' => esc_html__( 'Enter the AWS secret access key.', 'classifai' ), + ] + ); + + add_settings_field( + 'aws_region', + esc_html__( 'Region', 'classifai' ), + [ $this->feature_instance, 'render_input' ], + $this->feature_instance->get_option_name(), + $this->feature_instance->get_option_name() . '_section', + [ + 'option_index' => static::ID, + 'label_for' => 'aws_region', + 'input_type' => 'text', + 'default_value' => $settings['aws_region'], + 'class' => 'large-text classifai-provider-field hidden provider-scope-' . static::ID, + 'description' => wp_kses( + __( 'Enter the AWS Region. eg: us-east-1', 'classifai' ), + [ + 'code' => [], + ] + ), + ] + ); + + add_settings_field( + 'voice_engine', + esc_html__( 'Engine', 'classifai' ), + [ $this->feature_instance, 'render_select' ], + $this->feature_instance->get_option_name(), + $this->feature_instance->get_option_name() . '_section', + [ + 'option_index' => static::ID, + 'label_for' => 'voice_engine', + 'options' => array( + 'standard' => esc_html__( 'Standard', 'classifai' ), + 'neural' => esc_html__( 'Neural', 'classifai' ), + 'long-form' => esc_html__( 'Long Form', 'classifai' ), + ), + 'default_value' => $settings['voice_engine'], + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, + 'description' => sprintf( + wp_kses( + /* translators: %1$s is replaced with the OpenAI sign up URL */ + __( 'Amazon Polly offers Long-Form, Neural and Standard text-to-speech voices. Please check the documentation to review pricing for Long-Form, Neural and Standard usage.', 'classifai' ), + [ + 'a' => [ + 'href' => [], + 'title' => [], + ], + ] + ), + esc_url( 'https://docs.aws.amazon.com/polly/latest/dg/long-form-voice-overview.html' ), + esc_url( 'https://docs.aws.amazon.com/polly/latest/dg/NTTS-main.html' ), + esc_url( 'https://aws.amazon.com/polly/pricing/' ) + ), + ] + ); + + $voices_options = $this->get_voices_select_options( $settings['voice_engine'] ?? '' ); + if ( ! empty( $voices_options ) ) { + add_settings_field( + 'voice', + esc_html__( 'Voice', 'classifai' ), + [ $this->feature_instance, 'render_select' ], + $this->feature_instance->get_option_name(), + $this->feature_instance->get_option_name() . '_section', + [ + 'option_index' => static::ID, + 'label_for' => 'voice', + 'options' => $voices_options, + 'default_value' => $settings['voice'], + 'class' => 'classifai-aws-polly-voices classifai-provider-field hidden provider-scope-' . static::ID, + ] + ); + } + + do_action( 'classifai_' . static::ID . '_render_provider_fields', $this ); + } + + /** + * Returns the default settings for this provider. + * + * @return array + */ + public function get_default_provider_settings(): array { + $common_settings = [ + 'access_key_id' => '', + 'secret_access_key' => '', + 'aws_region' => '', + 'authenticated' => false, + 'voice_engine' => 'standard', + 'voices' => [], + 'voice' => '', + ]; + + switch ( $this->feature_instance::ID ) { + case TextToSpeech::ID: + return $common_settings; + } + + return []; + } + + /** + * Sanitization callback for settings. + * + * @param array $new_settings The settings being saved. + * @return array + */ + public function sanitize_settings( array $new_settings ): array { + $settings = $this->feature_instance->get_settings(); + $is_credentials_changed = false; + + $new_settings[ static::ID ]['authenticated'] = $settings[ static::ID ]['authenticated']; + $new_settings[ static::ID ]['voices'] = $settings[ static::ID ]['voices']; + + if ( + ! empty( $new_settings[ static::ID ]['access_key_id'] ) && + ! empty( $new_settings[ static::ID ]['secret_access_key'] ) && + ! empty( $new_settings[ static::ID ]['aws_region'] ) + ) { + $new_access_key_id = sanitize_text_field( $new_settings[ static::ID ]['access_key_id'] ); + $new_secret_access_key = sanitize_text_field( $new_settings[ static::ID ]['secret_access_key'] ); + $new_aws_region = sanitize_text_field( $new_settings[ static::ID ]['aws_region'] ); + + if ( + $new_access_key_id !== $settings[ static::ID ]['access_key_id'] || + $new_secret_access_key !== $settings[ static::ID ]['secret_access_key'] || + $new_aws_region !== $settings[ static::ID ]['aws_region'] + ) { + $is_credentials_changed = true; + } + + if ( $is_credentials_changed ) { + $new_settings[ static::ID ]['access_key_id'] = $new_access_key_id; + $new_settings[ static::ID ]['secret_access_key'] = $new_secret_access_key; + $new_settings[ static::ID ]['aws_region'] = $new_aws_region; + $new_settings[ static::ID ]['voices'] = $this->connect_to_service( + array( + 'access_key_id' => $new_access_key_id, + 'secret_access_key' => $new_secret_access_key, + 'aws_region' => $new_aws_region, + ) + ); + + if ( ! empty( $new_settings[ static::ID ]['voices'] ) ) { + $new_settings[ static::ID ]['authenticated'] = true; + } else { + $new_settings[ static::ID ]['voices'] = []; + $new_settings[ static::ID ]['authenticated'] = false; + } + } + } else { + $new_settings[ static::ID ]['access_key_id'] = $settings[ static::ID ]['access_key_id']; + $new_settings[ static::ID ]['secret_access_key'] = $settings[ static::ID ]['secret_access_key']; + $new_settings[ static::ID ]['aws_region'] = $settings[ static::ID ]['aws_region']; + + add_settings_error( + $this->feature_instance->get_option_name(), + 'classifai-ams-polly-auth-empty', + esc_html__( 'One or more credentials required to connect to the Amazon Polly service is empty.', 'classifai' ), + 'error' + ); + } + + $new_settings[ static::ID ]['voice'] = sanitize_text_field( $new_settings[ static::ID ]['voice'] ?? $settings[ static::ID ]['voice'] ); + + return $new_settings; + } + + /** + * Connects to the Amazon Polly service. + * + * @param array $args Overridable args. + * @return array + */ + public function connect_to_service( array $args = array() ): array { + $settings = $this->feature_instance->get_settings( static::ID ); + + $default = array( + 'access_key_id' => $settings[ static::ID ]['access_key_id'] ?? '', + 'secret_access_key' => $settings[ static::ID ]['secret_access_key'] ?? '', + 'aws_region' => $settings[ static::ID ]['aws_region'] ?? 'us-east-1', + ); + + $default = wp_parse_args( $args, $default ); + + // Return if credentials don't exist. + if ( empty( $default['access_key_id'] ) || empty( $default['secret_access_key'] ) ) { + return array(); + } + + try { + /** + * Filters the return value of the connect to services function. + * + * Returning a non-false value from the filter will short-circuit the describe voices request and return early with that value. + * This filter is useful for E2E tests. + * + * @since 3.1.0 + * @hook classifai_aws_polly_pre_connect_to_service + * + * @param {bool} $pre The value of pre connect to service. Default false. non-false value will short-circuit the describe voices request. + * + * @return {bool|mixed} The filtered value of connect to service. + */ + $pre = apply_filters( 'classifai_' . self::ID . '_pre_connect_to_service', false ); + + if ( false !== $pre ) { + return $pre; + } + + $polly_client = $this->get_polly_client( $args ); + $polly_voices = $polly_client->describeVoices(); + return $polly_voices->get( 'Voices' ); + } catch ( \Exception $e ) { + add_settings_error( + $this->feature_instance->get_option_name(), + 'aws-polly-auth-failed', + esc_html__( 'Connection to Amazon Polly failed.', 'classifai' ), + 'error' + ); + return array(); + } + } + + /** + * Returns HTML select dropdown options for voices. + * + * @param string $engine Engine type. + * @return array + */ + public function get_voices_select_options( string $engine = '' ): array { + $settings = $this->feature_instance->get_settings( static::ID ); + $voices = $settings['voices']; + $options = array(); + + if ( false === $voices ) { + return $options; + } + + foreach ( $voices as $voice ) { + if ( + ! is_array( $voice ) || + empty( $voice ) || + ( + ! empty( $engine ) && + ! in_array( $engine, $voice['SupportedEngines'], true ) + ) + ) { + continue; + } + + $options[ $voice['Id'] ] = sprintf( + '%1$s - %2$s (%3$s)', + esc_html( $voice['LanguageName'] ), + esc_html( $voice['Name'] ), + esc_html( $voice['Gender'] ) + ); + } + + // Sort the options. + asort( $options ); + + return $options; + } + + /** + * Synthesizes speech from a post item. + * + * @param int $post_id Post ID. + * @return string|WP_Error + */ + public function synthesize_speech( int $post_id ) { + if ( empty( $post_id ) ) { + return new WP_Error( + 'aws_polly_post_id_missing', + esc_html__( 'Post ID missing.', 'classifai' ) + ); + } + + // We skip the user cap check if running under WP-CLI. + if ( ! current_user_can( 'edit_post', $post_id ) && ( ! defined( 'WP_CLI' ) || ! WP_CLI ) ) { + return new WP_Error( + 'aws_polly_user_not_authorized', + esc_html__( 'Unauthorized user.', 'classifai' ) + ); + } + + $normalizer = new Normalizer(); + $feature = new TextToSpeech(); + $settings = $feature->get_settings(); + $post = get_post( $post_id ); + $post_content = $normalizer->normalize_content( $post->post_content, $post->post_title, $post_id ); + $content_hash = get_post_meta( $post_id, self::AUDIO_HASH_KEY, true ); + $saved_attachment_id = (int) get_post_meta( $post_id, $feature::AUDIO_ID_KEY, true ); + + // Don't regenerate the audio file it it already exists and the content hasn't changed. + if ( $saved_attachment_id ) { + + // Check if the audio file exists. + $audio_attachment_url = wp_get_attachment_url( $saved_attachment_id ); + + if ( $audio_attachment_url && ! empty( $content_hash ) && ( md5( $post_content ) === $content_hash ) ) { + return $saved_attachment_id; + } + } + + if ( mb_strlen( $post_content ) > 3000 ) { + return new WP_Error( + 'aws_polly_length_error', + esc_html__( 'Maximum text length has been exceeded.', 'classifai' ) + ); + } + + $voice = $settings[ static::ID ]['voice'] ?? ''; + + try { + /** + * Filter Synthesize speech args. + * + * @since 3.1.0 + * @hook classifai_aws_polly_synthesize_speech_args + * + * @param {array} Associative array of synthesize speech args. + * @param {int} $post_id Post ID. + * @param {object} $provider_instance Provider instance. + * @param {object} $feature_instance Feature instance. + * + * @return {array} The filtered array of synthesize speech args. + */ + $synthesize_data = apply_filters( + 'classifai_' . self::ID . '_synthesize_speech_args', + array( + 'OutputFormat' => 'mp3', + 'Text' => $post_content, + 'TextType' => 'text', + 'Engine' => $settings[ static::ID ]['voice_engine'] ?? 'standard', + 'VoiceId' => $voice, + ), + $post_id, + $this, + $this->feature_instance + ); + + /** + * Filters the return value of the synthesize speech function. + * + * Returning a non-false value from the filter will short-circuit the synthesize speech request and return early with that value. + * This filter is useful for E2E tests. + * + * @since 3.1.0 + * @hook classifai_aws_polly_pre_synthesize_speech + * + * @param {bool} $pre A value of pre synthesize speech. Default false. + * @param {array} $synthesize_data HTTP request arguments. + * + * @return {bool|mixed} The filtered value of pre synthesize speech. + */ + $pre = apply_filters( 'classifai_' . self::ID . '_pre_synthesize_speech', false, $synthesize_data ); + + if ( false !== $pre ) { + return $pre; + } + + $polly_client = $this->get_polly_client(); + $result = $polly_client->synthesizeSpeech( $synthesize_data ); + + update_post_meta( $post_id, self::AUDIO_HASH_KEY, md5( $post_content ) ); + $contents = $result['AudioStream']->getContents(); + return $contents; + } catch ( \Exception $e ) { + return new WP_Error( + 'aws_polly_http_error', + esc_html( $e->getMessage() ) + ); + } + } + + /** + * Common entry point for all REST endpoints for this provider. + * + * @param int $post_id The post ID we're processing. + * @param string $route_to_call The name of the route we're going to be processing. + * @param array $args Optional arguments to pass to the route. + * @return array|string|WP_Error + */ + public function rest_endpoint_callback( $post_id, string $route_to_call = '', array $args = [] ) { + if ( ! $post_id || ! get_post( $post_id ) ) { + return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required.', 'classifai' ) ); + } + + $route_to_call = strtolower( $route_to_call ); + $return = ''; + + // Handle all of our routes. + switch ( $route_to_call ) { + case 'synthesize': + $return = $this->synthesize_speech( $post_id, $args ); + break; + } + + return $return; + } + + /** + * Returns the debug information for the provider settings. + * + * @return array + */ + public function get_debug_information(): array { + $settings = $this->feature_instance->get_settings(); + $provider_settings = $settings[ static::ID ]; + $debug_info = []; + + if ( $this->feature_instance instanceof TextToSpeech ) { + $post_types = array_filter( + $settings['post_types'], + function ( $value ) { + return '0' !== $value; + } + ); + + $debug_info[ __( 'Allowed post types', 'classifai' ) ] = implode( ', ', $post_types ); + $debug_info[ __( 'Voice', 'classifai' ) ] = $provider_settings['voice']; + $debug_info[ __( 'Latest response - Voices', 'classifai' ) ] = $this->get_formatted_latest_response( $provider_settings['voices'] ); + } + + return apply_filters( + 'classifai_' . self::ID . '_debug_information', + $debug_info, + $settings, + $this->feature_instance + ); + } + + /** + * Returns aws polly client. + * + * @param array $aws_config AWS configuration array. + * @return \Aws\Polly\PollyClient|null + */ + public function get_polly_client( array $aws_config = array() ) { + $settings = $this->feature_instance->get_settings( static::ID ); + + $default = array( + 'access_key_id' => $settings['access_key_id'] ?? '', + 'secret_access_key' => $settings['secret_access_key'] ?? '', + 'aws_region' => $settings['aws_region'] ?? 'us-east-1', + ); + + $default = wp_parse_args( $aws_config, $default ); + + // Return if credentials don't exist. + if ( empty( $default['access_key_id'] ) || empty( $default['secret_access_key'] ) ) { + return null; + } + + // Set the AWS SDK configuration. + $aws_sdk_config = [ + 'region' => $default['aws_region'] ?? 'us-east-1', + 'version' => 'latest', + 'ua_append' => [ 'request-source/classifai' ], + 'credentials' => [ + 'key' => $default['access_key_id'], + 'secret' => $default['secret_access_key'], + ], + ]; + + $sdk = new Sdk( $aws_sdk_config ); + return $sdk->createPolly(); + } + + /** + * Returns the voice dropdown for the selected engine. + */ + public function get_voice_dropdown() { + if ( ! wp_doing_ajax() ) { + return; + } + + // Nonce check. + if ( ! check_ajax_referer( 'classifai', 'nonce', false ) ) { + $error = new WP_Error( 'classifai_nonce_error', __( 'Nonce could not be verified.', 'classifai' ) ); + wp_send_json_error( $error ); + exit(); + } + + // Set the feature instance if it's not already set. + if ( ! $this->feature_instance instanceof TextToSpeech ) { + $this->feature_instance = new TextToSpeech(); + } + + // Attachment ID check. + $engine = isset( $_POST['engine'] ) ? sanitize_text_field( wp_unslash( $_POST['engine'] ) ) : 'standard'; + $settings = $this->feature_instance->get_settings( static::ID ); + $voices_options = $this->get_voices_select_options( $engine ); + + ob_start(); + $this->feature_instance->render_select( + [ + 'option_index' => static::ID, + 'label_for' => 'voice', + 'options' => $voices_options, + 'default_value' => $settings['voice'], + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, + ] + ); + $voice_dropdown = ob_get_clean(); + + wp_send_json_success( $voice_dropdown ); + } +} diff --git a/includes/Classifai/Services/LanguageProcessing.php b/includes/Classifai/Services/LanguageProcessing.php index 66baadae3..0bfa5e3eb 100644 --- a/includes/Classifai/Services/LanguageProcessing.php +++ b/includes/Classifai/Services/LanguageProcessing.php @@ -45,6 +45,7 @@ public static function get_service_providers(): array { 'Classifai\Providers\Watson\NLU', 'Classifai\Providers\GoogleAI\GeminiAPI', 'Classifai\Providers\Azure\OpenAI', + 'Classifai\Providers\AWS\AmazonPolly', ] ); } diff --git a/readme.txt b/readme.txt index f8de8436d..1a956a583 100644 --- a/readme.txt +++ b/readme.txt @@ -23,7 +23,7 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro * Expand or condense text content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service) or [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) * Generate new images on demand to use in-content or as a featured image using [OpenAI's DALL·E 3 API](https://platform.openai.com/docs/guides/images) * Generate transcripts of audio files using [OpenAI's Whisper API](https://platform.openai.com/docs/guides/speech-to-text) -* Convert text content into audio and output a "read-to-me" feature on the front-end to play this audio using [Microsoft Azure's Text to Speech API](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/text-to-speech) +* Convert text content into audio and output a "read-to-me" feature on the front-end to play this audio using [Microsoft Azure's Text to Speech API](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/text-to-speech) or [Amazon Polly](https://aws.amazon.com/polly/) * Classify post content using [IBM Watson's Natural Language Understanding API](https://www.ibm.com/watson/services/natural-language-understanding/) and [OpenAI's Embedding API](https://platform.openai.com/docs/guides/embeddings) * BETA: Recommend content based on overall site traffic via [Microsoft Azure's AI Personalizer API](https://azure.microsoft.com/en-us/services/cognitive-services/personalizer/) _(note that this service has been deprecated by Microsoft and as such, will no longer work. We are looking to replace this with a new provider to maintain the same functionality)_ * Generate image alt text, image tags, and smartly crop images using [Microsoft Azure's AI Vision API](https://azure.microsoft.com/en-us/services/cognitive-services/computer-vision/) @@ -37,6 +37,7 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro * To utilize the Azure AI Vision Image Processing functionality or Text to Speech Language Processing functionality, you will need an active [Microsoft Azure](https://signup.azure.com/signup) account. * To utilize the Azure OpenAI Language Processing functionality, you will need an active [Microsoft Azure](https://signup.azure.com/signup) account and you will need to [apply](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUNTZBNzRKNlVQSFhZMU9aV09EVzYxWFdORCQlQCN0PWcu) for OpenAI access. * To utilize the Google Gemini Language Processing functionality, you will need an active [Google Gemini](https://ai.google.dev/tutorials/setup) account. +* To utilize the AWS Language Processing functionality, you will need an active [AWS](https://console.aws.amazon.com/) account. == Upgrade Notice == diff --git a/src/js/admin.js b/src/js/admin.js index 158af6ba0..328f258c4 100644 --- a/src/js/admin.js +++ b/src/js/admin.js @@ -368,3 +368,50 @@ document.addEventListener( 'DOMContentLoaded', function () { providerSelectEl.trigger( 'change' ); } ); } )( jQuery ); + +( function ( $ ) { + $( function () { + const engineSelectEl = $( 'select#voice_engine' ); + if ( ! engineSelectEl.length ) { + return; + } + + engineSelectEl.on( 'change', function () { + const engine = $( this ).val(); + $( 'select#voice' ).prop( 'disabled', true ); + $.ajax( { + url: ajaxurl, + type: 'POST', + data: { + action: 'classifai_get_voice_dropdown', + nonce: ClassifAI.ajax_nonce, + engine, + }, + success( response ) { + if ( response.success && response.data ) { + jQuery( '.classifai-aws-polly-voices td' ).html( + response.data + ); + } else { + // eslint-disable-next-line no-console + console.error( response.data ); + } + $( 'select#voice' ).prop( 'disabled', false ); + }, + error( jqXHR, textStatus, errorThrown ) { + // eslint-disable-next-line no-console + console.error( + 'Error: ', + textStatus, + ', Details: ', + errorThrown + ); + $( 'select#voice' ).prop( 'disabled', false ); + }, + } ); + } ); + + // Trigger 'change' on page load. + engineSelectEl.trigger( 'change' ); + } ); +} )( jQuery ); diff --git a/src/js/gutenberg-plugin.js b/src/js/gutenberg-plugin.js index 593449d86..42cc12c50 100644 --- a/src/js/gutenberg-plugin.js +++ b/src/js/gutenberg-plugin.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ import { ReactComponent as icon } from '../../assets/img/block-icon.svg'; import { handleClick } from './helpers'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useSelect, useDispatch, subscribe } from '@wordpress/data'; import { PluginDocumentSettingPanel } from '@wordpress/edit-post'; import { Button, @@ -632,4 +632,34 @@ const ClassifAIPlugin = () => { ); }; +let saveHappened = false; +let showingNotice = false; + +subscribe( () => { + if ( saveHappened === false ) { + saveHappened = wp.data.select( 'core/editor' ).isSavingPost() === true; + } + + if ( + saveHappened && + wp.data.select( 'core/editor' ).isSavingPost() === false && + showingNotice === false + ) { + const meta = wp.data + .select( 'core/editor' ) + .getCurrentPostAttribute( 'meta' ); + if ( meta && meta._classifai_text_to_speech_error ) { + showingNotice = true; + const error = JSON.parse( meta._classifai_text_to_speech_error ); + wp.data + .dispatch( 'core/notices' ) + .createErrorNotice( + `Audio generation failed. Error: ${ error.code } - ${ error.message }` + ); + saveHappened = false; + showingNotice = false; + } + } +} ); + registerPlugin( 'classifai-plugin', { render: ClassifAIPlugin } ); diff --git a/tests/cypress/integration/language-processing/text-to-speech-amazon-polly.test.js b/tests/cypress/integration/language-processing/text-to-speech-amazon-polly.test.js new file mode 100644 index 000000000..27ffccf47 --- /dev/null +++ b/tests/cypress/integration/language-processing/text-to-speech-amazon-polly.test.js @@ -0,0 +1,116 @@ +describe( '[Language Processing] Text to Speech (Amazon Polly) Tests', () => { + before( () => { + cy.login(); + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_text_to_speech_generation' + ); + cy.get( + '#classifai_feature_text_to_speech_generation_post_types_post' + ).check( 'post' ); + cy.get( '#provider' ).select( 'aws_polly' ); + cy.get( '#access_key_id' ).clear(); + cy.get( '#access_key_id' ).type( 'SAMPLE_ACCESS_KEY' ); + cy.get( '#secret_access_key' ).clear(); + cy.get( '#secret_access_key' ).type( 'SAMPLE_SECRET_ACCESS_KEY' ); + cy.get( '#aws_region' ).clear(); + cy.get( '#aws_region' ).type( 'SAMPLE_SECRET_ACCESS_KEY' ); + cy.get( '#status' ).check(); + cy.get( '#submit' ).click(); + + cy.get( '#voice' ).select( 'Aditi' ); + cy.get( '#submit' ).click(); + cy.optInAllFeatures(); + cy.disableClassicEditor(); + } ); + + beforeEach( () => { + cy.login(); + } ); + + it( 'Generates audio from text', () => { + cy.createPost( { + title: 'Text to Speech test', + content: + "This feature uses Amazon Polly's Text to Speech capabilities.", + } ); + + cy.get( 'button[aria-label="Close panel"]' ).click(); + cy.get( 'button[data-label="Post"]' ).click(); + cy.get( '.classifai-panel' ).click(); + cy.get( '#classifai-audio-controls__preview-btn' ).should( 'exist' ); + } ); + + it( 'Audio controls are visible if supported by post type', () => { + cy.visit( '/text-to-speech-test/' ); + cy.get( '.class-post-audio-controls' ).should( 'be.visible' ); + } ); + + it( 'a11y - aria-labels', () => { + cy.visit( '/text-to-speech-test/' ); + cy.get( '.dashicons-controls-play' ).should( 'be.visible' ); + cy.get( '.class-post-audio-controls' ).should( + 'have.attr', + 'aria-label', + 'Play audio' + ); + + cy.get( '.class-post-audio-controls' ).click(); + + cy.get( '.dashicons-controls-play' ).should( 'not.be.visible' ); + cy.get( '.class-post-audio-controls' ).should( + 'have.attr', + 'aria-label', + 'Pause audio' + ); + + cy.get( '.class-post-audio-controls' ).click(); + cy.get( '.dashicons-controls-play' ).should( 'be.visible' ); + cy.get( '.class-post-audio-controls' ).should( + 'have.attr', + 'aria-label', + 'Play audio' + ); + } ); + + it( 'a11y - keyboard accessibility', () => { + cy.visit( '/text-to-speech-test/' ); + cy.get( '.class-post-audio-controls' ) + .tab( { shift: true } ) + .tab() + .type( '{enter}' ); + cy.get( '.dashicons-controls-pause' ).should( 'be.visible' ); + cy.get( '.class-post-audio-controls' ).should( + 'have.attr', + 'aria-label', + 'Pause audio' + ); + + cy.get( '.class-post-audio-controls' ).type( '{enter}' ); + cy.get( '.dashicons-controls-play' ).should( 'be.visible' ); + cy.get( '.class-post-audio-controls' ).should( + 'have.attr', + 'aria-label', + 'Play audio' + ); + } ); + + it( 'Can see the enable button in a post (Classic Editor)', () => { + cy.enableClassicEditor(); + + cy.createClassicPost( { + title: 'Text to Speech test classic', + content: + "This feature uses Amazon Polly's Text to Speech capabilities.", + postType: 'post', + } ); + + cy.get( '#classifai-text-to-speech-meta-box' ).should( 'exist' ); + cy.get( '#classifai_synthesize_speech' ).check(); + cy.get( '#classifai-audio-preview' ).should( 'exist' ); + + cy.visit( '/text-to-speech-test/' ); + cy.get( '.class-post-audio-controls' ).should( 'be.visible' ); + + cy.disableClassicEditor(); + } ); +} ); diff --git a/tests/cypress/integration/language-processing/text-to-speech-microsoft-azure.test.js b/tests/cypress/integration/language-processing/text-to-speech-microsoft-azure.test.js index 17302c216..20f6c6cde 100644 --- a/tests/cypress/integration/language-processing/text-to-speech-microsoft-azure.test.js +++ b/tests/cypress/integration/language-processing/text-to-speech-microsoft-azure.test.js @@ -7,6 +7,7 @@ describe( '[Language Processing] Text to Speech (Microsoft Azure) Tests', () => cy.get( '#classifai_feature_text_to_speech_generation_post_types_post' ).check( 'post' ); + cy.get( '#provider' ).select( 'ms_azure_text_to_speech' ); cy.get( '#endpoint_url' ).clear(); cy.get( '#endpoint_url' ).type( 'https://service.com' ); cy.get( '#api_key' ).type( 'password' ); diff --git a/tests/test-plugin/amazon-polly-voices.json b/tests/test-plugin/amazon-polly-voices.json new file mode 100644 index 000000000..ed459b5f7 --- /dev/null +++ b/tests/test-plugin/amazon-polly-voices.json @@ -0,0 +1,28 @@ +[ + { + "AdditionalLanguageCodes": [ + "hi-IN" + ], + "Gender": "Female", + "Id": "Aditi", + "LanguageCode": "en-IN", + "LanguageName": "Indian English", + "Name": "Aditi", + "SupportedEngines": [ + "standard", + "neural" + ] + }, + { + "AdditionalLanguageCodes": [], + "Gender": "Female", + "Id": "Danielle", + "LanguageCode": "en-US", + "LanguageName": "US English", + "Name": "Danielle", + "SupportedEngines": [ + "long-form", + "neural" + ] + } +] diff --git a/tests/test-plugin/e2e-test-plugin.php b/tests/test-plugin/e2e-test-plugin.php index c6ce7a891..12ee9baa1 100644 --- a/tests/test-plugin/e2e-test-plugin.php +++ b/tests/test-plugin/e2e-test-plugin.php @@ -6,6 +6,10 @@ // Mock the ClassifAI HTTP request calls and provide known response. add_filter( 'pre_http_request', 'classifai_test_mock_http_requests', 10, 3 ); +// Mock the AWS Polly API request calls and provide known response. +add_filter( 'classifai_aws_polly_pre_connect_to_service', 'classifai_mock_aws_polly_connect_to_service' ); +add_filter( 'classifai_aws_polly_pre_synthesize_speech', 'classifai_mock_aws_polly_pre_synthesize_speech' ); + /** * Mock ClassifAI's HTTP requests. * @@ -165,3 +169,14 @@ function () { ); } ); + +// AWS Polly API mock for connect to service. +function classifai_mock_aws_polly_connect_to_service() { + $voices = file_get_contents( __DIR__ . '/amazon-polly-voices.json' ); + return json_decode( $voices, true ); +} + +// AWS Polly API mock for synthesize speech. +function classifai_mock_aws_polly_pre_synthesize_speech() { + return file_get_contents( __DIR__ . '/text-to-speech.txt' ); +}