diff --git a/includes/Classifai/Admin/BulkActions.php b/includes/Classifai/Admin/BulkActions.php index c6bc730b3..e01e75f9d 100644 --- a/includes/Classifai/Admin/BulkActions.php +++ b/includes/Classifai/Admin/BulkActions.php @@ -9,6 +9,7 @@ use Classifai\Providers\OpenAI\Whisper\Transcribe; use function Classifai\get_post_types_for_language_settings; use function Classifai\get_supported_post_types; +use function Classifai\get_tts_supported_post_types; /** * Handle bulk actions. @@ -68,15 +69,14 @@ public function register() { * Register bulk actions for language processing. */ public function register_language_processing_hooks() { - $this->chat_gpt = new ChatGPT( false ); - $this->embeddings = new Embeddings( false ); - $this->text_to_speech = new TextToSpeech( false ); + $this->chat_gpt = new ChatGPT( false ); + $this->embeddings = new Embeddings( false ); $user_roles = wp_get_current_user()->roles ?? []; $embedding_settings = $this->embeddings->get_settings(); $embeddings_post_types = []; $nlu_post_types = get_supported_post_types(); - $text_to_speech_post_types = $this->text_to_speech->get_supported_post_types(); + $text_to_speech_post_types = get_tts_supported_post_types(); $chat_gpt_post_types = []; $chat_gpt_settings = $this->chat_gpt->get_settings(); @@ -104,11 +104,6 @@ public function register_language_processing_hooks() { $this->embeddings = null; } - // Clear our TextToSpeech handler if no post types are set up. - if ( empty( $text_to_speech_post_types ) ) { - $this->text_to_speech = null; - } - // Merge our post types together and make them unique. $post_types = array_unique( array_merge( $chat_gpt_post_types, $embeddings_post_types, $nlu_post_types, $text_to_speech_post_types ) ); diff --git a/includes/Classifai/Helpers.php b/includes/Classifai/Helpers.php index 734bf1db6..47f47e3f1 100644 --- a/includes/Classifai/Helpers.php +++ b/includes/Classifai/Helpers.php @@ -3,6 +3,7 @@ namespace Classifai; use Classifai\Providers\Provider; +use Classifai\Providers\Azure; use Classifai\Services\Service; use Classifai\Services\ServicesManager; use WP_Error; @@ -275,6 +276,28 @@ function get_supported_post_types() { return $post_types; } +/** + * The list of post types that TTS supports. + * + * @return array Supported Post Types. + */ +function get_tts_supported_post_types() { + $classifai_settings = get_plugin_settings( 'language_processing', Azure\TextToSpeech::FEATURE_NAME ); + + if ( empty( $classifai_settings ) ) { + $post_types = []; + } else { + $post_types = []; + foreach ( $classifai_settings['post_types'] as $post_type => $enabled ) { + if ( ! empty( $enabled ) ) { + $post_types[] = $post_type; + } + } + } + + return $post_types; +} + /** * The list of post statuses that get the ClassifAI taxonomies. Defaults * to 'publish'. diff --git a/includes/Classifai/Providers/Azure/ComputerVision.php b/includes/Classifai/Providers/Azure/ComputerVision.php index 00ee86901..2a9555610 100644 --- a/includes/Classifai/Providers/Azure/ComputerVision.php +++ b/includes/Classifai/Providers/Azure/ComputerVision.php @@ -62,7 +62,7 @@ public function reset_settings() { * * @return array */ - private function get_default_settings() { + public function get_default_settings() { return [ 'valid' => false, 'url' => '', diff --git a/includes/Classifai/Providers/Azure/Personalizer.php b/includes/Classifai/Providers/Azure/Personalizer.php index e9600cef6..c094e1306 100644 --- a/includes/Classifai/Providers/Azure/Personalizer.php +++ b/includes/Classifai/Providers/Azure/Personalizer.php @@ -62,7 +62,7 @@ public function reset_settings() { * * @return array */ - private function get_default_settings() { + public function get_default_settings() { return [ 'authenticated' => false, 'url' => '', diff --git a/includes/Classifai/Providers/Azure/TextToSpeech.php b/includes/Classifai/Providers/Azure/TextToSpeech.php index 8ceeffba1..e7b156895 100644 --- a/includes/Classifai/Providers/Azure/TextToSpeech.php +++ b/includes/Classifai/Providers/Azure/TextToSpeech.php @@ -11,6 +11,7 @@ use WP_Http; use function Classifai\get_post_types_for_language_settings; +use function Classifai\get_tts_supported_post_types; use function Classifai\get_asset_info; class TextToSpeech extends Provider { @@ -30,12 +31,11 @@ class TextToSpeech extends Provider { const API_PATH = 'cognitiveservices/v1'; /** - * Meta key to store data indicating whether Text to Speech is enabled for - * the current post. + * Meta key to hide/unhide already generated audio file. * * @var string */ - const SYNTHESIZE_SPEECH_KEY = '_classifai_synthesize_speech'; + const DISPLAY_GENERATED_AUDIO = '_classifai_display_generated_audio'; /** * Meta key to get/set the ID of the speech audio file. @@ -87,21 +87,13 @@ public function __construct( $service ) { * Enqueue the editor scripts. */ public function enqueue_editor_assets() { - global $post; - - wp_enqueue_script( - 'classifai-editor', // Handle. - CLASSIFAI_PLUGIN_URL . 'dist/editor.js', - array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor', 'wp-edit-post' ), - CLASSIFAI_PLUGIN_VERSION, - true - ); + $post = get_post(); if ( empty( $post ) ) { return; } - $supported_post_types = self::get_supported_post_types(); + $supported_post_types = get_tts_supported_post_types(); if ( ! in_array( $post->post_type, $supported_post_types, true ) ) { return; @@ -115,13 +107,13 @@ public function enqueue_editor_assets() { true ); - wp_localize_script( + wp_add_inline_script( 'classifai-gutenberg-plugin', - 'classifaiTextToSpeechData', - [ - 'supportedPostTypes' => self::get_supported_post_types(), - 'noPermissions' => ! is_user_logged_in() || ! current_user_can( 'edit_post', $post->ID ), - ] + sprintf( + 'var classifaiTTSEnabled = %d;', + true + ), + 'before' ); } @@ -133,6 +125,12 @@ public function register() { add_action( 'rest_api_init', [ $this, 'add_synthesize_speech_meta_to_rest_api' ] ); add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ] ); add_action( 'save_post', [ $this, 'save_post_metadata' ], 5 ); + + $supported_post_type = get_tts_supported_post_types(); + foreach ( get_tts_supported_post_types() as $post_type ) { + add_action( 'rest_insert_' . $post_type, [ $this, 'rest_handle_audio' ], 10, 2 ); + } + add_filter( 'the_content', [ $this, 'render_post_audio_controls' ] ); } @@ -190,7 +188,7 @@ public function setup_fields_sections() { 'label_for' => 'post_types', 'option_index' => 'post_types', 'options' => $this->get_post_types_select_options(), - 'default_values' => $default_settings['post-types'], + 'default_values' => $default_settings['post_types'], ] ); @@ -240,6 +238,9 @@ public function sanitize_settings( $settings ) { if ( ! empty( $current_settings['voices'] ) ) { $current_settings['authenticated'] = true; + } else { + $current_settings['voices'] = []; + $current_settings['authenticated'] = false; } } } else { @@ -267,8 +268,6 @@ public function sanitize_settings( $settings ) { if ( isset( $settings['voice'] ) && ! empty( $settings['voice'] ) ) { $current_settings['voice'] = sanitize_text_field( $settings['voice'] ); - } else { - $current_settings['voice'] = ''; } return $current_settings; @@ -427,7 +426,7 @@ public function get_provider_debug_information( $settings = null, $configured = /** * Returns the default settings. */ - private function get_default_settings() { + public function get_default_settings() { return [ 'credentials' => array( 'url' => '', @@ -436,44 +435,108 @@ private function get_default_settings() { 'voices' => array(), 'voice' => '', 'authenticated' => false, - 'post-types' => array(), + 'post_types' => array(), ]; } /** - * Add `classifai_synthesize_speech` to rest API for view/edit. + * Initial audio generation state. + * + * Fetch the initial state of audio generation prior to the audio existing for the post. + * + * @param int|WP_Post|null $post Optional. Post ID or post object. `null`, `false`, `0` and other PHP falsey values + * return the current global post inside the loop. A numerically valid post ID that + * points to a non-existent post returns `null`. Defaults to global $post. + * @return bool The initial state of audio generation. Default true. + */ + public function get_audio_generation_initial_state( $post = null ) { + /** + * Initial state of the audio generation toggle when no audio already exists for the post. + * + * @since 2.3.0 + * @hook classifai_audio_generation_initial_state + * + * @param {bool} $state Initial state of audio generation toggle on a post. Default true. + * @param {WP_Post} $post The current Post object. + * + * @return {bool} Initial state the audio generation toggle should be set to when no audio exists. + */ + return apply_filters( 'classifai_audio_generation_initial_state', true, get_post( $post ) ); + } + + /** + * Subsequent audio generation state. + * + * Fetch the subsequent state of audio generation once audio is generated for the post. + * + * @param int|WP_Post|null $post Optional. Post ID or post object. `null`, `false`, `0` and other PHP falsey values + * return the current global post inside the loop. A numerically valid post ID that + * points to a non-existent post returns `null`. Defaults to global $post. + * @return bool The subsequent state of audio generation. Default false. + */ + public function get_audio_generation_subsequent_state( $post = null ) { + /** + * Subsequent state of the audio generation toggle when audio exists for the post. + * + * @since 2.3.0 + * @hook classifai_audio_generation_subsequent_state + * + * @param {bool} $state Subsequent state of audio generation toggle on a post. Default false. + * @param {WP_Post} $post The current Post object. + * + * @return {bool} Subsequent state the audio generation toggle should be set to when audio exists. + */ + return apply_filters( 'classifai_audio_generation_subsequent_state', false, get_post( $post ) ); + } + + /** + * Add audio related fields to rest API for view/edit. */ public function add_synthesize_speech_meta_to_rest_api() { - $settings = $this->get_settings(); - $supported_post_types = $settings['post_types'] ?? array(); - - $supported_post_types = array_keys( - array_filter( - $supported_post_types, - function( $post_type ) { - return ! is_null( $post_type ); - } + $supported_post_types = get_tts_supported_post_types(); + + register_rest_field( + $supported_post_types, + 'classifai_synthesize_speech', + array( + 'get_callback' => function( $object ) { + $audio_id = get_post_meta( $object['id'], self::AUDIO_ID_KEY, true ); + if ( + ( $this->get_audio_generation_initial_state( $object['id'] ) && ! $audio_id ) || + ( $this->get_audio_generation_subsequent_state( $object['id'] ) && $audio_id ) + ) { + return true; + } else { + return false; + } + }, + 'schema' => [ + 'type' => 'boolean', + 'context' => [ 'view', 'edit' ], + ], ) ); - if ( empty( $supported_post_types ) ) { - return; - } - register_rest_field( $supported_post_types, - 'classifai_synthesize_speech', + 'classifai_display_generated_audio', array( 'get_callback' => function( $object ) { - $process_content = get_post_meta( $object['id'], self::SYNTHESIZE_SPEECH_KEY, true ); - return ( 'no' === $process_content ) ? 'no' : 'yes'; + // Default to display the audio if available. + if ( metadata_exists( 'post', $object['id'], self::DISPLAY_GENERATED_AUDIO ) ) { + return (bool) get_post_meta( $object['id'], self::DISPLAY_GENERATED_AUDIO, true ); + } + return true; }, - 'update_callback' => function ( $value, $object ) { - $value = ( 'no' === $value ) ? 'no' : 'yes'; - return update_post_meta( $object->ID, self::SYNTHESIZE_SPEECH_KEY, $value ); + 'update_callback' => function( $value, $object ) { + if ( $value ) { + delete_post_meta( $object->ID, self::DISPLAY_GENERATED_AUDIO ); + } else { + update_post_meta( $object->ID, self::DISPLAY_GENERATED_AUDIO, false ); + } }, 'schema' => [ - 'type' => 'string', + 'type' => 'boolean', 'context' => [ 'view', 'edit' ], ], ) @@ -495,13 +558,42 @@ function( $post_type ) { ); } + /** + * Handles audio generation on rest updates / inserts. + * + * @param WP_Post $post Inserted or updated post object. + * @param WP_REST_Request $request Request object. + */ + public function rest_handle_audio( $post, $request ) { + + $audio_id = get_post_meta( $request->get_param( 'id' ), self::AUDIO_ID_KEY, true ); + + // Since we have dynamic generation option agnostic to meta saves we need a flag to differentiate audio generation accurately + $process_content = false; + if ( + ( $this->get_audio_generation_initial_state( $post ) && ! $audio_id ) || + ( $this->get_audio_generation_subsequent_state( $post ) && $audio_id ) + ) { + $process_content = true; + } + + // Add/Update audio if it was requested. + if ( + ( $process_content && null === $request->get_param( 'classifai_synthesize_speech' ) ) || + true === $request->get_param( 'classifai_synthesize_speech' ) + ) { + $save_post_handler = new SavePostHandler(); + $save_post_handler->synthesize_speech( $request->get_param( 'id' ) ); + } + } + /** * Add meta box to post types that support speech synthesis. * * @param string $post_type Post type. */ public function add_meta_box( $post_type ) { - if ( ! in_array( $post_type, $this->get_supported_post_types(), true ) ) { + if ( ! in_array( $post_type, get_tts_supported_post_types(), true ) ) { return; } @@ -511,7 +603,7 @@ public function add_meta_box( $post_type ) { [ $this, 'render_meta_box' ], null, 'side', - 'low', + 'high', array( '__back_compat_meta_box' => true ) ); } @@ -524,38 +616,65 @@ public function add_meta_box( $post_type ) { public function render_meta_box( $post ) { wp_nonce_field( 'classifai_text_to_speech_meta_action', 'classifai_text_to_speech_meta' ); - $process_content = get_post_meta( $post->ID, self::SYNTHESIZE_SPEECH_KEY, true ); - $process_content = ( 'no' === $process_content ) ? 'no' : 'yes'; + $source_url = false; + $audio_id = get_post_meta( $post->ID, self::AUDIO_ID_KEY, true ); + if ( $audio_id ) { + $source_url = wp_get_attachment_url( $audio_id ); + } + + $process_content = false; + if ( + ( $this->get_audio_generation_initial_state( $post ) && ! $audio_id ) || + ( $this->get_audio_generation_subsequent_state( $post ) && $audio_id ) + ) { + $process_content = true; + } + + $display_audio = true; + if ( metadata_exists( 'post', $post->ID, self::DISPLAY_GENERATED_AUDIO ) && + ! (bool) get_post_meta( $post->ID, self::DISPLAY_GENERATED_AUDIO, true ) ) { + $display_audio = false; + } - $post_type = get_post_type_object( get_post_type( $post ) ); $post_type_label = esc_html__( 'Post', 'classifai' ); + $post_type = get_post_type_object( get_post_type( $post ) ); if ( $post_type ) { $post_type_label = $post_type->labels->singular_name; } - $audio_id = get_post_meta( $post->ID, self::AUDIO_ID_KEY, true ); ?> -

+ +

+ +