<?php
namespace Classifai\Features;
use Classifai\Services\LanguageProcessing;
use Classifai\Providers\Azure\Speech;
use Classifai\Providers\AWS\AmazonPolly;
use Classifai\Providers\OpenAI\TextToSpeech as OpenAITTS;
use Classifai\Normalizer;
use WP_REST_Server;
use WP_REST_Request;
use WP_Error;
use function Classifai\get_asset_info;
/**
* Class TextToSpeech
*/
class TextToSpeech extends Feature {
/**
* ID of the current feature.
*
* @var string
*/
const ID = 'feature_text_to_speech_generation';
/**
* Meta key to get/set the ID of the speech audio file.
*
* @var string
*/
const AUDIO_ID_KEY = '_classifai_post_audio_id';
/**
* Meta key to get/set the timestamp indicating when the speech was generated.
* Used for cache-busting as the audio filename remains static for a given post.
*
* @var string
*/
const AUDIO_TIMESTAMP_KEY = '_classifai_post_audio_timestamp';
/**
* Meta key to hide/unhide already generated audio file.
*
* @var string
*/
const DISPLAY_GENERATED_AUDIO = '_classifai_display_generated_audio';
/**
* Meta key to get/set the audio hash that helps to indicate if there is any need
* for the audio file to be regenerated or not.
*
* @var string
*/
const AUDIO_HASH_KEY = '_classifai_post_audio_hash';
/**
* Constructor.
*/
public function __construct() {
$this->label = __( 'Text to Speech', 'classifai' );
// Contains all providers that are registered to the service.
$this->provider_instances = $this->get_provider_instances( LanguageProcessing::get_service_providers() );
// Contains just the providers this feature supports.
$this->supported_providers = [
AmazonPolly::ID => __( 'Amazon Polly', 'classifai' ),
Speech::ID => __( 'Microsoft Azure AI Speech', 'classifai' ),
OpenAITTS::ID => __( 'OpenAI Text to Speech', 'classifai' ),
];
}
/**
* Set up necessary hooks.
*
* We utilize this so we can register the REST route.
*/
public function setup() {
parent::setup();
add_action( 'rest_api_init', [ $this, 'register_endpoints' ] );
if ( $this->is_enabled() ) {
add_filter( 'the_content', [ $this, 'render_post_audio_controls' ] );
}
}
/**
* Set up necessary hooks.
*/
public function feature_setup() {
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
add_action( 'rest_api_init', [ $this, 'add_meta_to_rest_api' ] );
foreach ( $this->get_supported_post_types() as $post_type ) {
add_action( 'rest_insert_' . $post_type, [ $this, 'rest_handle_audio' ], 10, 2 );
}
add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ] );
add_action( 'admin_notices', [ $this, 'show_error_if' ] );
add_action( 'save_post', [ $this, 'save_post_metadata' ], 5 );
}
/**
* Enqueue the editor scripts.
*
* @since 2.4.0 Use get_asset_info to get the asset version and dependencies.
*/
public function enqueue_editor_assets() {
if ( ! $this->is_feature_enabled() ) {
return;
}
$post = get_post();
if ( empty( $post ) ) {
return;
}
wp_enqueue_script(
'classifai-plugin-text-to-speech',
CLASSIFAI_PLUGIN_URL . 'dist/classifai-plugin-text-to-speech.js',
array_merge(
get_asset_info( 'classifai-plugin-text-to-speech', 'dependencies' ),
array( 'lodash' ),
array( Feature::PLUGIN_AREA_SCRIPT )
),
get_asset_info( 'classifai-plugin-text-to-speech', 'version' ),
true
);
}
/**
* Add audio related fields to rest API for view/edit.
*/
public function add_meta_to_rest_api() {
if ( ! $this->is_feature_enabled() ) {
return;
}
$supported_post_types = $this->get_supported_post_types();
register_rest_field(
$supported_post_types,
'classifai_synthesize_speech',
array(
'get_callback' => function ( $data ) {
$audio_id = get_post_meta( $data['id'], self::AUDIO_ID_KEY, true );
if (
( $this->get_audio_generation_initial_state( $data['id'] ) && ! $audio_id ) ||
( $this->get_audio_generation_subsequent_state( $data['id'] ) && $audio_id )
) {
return true;
} else {
return false;
}
},
'schema' => [
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
],
)
);
register_rest_field(
$supported_post_types,
'classifai_display_generated_audio',
array(
'get_callback' => function ( $data ) {
// Default to display the audio if available.
if ( metadata_exists( 'post', $data['id'], self::DISPLAY_GENERATED_AUDIO ) ) {
return (bool) get_post_meta( $data['id'], self::DISPLAY_GENERATED_AUDIO, true );
}
return true;
},
'update_callback' => function ( $value, $data ) {
if ( $value ) {
delete_post_meta( $data->ID, self::DISPLAY_GENERATED_AUDIO );
} else {
update_post_meta( $data->ID, self::DISPLAY_GENERATED_AUDIO, false );
}
},
'schema' => [
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
],
)
);
register_rest_field(
$supported_post_types,
'classifai_post_audio_id',
array(
'get_callback' => function ( $data ) {
$post_audio_id = get_post_meta( $data['id'], self::AUDIO_ID_KEY, true );
return (int) $post_audio_id;
},
'schema' => [
'type' => 'integer',
'context' => [ 'view', 'edit' ],
],
)
);
}
/**
* 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( \WP_Post $post, WP_REST_Request $request ) {
if ( ! $this->is_feature_enabled() ) {
return;
}
$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' )
) {
$results = $this->run( $request->get_param( 'id' ), 'synthesize' );
if ( $results && ! is_wp_error( $results ) ) {
$this->save( $results, $request->get_param( 'id' ) );
delete_post_meta( $post->ID, '_classifai_text_to_speech_error' );
} elseif ( is_wp_error( $results ) ) {
update_post_meta(
$post->ID,
'_classifai_text_to_speech_error',
wp_json_encode(
[
'code' => $results->get_error_code(),
'message' => $results->get_error_message(),
]
)
);
}
}
}
/**
* Register any needed endpoints.
*/
public function register_endpoints() {
$post_types = $this->get_supported_post_types();
foreach ( $post_types as $post_type ) {
register_meta(
$post_type,
'_classifai_text_to_speech_error',
[
'show_in_rest' => true,
'single' => true,
'auth_callback' => '__return_true',
]
);
}
register_rest_route(
'classifai/v1',
'synthesize-speech/(?P<id>\d+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'rest_endpoint_callback' ),
'args' => array(
'id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'ID of post to run text to speech conversion on.', 'classifai' ),
),
),
'permission_callback' => [ $this, 'speech_synthesis_permissions_check' ],
]
);
}
/**
* Check if a given request has access to generate audio for the post.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool
*/
public function speech_synthesis_permissions_check( WP_REST_Request $request ) {
$post_id = $request->get_param( 'id' );
// Ensure we have a logged in user that can edit the item.
if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
return false;
}
$post_type = get_post_type( $post_id );
$post_type_obj = get_post_type_object( $post_type );
// Ensure the post type is allowed in REST endpoints.
if ( ! $post_type || empty( $post_type_obj ) || empty( $post_type_obj->show_in_rest ) ) {
return false;
}
// Ensure the post type is supported by this feature.
$supported = $this->get_supported_post_types();
if ( ! in_array( $post_type, $supported, true ) ) {
return new WP_Error( 'not_enabled', esc_html__( 'Speech synthesis is not enabled for current item.', 'classifai' ) );
}
// Ensure the feature is enabled. Also runs a user check.
if ( ! $this->is_feature_enabled() ) {
return new WP_Error( 'not_enabled', esc_html__( 'Speech synthesis is not currently enabled.', 'classifai' ) );
}
return true;
}
/**
* Generic request handler for all our custom routes.
*
* @param WP_REST_Request $request The full request object.
* @return \WP_REST_Response
*/
public function rest_endpoint_callback( WP_REST_Request $request ) {
$route = $request->get_route();
if ( strpos( $route, '/classifai/v1/synthesize-speech' ) === 0 ) {
$results = $this->run( $request->get_param( 'id' ), 'synthesize' );
if ( $results && ! is_wp_error( $results ) ) {
$attachment_id = $this->save( $results, $request->get_param( 'id' ) );
if ( ! is_wp_error( $attachment_id ) ) {
return rest_ensure_response(
array(
'success' => true,
'audio_id' => $attachment_id,
)
);
}
}
return rest_ensure_response(
array(
'success' => false,
'code' => $results->get_error_code(),
'message' => $results->get_error_message(),
)
);
}
return parent::rest_endpoint_callback( $request );
}
/**
* Adds a meta box for Classic content to trigger Text to Speech.
*
* @param string $post_type The post type.
*/
public function add_meta_box( string $post_type ) {
if (
! in_array( $post_type, $this->get_supported_post_types(), true ) ||
! $this->is_feature_enabled()
) {
return;
}
\add_meta_box(
'classifai-text-to-speech-meta-box',
__( 'ClassifAI Text to Speech Processing', 'classifai' ),
[ $this, 'render_meta_box' ],
null,
'side',
'high',
array( '__back_compat_meta_box' => true )
);
}
/**
* Render meta box content.
*
* @param \WP_Post $post WP_Post object.
*/
public function render_meta_box( \WP_Post $post ) {
wp_nonce_field( 'classifai_text_to_speech_meta_action', 'classifai_text_to_speech_meta' );
$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_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;
}
?>
<p>
<label for="classifai_synthesize_speech">
<input type="checkbox" value="1" id="classifai_synthesize_speech" name="classifai_synthesize_speech" <?php checked( $process_content ); ?> />
<?php esc_html_e( 'Enable audio generation', 'classifai' ); ?>
</label>
<span class="description">
<?php
/* translators: %s Post type label */
printf( esc_html__( 'ClassifAI will generate audio for this %s when it is published or updated.', 'classifai' ), esc_html( $post_type_label ) );
?>
</span>
</p>
<p <?php echo $source_url ? '' : 'class="hidden"'; ?>>
<label for="classifai_display_generated_audio">
<input type="checkbox" value="1" id="classifai_display_generated_audio" name="classifai_display_generated_audio" <?php checked( $display_audio ); ?> />
<?php esc_html_e( 'Display audio controls', 'classifai' ); ?>
</label>
<span class="description">
<?php
esc_html__( 'Controls the display of the audio player on the front-end.', 'classifai' );
?>
</span>
</p>
<?php
if ( $source_url ) {
$cache_busting_url = add_query_arg(
[
'ver' => time(),
],
$source_url
);
?>
<p>
<audio id="classifai-audio-preview" controls controlslist="nodownload" src="<?php echo esc_url( $cache_busting_url ); ?>"></audio>
</p>
<?php
}
}
/**
* Process the meta box save.
*
* @param int $post_id Post ID.
*/
public function save_post_metadata( int $post_id ) {
if (
! in_array( get_post_type( $post_id ), $this->get_supported_post_types(), true ) ||
! $this->is_feature_enabled()
) {
return;
}
if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ! current_user_can( 'edit_post', $post_id ) || 'revision' === get_post_type( $post_id ) ) {
return;
}
if ( empty( $_POST['classifai_text_to_speech_meta'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['classifai_text_to_speech_meta'] ) ), 'classifai_text_to_speech_meta_action' ) ) {
return;
}
if ( ! isset( $_POST['classifai_display_generated_audio'] ) ) {
update_post_meta( $post_id, self::DISPLAY_GENERATED_AUDIO, false );
} else {
delete_post_meta( $post_id, self::DISPLAY_GENERATED_AUDIO );
}
if ( isset( $_POST['classifai_synthesize_speech'] ) ) {
$results = $this->run( $post_id, 'synthesize' );
if ( $results && ! is_wp_error( $results ) ) {
$this->save( $results, $post_id );
delete_post_meta( $post_id, '_classifai_text_to_speech_error' );
} elseif ( is_wp_error( $results ) ) {
update_post_meta(
$post_id,
'_classifai_text_to_speech_error',
wp_json_encode(
[
'code' => $results->get_error_code(),
'message' => $results->get_error_message(),
]
)
);
}
}
}
/**
* Save the returned result.
*
* @param string $result The results to save.
* @param int $post_id The post ID.
* @return int|WP_Error
*/
public function save( string $result, int $post_id ) {
$saved_attachment_id = (int) get_post_meta( $post_id, self::AUDIO_ID_KEY, true );
// The audio file name.
$audio_file_name = sprintf(
'post-as-audio-%1$s.mp3',
$post_id
);
// Upload the audio stream as an .mp3 file.
$file_data = wp_upload_bits(
$audio_file_name,
null,
$result
);
if ( isset( $file_data['error'] ) && ! empty( $file_data['error'] ) ) {
return new WP_Error(
'text_to_speech_upload_bits_failure',
esc_html( $file_data['error'] )
);
}
// Insert the audio file as attachment.
$attachment_id = wp_insert_attachment(
array(
'guid' => $file_data['file'],
'post_title' => $audio_file_name,
'post_mime_type' => $file_data['type'],
),
$file_data['file'],
$post_id
);
// Return error if creation of attachment fails.
if ( ! $attachment_id ) {
return new WP_Error(
'text_to_speech_resource_creation_failure',
esc_html__( 'Audio creation failed.', 'classifai' )
);
}
// If audio already exists for this post, delete it.
if ( $saved_attachment_id ) {
wp_delete_attachment( $saved_attachment_id, true );
delete_post_meta( $post_id, self::AUDIO_ID_KEY );
delete_post_meta( $post_id, self::AUDIO_TIMESTAMP_KEY );
}
update_post_meta( $post_id, self::AUDIO_ID_KEY, absint( $attachment_id ) );
update_post_meta( $post_id, self::AUDIO_TIMESTAMP_KEY, time() );
return $attachment_id;
}
/**
* Adds audio controls to the post that has speech synthesis enabled.
*
* @param string $content Post content.
* @return string
*/
public function render_post_audio_controls( string $content ): string {
$_post = get_post();
if (
! $_post instanceof \WP_Post ||
! is_singular( $_post->post_type ) ||
! in_array( $_post->post_type, $this->get_supported_post_types(), true )
) {
return $content;
}
/**
* Filter to disable the rendering of the Text to Speech block.
*
* @since 2.2.0
* @hook classifai_disable_post_to_audio_block
*
* @param {bool} $is_disabled Whether to disable the display or not. By default - false.
* @param {WP_Post} $_post The Post object.
*
* @return {bool} Whether the audio block should be shown.
*/
if ( apply_filters( 'classifai_disable_post_to_audio_block', false, $_post ) ) {
return $content;
}
// Respect the audio display settings of the post.
if ( metadata_exists( 'post', $_post->ID, self::DISPLAY_GENERATED_AUDIO ) &&
! (bool) get_post_meta( $_post->ID, self::DISPLAY_GENERATED_AUDIO, true ) ) {
return $content;
}
$audio_attachment_id = (int) get_post_meta( $_post->ID, self::AUDIO_ID_KEY, true );
if ( ! $audio_attachment_id ) {
return $content;
}
$audio_attachment_url = wp_get_attachment_url( $audio_attachment_id );
if ( ! $audio_attachment_url ) {
return $content;
}
$audio_timestamp = (int) get_post_meta( $_post->ID, self::AUDIO_TIMESTAMP_KEY, true );
if ( $audio_timestamp ) {
$audio_attachment_url = add_query_arg( 'ver', filter_var( $audio_timestamp, FILTER_SANITIZE_NUMBER_INT ), $audio_attachment_url );
}
/**
* Filters the audio player markup before display.
*
* Returning a non-false value from this filter will short-circuit building
* the block markup and instead will return your custom markup prepended to
* the post_content.
*
* Note that by using this filter, the custom CSS and JS files will no longer
* be enqueued, so you'll be responsible for either loading them yourself or
* loading custom ones.
*
* @hook classifai_pre_render_post_audio_controls
* @since 2.2.3
*
* @param {bool|string} $markup Audio markup to use. Defaults to false.
* @param {string} $content Content of the current post.
* @param {WP_Post} $_post The Post object.
* @param {int} $audio_attachment_id The audio attachment ID.
* @param {string} $audio_attachment_url The URL to the audio attachment file.
*
* @return {bool|string} Custom audio block markup. Will be prepended to the post content.
*/
$markup = apply_filters( 'classifai_pre_render_post_audio_controls', false, $content, $_post, $audio_attachment_id, $audio_attachment_url );
if ( false !== $markup ) {
return (string) $markup . $content;
}
wp_enqueue_script(
'classifai-plugin-text-to-speech-frontend-js',
CLASSIFAI_PLUGIN_URL . 'dist/classifai-plugin-text-to-speech-frontend.js',
get_asset_info( 'classifai-plugin-text-to-speech-frontend', 'dependencies' ),
get_asset_info( 'classifai-plugin-text-to-speech-frontend', 'version' ),
true
);
wp_enqueue_style(
'classifai-plugin-text-to-speech-frontend-css',
CLASSIFAI_PLUGIN_URL . 'dist/classifai-plugin-text-to-speech-frontend.css',
array( 'dashicons' ),
get_asset_info( 'classifai-plugin-text-to-speech-frontend', 'version' ),
'all'
);
ob_start();
?>
<div>
<div class='classifai-listen-to-post-wrapper'>
<div class="class-post-audio-controls" tabindex="0" role="button" aria-label="<?php esc_attr_e( 'Play audio', 'classifai' ); ?>" data-aria-pause-audio="<?php esc_attr_e( 'Pause audio', 'classifai' ); ?>">
<span class="dashicons dashicons-controls-play"></span>
<span class="dashicons dashicons-controls-pause"></span>
</div>
<div class='classifai-post-audio-heading'>
<?php
$listen_to_post_text = sprintf(
/**
* Hook to filter the text next to the audio controls on the frontend.
*
* @since 2.2.0
* @hook classifai_listen_to_this_post_text
*
* @param {string} The text to filter.
* @param {int} Post ID.
*
* @return {string} Filtered text.
*/
apply_filters( 'classifai_listen_to_this_post_text', '%s %s', $_post->ID ),
esc_html__( 'Listen to this', 'classifai' ),
esc_html( $_post->post_type )
);
echo wp_kses_post( $listen_to_post_text );
?>
</div>
</div>
<audio id="classifai-post-audio-player" src="<?php echo esc_url( $audio_attachment_url ); ?>"></audio>
</div>
<?php
return ob_get_clean() . $content;
}
/**
* Get the description for the enable field.
*
* @return string
*/
public function get_enable_description(): string {
return esc_html__( 'Generate an audio file from post content and output a "Read to Me" component.', 'classifai' );
}
/**
* Add any needed custom fields.
*/
public function add_custom_settings_fields() {
$settings = $this->get_settings();
$post_types = \Classifai\get_post_types_for_language_settings();
$post_type_options = array();
foreach ( $post_types as $post_type ) {
$post_type_options[ $post_type->name ] = $post_type->label;
}
add_settings_field(
'post_types',
esc_html__( 'Allowed post types', 'classifai' ),
[ $this, 'render_checkbox_group' ],
$this->get_option_name(),
$this->get_option_name() . '_section',
[
'label_for' => 'post_types',
'options' => $post_type_options,
'default_values' => $settings['post_types'],
'description' => __( 'Choose which post types support this feature.', 'classifai' ),
]
);
}
/**
* Returns the select options for post types.
*
* @return array
*/
protected function get_post_types_select_options(): array {
$post_types = \Classifai\get_post_types_for_language_settings();
$options = array();
foreach ( $post_types as $post_type ) {
$options[ $post_type->name ] = $post_type->label;
}
return $options;
}
/**
* Returns the default settings for the feature.
*
* @return array
*/
public function get_feature_default_settings(): array {
return [
'post_types' => [
'post' => 'post',
],
'provider' => Speech::ID,
];
}
/**
* Sanitizes the default feature settings.
*
* @param array $new_settings Settings being saved.
* @return array
*/
public function sanitize_default_feature_settings( array $new_settings ): array {
$settings = $this->get_settings();
$post_types = \Classifai\get_post_types_for_language_settings();
foreach ( $post_types as $post_type ) {
if ( ! isset( $new_settings['post_types'][ $post_type->name ] ) ) {
$new_settings['post_types'][ $post_type->name ] = $settings['post_types'];
} else {
$new_settings['post_types'][ $post_type->name ] = sanitize_text_field( $new_settings['post_types'][ $post_type->name ] );
}
}
return $new_settings;
}
/**
* 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 ): bool {
/**
* 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 ): bool {
/**
* 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 ) );
}
/**
* Normalizes the post content for text to speech generation.
*
* @param int $post_id The post ID.
*
* @return string The normalized post content.
*/
public function normalize_post_content( int $post_id ): string {
add_filter( 'classifai_pre_normalize', [ $this, 'strip_sub_sup_tags' ] );
$normalizer = new Normalizer();
$post = get_post( $post_id );
$post_content = $normalizer->normalize_content( $post->post_content, $post->post_title, $post_id );
remove_filter( 'classifai_pre_normalize', [ $this, 'strip_sub_sup_tags' ] );
return $post_content;
}
/**
* Filters the post content by stripping off HTML subscript and superscript tags
* with its content for text to speech generation.
*
* @param string $post_content The post content.
*
* @return string The filtered post content.
*/
public function strip_sub_sup_tags( string $post_content ): string {
$post_content = preg_replace( '/<sub>.*?<\/sub>|<sup>.*?<\/sup>/', '', $post_content );
return $post_content;
}
/**
* Generates feature setting data required for migration from
* ClassifAI < 3.0.0 to 3.0.0
*
* @return array
*/
public function migrate_settings() {
$old_settings = get_option( 'classifai_azure_text_to_speech', array() );
$new_settings = $this->get_default_settings();
if ( isset( $old_settings['enable_text_to_speech'] ) ) {
$new_settings['status'] = $old_settings['enable_text_to_speech'];
}
$new_settings['provider'] = 'ms_azure_text_to_speech';
if ( isset( $old_settings['credentials']['url'] ) ) {
$new_settings['ms_azure_text_to_speech']['endpoint_url'] = $old_settings['credentials']['url'];
}
if ( isset( $old_settings['credentials']['api_key'] ) ) {
$new_settings['ms_azure_text_to_speech']['api_key'] = $old_settings['credentials']['api_key'];
}
if ( isset( $old_settings['authenticated'] ) ) {
$new_settings['ms_azure_text_to_speech']['authenticated'] = $old_settings['authenticated'];
}
if ( isset( $old_settings['voices'] ) ) {
$new_settings['ms_azure_text_to_speech']['voices'] = $old_settings['voices'];
}
if ( isset( $old_settings['voice'] ) ) {
$new_settings['ms_azure_text_to_speech']['voice'] = $old_settings['voice'];
}
if ( isset( $old_settings['text_to_speech_users'] ) ) {
$new_settings['users'] = $old_settings['text_to_speech_users'];
}
if ( isset( $old_settings['text_to_speech_roles'] ) ) {
$new_settings['roles'] = $old_settings['text_to_speech_roles'];
}
if ( isset( $old_settings['text_to_speech_user_based_opt_out'] ) ) {
$new_settings['user_based_opt_out'] = $old_settings['text_to_speech_user_based_opt_out'];
}
if ( isset( $old_settings['post_types'] ) ) {
$new_settings['post_types'] = $old_settings['post_types'];
}
return $new_settings;
}
/**
* Outputs an admin notice with the error message if needed.
*/
public function show_error_if() {
global $post;
if ( empty( $post ) ) {
return;
}
$post_id = $post->ID;
if ( empty( $post_id ) ) {
return;
}
$error = get_post_meta( $post_id, '_classifai_text_to_speech_error', true );
if ( ! empty( $error ) ) {
delete_post_meta( $post_id, '_classifai_text_to_speech_error' );
$error = (array) json_decode( $error );
$code = ! empty( $error['code'] ) ? $error['code'] : 500;
$message = ! empty( $error['message'] ) ? $error['message'] : 'Unknown API error';
?>
<div class="notice notice-error is-dismissible">
<p>
<?php esc_html_e( 'Error: Audio generation failed.', 'classifai' ); ?>
</p>
<p>
<?php echo esc_html( $code ); ?>
-
<?php echo esc_html( $message ); ?>
</p>
</div>
<?php
}
}
}