From 3bac3089cbab8f91daa01a0d63802c1570590027 Mon Sep 17 00:00:00 2001 From: 10upbot on GitHub <10upbot+github@10up.com> Date: Wed, 4 Mar 2020 18:18:21 +0000 Subject: [PATCH] Committing built version of 1b2d4f581af587335031cf8de70471ade562a81c --- README.md | 64 +++- assets/img/menu-icon.svg | 3 + classifai.php | 13 +- composer.json | 14 +- config.php | 2 +- dist/js/admin.min.js | 1 + dist/js/media.min.js | 1 + includes/Classifai/Admin/BulkActions.php | 132 +++++++ includes/Classifai/Admin/Notifications.php | 2 +- includes/Classifai/Admin/SavePostHandler.php | 15 +- .../Classifai/Command/ClassifaiCommand.php | 121 +++++++ .../Classifai/Command/RSSImporterCommand.php | 2 +- includes/Classifai/Helpers.php | 71 +++- includes/Classifai/Plugin.php | 6 +- .../Providers/Azure/ComputerVision.php | 252 ++++++++++--- .../Providers/Azure/SmartCropping.php | 339 ++++++++++++++++++ includes/Classifai/Providers/Provider.php | 13 + includes/Classifai/Providers/Watson/NLU.php | 141 ++++++-- .../Classifai/Services/ImageProcessing.php | 91 +++++ .../Classifai/Services/ServicesManager.php | 3 +- .../Classifai/Taxonomy/TaxonomyFactory.php | 2 +- includes/Classifai/Watson/Classifier.php | 3 +- languages/classifai.pot | 200 ++++++++--- wpacceptance.json | 16 + 24 files changed, 1332 insertions(+), 175 deletions(-) create mode 100644 assets/img/menu-icon.svg create mode 100644 dist/js/admin.min.js create mode 100644 dist/js/media.min.js create mode 100644 includes/Classifai/Admin/BulkActions.php create mode 100644 includes/Classifai/Providers/Azure/SmartCropping.php create mode 100644 wpacceptance.json diff --git a/README.md b/README.md index 91587a099..81c344674 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # ![ClassifAI](https://classifaiplugin.com/wp-content/themes/classifai-theme/assets/img/logo.svg "ClassifAI") > Enhance your WordPress content with Artificial Intelligence and Machine Learning services. -[![Support Level](https://img.shields.io/badge/support-active-green.svg)](#support-level) [![Build Status](https://travis-ci.com/10up/classifai.svg?token=Jy6DFK4YVZbgtyNHcjm5&branch=develop)](https://travis-ci.com/10up/classifai) [![Release Version](https://img.shields.io/github/release/10up/classifai.svg)](https://github.com/10up/classifai/releases/latest) ![WordPress tested up to version](https://img.shields.io/badge/WordPress-v5.2%20tested-success.svg) [![GPLv2 License](https://img.shields.io/github/license/10up/classifai.svg)](https://github.com/10up/classifai/blob/develop/LICENSE.md) +[![Support Level](https://img.shields.io/badge/support-active-green.svg)](#support-level) [![Build Status](https://travis-ci.com/10up/classifai.svg?token=Jy6DFK4YVZbgtyNHcjm5&branch=develop)](https://travis-ci.com/10up/classifai) [![Release Version](https://img.shields.io/github/release/10up/classifai.svg)](https://github.com/10up/classifai/releases/latest) ![WordPress tested up to version](https://img.shields.io/badge/WordPress-v5.3%20tested-success.svg) [![GPLv2 License](https://img.shields.io/github/license/10up/classifai.svg)](https://github.com/10up/classifai/blob/develop/LICENSE.md) ## Table of Contents * [Features](#features) * [Requirements](#requirements) +* [Pricing](#pricing) * [Installation](#installation) -* [Set Up Content Tagging](#set-up-content-tagging-via-ibm-watson) +* [Register ClassifAI account](#register-classifai-account) +* [Set Up Language Processing](#set-up-language-processing-via-ibm-watson) * [Set Up Image Processing](#set-up-image-processing-via-microsoft-azure) * [WP CLI Usage Instructions](#wp-cli-usage-instructions) * [Data Gathering](#data-gathering) @@ -20,15 +22,25 @@ * Classify your content using [IBM Watson's Natural Language Understanding API](https://www.ibm.com/watson/services/natural-language-understanding/) and [Microsoft Azure's Computer Vision API](https://azure.microsoft.com/en-us/services/cognitive-services/computer-vision/) * Supports Watson's [Categories](https://console.bluemix.net/docs/services/natural-language-understanding/index.html#categories), [Keywords](https://console.bluemix.net/docs/services/natural-language-understanding/index.html#keywords), [Concepts](https://console.bluemix.net/docs/services/natural-language-understanding/index.html#concepts) & [Entities](https://console.bluemix.net/docs/services/natural-language-understanding/index.html#entities) and Azure's [Describe Image](https://westus.dev.cognitive.microsoft.com/docs/services/5adf991815e1060e6355ad44/operations/56f91f2e778daf14a499e1fe) * Automatically classify content and images on save +* Manually generate alt text and image tags for images +* [Smartly crop images](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/computervision/generatethumbnail) around a region of interest identified by Computer Vision * Bulk classify content with [WP-CLI](https://wp-cli.org/) ## Requirements * PHP 7.0+ * [WordPress](http://wordpress.org) 5.0+ -* To utilize the Lanaguage Processing functionality, you will need an active [IBM Watson](https://cloud.ibm.com/registration) account. +* To utilize the Language Processing functionality, you will need an active [IBM Watson](https://cloud.ibm.com/registration) account. * To utilize the Image Processing functionality, you will need an active [Microsoft Azure](https://signup.azure.com/signup) account. +## Pricing + +Note that there is no cost to using ClassifAI and that both IBM Watson and Microsoft Azure have free plans for their AI services, but that above those free plans there are paid levels as well. So if you expect to process a high volume of content, then you'll want to review the pricing plans for these services to understand if you'll incur any costs. For the most part, both services' free plans are quite generous and should at least allow for testing ClassifAI to better understand its featureset and could at best allow for totally free usage. + +The service that powers ClassifAI's Language Processing, IBM Watson's Natural Language Understanding ("NLU"), has a ["lite" pricing tier](https://www.ibm.com/cloud/watson-natural-language-understanding/pricing) that offers 30,000 free NLU items per month. + +The service that powers ClassifAI's Image Processing, Microsoft Azure's Computer Vision, has a ["free" pricing tier](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/computer-vision/) that offers 20 transactions per minute and 5,000 transactions per month. + ## Installation #### 1. Download or Clone this repo, install dependencies and build. @@ -37,7 +49,20 @@ #### 2. Activate Plugin -## Set Up Content Tagging (via IBM Watson) +## Register ClassifAI account + +ClassifAI is a sophisticated solution that we want organizations of all shapes and sizes to count on. To keep adopters apprised of major updates and beta testing opportunities, gather feedback, and prioritize common use cases, we're asking for a little bit of information in exchange for a free key. Your information will be kept confidential. + +#### 1. Register for a ClassifAI account +- Register for a free ClassifAI account [here](https://classifaiplugin.com/#cta). +- Check for an email from `ClassifAI Team` which contains the registration key. +- Note that the email will be sent from `opensource@10up.com`, so please whitelist this email address if needed. + +#### 2. Configure ClassifAI API Keys under admin area > ClassifAI +- In the `Registered Email` field, enter the email you used for registration. +- In the `Registration Key` field, enter the registration key from the email in step 1 above. + +## Set Up Language Processing (via IBM Watson) #### 1. Sign up for Watson services - [Register for an IBM Cloud account](https://cloud.ibm.com/registration) or sign into your existing one. @@ -51,7 +76,6 @@ ##### If your credentials contain an API Key, then: - In the `API URL` field enter the URL -- In the `API User` field, enter `apikey`. - Enter your API Key in the `API Key` field. ##### If your credentials contain a username and password, then: @@ -61,20 +85,18 @@ #### 3. Configure Post Types to classify and IBM Watson Features to enable under ClassifAI > Language Processing - Choose which public post types to classify when saved. -- Chose whether to assign category, keyword, entity, and concept as well as the taxonomies used for each. +- Choose whether to assign category, keyword, entity, and concept as well as the taxonomies used for each. #### 4. Save Post or run WP CLI command to batch classify posts ## Set Up Image Processing (via Microsoft Azure) -Note that [Computer Vision](https://docs.microsoft.com/en-us/azure/cognitive-services/computer-vision/home#image-requirements) can analyze images that meet the following requirements: +Note that [Computer 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: - The image must be presented in JPEG, PNG, GIF, or BMP format - The file size of the image must be less than 4 megabytes (MB) - The dimensions of the image must be greater than 50 x 50 pixels - The file must be externally accessible via URL (i.e. local sites and setups that block direct file access will not work out of the box) -Note that Computer Vision has a [free pricing tier](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/computer-vision/) that offers 20 transactions per minute and 5,000 transactions per month. - #### 1. Sign up for Azure services - [Register for a Microsoft Azure account](https://azure.microsoft.com/en-us/free/) or sign into your existing one. - Log into your account and create a new [*Computer Vision*](https://portal.azure.com/#blade/Microsoft_Azure_Marketplace/GalleryFeaturedMenuItemBlade/selectedMenuItemId/CognitiveServices_MP/dontDiscardJourney/true/launchingContext/%7B%22source%22%3A%22Resources%20Microsoft.CognitiveServices%2Faccounts%22%7D/resetMenuId/) Service if you do not already have one. It may take a minute for your account to fully populate with the default resource group to use. @@ -175,6 +197,26 @@ Prints the normalized text that will be sent to the NLU API default: `false` +#### 3. Classify Image + +`$ wp classifai image {image_ids} [--limit=int] [--force]` + +Directly classify images using Azure Computer Vision. + +##### Options + +`--limit=int` + +Limit number of images to classify. + +default: `false` + +`--force` + +Force classifying images regardless of their `alt`. + +default: `false` + ## Data Gathering ClassifAI connects your WordPress site directly to your account with specific service provider(s) (e.g. Microsoft Azure AI, IBM Watson), so no data is gathered by 10up. The data gathered in our [registration form](https://classifaiplugin.com/#cta) is used simply to stay in touch with users so we can provide product updates and news. More information is available in the [Privacy Policy on ClassifAIplugin.com](https://drive.google.com/open?id=1Hn4XEWmNGqeMzLqnS7Uru2Hl2vJeLc7cI7225ztThgQ). @@ -185,11 +227,11 @@ ClassifAI connects your WordPress site directly to your account with specific se ## Changelog -A complete listing of all notable changes to Distributor are documented in [CHANGELOG.md](https://github.com/10up/classifai/blob/develop/CHANGELOG.md). +A complete listing of all notable changes to ClassifAI are documented in [CHANGELOG.md](https://github.com/10up/classifai/blob/develop/CHANGELOG.md). ## Contributing -Please read [CODE_OF_CONDUCT.md](https://github.com/10up/classifai/blob/develop/CODE_OF_CONDUCT.md) for details on our code of conduct and [CONTRIBUTING.md](https://github.com/10up/classifai/blob/develop/CONTRIBUTING.md) for details on the process for submitting pull requests to us. +Please read [CODE_OF_CONDUCT.md](https://github.com/10up/classifai/blob/develop/CODE_OF_CONDUCT.md) for details on our code of conduct, [CONTRIBUTING.md](https://github.com/10up/classifai/blob/develop/CONTRIBUTING.md) for details on the process for submitting pull requests to us, and [CREDITS.md](https://github.com/10up/classifai/blob/develop/CREDITS.md) for a listing of maintainers, contributors, and libraries for ClassifAI. ## Like what you see? diff --git a/assets/img/menu-icon.svg b/assets/img/menu-icon.svg new file mode 100644 index 000000000..7c92590b4 --- /dev/null +++ b/assets/img/menu-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/classifai.php b/classifai.php index b8c484c36..cea9e92e4 100644 --- a/classifai.php +++ b/classifai.php @@ -3,7 +3,7 @@ * Plugin Name: ClassifAI * Plugin URI: https://github.com/10up/classifai * Description: Enhance your WordPress content with Artificial Intelligence and Machine Learning services. - * Version: 1.4.0 + * Version: 1.5.0 * Author: 10up * Author URI: https://10up.com * License: GPLv2 @@ -22,7 +22,16 @@ function() { if ( version_compare( PHP_VERSION, '7.0.0', '<' ) ) { wp_die( - esc_html__( 'ClassifAI requires PHP version 7.', 'classifai' ), + sprintf( + wp_kses( + /* translators: PHP Update guide URL */ + __( 'ClassifAI requires PHP version 7. Click here to learn how to update your PHP version.', 'classifai' ), + array( + 'a' => array( 'href' => array() ), + ) + ), + esc_url( 'https://wordpress.org/support/update-php/' ) + ), esc_html__( 'Error Activating', 'classifai' ) ); } diff --git a/composer.json b/composer.json index e35c99cf2..398d3e463 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "10up/classifai", - "description": "Classifies WordPress content using IBM Watson NLU API", + "description": "Enhance your WordPress content with Artificial Intelligence and Machine Learning services.", "authors": [ { "name": "10up", @@ -10,8 +10,8 @@ "type": "wordpress-plugin", "license": "GPLv2", "require": { - "php": ">=7.0", - "yahnis-elsts/plugin-update-checker": "^4.4" + "php": ">=7.2", + "yahnis-elsts/plugin-update-checker": "4.6" }, "autoload": { "psr-4": { @@ -22,7 +22,9 @@ ] }, "require-dev": { - "phpunit/phpunit": "~5.7.0", - "10up/phpcs-composer": "dev-master" - } + "phpunit/phpunit": "^7.5", + "10up/phpcs-composer": "dev-master", + "10up/wpacceptance": "dev-master" + }, + "minimum-stability": "dev" } diff --git a/config.php b/config.php index 1d4d6005a..691b931fb 100644 --- a/config.php +++ b/config.php @@ -4,7 +4,7 @@ * declared here instead of a Class. */ -$plugin_version = '1.4.0'; +$plugin_version = '1.5.0'; if ( file_exists( __DIR__ . '/.commit' ) ) { $plugin_version .= '-' . file_get_contents( __DIR__ . '/.commit' ); diff --git a/dist/js/admin.min.js b/dist/js/admin.min.js new file mode 100644 index 000000000..bfc0edf72 --- /dev/null +++ b/dist/js/admin.min.js @@ -0,0 +1 @@ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=2)}({2:function(e,t){function n(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=[],r=!0,i=!1,o=void 0;try{for(var a,l=e[Symbol.iterator]();!(r=(a=l.next()).done)&&(n.push(a.value),!t||n.length!==t);r=!0);}catch(e){i=!0,o=e}finally{try{r||null==l.return||l.return()}finally{if(i)throw o}}return n}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}!function(){var e=document.getElementById("classifai-waston-cred-toggle"),t=document.getElementById("classifai-settings-watson_username");if(null!==e&&null!==t){var r=t.closest("tr"),i=n(document.getElementById("classifai-settings-watson_password").closest("tr").getElementsByTagName("label"),1)[0];null!==e&&e.addEventListener("click",function(n){if(n.preventDefault(),r.classList.toggle("hidden"),r.classList.contains("hidden"))return e.innerText=ClassifAI.use_password,i.innerText=ClassifAI.api_key,void(t.value="apikey");e.innerText=ClassifAI.use_key,i.innerText=ClassifAI.api_password})}}()}}); \ No newline at end of file diff --git a/dist/js/media.min.js b/dist/js/media.min.js new file mode 100644 index 000000000..4594a1894 --- /dev/null +++ b/dist/js/media.min.js @@ -0,0 +1 @@ +!function(t){var e={};function n(r){if(e[r])return e[r].exports;var a=e[r]={i:r,l:!1,exports:{}};return t[r].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var a in t)n.d(r,a,function(e){return t[e]}.bind(null,a));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=1)}([,function(t,e){function n(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,a=!1,i=void 0;try{for(var o,u=t[Symbol.iterator]();!(r=(o=u.next()).done)&&(n.push(o.value),!e||n.length!==e);r=!0);}catch(t){a=!0,i=t}finally{try{r||null==u.return||u.return()}finally{if(a)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var r,a;r=jQuery,a=function(t){var e=t.button,r=t.endpoint,a=t.callback,i=void 0!==a&&a,o=e.getAttribute("data-id"),u=n(e.parentNode.getElementsByClassName("spinner"),1)[0],c=wp.i18n.__;e.setAttribute("disabled","disabled"),u.style.display="inline-block",u.classList.add("is-active");var l="".concat(r).concat(o);wp.apiRequest({path:l}).then(function(t){e.removeAttribute("disabled"),u.style.display="none",u.classList.remove("is-active"),e.textContent=c("Rescan","classifai"),i&&i(t)})},r(document).ready(function(){wp.media.frame&&wp.media.frame.on("edit:attachment",function(){var t=document.getElementById("classifai-rescan-alt-tags"),e=document.getElementById("classifai-rescan-image-tags");t.addEventListener("click",function(t){return a({button:t.target,endpoint:"/classifai/v1/alt-tags/",callback:function(t){t&&(document.getElementById("attachment-details-two-column-alt-text").value=t)}})}),e.addEventListener("click",function(t){return a({button:t.target,endpoint:"/classifai/v1/image-tags/"})})})})}]); \ No newline at end of file diff --git a/includes/Classifai/Admin/BulkActions.php b/includes/Classifai/Admin/BulkActions.php new file mode 100644 index 000000000..6b1ae9c1c --- /dev/null +++ b/includes/Classifai/Admin/BulkActions.php @@ -0,0 +1,132 @@ +save_post_handler = new SavePostHandler(); + + foreach ( $post_types as $post_type ) { + add_filter( "bulk_actions-edit-$post_type", [ $this, 'register_bulk_actions' ] ); + add_filter( "handle_bulk_actions-edit-$post_type", [ $this, 'bulk_action_handler' ], 10, 3 ); + + if ( is_post_type_hierarchical( $post_type ) ) { + add_action( 'page_row_actions', [ $this, 'register_row_action' ], 10, 2 ); + } else { + add_action( 'post_row_actions', [ $this, 'register_row_action' ], 10, 2 ); + } + } + + add_action( 'admin_notices', [ $this, 'bulk_action_admin_notice' ] ); + } + + /** + * Register Classifai bulk action. + * + * @param array $bulk_actions Current bulk actions. + * + * @return array + */ + public function register_bulk_actions( $bulk_actions ) { + $bulk_actions['classify'] = __( 'Classify', 'classifai' ); + return $bulk_actions; + } + + /** + * Handle bulk actions. + * + * @param string $redirect_to Redirect URL after bulk actions. + * @param string $doaction Action ID. + * @param array $post_ids Post ids to apply bulk actions to. + * + * @return string + */ + public function bulk_action_handler( $redirect_to, $doaction, $post_ids ) { + if ( 'classify' !== $doaction ) { + return $redirect_to; + } + foreach ( $post_ids as $post_id ) { + $this->save_post_handler->classify( $post_id ); + } + $redirect_to = add_query_arg( 'bulk_classified', count( $post_ids ), $redirect_to ); + return $redirect_to; + } + + /** + * Display an admin notice after classifying posts in bulk. + */ + public function bulk_action_admin_notice() { + if ( empty( $_REQUEST['bulk_classified'] ) ) { + return; + } + + $classified_posts_count = intval( $_REQUEST['bulk_classified'] ); + + $output = '

