diff --git a/includes/Classifai/Features/DescriptiveTextGenerator.php b/includes/Classifai/Features/DescriptiveTextGenerator.php
new file mode 100644
index 000000000..04048b314
--- /dev/null
+++ b/includes/Classifai/Features/DescriptiveTextGenerator.php
@@ -0,0 +1,206 @@
+provider_instances = $this->get_provider_instances( $service_providers );
+ }
+
+ /**
+ * Returns the label of the feature.
+ *
+ * @return string
+ */
+ public function get_label() {
+ return apply_filters(
+ 'classifai_' . static::ID . '_label',
+ __( 'Descriptive Text Generator', 'classifai' )
+ );
+ }
+
+ /**
+ * Returns the providers supported by the feature.
+ *
+ * @internal
+ *
+ * @return array
+ */
+ protected function get_providers() {
+ return apply_filters(
+ 'classifai_' . static::ID . '_providers',
+ [
+ ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ),
+ ]
+ );
+ }
+
+ /**
+ * Sets up the fields and sections for the feature.
+ */
+ public function setup_fields_sections() {
+ $settings = $this->get_settings();
+
+ add_settings_section(
+ $this->get_option_name() . '_section',
+ esc_html__( 'Feature settings', 'classifai' ),
+ '__return_empty_string',
+ $this->get_option_name()
+ );
+
+ add_settings_field(
+ 'status',
+ esc_html__( 'Enable descriptive text generation', 'classifai' ),
+ [ $this, 'render_input' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'status',
+ 'input_type' => 'checkbox',
+ 'default_value' => $settings['status'],
+ 'description' => __( 'A button will be added to the status panel that can be used to generate titles.', 'classifai' ),
+ ]
+ );
+
+ add_settings_field(
+ 'roles',
+ esc_html__( 'Allowed roles', 'classifai' ),
+ [ $this, 'render_checkbox_group' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'roles',
+ 'options' => $this->roles,
+ 'default_values' => $settings['roles'],
+ 'description' => __( 'Choose which roles are allowed to generate titles.', 'classifai' ),
+ ]
+ );
+
+ add_settings_field(
+ 'provider',
+ esc_html__( 'Select a provider', 'classifai' ),
+ [ $this, 'render_select' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'provider',
+ 'options' => $this->get_providers(),
+ 'default_value' => $settings['provider'],
+ ]
+ );
+
+ foreach( array_keys( $this->get_providers() ) as $provider_id ) {
+ $provider = $this->get_feature_provider_instance( $provider_id );
+
+ if ( method_exists( $provider, 'render_provider_fields' ) ) {
+ $provider->render_provider_fields();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the feature meets all the criteria to be enabled.
+ *
+ * @return boolean
+ */
+ public function is_feature_enabled() {
+ $access = false;
+ $settings = $this->get_settings();
+ $provider_id = $settings['provider'] ?? ComputerVision::ID;
+ $user_roles = wp_get_current_user()->roles ?? [];
+ $feature_roles = $settings['roles'] ?? [];
+
+ $user_access = ! empty( $feature_roles ) && ! empty( array_intersect( $user_roles, $feature_roles ) );
+ $provider_access = $settings[ $provider_id ]['authenticated'] ?? false;
+ $feature_status = isset( $settings['status'] ) && '1' === $settings['status'];
+ $access = $user_access && $provider_access && $feature_status;
+
+ /**
+ * Filter to override permission to the generate title feature.
+ *
+ * @since 2.3.0
+ * @hook classifai_openai_chatgpt_{$feature}
+ *
+ * @param {bool} $access Current access value.
+ * @param {array} $settings Current feature settings.
+ *
+ * @return {bool} Should the user have access?
+ */
+ return apply_filters( 'classifai_' . static::ID . '_is_feature_enabled', $access, $settings );
+ }
+
+ /**
+ * Returns the default settings for the feature.
+ *
+ * The root-level keys are the setting keys that are independent of the provider.
+ * Provider specific settings should be nested under the provider key.
+ *
+ * @internal
+ *
+ * @todo Add a filter hook to allow other plugins to add their own settings.
+ *
+ * @return array
+ */
+ protected function get_default_settings() {
+ $provider_settings = [];
+ $feature_settings = [
+ 'status' => '0',
+ 'roles' => $this->roles,
+ 'provider' => ComputerVision::ID,
+ ];
+
+ $provider_instance = $this->get_feature_provider_instance( ComputerVision::ID );
+ $provider_settings[ ComputerVision::ID ] = $provider_instance->get_default_provider_settings();
+
+ return
+ apply_filters(
+ 'classifai_' . static::ID . '_get_default_settings',
+ array_merge(
+ $feature_settings,
+ $provider_settings
+ )
+ );
+ }
+
+ /**
+ * Sanitizes the settings before saving.
+ *
+ * @param array $new_settings The settings to be sanitized on save.
+ *
+ * @internal
+ *
+ * @return array
+ */
+ public function sanitize_settings( $new_settings ) {
+ $settings = $this->get_settings();
+
+ $new_settings['status'] = $new_settings['status'] ?? $settings['status'];
+ $new_settings['roles'] = isset( $new_settings['roles'] ) ? array_map( 'sanitize_text_field', $new_settings['roles'] ) : $settings['roles'];
+ $new_settings['provider'] = isset( $new_settings['provider'] ) ? sanitize_text_field( $new_settings['provider'] ) : $settings['provider'];
+
+ $provider_instance = $this->get_feature_provider_instance( $new_settings['provider'] );
+ $new_settings = $provider_instance->sanitize_settings( $new_settings );
+
+ return apply_filters(
+ 'classifai_' . static::ID . '_sanitize_settings',
+ $new_settings,
+ $settings
+ );
+ }
+}
diff --git a/includes/Classifai/Features/ImageTagsGenerator.php b/includes/Classifai/Features/ImageTagsGenerator.php
new file mode 100644
index 000000000..3c15adf80
--- /dev/null
+++ b/includes/Classifai/Features/ImageTagsGenerator.php
@@ -0,0 +1,206 @@
+provider_instances = $this->get_provider_instances( $service_providers );
+ }
+
+ /**
+ * Returns the label of the feature.
+ *
+ * @return string
+ */
+ public function get_label() {
+ return apply_filters(
+ 'classifai_' . static::ID . '_label',
+ __( 'Image Tags Generator', 'classifai' )
+ );
+ }
+
+ /**
+ * Returns the providers supported by the feature.
+ *
+ * @internal
+ *
+ * @return array
+ */
+ protected function get_providers() {
+ return apply_filters(
+ 'classifai_' . static::ID . '_providers',
+ [
+ ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ),
+ ]
+ );
+ }
+
+ /**
+ * Sets up the fields and sections for the feature.
+ */
+ public function setup_fields_sections() {
+ $settings = $this->get_settings();
+
+ add_settings_section(
+ $this->get_option_name() . '_section',
+ esc_html__( 'Feature settings', 'classifai' ),
+ '__return_empty_string',
+ $this->get_option_name()
+ );
+
+ add_settings_field(
+ 'status',
+ esc_html__( 'Enable image tag generation', 'classifai' ),
+ [ $this, 'render_input' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'status',
+ 'input_type' => 'checkbox',
+ 'default_value' => $settings['status'],
+ 'description' => __( 'Image tags will be added automatically.', 'classifai' ),
+ ]
+ );
+
+ add_settings_field(
+ 'roles',
+ esc_html__( 'Allowed roles', 'classifai' ),
+ [ $this, 'render_checkbox_group' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'roles',
+ 'options' => $this->roles,
+ 'default_values' => $settings['roles'],
+ 'description' => __( 'Choose which roles are allowed to generate image tags.', 'classifai' ),
+ ]
+ );
+
+ add_settings_field(
+ 'provider',
+ esc_html__( 'Select a provider', 'classifai' ),
+ [ $this, 'render_select' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'provider',
+ 'options' => $this->get_providers(),
+ 'default_value' => $settings['provider'],
+ ]
+ );
+
+ foreach( array_keys( $this->get_providers() ) as $provider_id ) {
+ $provider = $this->get_feature_provider_instance( $provider_id );
+
+ if ( method_exists( $provider, 'render_provider_fields' ) ) {
+ $provider->render_provider_fields();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the feature meets all the criteria to be enabled.
+ *
+ * @return boolean
+ */
+ public function is_feature_enabled() {
+ $access = false;
+ $settings = $this->get_settings();
+ $provider_id = $settings['provider'] ?? ComputerVision::ID;
+ $user_roles = wp_get_current_user()->roles ?? [];
+ $feature_roles = $settings['roles'] ?? [];
+
+ $user_access = ! empty( $feature_roles ) && ! empty( array_intersect( $user_roles, $feature_roles ) );
+ $provider_access = $settings[ $provider_id ]['authenticated'] ?? false;
+ $feature_status = isset( $settings['status'] ) && '1' === $settings['status'];
+ $access = $user_access && $provider_access && $feature_status;
+
+ /**
+ * Filter to override permission to the generate title feature.
+ *
+ * @since 2.3.0
+ * @hook classifai_openai_chatgpt_{$feature}
+ *
+ * @param {bool} $access Current access value.
+ * @param {array} $settings Current feature settings.
+ *
+ * @return {bool} Should the user have access?
+ */
+ return apply_filters( 'classifai_' . static::ID . '_is_feature_enabled', $access, $settings );
+ }
+
+ /**
+ * Returns the default settings for the feature.
+ *
+ * The root-level keys are the setting keys that are independent of the provider.
+ * Provider specific settings should be nested under the provider key.
+ *
+ * @internal
+ *
+ * @todo Add a filter hook to allow other plugins to add their own settings.
+ *
+ * @return array
+ */
+ protected function get_default_settings() {
+ $provider_settings = [];
+ $feature_settings = [
+ 'status' => '0',
+ 'roles' => $this->roles,
+ 'provider' => ComputerVision::ID,
+ ];
+
+ $provider_instance = $this->get_feature_provider_instance( ComputerVision::ID );
+ $provider_settings[ ComputerVision::ID ] = $provider_instance->get_default_provider_settings();
+
+ return
+ apply_filters(
+ 'classifai_' . static::ID . '_get_default_settings',
+ array_merge(
+ $feature_settings,
+ $provider_settings
+ )
+ );
+ }
+
+ /**
+ * Sanitizes the settings before saving.
+ *
+ * @param array $new_settings The settings to be sanitized on save.
+ *
+ * @internal
+ *
+ * @return array
+ */
+ public function sanitize_settings( $new_settings ) {
+ $settings = $this->get_settings();
+
+ $new_settings['status'] = $new_settings['status'] ?? $settings['status'];
+ $new_settings['roles'] = isset( $new_settings['roles'] ) ? array_map( 'sanitize_text_field', $new_settings['roles'] ) : $settings['roles'];
+ $new_settings['provider'] = isset( $new_settings['provider'] ) ? sanitize_text_field( $new_settings['provider'] ) : $settings['provider'];
+
+ $provider_instance = $this->get_feature_provider_instance( $new_settings['provider'] );
+ $new_settings = $provider_instance->sanitize_settings( $new_settings );
+
+ return apply_filters(
+ 'classifai_' . static::ID . '_sanitize_settings',
+ $new_settings,
+ $settings
+ );
+ }
+}
diff --git a/includes/Classifai/Features/ImageToText.php b/includes/Classifai/Features/ImageToText.php
new file mode 100644
index 000000000..aabce1093
--- /dev/null
+++ b/includes/Classifai/Features/ImageToText.php
@@ -0,0 +1,206 @@
+provider_instances = $this->get_provider_instances( $service_providers );
+ }
+
+ /**
+ * Returns the label of the feature.
+ *
+ * @return string
+ */
+ public function get_label() {
+ return apply_filters(
+ 'classifai_' . static::ID . '_label',
+ __( 'Text extraction from images', 'classifai' )
+ );
+ }
+
+ /**
+ * Returns the providers supported by the feature.
+ *
+ * @internal
+ *
+ * @return array
+ */
+ protected function get_providers() {
+ return apply_filters(
+ 'classifai_' . static::ID . '_providers',
+ [
+ ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ),
+ ]
+ );
+ }
+
+ /**
+ * Sets up the fields and sections for the feature.
+ */
+ public function setup_fields_sections() {
+ $settings = $this->get_settings();
+
+ add_settings_section(
+ $this->get_option_name() . '_section',
+ esc_html__( 'Feature settings', 'classifai' ),
+ '__return_empty_string',
+ $this->get_option_name()
+ );
+
+ add_settings_field(
+ 'status',
+ esc_html__( 'Enable text extraction from images', 'classifai' ),
+ [ $this, 'render_input' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'status',
+ 'input_type' => 'checkbox',
+ 'default_value' => $settings['status'],
+ 'description' => __( 'OCR detects text in images (e.g., handwritten notes) and saves that as post content.', 'classifai' ),
+ ]
+ );
+
+ add_settings_field(
+ 'roles',
+ esc_html__( 'Allowed roles', 'classifai' ),
+ [ $this, 'render_checkbox_group' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'roles',
+ 'options' => $this->roles,
+ 'default_values' => $settings['roles'],
+ 'description' => __( 'Choose which roles are allowed to generate image tags.', 'classifai' ),
+ ]
+ );
+
+ add_settings_field(
+ 'provider',
+ esc_html__( 'Select a provider', 'classifai' ),
+ [ $this, 'render_select' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'provider',
+ 'options' => $this->get_providers(),
+ 'default_value' => $settings['provider'],
+ ]
+ );
+
+ foreach( array_keys( $this->get_providers() ) as $provider_id ) {
+ $provider = $this->get_feature_provider_instance( $provider_id );
+
+ if ( method_exists( $provider, 'render_provider_fields' ) ) {
+ $provider->render_provider_fields();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the feature meets all the criteria to be enabled.
+ *
+ * @return boolean
+ */
+ public function is_feature_enabled() {
+ $access = false;
+ $settings = $this->get_settings();
+ $provider_id = $settings['provider'] ?? ComputerVision::ID;
+ $user_roles = wp_get_current_user()->roles ?? [];
+ $feature_roles = $settings['roles'] ?? [];
+
+ $user_access = ! empty( $feature_roles ) && ! empty( array_intersect( $user_roles, $feature_roles ) );
+ $provider_access = $settings[ $provider_id ]['authenticated'] ?? false;
+ $feature_status = isset( $settings['status'] ) && '1' === $settings['status'];
+ $access = $user_access && $provider_access && $feature_status;
+
+ /**
+ * Filter to override permission to the generate title feature.
+ *
+ * @since 2.3.0
+ * @hook classifai_openai_chatgpt_{$feature}
+ *
+ * @param {bool} $access Current access value.
+ * @param {array} $settings Current feature settings.
+ *
+ * @return {bool} Should the user have access?
+ */
+ return apply_filters( 'classifai_' . static::ID . '_is_feature_enabled', $access, $settings );
+ }
+
+ /**
+ * Returns the default settings for the feature.
+ *
+ * The root-level keys are the setting keys that are independent of the provider.
+ * Provider specific settings should be nested under the provider key.
+ *
+ * @internal
+ *
+ * @todo Add a filter hook to allow other plugins to add their own settings.
+ *
+ * @return array
+ */
+ protected function get_default_settings() {
+ $provider_settings = [];
+ $feature_settings = [
+ 'status' => '0',
+ 'roles' => $this->roles,
+ 'provider' => ComputerVision::ID,
+ ];
+
+ $provider_instance = $this->get_feature_provider_instance( ComputerVision::ID );
+ $provider_settings[ ComputerVision::ID ] = $provider_instance->get_default_provider_settings();
+
+ return
+ apply_filters(
+ 'classifai_' . static::ID . '_get_default_settings',
+ array_merge(
+ $feature_settings,
+ $provider_settings
+ )
+ );
+ }
+
+ /**
+ * Sanitizes the settings before saving.
+ *
+ * @param array $new_settings The settings to be sanitized on save.
+ *
+ * @internal
+ *
+ * @return array
+ */
+ public function sanitize_settings( $new_settings ) {
+ $settings = $this->get_settings();
+
+ $new_settings['status'] = $new_settings['status'] ?? $settings['status'];
+ $new_settings['roles'] = isset( $new_settings['roles'] ) ? array_map( 'sanitize_text_field', $new_settings['roles'] ) : $settings['roles'];
+ $new_settings['provider'] = isset( $new_settings['provider'] ) ? sanitize_text_field( $new_settings['provider'] ) : $settings['provider'];
+
+ $provider_instance = $this->get_feature_provider_instance( $new_settings['provider'] );
+ $new_settings = $provider_instance->sanitize_settings( $new_settings );
+
+ return apply_filters(
+ 'classifai_' . static::ID . '_sanitize_settings',
+ $new_settings,
+ $settings
+ );
+ }
+}
diff --git a/includes/Classifai/Features/SmartCropping.php b/includes/Classifai/Features/SmartCropping.php
new file mode 100644
index 000000000..916e8a3e0
--- /dev/null
+++ b/includes/Classifai/Features/SmartCropping.php
@@ -0,0 +1,206 @@
+provider_instances = $this->get_provider_instances( $service_providers );
+ }
+
+ /**
+ * Returns the label of the feature.
+ *
+ * @return string
+ */
+ public function get_label() {
+ return apply_filters(
+ 'classifai_' . static::ID . '_label',
+ __( 'Smart Cropping', 'classifai' )
+ );
+ }
+
+ /**
+ * Returns the providers supported by the feature.
+ *
+ * @internal
+ *
+ * @return array
+ */
+ protected function get_providers() {
+ return apply_filters(
+ 'classifai_' . static::ID . '_providers',
+ [
+ ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ),
+ ]
+ );
+ }
+
+ /**
+ * Sets up the fields and sections for the feature.
+ */
+ public function setup_fields_sections() {
+ $settings = $this->get_settings();
+
+ add_settings_section(
+ $this->get_option_name() . '_section',
+ esc_html__( 'Feature settings', 'classifai' ),
+ '__return_empty_string',
+ $this->get_option_name()
+ );
+
+ add_settings_field(
+ 'status',
+ esc_html__( 'Enable smart cropping', 'classifai' ),
+ [ $this, 'render_input' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'status',
+ 'input_type' => 'checkbox',
+ 'default_value' => $settings['status'],
+ 'description' => __( 'AI Vision detects and saves the most visually interesting part of your image (i.e., faces, animals, notable text).', 'classifai' ),
+ ]
+ );
+
+ add_settings_field(
+ 'roles',
+ esc_html__( 'Allowed roles', 'classifai' ),
+ [ $this, 'render_checkbox_group' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'roles',
+ 'options' => $this->roles,
+ 'default_values' => $settings['roles'],
+ 'description' => __( 'Choose which roles are allowed to generate image tags.', 'classifai' ),
+ ]
+ );
+
+ add_settings_field(
+ 'provider',
+ esc_html__( 'Select a provider', 'classifai' ),
+ [ $this, 'render_select' ],
+ $this->get_option_name(),
+ $this->get_option_name() . '_section',
+ [
+ 'label_for' => 'provider',
+ 'options' => $this->get_providers(),
+ 'default_value' => $settings['provider'],
+ ]
+ );
+
+ foreach( array_keys( $this->get_providers() ) as $provider_id ) {
+ $provider = $this->get_feature_provider_instance( $provider_id );
+
+ if ( method_exists( $provider, 'render_provider_fields' ) ) {
+ $provider->render_provider_fields();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the feature meets all the criteria to be enabled.
+ *
+ * @return boolean
+ */
+ public function is_feature_enabled() {
+ $access = false;
+ $settings = $this->get_settings();
+ $provider_id = $settings['provider'] ?? ComputerVision::ID;
+ $user_roles = wp_get_current_user()->roles ?? [];
+ $feature_roles = $settings['roles'] ?? [];
+
+ $user_access = ! empty( $feature_roles ) && ! empty( array_intersect( $user_roles, $feature_roles ) );
+ $provider_access = $settings[ $provider_id ]['authenticated'] ?? false;
+ $feature_status = isset( $settings['status'] ) && '1' === $settings['status'];
+ $access = $user_access && $provider_access && $feature_status;
+
+ /**
+ * Filter to override permission to the generate title feature.
+ *
+ * @since 2.3.0
+ * @hook classifai_openai_chatgpt_{$feature}
+ *
+ * @param {bool} $access Current access value.
+ * @param {array} $settings Current feature settings.
+ *
+ * @return {bool} Should the user have access?
+ */
+ return apply_filters( 'classifai_' . static::ID . '_is_feature_enabled', $access, $settings );
+ }
+
+ /**
+ * Returns the default settings for the feature.
+ *
+ * The root-level keys are the setting keys that are independent of the provider.
+ * Provider specific settings should be nested under the provider key.
+ *
+ * @internal
+ *
+ * @todo Add a filter hook to allow other plugins to add their own settings.
+ *
+ * @return array
+ */
+ protected function get_default_settings() {
+ $provider_settings = [];
+ $feature_settings = [
+ 'status' => '0',
+ 'roles' => $this->roles,
+ 'provider' => ComputerVision::ID,
+ ];
+
+ $provider_instance = $this->get_feature_provider_instance( ComputerVision::ID );
+ $provider_settings[ ComputerVision::ID ] = $provider_instance->get_default_provider_settings();
+
+ return
+ apply_filters(
+ 'classifai_' . static::ID . '_get_default_settings',
+ array_merge(
+ $feature_settings,
+ $provider_settings
+ )
+ );
+ }
+
+ /**
+ * Sanitizes the settings before saving.
+ *
+ * @param array $new_settings The settings to be sanitized on save.
+ *
+ * @internal
+ *
+ * @return array
+ */
+ public function sanitize_settings( $new_settings ) {
+ $settings = $this->get_settings();
+
+ $new_settings['status'] = $new_settings['status'] ?? $settings['status'];
+ $new_settings['roles'] = isset( $new_settings['roles'] ) ? array_map( 'sanitize_text_field', $new_settings['roles'] ) : $settings['roles'];
+ $new_settings['provider'] = isset( $new_settings['provider'] ) ? sanitize_text_field( $new_settings['provider'] ) : $settings['provider'];
+
+ $provider_instance = $this->get_feature_provider_instance( $new_settings['provider'] );
+ $new_settings = $provider_instance->sanitize_settings( $new_settings );
+
+ return apply_filters(
+ 'classifai_' . static::ID . '_sanitize_settings',
+ $new_settings,
+ $settings
+ );
+ }
+}
diff --git a/includes/Classifai/Plugin.php b/includes/Classifai/Plugin.php
index 502c3c3d4..255b29db9 100644
--- a/includes/Classifai/Plugin.php
+++ b/includes/Classifai/Plugin.php
@@ -107,7 +107,7 @@ public function init_services() {
'classifai_services',
[
'language_processing' => 'Classifai\Services\LanguageProcessing',
- // 'image_processing' => 'Classifai\Services\ImageProcessing',
+ 'image_processing' => 'Classifai\Services\ImageProcessing',
'personalizer' => 'Classifai\Services\Personalizer',
]
);
diff --git a/includes/Classifai/Providers/Azure/ComputerVision.php b/includes/Classifai/Providers/Azure/ComputerVision.php
index e441083ae..6d10d27a5 100644
--- a/includes/Classifai/Providers/Azure/ComputerVision.php
+++ b/includes/Classifai/Providers/Azure/ComputerVision.php
@@ -5,6 +5,8 @@
namespace Classifai\Providers\Azure;
+use Classifai\Features\DescriptiveTextGenerator;
+use Classifai\Features\ImageTagsGenerator;
use Classifai\Providers\Provider;
use DOMDocument;
use WP_Error;
@@ -30,12 +32,11 @@ class ComputerVision extends Provider {
*
* @param string $service The service this class belongs to.
*/
- public function __construct( $service ) {
+ public function __construct( $feature_instance = null ) {
parent::__construct(
'Microsoft Azure',
'AI Vision',
- 'computer_vision',
- $service
+ 'computer_vision'
);
// Set the onboarding options.
@@ -50,6 +51,8 @@ public function __construct( $service ) {
'enable_read_pdf' => __( 'Scan PDFs for text', 'classifai' ),
),
);
+
+ $this->feature_instance = $feature_instance;
}
/**
@@ -59,6 +62,316 @@ public function reset_settings() {
update_option( $this->get_option_name(), $this->get_default_settings() );
}
+ public function render_provider_fields() {
+ $settings = $this->feature_instance->get_settings( static::ID );
+
+ add_settings_field(
+ 'endpoint_url',
+ esc_html__( 'Endpoint URL', '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' => 'endpoint_url',
+ 'input_type' => 'text',
+ 'default_value' => $settings['endpoint_url'],
+ 'description' => __( 'Supported protocol and hostname endpoints, e.g., https://REGION.api.cognitive.microsoft.com
or https://EXAMPLE.cognitiveservices.azure.com
. This can look different based on your setting choices in Azure.', 'classifai' ),
+ 'class' => 'large-text classifai-provider-field hidden' . ' provider-scope-' . static::ID, // Important to add this.
+ ]
+ );
+
+ add_settings_field(
+ 'api_key',
+ esc_html__( 'API 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' => 'api_key',
+ 'input_type' => 'password',
+ 'default_value' => $settings['api_key'],
+ 'class' => 'classifai-provider-field hidden' . ' provider-scope-' . static::ID, // Important to add this.
+ ]
+ );
+
+ switch ( $this->feature_instance::ID ) {
+ case DescriptiveTextGenerator::ID:
+ $this->add_descriptive_text_generation_fields();
+ break;
+
+ case ImageTagsGenerator::ID:
+ $this->add_image_tags_generation_fields();
+ break;
+ }
+
+ do_action( 'classifai_' . static::ID . '_render_provider_fields', $this );
+ }
+
+ public function add_descriptive_text_generation_fields() {
+ $settings = $this->feature_instance->get_settings( static::ID );
+
+ $checkbox_options = array(
+ 'alt' => esc_html__( 'Alt text', 'classifai' ),
+ 'caption' => esc_html__( 'Image caption', 'classifai' ),
+ 'description' => esc_html__( 'Image description', 'classifai' ),
+ );
+
+ add_settings_field(
+ 'descriptive_text_fields',
+ esc_html__( 'Generate descriptive text', 'classifai' ),
+ [ $this->feature_instance, 'render_checkbox_group' ],
+ $this->feature_instance->get_option_name(),
+ $this->feature_instance->get_option_name() . '_section',
+ [
+ 'option_index' => static::ID,
+ 'label_for' => 'descriptive_text_fields',
+ 'options' => $checkbox_options,
+ 'default_values' => $settings['descriptive_text_fields'],
+ 'description' => __( 'Choose image fields where the generated captions should be applied.', 'classifai' ),
+ 'class' => 'classifai-provider-field hidden' . ' provider-scope-' . static::ID, // Important to add this.
+ ]
+ );
+
+ add_settings_field(
+ 'descriptive_confidence_threshold',
+ esc_html__( 'Descriptive text confidence threshold', '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' => 'descriptive_confidence_threshold',
+ 'input_type' => 'number',
+ 'min' => 1,
+ 'step' => 1,
+ 'default_value' => $settings['descriptive_confidence_threshold'],
+ 'description' => esc_html__( 'Minimum confidence score for automatically added alt text, numeric value from 0-100. Recommended to be set to at least 75.', 'classifai' ),
+ 'class' => 'classifai-provider-field hidden' . ' provider-scope-' . static::ID, // Important to add this.
+ ]
+ );
+ }
+
+ public function add_image_tags_generation_fields() {
+ $settings = $this->feature_instance->get_settings( static::ID );
+
+ $attachment_taxonomies = get_object_taxonomies( 'attachment', 'objects' );
+ $options = [];
+
+ foreach ( $attachment_taxonomies as $name => $taxonomy ) {
+ $options[ $name ] = $taxonomy->label;
+ }
+
+ add_settings_field(
+ 'tag_taxonomy',
+ esc_html__( 'Tag taxonomy', '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' => 'tag_taxonomy',
+ 'options' => $options,
+ 'default_value' => $settings['tag_taxonomy'],
+ 'class' => 'classifai-provider-field hidden' . ' provider-scope-' . static::ID, // Important to add this.
+ ]
+ );
+
+ add_settings_field(
+ 'tag_confidence_threshold',
+ esc_html__( 'Tag confidence threshold', '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' => 'tag_confidence_threshold',
+ 'input_type' => 'number',
+ 'min' => 1,
+ 'step' => 1,
+ 'default_value' => $settings['tag_confidence_threshold'],
+ 'description' => esc_html__( 'Minimum confidence score for automatically added image tags, numeric value from 0-100. Recommended to be set to at least 70.', 'classifai' ),
+ 'class' => 'classifai-provider-field hidden' . ' provider-scope-' . static::ID, // Important to add this.
+ ]
+ );
+ }
+
+ public function get_default_provider_settings() {
+ $common_settings = [
+ 'endpoint_url' => '',
+ 'api_key' => '',
+ 'authenticated' => false,
+ ];
+
+ switch ( $this->feature_instance::ID ) {
+ case DescriptiveTextGenerator::ID:
+ return array_merge(
+ $common_settings,
+ [
+ 'descriptive_text_fields' => [
+ 'alt' => 0,
+ 'caption' => 0,
+ 'description' => 0,
+ ],
+ 'descriptive_confidence_threshold' => 75
+ ]
+ );
+
+ case ImageTagsGenerator::ID:
+ $attachment_taxonomies = get_object_taxonomies( 'attachment', 'objects' );
+ $options = [];
+
+ foreach ( $attachment_taxonomies as $name => $taxonomy ) {
+ $options[ $name ] = $taxonomy->label;
+ }
+
+ return array_merge(
+ $common_settings,
+ [
+ 'tag_confidence_threshold' => 70,
+ 'tag_taxonomy' => array_key_first( $options ),
+ ]
+ );
+ }
+
+ return $common_settings;
+ }
+
+ /**
+ * Sanitization
+ *
+ * @param array $settings The settings being saved.
+ *
+ * @return array|mixed
+ */
+ public function sanitize_settings( $new_settings ) {
+ $settings = $this->feature_instance->get_settings( static::ID );
+
+ if ( ! empty( $new_settings[ static::ID ]['endpoint_url'] ) && ! empty( $new_settings[ static::ID ]['api_key'] ) ) {
+ $new_settings[ static::ID ]['authenticated'] = $settings[ static::ID ]['authenticated'];
+ $new_settings[ static::ID ]['endpoint_url'] = esc_url_raw( $settings['url'] );
+ $new_settings[ static::ID ]['api_key'] = sanitize_text_field( $settings['api_key'] );
+
+ $auth_check = $this->authenticate_credentials(
+ $new_settings[ static::ID ]['endpoint_url'],
+ $new_settings[ static::ID ]['api_key']
+ );
+
+ if ( is_wp_error( $auth_check ) ) {
+ $new_settings[ static::ID ]['authenticated'] = false;
+ } else {
+ $new_settings[ static::ID ]['authenticated'] = true;
+ }
+ } else {
+ $new_settings[ static::ID ]['endpoint_url'] = $settings[ static::ID ]['endpoint_url'];
+ $new_settings[ static::ID ]['api_key'] = $settings[ static::ID ]['api_key'];
+ }
+
+ if ( $this->feature_instance instanceof DescriptiveTextGenerator ) {
+ $new_settings[ static::ID ][ 'descriptive_confidence_threshold' ] = absint( $new_settings[ static::ID ]['descriptive_confidence_threshold'] ?? $settings[ static::ID ]['descriptive_confidence_threshold'] );
+ $new_settings[ static::ID ][ 'descriptive_text_fields' ] = array_map( 'sanitize_text_field', $new_settings[ static::ID ]['descriptive_text_fields'] ?? $settings[ static::ID ]['descriptive_text_fields'] );
+ }
+
+ if ( $this->feature_instance instanceof ImageTagsGenerator ) {
+ $new_settings[ static::ID ][ 'tag_confidence_threshold' ] = absint( $new_settings[ static::ID ]['tag_confidence_threshold'] ?? $settings[ static::ID ]['descriptive_confidence_threshold'] );
+ $new_settings[ static::ID ][ 'tag_taxonomy' ] = array_map( 'sanitize_text_field', $new_settings[ static::ID ]['tag_taxonomy'] ?? $settings[ static::ID ]['descriptive_text_fields'] );
+ }
+
+ return $new_settings;
+
+ $new_settings = [];
+ if ( ! empty( $settings['url'] ) && ! empty( $settings['api_key'] ) ) {
+ $auth_check = $this->authenticate_credentials( $settings['url'], $settings['api_key'] );
+ if ( is_wp_error( $auth_check ) ) {
+ $settings_errors['classifai-registration-credentials-error'] = $auth_check->get_error_message();
+ $new_settings['authenticated'] = false;
+ } else {
+ $new_settings['authenticated'] = true;
+ }
+ $new_settings['url'] = esc_url_raw( $settings['url'] );
+ $new_settings['api_key'] = sanitize_text_field( $settings['api_key'] );
+ } else {
+ $new_settings['valid'] = false;
+ $new_settings['url'] = '';
+ $new_settings['api_key'] = '';
+
+ $settings_errors['classifai-registration-credentials-empty'] = __( 'Please enter your credentials', 'classifai' );
+ }
+
+ $checkbox_settings = [
+ 'enable_image_tagging',
+ 'enable_smart_cropping',
+ 'enable_ocr',
+ 'enable_read_pdf',
+ ];
+
+ foreach ( $checkbox_settings as $checkbox_setting ) {
+
+ if ( empty( $settings[ $checkbox_setting ] ) || 1 !== (int) $settings[ $checkbox_setting ] ) {
+ $new_settings[ $checkbox_setting ] = 'no';
+ } else {
+ $new_settings[ $checkbox_setting ] = '1';
+ }
+ }
+
+ if ( isset( $settings['caption_threshold'] ) && is_numeric( $settings['caption_threshold'] ) && (int) $settings['caption_threshold'] >= 0 && (int) $settings['caption_threshold'] <= 100 ) {
+ $new_settings['caption_threshold'] = absint( $settings['caption_threshold'] );
+ } else {
+ $new_settings['caption_threshold'] = 75;
+ }
+
+ if ( isset( $settings['tag_threshold'] ) && is_numeric( $settings['tag_threshold'] ) && (int) $settings['tag_threshold'] >= 0 && (int) $settings['tag_threshold'] <= 100 ) {
+ $new_settings['tag_threshold'] = absint( $settings['tag_threshold'] );
+ } else {
+ $new_settings['tag_threshold'] = 75;
+ }
+
+ if ( isset( $settings['image_tag_taxonomy'] ) && taxonomy_exists( $settings['image_tag_taxonomy'] ) ) {
+ $new_settings['image_tag_taxonomy'] = $settings['image_tag_taxonomy'];
+ } elseif ( taxonomy_exists( 'classifai-image-tags' ) ) {
+ $new_settings['image_tag_taxonomy'] = 'classifai-image-tags';
+ }
+
+ if ( isset( $settings['enable_image_captions'] ) ) {
+ if ( is_array( $settings['enable_image_captions'] ) ) {
+ $new_settings['enable_image_captions'] = $settings['enable_image_captions'];
+ } elseif ( 1 === (int) $settings['enable_image_captions'] ) {
+ // Handle submission from onboarding wizard.
+ $new_settings['enable_image_captions'] = array(
+ 'alt' => 'alt',
+ 'caption' => 0,
+ 'description' => 0,
+ );
+ }
+ } else {
+ $new_settings['enable_image_captions'] = array(
+ 'alt' => 0,
+ 'caption' => 0,
+ 'description' => 0,
+ );
+ }
+
+ if ( ! empty( $settings_errors ) ) {
+
+ $registered_settings_errors = wp_list_pluck( get_settings_errors( $this->get_option_name() ), 'code' );
+
+ foreach ( $settings_errors as $code => $message ) {
+
+ if ( ! in_array( $code, $registered_settings_errors, true ) ) {
+ add_settings_error(
+ $this->get_option_name(),
+ $code,
+ esc_html( $message ),
+ 'error'
+ );
+ }
+ }
+ }
+
+ return $new_settings;
+ }
+
/**
* Default settings for ComputerVision
*
@@ -1094,106 +1407,6 @@ public function setup_fields_sections() {
);
}
- /**
- * Sanitization
- *
- * @param array $settings The settings being saved.
- *
- * @return array|mixed
- */
- public function sanitize_settings( $settings ) {
- $new_settings = [];
- if ( ! empty( $settings['url'] ) && ! empty( $settings['api_key'] ) ) {
- $auth_check = $this->authenticate_credentials( $settings['url'], $settings['api_key'] );
- if ( is_wp_error( $auth_check ) ) {
- $settings_errors['classifai-registration-credentials-error'] = $auth_check->get_error_message();
- $new_settings['authenticated'] = false;
- } else {
- $new_settings['authenticated'] = true;
- }
- $new_settings['url'] = esc_url_raw( $settings['url'] );
- $new_settings['api_key'] = sanitize_text_field( $settings['api_key'] );
- } else {
- $new_settings['valid'] = false;
- $new_settings['url'] = '';
- $new_settings['api_key'] = '';
-
- $settings_errors['classifai-registration-credentials-empty'] = __( 'Please enter your credentials', 'classifai' );
- }
-
- $checkbox_settings = [
- 'enable_image_tagging',
- 'enable_smart_cropping',
- 'enable_ocr',
- 'enable_read_pdf',
- ];
-
- foreach ( $checkbox_settings as $checkbox_setting ) {
-
- if ( empty( $settings[ $checkbox_setting ] ) || 1 !== (int) $settings[ $checkbox_setting ] ) {
- $new_settings[ $checkbox_setting ] = 'no';
- } else {
- $new_settings[ $checkbox_setting ] = '1';
- }
- }
-
- if ( isset( $settings['caption_threshold'] ) && is_numeric( $settings['caption_threshold'] ) && (int) $settings['caption_threshold'] >= 0 && (int) $settings['caption_threshold'] <= 100 ) {
- $new_settings['caption_threshold'] = absint( $settings['caption_threshold'] );
- } else {
- $new_settings['caption_threshold'] = 75;
- }
-
- if ( isset( $settings['tag_threshold'] ) && is_numeric( $settings['tag_threshold'] ) && (int) $settings['tag_threshold'] >= 0 && (int) $settings['tag_threshold'] <= 100 ) {
- $new_settings['tag_threshold'] = absint( $settings['tag_threshold'] );
- } else {
- $new_settings['tag_threshold'] = 75;
- }
-
- if ( isset( $settings['image_tag_taxonomy'] ) && taxonomy_exists( $settings['image_tag_taxonomy'] ) ) {
- $new_settings['image_tag_taxonomy'] = $settings['image_tag_taxonomy'];
- } elseif ( taxonomy_exists( 'classifai-image-tags' ) ) {
- $new_settings['image_tag_taxonomy'] = 'classifai-image-tags';
- }
-
- if ( isset( $settings['enable_image_captions'] ) ) {
- if ( is_array( $settings['enable_image_captions'] ) ) {
- $new_settings['enable_image_captions'] = $settings['enable_image_captions'];
- } elseif ( 1 === (int) $settings['enable_image_captions'] ) {
- // Handle submission from onboarding wizard.
- $new_settings['enable_image_captions'] = array(
- 'alt' => 'alt',
- 'caption' => 0,
- 'description' => 0,
- );
- }
- } else {
- $new_settings['enable_image_captions'] = array(
- 'alt' => 0,
- 'caption' => 0,
- 'description' => 0,
- );
- }
-
- if ( ! empty( $settings_errors ) ) {
-
- $registered_settings_errors = wp_list_pluck( get_settings_errors( $this->get_option_name() ), 'code' );
-
- foreach ( $settings_errors as $code => $message ) {
-
- if ( ! in_array( $code, $registered_settings_errors, true ) ) {
- add_settings_error(
- $this->get_option_name(),
- $code,
- esc_html( $message ),
- 'error'
- );
- }
- }
- }
-
- return $new_settings;
- }
-
/**
* Authenticates our credentials.
*
diff --git a/includes/Classifai/Providers/Azure/Personalizer.php b/includes/Classifai/Providers/Azure/Personalizer.php
index 0f826d4ea..cf5bd7175 100644
--- a/includes/Classifai/Providers/Azure/Personalizer.php
+++ b/includes/Classifai/Providers/Azure/Personalizer.php
@@ -34,7 +34,7 @@ class Personalizer extends Provider {
*
* @param string $service The service this class belongs to.
*/
- public function __construct( $service ) {
+ public function __construct( $service = null ) {
parent::__construct(
'Microsoft Azure',
'AI Personalizer',
diff --git a/includes/Classifai/Providers/Azure/Speech.php b/includes/Classifai/Providers/Azure/Speech.php
index fc86f11bb..6c9bc630d 100644
--- a/includes/Classifai/Providers/Azure/Speech.php
+++ b/includes/Classifai/Providers/Azure/Speech.php
@@ -72,7 +72,7 @@ class Speech extends Provider {
*
* @param \Classifai\Features\Feature $feature_instance The feature instance.
*/
- public function __construct( $feature_instance ) {
+ public function __construct( $feature_instance = null ) {
parent::__construct(
'Microsoft Azure',
self::FEATURE_NAME,
diff --git a/includes/Classifai/Providers/OpenAI/ChatGPT.php b/includes/Classifai/Providers/OpenAI/ChatGPT.php
index 8b51d3268..ced19e054 100644
--- a/includes/Classifai/Providers/OpenAI/ChatGPT.php
+++ b/includes/Classifai/Providers/OpenAI/ChatGPT.php
@@ -75,7 +75,7 @@ class ChatGPT extends Provider {
*
* @param \Classifai\Features\Feature $feature_instance The feature instance.
*/
- public function __construct( $feature_instance ) {
+ public function __construct( $feature_instance = null ) {
parent::__construct(
'OpenAI ChatGPT',
'ChatGPT',
diff --git a/includes/Classifai/Providers/OpenAI/DallE.php b/includes/Classifai/Providers/OpenAI/DallE.php
index 32f23bd09..8f8eba38e 100644
--- a/includes/Classifai/Providers/OpenAI/DallE.php
+++ b/includes/Classifai/Providers/OpenAI/DallE.php
@@ -35,7 +35,7 @@ class DallE extends Provider {
*
* @param string $service The service this class belongs to.
*/
- public function __construct( $service ) {
+ public function __construct( $service = null ) {
parent::__construct(
'OpenAI',
'DALLĀ·E',
diff --git a/includes/Classifai/Providers/OpenAI/Whisper.php b/includes/Classifai/Providers/OpenAI/Whisper.php
index 0adc376cf..5fd5f3bfe 100644
--- a/includes/Classifai/Providers/OpenAI/Whisper.php
+++ b/includes/Classifai/Providers/OpenAI/Whisper.php
@@ -30,7 +30,7 @@ class Whisper extends Provider {
*
* @param \Classifai\Features\Feature $feature_instance The feature instance.
*/
- public function __construct( $feature_instance ) {
+ public function __construct( $feature_instance = null ) {
parent::__construct(
'OpenAI Whisper',
'Whisper',
diff --git a/includes/Classifai/Services/ImageProcessing.php b/includes/Classifai/Services/ImageProcessing.php
index e5621359e..600e79532 100644
--- a/includes/Classifai/Services/ImageProcessing.php
+++ b/includes/Classifai/Services/ImageProcessing.php
@@ -39,6 +39,15 @@ public function init() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_media_scripts' ] );
}
+ public static function get_service_providers() {
+ return apply_filters(
+ 'classifai_language_processing_service_providers',
+ [
+ 'Classifai\Providers\Azure\ComputerVision',
+ ]
+ );
+ }
+
/**
* Enqueue the script for the media modal.
*
diff --git a/includes/Classifai/Services/Service.php b/includes/Classifai/Services/Service.php
index fdfa514b9..21eaf2a00 100644
--- a/includes/Classifai/Services/Service.php
+++ b/includes/Classifai/Services/Service.php
@@ -72,7 +72,7 @@ public function init() {
if ( ! empty( $this->providers ) && is_array( $this->providers ) ) {
foreach ( $this->providers as $provider ) {
if ( class_exists( $provider ) ) {
- $this->provider_classes[] = new $provider( $this->menu_slug );
+ $this->provider_classes[] = new $provider();
}
}
}
diff --git a/includes/Classifai/Services/ServicesManager.php b/includes/Classifai/Services/ServicesManager.php
index b8ad0215c..172adc747 100644
--- a/includes/Classifai/Services/ServicesManager.php
+++ b/includes/Classifai/Services/ServicesManager.php
@@ -42,7 +42,8 @@ public function __construct( $services = [] ) {
* Register the actions required for the settings page.
*/
public function register() {
- add_filter( 'language_processing_features', [ $this, 'language_processing_features' ] );
+ add_filter( 'language_processing_features', [ $this, 'register_language_processing_features' ] );
+ add_filter( 'image_processing_features', [ $this, 'register_image_processing_features' ] );
foreach ( $this->services as $key => $service ) {
if ( class_exists( $service ) ) {
@@ -59,7 +60,7 @@ public function register() {
add_filter( 'classifai_debug_information', [ $this, 'add_debug_information' ], 1 );
}
- public function language_processing_features() {
+ public function register_language_processing_features() {
return [
'\Classifai\Features\TitleGeneration',
'\Classifai\Features\ExcerptGeneration',
@@ -69,6 +70,15 @@ public function language_processing_features() {
];
}
+ public function register_image_processing_features() {
+ return [
+ '\Classifai\Features\DescriptiveTextGenerator',
+ '\Classifai\Features\ImageTagsGenerator',
+ '\Classifai\Features\SmartCropping',
+ '\Classifai\Features\ImageToText',
+ ];
+ }
+
/**
* Get general ClassifAI settings
*