'; + $output .= sprintf( + _n( + 'Classified %s post.', + 'Classified %s posts.', + $classified_posts_count, + 'classifai' + ), + $classified_posts_count + ); + $output .= '

'; + echo wp_kses( + $output, + [ + 'div' => [ + 'class' => [], + 'id' => [], + ], + 'p' => [], + ] + ); + } + + /** + * Register Classifai row action. + * + * @param array $actions Current row actions. + * @param \WP_Post $post Post object. + * + * @return array + */ + public function register_row_action( $actions, $post ) { + $actions['classify'] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( 'edit.php?action=classify&ids=' . $post->ID ), 'bulk-posts' ) ), + esc_html__( 'Classify', 'classifai' ) + ); + + return $actions; + } +} diff --git a/includes/Classifai/Admin/Notifications.php b/includes/Classifai/Admin/Notifications.php index 171bde423..ba96333a7 100644 --- a/includes/Classifai/Admin/Notifications.php +++ b/includes/Classifai/Admin/Notifications.php @@ -50,7 +50,7 @@ public function maybe_render_notices() { if ( $needs_setup ) { printf( '

' . esc_html__( 'ClassifAI requires setup', 'classifai' ) . '

', - esc_url( admin_url( 'options-general.php?page=classifai_settings' ) ) + esc_url( admin_url( 'admin.php?page=classifai_settings' ) ) ); delete_transient( 'classifai_activation_notice' ); } diff --git a/includes/Classifai/Admin/SavePostHandler.php b/includes/Classifai/Admin/SavePostHandler.php index bb1454d6b..e53e5d78d 100644 --- a/includes/Classifai/Admin/SavePostHandler.php +++ b/includes/Classifai/Admin/SavePostHandler.php @@ -188,9 +188,20 @@ public function show_error_if() { * @return bool */ public function is_rest_route() { - if ( isset( $_SERVER['REQUEST_URI'] ) && false !== strpos( $_SERVER['REQUEST_URI'], 'wp-json/wp/v2/post' ) ) { - return true; + + if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { + return false; } + + // Support custom post types with custom rest base. + $rest_bases = apply_filters( 'classifai_rest_bases', array( 'posts', 'pages' ) ); + + foreach ( $rest_bases as $rest_base ) { + if ( false !== strpos( $_SERVER['REQUEST_URI'], 'wp-json/wp/v2/' . $rest_base ) ) { + return true; + } + } + return false; } diff --git a/includes/Classifai/Command/ClassifaiCommand.php b/includes/Classifai/Command/ClassifaiCommand.php index daa399232..0032e036e 100644 --- a/includes/Classifai/Command/ClassifaiCommand.php +++ b/includes/Classifai/Command/ClassifaiCommand.php @@ -6,6 +6,7 @@ use Classifai\Watson\Classifier; use Classifai\Watson\Normalizer; use Classifai\PostClassifier; +use Classifai\Providers\Azure\ComputerVision; /** * ClassifaiCommand is the command line interface of the ClassifAI plugin. @@ -205,6 +206,79 @@ public function text( $args = [], $opts = [] ) { } } + + /** + * Batch classifies attachments(s) using the current ClassifAI configuration. + * + * ## Options + * + * [] + * : Comma delimeted Attachment IDs to classify + * + * [--limit=] + * : Limit classification to N attachments. Default 100. + * + * [--skip=] + * : Skip first N attachments. Default false. + * + * [--force] + * : Force classification to N attachments. Default false. + * + * @param array $args Arguments. + * @param array $opts Options. + */ + public function image( $args = [], $opts = [] ) { + $default_opts = [ + 'limit' => false, + 'force' => false, + ]; + + $opts = wp_parse_args( $opts, $default_opts ); + + if ( ! empty( $args[0] ) ) { + $attachment_ids = explode( ',', $args[0] ); + } else { + $attachment_ids = $this->get_attachment_to_classify( $opts ); + } + + $total = count( $attachment_ids ); + $classifier = new ComputerVision( false ); + + if ( empty( $total ) ) { + return \WP_CLI::log( 'No images to classify.' ); + } + + $limit_total = $total; + if ( $opts['limit'] ) { + $limit_total = min( $total, intval( $opts['limit'] ) ); + } + + $errors = []; + $message = "Classifying $limit_total images ..."; + $progress_bar = \WP_CLI\Utils\make_progress_bar( $message, $limit_total ); + + for ( $index = 0; $index < $limit_total; $index++ ) { + $attachment_id = $attachment_ids[ $index ]; + + $progress_bar->tick(); + + $current_meta = wp_get_attachment_metadata( $attachment_id ); + \WP_CLI::line( 'Processing ' . $attachment_id ); + $classifier->process_image( $current_meta, $attachment_id ); + } + + $progress_bar->finish(); + + $total_errors = count( $errors ); + $total_success = $total - $total_errors; + + \WP_CLI::success( "Classified $total_success images, $total_errors errors." ); + + foreach ( $errors as $attachment_id => $error ) { + \WP_CLI::log( $attachment_id . ': ' . $error->get_error_code() . ' - ' . $error->get_error_message() ); + } + } + /** * Prints the Basic Auth header based on credentials configured in * the plugin. @@ -279,6 +353,53 @@ private function get_posts_to_classify( $opts = [] ) { return $posts; } + /** + * Returns the list of attachment ids to classify with Azure Compute Vision + * + * @param array $opts Options from WP CLI. + * @return array + */ + private function get_attachment_to_classify( $opts = [] ) { + $limit = is_numeric( $opts['limit'] ) ? $opts['limit'] : 100; + + $query_params = [ + 'post_type' => 'attachment', + 'post_mime_type' => array( 'image/jpeg', 'image/png', 'image/gif', 'image/bmp' ), + 'post_status' => 'any', + 'fields' => 'ids', + 'posts_per_page' => $limit, + ]; + + if ( ! empty( $opts['skip'] ) ) { + $query_params['offset'] = $opts['skip']; + } + + if ( ! $opts['force'] ) { + $query_params['meta_query'] = [ + 'relation' => 'OR', + [ + 'key' => '_wp_attachment_image_alt', + 'compare' => 'NOT EXISTS', + 'value' => '', + ], + [ + 'key' => '_wp_attachment_image_alt', + 'compare' => '=', + 'value' => '', + ], + ]; + } + + \WP_CLI::log( 'Fetching images to classify ...' ); + + $query = new \WP_Query( $query_params ); + $images = $query->posts; + + \WP_CLI::log( 'Fetching images to classify ... DONE (' . count( $images ) . ')' ); + + return $images; + } + /** * TODO: gc * diff --git a/includes/Classifai/Command/RSSImporterCommand.php b/includes/Classifai/Command/RSSImporterCommand.php index 3851381ac..68bb3cc44 100644 --- a/includes/Classifai/Command/RSSImporterCommand.php +++ b/includes/Classifai/Command/RSSImporterCommand.php @@ -61,7 +61,7 @@ public function import_post( $info, $source ) { return false; } - $post_date_gmt = date( 'Y-m-d H:i:s', strtotime( $info['date'] ) ); + $post_date_gmt = gmdate( 'Y-m-d H:i:s', strtotime( $info['date'] ) ); $post = [ 'post_title' => $info['title'], diff --git a/includes/Classifai/Helpers.php b/includes/Classifai/Helpers.php index 9d3ad68c2..93f4e3a09 100644 --- a/includes/Classifai/Helpers.php +++ b/includes/Classifai/Helpers.php @@ -143,7 +143,7 @@ function get_watson_username() { if ( ! empty( $creds['watson_username'] ) ) { return $creds['watson_username']; - } else if ( defined( 'WATSON_USERNAME' ) ) { + } elseif ( defined( 'WATSON_USERNAME' ) ) { return WATSON_USERNAME; } else { return ''; @@ -164,7 +164,7 @@ function get_watson_password() { if ( ! empty( $creds['watson_password'] ) ) { return $creds['watson_password']; - } else if ( defined( 'WATSON_PASSWORD' ) ) { + } elseif ( defined( 'WATSON_PASSWORD' ) ) { return WATSON_PASSWORD; } else { return ''; @@ -302,3 +302,70 @@ function get_feature_taxonomy( $feature ) { */ return apply_filters( 'classifai_taxonomy_for_feature', $taxonomy, $feature ); } + +/** + * Provides the max filesize for the Computer Vision service. + * + * @since 1.4.0 + * + * @return int + */ +function computer_vision_max_filesize() { + /** + * Filters the Computer Vision maximum allowed filesize. + * + * @param int Default 4MB. + */ + return apply_filters( 'classifai_computer_vision_max_filesize', 4 * MB_IN_BYTES ); // 4MB default. +} + +/** + * Callback for sorting images by width plus height, descending. + * + * @since 1.5.0 + * + * @param array $size_1 Associative array containing width and height values. + * @param array $size_2 Associative array containing width and height values. + * @return int Returns -1 if $size_1 is larger, 1 if $size_2 is larger, and 0 if they are equal. + */ +function sort_images_by_size_cb( $size_1, $size_2 ) { + $size_1_total = $size_1['width'] + $size_1['height']; + $size_2_total = $size_2['width'] + $size_2['height']; + + if ( $size_1_total === $size_2_total ) { + return 0; + } + + return $size_1_total > $size_2_total ? -1 : 1; +} + +/** + * Retrieves the URL of the largest version of an attachment image lower than a specified max size. + * + * @since 1.4.0 + * + * @param string $full_image The path to the full-sized image source file. + * @param string $full_url The URL of the full-sized image. + * @param array $sizes Intermediate size data from attachment meta. + * @param int $max The maximum acceptable size. + * @return string|null The image URL, or null if no acceptable image found. + */ +function get_largest_acceptable_image_url( $full_image, $full_url, $sizes, $max = MB_IN_BYTES ) { + $file_size = @filesize( $full_image ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + if ( $file_size && $max >= $file_size ) { + return $full_url; + } + + usort( $sizes, __NAMESPACE__ . '\sort_images_by_size_cb' ); + + foreach ( $sizes as $size ) { + $sized_file = str_replace( basename( $full_image ), $size['file'], $full_image ); + $file_size = @filesize( $sized_file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + + if ( $file_size && $max >= $file_size ) { + return str_replace( basename( $full_url ), $size['file'], $full_url ); + } + } + + return null; +} diff --git a/includes/Classifai/Plugin.php b/includes/Classifai/Plugin.php index 00d9e0992..991c2e815 100644 --- a/includes/Classifai/Plugin.php +++ b/includes/Classifai/Plugin.php @@ -53,8 +53,9 @@ public function init() { $post_type, '_classifai_error', [ - 'show_in_rest' => true, - 'single' => true, + 'show_in_rest' => true, + 'single' => true, + 'auth_callback' => '__return_true', ] ); } @@ -105,6 +106,7 @@ public function init_admin_helpers() { $this->admin_helpers = [ 'notifications' => new Admin\Notifications(), 'debug_info' => new Admin\DebugInfo(), + 'bulk_actions' => new Admin\BulkActions(), ]; foreach ( $this->admin_helpers as $instance ) { diff --git a/includes/Classifai/Providers/Azure/ComputerVision.php b/includes/Classifai/Providers/Azure/ComputerVision.php index 7ef7876a0..24b211c42 100644 --- a/includes/Classifai/Providers/Azure/ComputerVision.php +++ b/includes/Classifai/Providers/Azure/ComputerVision.php @@ -7,6 +7,9 @@ use Classifai\Providers\Provider; +use function Classifai\computer_vision_max_filesize; +use function Classifai\get_largest_acceptable_image_url; + class ComputerVision extends Provider { /** @@ -48,6 +51,7 @@ public function can_register() { if ( empty( $options ) ) { return false; } + return true; } @@ -56,24 +60,114 @@ public function can_register() { */ public function register() { add_filter( 'wp_generate_attachment_metadata', [ $this, 'process_image' ], 10, 2 ); + add_action( 'add_meta_boxes_attachment', [ $this, 'setup_attachment_meta_box' ] ); + add_action( 'edit_attachment', [ $this, 'maybe_rescan_image' ] ); + add_filter( 'posts_clauses', [ $this, 'filter_attachment_query_keywords' ], 10, 1 ); + add_filter( 'wp_generate_attachment_metadata', [ $this, 'smart_crop_image' ], 8, 2 ); + add_filter( 'wp_generate_attachment_metadata', [ $this, 'generate_image_alt_tags' ], 8, 2 ); add_filter( 'posts_clauses', [ $this, 'filter_attachment_query_keywords' ], 10, 1 ); } + /** + * Adds a meta box for rescanning options if the settings are configured + */ + public function setup_attachment_meta_box() { + add_meta_box( + 'attachment_meta_box', + __( 'Azure Computer Vision Scan' ), + [ $this, 'attachment_data_meta_box' ], + 'attachment', + 'side', + 'high' + ); + } + + /** + * Display meta data + * + * @param \WP_Post $post The post object. + */ + public function attachment_data_meta_box( $post ) { + $captions = get_post_meta( $post->ID, '_wp_attachment_image_alt', true ) ? __( 'Rescan Captions', 'classifai' ) : __( 'Generate Captions', 'classifai' ); + $tags = ! empty( wp_get_object_terms( $post->ID, 'classifai-image-tags' ) ) ? __( 'Rescan Tags', 'classifai' ) : __( 'Generate Tags', 'classifai' ); + ?> +
+
+ +
+
+ +
+
+ scan_image( $image_url ); + if ( ! is_wp_error( $image_scan ) ) { + // Are we updating the captions? + if ( filter_input( INPUT_POST, 'rescan-captions' ) && isset( $image_scan->description->captions ) ) { + $this->generate_alt_tags( $image_scan->description->captions, $attachment_id ); + } + // Are we updating the tags? + if ( filter_input( INPUT_POST, 'rescan-tags' ) && isset( $image_scan->tags ) ) { + $this->generate_image_tags( $image_scan->tags, $attachment_id ); + } + } + } /** - * Provides the max filesize for the ComputerVision service. + * Adds smart-cropped image thumbnails to the attachment metadata. * - * @return int + * @since 1.5.0 + * @filter wp_generate_attachment_metadata * - * @since 1.4.0 + * @param array $metadata Attachment metadata. + * @param int $attachment_id Attachment ID. + * @return array Filtered attachment metadata. */ - public function get_max_filesize() { + public function smart_crop_image( $metadata, $attachment_id ) { + $settings = $this->get_settings(); + + if ( ! is_array( $metadata ) || ! is_array( $settings ) ) { + return $metadata; + } + + $should_smart_crop = isset( $settings['enable_smart_cropping'] ) && '1' === $settings['enable_smart_cropping']; + /** - * Filters the ComputerVision maximum allowed filesize. + * Filters whether to apply smart cropping to the current image. + * + * @since 1.5.0 * - * @param int Default 4MB. + * @param boolean Whether to apply smart cropping. The default value is set in ComputerVision settings. + * @param array Image metadata. + * @param int The attachment ID. */ - return apply_filters( 'classifai_computervision_max_filesize', 4 * MB_IN_BYTES ); // 4MB default. + if ( ! apply_filters( 'classifai_should_smart_crop_image', $should_smart_crop, $metadata, $attachment_id ) ) { + return $metadata; + } + + // Direct file system access is required for the current implementation of this feature. + $access_type = get_filesystem_method(); + if ( 'direct' !== $access_type || ! WP_Filesystem() ) { + return $metadata; + } + + $smart_cropping = new SmartCropping( $settings ); + + return $smart_cropping->generate_attachment_metadata( $metadata, intval( $attachment_id ) ); } /** @@ -84,7 +178,7 @@ public function get_max_filesize() { * * @return mixed */ - public function process_image( $metadata, $attachment_id ) { + public function generate_image_alt_tags( $metadata, $attachment_id ) { $settings = $this->get_settings(); if ( @@ -92,10 +186,11 @@ public function process_image( $metadata, $attachment_id ) { 'no' !== $settings['enable_image_captions'] ) { if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) { - $image_url = $this->get_largest_acceptable_image_url( + $image_url = get_largest_acceptable_image_url( get_attached_file( $attachment_id ), wp_get_attachment_url( $attachment_id, 'full' ), - $metadata['sizes'] + $metadata['sizes'], + computer_vision_max_filesize() ); } else { $image_url = wp_get_attachment_url( $attachment_id, 'full' ); @@ -103,6 +198,7 @@ public function process_image( $metadata, $attachment_id ) { if ( ! empty( $image_url ) ) { $image_scan = $this->scan_image( $image_url ); + set_transient( 'classifai_azure_computer_vision_image_scan_latest_response', $image_scan, DAY_IN_SECONDS * 30 ); if ( ! is_wp_error( $image_scan ) ) { // Check for captions if ( isset( $image_scan->description->captions ) ) { @@ -121,48 +217,6 @@ public function process_image( $metadata, $attachment_id ) { return $metadata; } - /** - * Retrieves the URL of the largest version of an attachment image accepted by the ComputerVision service. - * - * @param string $full_image The path to the full-sized image source file. - * @param string $full_url The URL of the full-sized image. - * @param array $sizes Intermediate size data from attachment meta. - * @return string|null The image URL, or null if no acceptable image found. - * - * @since 1.4.0 - */ - public function get_largest_acceptable_image_url( $full_image, $full_url, $sizes ) { - $file_size = @filesize( $full_image ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - if ( $file_size && $this->get_max_filesize() >= $file_size ) { - return $full_url; - } - - // Sort the image sizes in order of total width + height, descending. - $sort_sizes = function( $size_1, $size_2 ) { - $size_1_total = $size_1['width'] + $size_1['height']; - $size_2_total = $size_2['width'] + $size_2['height']; - - if ( $size_1_total === $size_2_total ) { - return 0; - } - - return $size_1_total > $size_2_total ? -1 : 1; - }; - - usort( $sizes, $sort_sizes ); - - foreach ( $sizes as $size ) { - $sized_file = str_replace( basename( $full_image ), $size['file'], $full_image ); - $file_size = @filesize( $sized_file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - - if ( $file_size && $this->get_max_filesize() >= $file_size ) { - return str_replace( basename( $full_url ), $size['file'], $full_url ); - } - } - - return null; - } - /** * Scan the image and return the captions. * @@ -224,6 +278,7 @@ protected function prep_api_url() { * @param int $attachment_id Post ID for the attachment. */ protected function generate_alt_tags( $captions, $attachment_id ) { + $rtn = ''; /** * Filter the captions returned from the API. * @@ -238,6 +293,7 @@ protected function generate_alt_tags( $captions, $attachment_id ) { // Save the first caption as the alt text if it passes the threshold. if ( $captions[0]->confidence * 100 > $threshold ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', $captions[0]->text ); + $rtn = $captions[0]->text; } else { /** * Fire an action if there were no captions added. @@ -249,6 +305,8 @@ protected function generate_alt_tags( $captions, $attachment_id ) { } // Save all the results for later. update_post_meta( $attachment_id, 'classifai_computer_vision_captions', $captions ); + // return the caption or empty string + return $rtn; } } @@ -392,8 +450,25 @@ public function setup_fields_sections() { $this->get_option_name(), $this->get_option_name(), [ - 'label_for' => 'image_tag_taxonomy', - 'options' => $options, + 'label_for' => 'image_tag_taxonomy', + 'options' => $options, + ] + ); + + add_settings_field( + 'enable-smart-cropping', + esc_html__( 'Enable smart cropping', 'classifai' ), + [ $this, 'render_input' ], + $this->get_option_name(), + $this->get_option_name(), + [ + 'label_for' => 'enable_smart_cropping', + 'input_type' => 'checkbox', + 'default_value' => false, + 'description' => __( + 'Crop images around a region of interest identified by ComputerVision', + 'classifai' + ), ] ); } @@ -457,6 +532,9 @@ public function sanitize_settings( $settings ) { $new_settings['image_tag_taxonomy'] = $settings['image_tag_taxonomy']; } + $smart_cropping_enabled = isset( $settings['enable_smart_cropping'] ) ? '1' : 'no'; + $new_settings['enable_smart_cropping'] = $smart_cropping_enabled; + return $new_settings; } @@ -497,6 +575,7 @@ protected function authenticate_credentials( $url, $api_key ) { * Provides debug information related to the provider. * * @param null|array $settings Settings array. If empty, settings will be retrieved. + * @return array Keyed array of debug information. * @since 1.4.0 */ public function get_provider_debug_information( $settings = null ) { @@ -507,12 +586,33 @@ public function get_provider_debug_information( $settings = null ) { $authenticated = 1 === intval( $settings['authenticated'] ?? 0 ); return [ - __( 'Authenticated', 'classifai' ) => $authenticated ? __( 'yes', 'classifai' ) : __( 'no', 'classifai' ), - __( 'API URL', 'classifai' ) => $settings['url'] ?? '', - __( 'Caption threshold', 'classifai' ) => $settings['caption_threshold'] ?? null, + __( 'Authenticated', 'classifai' ) => $authenticated ? __( 'yes', 'classifai' ) : __( 'no', 'classifai' ), + __( 'API URL', 'classifai' ) => $settings['url'] ?? '', + __( 'Caption threshold', 'classifai' ) => $settings['caption_threshold'] ?? null, + __( 'Latest response - Image Scan', 'classifai' ) => $this->get_formatted_latest_response( get_transient( 'classifai_azure_computer_vision_image_scan_latest_response' ) ), + __( 'Latest response - Smart Cropping', 'classifai' ) => $this->get_formatted_latest_response( get_transient( 'classifai_azure_computer_vision_smart_cropping_latest_response' ) ), ]; } + /** + * Format the result of most recent request. + * + * @param mixed $data Response data to format. + * + * @return string + */ + private function get_formatted_latest_response( $data ) { + if ( ! $data ) { + return __( 'N/A', 'classifai' ); + } + + if ( is_wp_error( $data ) ) { + return $data->get_error_message(); + } + + return preg_replace( '/,"/', ', "', wp_json_encode( $data ) ); + } + /** * Filter the SQL clauses of an attachment query to include tags and alt text. * @@ -541,4 +641,42 @@ public function filter_attachment_query_keywords( $clauses ) { return $clauses; } + + /** + * Common entry point for all REST endpoints for this provider. + * This is called by the Service. + * + * @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. + * + * @return mixed + */ + public function rest_endpoint_callback( $post_id, $route_to_call ) { + $metadata = wp_get_attachment_metadata( $post_id ); + $image_url = get_largest_acceptable_image_url( + get_attached_file( $post_id ), + wp_get_attachment_url( $post_id ), + $metadata['sizes'] + ); + $image_scan_results = $this->scan_image( $image_url ); + + if ( is_wp_error( $image_scan_results ) ) { + return $image_scan_results; + } + + switch ( $route_to_call ) { + case 'alt-tags': + if ( isset( $image_scan_results->description->captions ) ) { + // Process the captions. + return $this->generate_alt_tags( $image_scan_results->description->captions, $post_id ); + } + break; + case 'image-tags': + if ( isset( $image_scan_results->tags ) ) { + // Process the tags. + return $this->generate_image_tags( $image_scan_results->tags, $post_id ); + } + break; + } + } } diff --git a/includes/Classifai/Providers/Azure/SmartCropping.php b/includes/Classifai/Providers/Azure/SmartCropping.php new file mode 100644 index 000000000..5e8692294 --- /dev/null +++ b/includes/Classifai/Providers/Azure/SmartCropping.php @@ -0,0 +1,339 @@ +settings = $settings; + } + + /** + * Provides the global WP_Filesystem_Base class instance. + * + * @since 1.5.0 + * + * @return WP_Filesystem_Base + */ + public function get_wp_filesystem() { + global $wp_filesystem; + + if ( is_null( $this->wp_filesystem ) ) { + if ( ! $wp_filesystem ) { + WP_Filesystem(); // Initiates the global. + } + + $this->wp_filesystem = $wp_filesystem; + } + + /** + * Filters the filesystem class instance used to save image files. + * + * @since 1.5.0 + * + * @param WP_Filesystem_Base + */ + return apply_filters( 'classifai_smart_crop_wp_filesystem', $this->wp_filesystem ); + } + + /** + * Provides the maximum allowable width or height in pixels accepted by the generateThumbnail endpoint. + * + * @since 1.5.0 + * @see https://docs.microsoft.com/en-us/rest/api/cognitiveservices/computervision/generatethumbnail/generatethumbnail#uri-parameters + * + * @return int + */ + public function get_max_pixel_dimension() { + /** + * Filters the maximum allowable width or height of an image to be cropped. Default 1024. + * + * @param int The width/height in pixels. + */ + return apply_filters( 'classifai_smart_crop_max_pixel_dimension', 1024 ); + } + + /** + * Returns whether smart cropping should be applied to images of a given size. + * + * @since 1.5.0 + * + * @param string $size An image size. + * @return boolean + */ + public function should_crop( $size ) { + if ( 'thumbnail' === $size ) { + return boolval( get_option( 'thumbnail_crop', false ) ); + } + + $image_sizes = wp_get_additional_image_sizes(); + if ( ! isset( $image_sizes[ $size ] ) + || ! isset( $image_sizes[ $size ]['height'] ) + || ! isset( $image_sizes[ $size ]['width'] ) ) { + return false; + } + + // If positions are specified in the add_image_size crop argument, as indicated by the crop field being an + // array, then that should take priority and smart cropping should not run. + if ( is_array( $image_sizes[ $size ]['crop'] ) ) { + $return = false; + } else { + $return = boolval( $image_sizes[ $size ]['crop'] ); + } + + $max_pixels = $this->get_max_pixel_dimension(); + if ( $max_pixels < $image_sizes[ $size ]['height'] || $max_pixels < $image_sizes[ $size ]['width'] ) { + $return = false; + } + + /** + * Filters whether to smart crop images of a given size. + * + * @since 1.5.0 + * + * @param boolean Whether non-position-based cropping was opted into when registering the image size. + * @param string The image size. + */ + return apply_filters( 'classifai_should_crop_size', $return, $size ); + } + + /** + * Filters attachment meta data + * + * @since 1.5.0 + * + * @param array $metadata Image attachment metadata. + * @param int $attachment_id Attachment ID. + * @return array Filtered image attachment metadata. + */ + public function generate_attachment_metadata( $metadata, $attachment_id ) { + if ( ! isset( $metadata['sizes'] ) || empty( $metadata['sizes'] ) ) { + return $metadata; + } + + foreach ( $metadata['sizes'] as $size => $size_data ) { + if ( ! $this->should_crop( $size ) ) { + continue; + } + + $data = [ + 'width' => $size_data['width'], + 'height' => $size_data['height'], + ]; + + $better_thumb_filename = $this->get_cropped_thumbnail( $attachment_id, $data ); + if ( ! empty( $better_thumb_filename ) ) { + $metadata['sizes'][ $size ]['file'] = basename( $better_thumb_filename ); + } + } + + return $metadata; + } + + /** + * Gets a cropped thumbnail from the Azure API. + * + * @since 1.5.0. + * + * @param int $attachment_id Attachment ID. + * @param array $size_data Attachment metadata size data. + * @return bool|mixed The thumbnail file name or false on failure. + */ + public function get_cropped_thumbnail( $attachment_id, $size_data ) { + /** + * Filters the image URL to send to Computer Vision for smart cropping. A non-null value will override default + * plugin behavior. + * + * @since 1.5.0 + * + * @param null|string Null to use default plugin behavior; string to override. + * @param int The attachment image ID. + */ + $url = apply_filters( 'classifai_smart_cropping_source_url', null, $attachment_id ); + + if ( empty( $url ) ) { + $url = get_largest_acceptable_image_url( + get_attached_file( $attachment_id ), + wp_get_attachment_url( $attachment_id, 'full' ), + $size_data, + computer_vision_max_filesize() + ); + } + + if ( empty( $url ) || empty( $size_data ) || ! is_array( $size_data ) ) { + return false; + } + + $data = [ + 'width' => $size_data['width'], + 'height' => $size_data['height'], + 'url' => $url, + ]; + + $new_thumb_image = $this->request_cropped_thumbnail( $data ); + set_transient( 'classifai_azure_computer_vision_smart_cropping_latest_response', $new_thumb_image, DAY_IN_SECONDS * 30 ); + if ( empty( $new_thumb_image ) ) { + return false; + } + + $attached_file = get_attached_file( $attachment_id ); + $file_path_info = pathinfo( $attached_file ); + $new_thumb_file_name = str_replace( + $file_path_info['filename'], + sprintf( + '%s-%dx%d', + $file_path_info['filename'], + $size_data['width'], + $size_data['height'] + ), + $attached_file + ); + + /** + * Filters the file name of the smart-cropped image. By default, the filename mirrors what is generated by + * core -- e.g., my-thumb-150x150.jpg -- so will override the core-generated image. Apply this filter to keep + * the original file in the file system. + * + * @since 1.5.0 + * + * @param string Default file name. + * @param int The ID of the attachment being processed. + * @param array Width and height data for the image. + */ + $new_thumb_file_name = apply_filters( + 'classifai_smart_cropping_thumb_file_name', + $new_thumb_file_name, + $attachment_id, + $size_data + ); + + $filesystem = $this->get_wp_filesystem(); + if ( $filesystem && $filesystem->put_contents( $new_thumb_file_name, $new_thumb_image ) ) { + return $new_thumb_file_name; + } + + return false; + } + + /** + * Builds the API url. + * + * @since 1.5.0 + * + * @return string + */ + public function get_api_url() { + return sprintf( '%s%s', trailingslashit( $this->settings['url'] ), static::API_PATH ); + } + + /** + * Fetch thumbnail using Azure API. + * + * @since 1.5.0 + * + * @param array $data Data for an attachment image size. + * @return bool|string + */ + public function request_cropped_thumbnail( $data ) { + $url = add_query_arg( + [ + 'height' => $data['height'], + 'width' => $data['width'], + 'smartCropping' => true, + ], + $this->get_api_url() + ); + + $response = wp_remote_post( + $url, + [ + 'body' => wp_json_encode( + [ + 'url' => $data['url'], + ] + ), + 'headers' => [ + 'Content-Type' => 'application/json', + 'Ocp-Apim-Subscription-Key' => $this->settings['api_key'], + ], + ] + ); + + /** + * Fires after the request to the generateThumbnail smart-cropping endpoint has run. + * + * @since 1.5.0 + * + * @param array|WP_Error Response data or a WP_Error if the request failed. + * @param string The request URL with query args added. + * @param array Array containing the image height and width. + */ + do_action( 'classifai_smart_cropping_after_request', $response, $url, $data ); + + if ( 200 === wp_remote_retrieve_response_code( $response ) ) { + return wp_remote_retrieve_body( $response ); + } + + /** + * Fires when the generateThumbnail smart-cropping API response did not have a 200 status code. + * + * @since 1.5.0 + * + * @param array|WP_Error Response data or a WP_Error if the request failed. + * @param string The request URL with query args added. + * @param array Array containing the image height and width. + */ + do_action( 'classifai_smart_cropping_unsuccessful_response', $response, $url, $data ); + + return false; + } +} diff --git a/includes/Classifai/Providers/Provider.php b/includes/Classifai/Providers/Provider.php index 8cecbdb23..5cd9531f7 100644 --- a/includes/Classifai/Providers/Provider.php +++ b/includes/Classifai/Providers/Provider.php @@ -219,4 +219,17 @@ public function sanitize_settings( $settings ) { * @since 1.4.0 */ abstract public function get_provider_debug_information(); + + /** + * Common entry point for all REST endpoints for this provider. + * This is called by the Service. + * + * @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. + * + * @return mixed + */ + public function rest_endpoint_callback( $post_id, $route_to_call ) { + return null; + } } diff --git a/includes/Classifai/Providers/Watson/NLU.php b/includes/Classifai/Providers/Watson/NLU.php index ebb55cb66..e9a46c9bc 100644 --- a/includes/Classifai/Providers/Watson/NLU.php +++ b/includes/Classifai/Providers/Watson/NLU.php @@ -112,6 +112,7 @@ public function can_register() { */ public function register() { add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); $this->taxonomy_factory = new TaxonomyFactory(); $this->taxonomy_factory->build_all(); @@ -168,43 +169,65 @@ public function get_settings( $index = false ) { public function enqueue_editor_assets() { wp_enqueue_script( 'classifai-editor', // Handle. - CLASSIFAI_PLUGIN_URL . '/dist/js/editor.min.js', + CLASSIFAI_PLUGIN_URL . 'dist/js/editor.min.js', array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor', 'wp-edit-post' ), CLASSIFAI_PLUGIN_VERSION, true ); - if ( function_exists( 'is_gutenberg_page' ) && is_gutenberg_page() ) { - wp_enqueue_script( - 'classifai-gutenberg-support', - CLASSIFAI_PLUGIN_URL . 'assets/js/classifai-gutenberg-support.js', - [ 'editor' ], - CLASSIFAI_PLUGIN_VERSION, - true - ); - } } /** - * Adds ClassifAI Gutenberg Support if on the Gutenberg editor page + * Enqueue the admin scripts. */ - public function init_admin_scripts() { - if ( function_exists( 'is_gutenberg_page' ) && is_gutenberg_page() ) { - wp_enqueue_script( - 'classifai-gutenberg-support', - CLASSIFAI_PLUGIN_URL . 'assets/js/classifai-gutenberg-support.js', - [ 'editor' ], - CLASSIFAI_PLUGIN_VERSION, - true - ); - } + public function enqueue_admin_assets() { + wp_enqueue_script( + 'classifai-admin', + CLASSIFAI_PLUGIN_URL . 'dist/js/admin.min.js', + [], + CLASSIFAI_PLUGIN_VERSION, + true + ); + wp_localize_script( + 'classifai-admin', + 'ClassifAI', + [ + 'api_password' => __( 'API Password', 'classifai' ), + 'api_key' => __( 'API Key', 'classifai' ), + 'use_key' => __( 'Use an API Key instead?', 'classifai' ), + 'use_password' => __( 'Use a username/password instead?', 'classifai' ), + ] + ); } /** * Setup fields */ public function setup_fields_sections() { + // Add the settings section. + add_settings_section( + $this->get_option_name(), + $this->provider_service_name, + function() { + printf( + wp_kses( + __( 'Don\'t have an IBM Cloud account yet? Register for one and set up a Natural Language Understanding Resource to get your API key.', 'classifai' ), + [ + 'a' => [ + 'href' => [], + 'title' => [], + ], + ] + ), + esc_url( 'https://cloud.ibm.com/registration' ), + esc_url( 'https://cloud.ibm.com/catalog/services/natural-language-understanding' ) + ); + }, + $this->get_option_name() + ); + // Create the Credentials Section. $this->do_credentials_section(); + // Create content tagging section $this->do_nlu_features_sections(); } @@ -213,7 +236,6 @@ public function setup_fields_sections() { * Helper method to create the credentials section */ protected function do_credentials_section() { - add_settings_section( $this->get_option_name(), $this->provider_service_name, '', $this->get_option_name() ); add_settings_field( 'url', esc_html__( 'API URL', 'classifai' ), @@ -224,6 +246,7 @@ protected function do_credentials_section() { 'label_for' => 'watson_url', 'option_index' => 'credentials', 'input_type' => 'text', + 'large' => true, ] ); add_settings_field( @@ -237,12 +260,13 @@ protected function do_credentials_section() { 'option_index' => 'credentials', 'input_type' => 'text', 'default_value' => 'apikey', - 'description' => __( 'If your credentials do not include a username, it is typically apikey', 'classifai' ), + 'large' => true, + 'class' => $this->use_usename_password() ? 'hidden' : '', ] ); add_settings_field( 'password', - esc_html__( 'API Key / Password', 'classifai' ), + esc_html__( 'API Key', 'classifai' ), [ $this, 'render_input' ], $this->get_option_name(), $this->get_option_name(), @@ -250,17 +274,36 @@ protected function do_credentials_section() { 'label_for' => 'watson_password', 'option_index' => 'credentials', 'input_type' => 'password', + 'large' => true, ] ); + add_settings_field( + 'toggle', + '', + function() { + printf( + '%s', + $this->use_usename_password() + ? esc_html__( 'Use a username/password instead?', 'classifai' ) + : esc_html__( 'Use an API Key instead?', 'classifai' ) + ); + }, + $this->get_option_name(), + $this->get_option_name() + ); + } + + /** + * Check if a username/password is using instead of API key. + */ + protected function use_usename_password() { + return 'apikey' === $this->get_settings( 'credentials' )['watson_username']; } /** * Helper method to create the watson features section */ protected function do_nlu_features_sections() { - // Add the settings section. - add_settings_section( $this->get_option_name(), $this->provider_service_name, '', $this->get_option_name() ); - add_settings_field( 'post-types', esc_html__( 'Post Types to Classify', 'classifai' ), @@ -307,7 +350,7 @@ public function render_input( $args ) { case 'text': case 'password': $attrs = ' value="' . esc_attr( $value ) . '"'; - $class = 'regular-text'; + $class = empty( $args['large'] ) ? 'regular-text' : 'large-text'; break; case 'number': $attrs = ' value="' . esc_attr( $value ) . '"'; @@ -607,11 +650,43 @@ function( $post_type ) use ( $settings_post_types ) { $credentials = $settings['credentials'] ?? []; return [ - __( 'Configured', 'classifai' ) => $configured ? __( 'yes', 'classifai' ) : __( 'no', 'classifai' ), - __( 'API URL', 'classifai' ) => $credentials['watson_url'] ?? '', - __( 'API username', 'classifai' ) => $credentials['watson_username'] ?? '', - __( 'Post types', 'classifai' ) => implode( ', ', $post_types ), - __( 'Features', 'classifai' ) => preg_replace( '/,"/', ', "', wp_json_encode( $settings['features'] ?? '' ) ), + __( 'Configured', 'classifai' ) => $configured ? __( 'yes', 'classifai' ) : __( 'no', 'classifai' ), + __( 'API URL', 'classifai' ) => $credentials['watson_url'] ?? '', + __( 'API username', 'classifai' ) => $credentials['watson_username'] ?? '', + __( 'Post types', 'classifai' ) => implode( ', ', $post_types ), + __( 'Features', 'classifai' ) => preg_replace( '/,"/', ', "', wp_json_encode( $settings['features'] ?? '' ) ), + __( 'Latest response', 'classifai' ) => $this->get_formatted_latest_response(), ]; } + + /** + * Format the result of most recent request. + * + * @return string + */ + private function get_formatted_latest_response() { + $data = get_transient( 'classifai_watson_nlu_latest_response' ); + + if ( ! $data ) { + return __( 'N/A', 'classifai' ); + } + + if ( is_wp_error( $data ) ) { + return $data->get_error_message(); + } + + $formatted_data = array_intersect_key( + $data, + [ + 'usage' => 1, + 'language' => 1, + ] + ); + + foreach ( array_diff_key( $data, $formatted_data ) as $key => $value ) { + $formatted_data[ $key ] = count( $value ); + } + + return preg_replace( '/,"/', ', "', wp_json_encode( $formatted_data ) ); + } } diff --git a/includes/Classifai/Services/ImageProcessing.php b/includes/Classifai/Services/ImageProcessing.php index d57ef49b6..3b9120382 100644 --- a/includes/Classifai/Services/ImageProcessing.php +++ b/includes/Classifai/Services/ImageProcessing.php @@ -29,9 +29,100 @@ public function init() { parent::init(); $this->register_image_tags_taxonomy(); add_filter( 'attachment_fields_to_edit', [ $this, 'custom_fields_edit' ] ); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + add_filter( 'attachment_fields_to_edit', [ $this, 'add_rescan_button_to_media_modal' ], 10, 2 ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_media_scripts' ] ); } /** + * Enqueue the script for the media modal. + */ + public function enqueue_media_scripts() { + wp_enqueue_script( 'media-script', CLASSIFAI_PLUGIN_URL . '/dist/js/media.min.js', array( 'jquery', 'media-editor' ), CLASSIFAI_PLUGIN_VERSION, true ); + } + + /** + * Adds the rescan buttons to the media modal. + * + * @param array $form_fields Array of fields + * @param \WP_post $post Post object for the attachment being viewed. + */ + public function add_rescan_button_to_media_modal( $form_fields, $post ) { + $screen = get_current_screen(); + // Screen returns null on the Media library page. + if ( ! $screen ) { + $alt_tags_text = empty( get_post_meta( $post->ID, '_wp_attachment_image_alt', true ) ) ? __( 'Generate', 'classifai' ) : __( 'Rescan', 'classifai' ); + $image_tags_text = empty( wp_get_object_terms( $post->ID, 'classifai-image-tags' ) ) ? __( 'Generate', 'classifai' ) : __( 'Rescan', 'classifai' ); + $form_fields['rescan_alt_tags'] = [ + 'label' => __( 'Classifai Alt Tags', 'classifai' ), + 'input' => 'html', + 'html' => '', + ]; + $form_fields['rescan_captions'] = [ + 'label' => __( 'Classifai Image Tags', 'classifai' ), + 'input' => 'html', + 'html' => '', + ]; + } + return $form_fields; + } + + /** + * Create endpoints for services + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'alt-tags/(?P\d+)', + [ + 'methods' => 'GET', + 'callback' => [ $this, 'provider_endpoint_callback' ], + 'args' => [ 'route' => 'alt-tags' ], + ] + ); + register_rest_route( + 'classifai/v1', + 'image-tags/(?P\d+)', + [ + 'methods' => 'GET', + 'callback' => [ $this, 'provider_endpoint_callback' ], + 'args' => [ 'route' => 'image-tags' ], + ] + ); + } + + /** + * Single call back to pass the route callback to the provider. + * + * @param \WP_REST_Request $request The full request object. + * + * @return mixed + */ + public function provider_endpoint_callback( $request ) { + $response = true; + $attachment_id = $request->get_param( 'id' ); + $custom_attributes = $request->get_attributes(); + $route_to_call = isset( $custom_attributes['args'] ) && isset( $custom_attributes['args']['route'] ) ? $custom_attributes['args']['route'] : false; + + // Check to be sure the post both exists and is an attachment. + if ( ! get_post( $attachment_id ) || 'attachment' !== get_post_type( $attachment_id ) ) { + return new \WP_Error( 'incorrect ID', "{$attachment_id} is not found or not an attachement", array( 'status' => 404 ) ); + } + // If no args, we can't pass the call into the active provider. + if ( false === $route_to_call ) { + return new \WP_Error( 'no route', 'No route indicated for the provider class to use.', array( 'status' => 404 ) ); + } + + // Call the provider endpoint function + if ( isset( $this->provider_classes[0] ) ) { + $response = $this->provider_classes[0]->rest_endpoint_callback( $attachment_id, $route_to_call ); + } + + return $response; + } + + + /**s * Register a common image tag taxonomy */ protected function register_image_tags_taxonomy() { diff --git a/includes/Classifai/Services/ServicesManager.php b/includes/Classifai/Services/ServicesManager.php index f137a9599..4b3c6f9fe 100644 --- a/includes/Classifai/Services/ServicesManager.php +++ b/includes/Classifai/Services/ServicesManager.php @@ -264,7 +264,8 @@ public function register_top_level_admin_menu_item() { $this->menu_title, 'manage_options', 'classifai_settings', - [ $this, 'render_settings_page' ] + [ $this, 'render_settings_page' ], + CLASSIFAI_PLUGIN_URL . 'assets/img/menu-icon.svg' ); $this->init_services_settings(); diff --git a/includes/Classifai/Taxonomy/TaxonomyFactory.php b/includes/Classifai/Taxonomy/TaxonomyFactory.php index 9e6bc16be..289c61659 100644 --- a/includes/Classifai/Taxonomy/TaxonomyFactory.php +++ b/includes/Classifai/Taxonomy/TaxonomyFactory.php @@ -61,7 +61,7 @@ public function build_all() { public function build_if( $taxonomy, $supported_post_types = [] ) { if ( ! $this->exists( $taxonomy ) ) { $this->taxonomies[ $taxonomy ] = $this->build( $taxonomy ); - $instance = $this->taxonomies[ $taxonomy ]; + $instance = $this->taxonomies[ $taxonomy ]; $instance->register(); if ( ! empty( $supported_post_types ) ) { diff --git a/includes/Classifai/Watson/Classifier.php b/includes/Classifai/Watson/Classifier.php index d021b0cf2..2cd1fe620 100644 --- a/includes/Classifai/Watson/Classifier.php +++ b/includes/Classifai/Watson/Classifier.php @@ -56,7 +56,7 @@ public function classify( $text, $options = [], $request_options = [] ) { $body = $this->get_body( $text, $options ); $request_options['body'] = $body; - $request = $this->get_request(); + $request = $this->get_request(); if ( empty( $request_options['timeout'] ) && ! empty( $options['timeout'] ) ) { $request_options['timeout'] = $options['timeout']; @@ -65,6 +65,7 @@ public function classify( $text, $options = [], $request_options = [] ) { } $classified_data = $request->post( $this->get_endpoint(), $request_options ); + set_transient( 'classifai_watson_nlu_latest_response', $classified_data, DAY_IN_SECONDS * 30 ); /** * Filter the classified data returned from the API call. * diff --git a/languages/classifai.pot b/languages/classifai.pot index e17668cdc..a214d4c4f 100644 --- a/languages/classifai.pot +++ b/languages/classifai.pot @@ -1,35 +1,38 @@ -# Copyright (C) 2019 10up +# Copyright (C) 2020 10up # This file is distributed under the GPLv2. msgid "" msgstr "" -"Project-Id-Version: ClassifAI 1.4.0\n" +"Project-Id-Version: ClassifAI 1.5.0\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/classifai\n" -"POT-Creation-Date: 2019-09-26 21:07:08+00:00\n" +"POT-Creation-Date: 2020-02-27 17:24:07+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2019-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2020-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "X-Generator: node-wp-i18n 1.2.3\n" -#: classifai.php:25 -msgid "ClassifAI requires PHP version 7." +#: classifai.php:28 +#. translators: PHP Update guide URL +msgid "" +"ClassifAI requires PHP version 7. Click here to learn " +"how to update your PHP version." msgstr "" -#: classifai.php:26 +#: classifai.php:35 msgid "Error Activating" msgstr "" -#: classifai.php:82 +#: classifai.php:91 msgid "Fatal Error: Composer not setup in %" msgstr "" -#: classifai.php:109 +#: classifai.php:118 msgid "Error: Please run $ composer install in the classifai plugin directory." msgstr "" -#: classifai.php:203 +#: classifai.php:212 #. translators: %1$s: CLI install commands, %2$s: classifai url msgid "" "You appear to be running a development version of ClassifAI. Certain " @@ -38,6 +41,17 @@ msgid "" "install the stable version of ClassifAI instead." msgstr "" +#: includes/Classifai/Admin/BulkActions.php:58 +#: includes/Classifai/Admin/BulkActions.php:127 +msgid "Classify" +msgstr "" + +#: includes/Classifai/Admin/BulkActions.php:94 +msgid "Classified %s post." +msgid_plural "Classified %s posts." +msgstr[0] "" +msgstr[1] "" + #: includes/Classifai/Admin/DebugInfo.php:62 msgid "Version" msgstr "" @@ -54,90 +68,132 @@ msgid "ClassifAI requires setup" msgstr "" #: includes/Classifai/Providers/AWS/Comprehend.php:50 -#: includes/Classifai/Providers/Azure/ComputerVision.php:308 +#: includes/Classifai/Providers/Azure/ComputerVision.php:366 msgid "Endpoint URL" msgstr "" #: includes/Classifai/Providers/AWS/Comprehend.php:61 -#: includes/Classifai/Providers/Azure/ComputerVision.php:320 +#: includes/Classifai/Providers/Azure/ComputerVision.php:378 +#: includes/Classifai/Providers/Watson/NLU.php:195 +#: includes/Classifai/Providers/Watson/NLU.php:269 msgid "API Key" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:315 +#: includes/Classifai/Providers/Azure/ComputerVision.php:77 +msgid "Azure Computer Vision Scan" +msgstr "" + +#: includes/Classifai/Providers/Azure/ComputerVision.php:91 +msgid "Rescan Captions" +msgstr "" + +#: includes/Classifai/Providers/Azure/ComputerVision.php:91 +msgid "Generate Captions" +msgstr "" + +#: includes/Classifai/Providers/Azure/ComputerVision.php:92 +msgid "Rescan Tags" +msgstr "" + +#: includes/Classifai/Providers/Azure/ComputerVision.php:92 +msgid "Generate Tags" +msgstr "" + +#: includes/Classifai/Providers/Azure/ComputerVision.php:373 msgid "e.g. https://REGION.api.cognitive.microsoft.com/" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:331 +#: includes/Classifai/Providers/Azure/ComputerVision.php:389 msgid "Automatically Caption Images" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:339 -msgid "Uploaded images will be auto-captioned" +#: includes/Classifai/Providers/Azure/ComputerVision.php:397 +msgid "Images will be captioned with alt text upon upload" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:344 +#: includes/Classifai/Providers/Azure/ComputerVision.php:402 msgid "Caption Confidence Threshold" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:352 +#: includes/Classifai/Providers/Azure/ComputerVision.php:410 msgid "" "Minimum confidence score for automatically applied image captions, numeric " "value from 0-100. Recommended to be set to at least 75." msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:357 +#: includes/Classifai/Providers/Azure/ComputerVision.php:415 msgid "Automatically Tag Images" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:365 -msgid "Uploaded images will be auto-tagged" +#: includes/Classifai/Providers/Azure/ComputerVision.php:423 +msgid "Images will be tagged upon upload" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:370 +#: includes/Classifai/Providers/Azure/ComputerVision.php:428 msgid "Tag Confidence Threshold" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:378 -#: includes/Classifai/Providers/Azure/ComputerVision.php:397 +#: includes/Classifai/Providers/Azure/ComputerVision.php:436 msgid "" "Minimum confidence score for automatically applied image tags, numeric " -"value from 0-100. Recommended to be set to at least 75." +"value from 0-100. Recommended to be set to at least 70." msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:390 +#: includes/Classifai/Providers/Azure/ComputerVision.php:448 msgid "Tag Taxonomy" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:434 +#: includes/Classifai/Providers/Azure/ComputerVision.php:460 +msgid "Enable smart cropping" +msgstr "" + +#: includes/Classifai/Providers/Azure/ComputerVision.php:468 +msgid "Crop images around a region of interest identified by ComputerVision" +msgstr "" + +#: includes/Classifai/Providers/Azure/ComputerVision.php:508 msgid "Please enter your credentials" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:511 +#: includes/Classifai/Providers/Azure/ComputerVision.php:589 msgid "Authenticated" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:511 -#: includes/Classifai/Providers/Watson/NLU.php:610 -#: includes/Classifai/Services/ServicesManager.php:363 +#: includes/Classifai/Providers/Azure/ComputerVision.php:589 +#: includes/Classifai/Providers/Watson/NLU.php:653 +#: includes/Classifai/Services/ServicesManager.php:364 msgid "yes" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:511 -#: includes/Classifai/Providers/Watson/NLU.php:610 -#: includes/Classifai/Services/ServicesManager.php:363 +#: includes/Classifai/Providers/Azure/ComputerVision.php:589 +#: includes/Classifai/Providers/Watson/NLU.php:653 +#: includes/Classifai/Services/ServicesManager.php:364 msgid "no" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:512 -#: includes/Classifai/Providers/Watson/NLU.php:219 -#: includes/Classifai/Providers/Watson/NLU.php:611 +#: includes/Classifai/Providers/Azure/ComputerVision.php:590 +#: includes/Classifai/Providers/Watson/NLU.php:241 +#: includes/Classifai/Providers/Watson/NLU.php:654 msgid "API URL" msgstr "" -#: includes/Classifai/Providers/Azure/ComputerVision.php:513 +#: includes/Classifai/Providers/Azure/ComputerVision.php:591 msgid "Caption threshold" msgstr "" +#: includes/Classifai/Providers/Azure/ComputerVision.php:592 +msgid "Latest response - Image Scan" +msgstr "" + +#: includes/Classifai/Providers/Azure/ComputerVision.php:593 +msgid "Latest response - Smart Cropping" +msgstr "" + +#: includes/Classifai/Providers/Azure/ComputerVision.php:606 +#: includes/Classifai/Providers/Watson/NLU.php:671 +msgid "N/A" +msgstr "" + #: includes/Classifai/Providers/Provider.php:185 msgid "Please Choose" msgstr "" @@ -190,50 +246,86 @@ msgstr "" msgid "Concept Taxonomy" msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:231 -msgid "API Username" +#: includes/Classifai/Providers/Watson/NLU.php:194 +msgid "API Password" +msgstr "" + +#: includes/Classifai/Providers/Watson/NLU.php:196 +#: includes/Classifai/Providers/Watson/NLU.php:288 +msgid "Use an API Key instead?" msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:240 -msgid "If your credentials do not include a username, it is typically apikey" +#: includes/Classifai/Providers/Watson/NLU.php:197 +#: includes/Classifai/Providers/Watson/NLU.php:287 +msgid "Use a username/password instead?" msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:245 -msgid "API Key / Password" +#: includes/Classifai/Providers/Watson/NLU.php:213 +msgid "" +"Don't have an IBM Cloud account yet? Register for one and set up a Natural Language Understanding Resource to get your API " +"key." msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:266 +#: includes/Classifai/Providers/Watson/NLU.php:254 +msgid "API Username" +msgstr "" + +#: includes/Classifai/Providers/Watson/NLU.php:309 msgid "Post Types to Classify" msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:390 +#: includes/Classifai/Providers/Watson/NLU.php:433 msgid "Watson Category Settings" msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:395 +#: includes/Classifai/Providers/Watson/NLU.php:438 msgid "Enable" msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:499 +#: includes/Classifai/Providers/Watson/NLU.php:542 msgid "IBM Watson NLU Authentication Failed. Please check credentials." msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:610 +#: includes/Classifai/Providers/Watson/NLU.php:653 msgid "Configured" msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:612 +#: includes/Classifai/Providers/Watson/NLU.php:655 msgid "API username" msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:613 +#: includes/Classifai/Providers/Watson/NLU.php:656 msgid "Post types" msgstr "" -#: includes/Classifai/Providers/Watson/NLU.php:614 +#: includes/Classifai/Providers/Watson/NLU.php:657 msgid "Features" msgstr "" +#: includes/Classifai/Providers/Watson/NLU.php:658 +msgid "Latest response" +msgstr "" + +#: includes/Classifai/Services/ImageProcessing.php:54 +#: includes/Classifai/Services/ImageProcessing.php:55 +msgid "Generate" +msgstr "" + +#: includes/Classifai/Services/ImageProcessing.php:54 +#: includes/Classifai/Services/ImageProcessing.php:55 +msgid "Rescan" +msgstr "" + +#: includes/Classifai/Services/ImageProcessing.php:57 +msgid "Classifai Alt Tags" +msgstr "" + +#: includes/Classifai/Services/ImageProcessing.php:62 +msgid "Classifai Image Tags" +msgstr "" + #: includes/Classifai/Services/LanguageProcessing.php:14 msgid "Language Processing" msgstr "" @@ -266,11 +358,11 @@ msgstr "" msgid "ClassifAI %s" msgstr "" -#: includes/Classifai/Services/ServicesManager.php:365 +#: includes/Classifai/Services/ServicesManager.php:366 msgid "Valid license" msgstr "" -#: includes/Classifai/Services/ServicesManager.php:370 +#: includes/Classifai/Services/ServicesManager.php:371 msgid "Email" msgstr "" diff --git a/wpacceptance.json b/wpacceptance.json new file mode 100644 index 000000000..4e3dfc43f --- /dev/null +++ b/wpacceptance.json @@ -0,0 +1,16 @@ +{ + "name": "classifai", + "license": "GPLv2", + "tests": [ + "./tests/e2e/wpacceptance/*.php" + ], + "project_path": "%WP_ROOT%/wp-content/plugins/classifai", + "enforce_clean_db": true, + "repository": "10up", + "snapshot_id": "d611e1485da06e153aef192471893fc6", + "bootstrap": "tests\/e2e\/wpacceptance\/bootstrap.php", + "exclude": [ + "node_modules", + "vendor" + ] +}