init();
// Check if this isn't an auto save.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Check to make sure this is not a new post creation.
if ( ! $updated ) {
return;
}
// Validate if this post which is being saved is one of supported types. If not, return.
$post_types_supported = $common->get_posttypes_array();
$post_type = get_post_type($post_id);
if (!in_array($post_type, $post_types_supported )) {
return;
}
$is_quick_edit = isset($_POST['_inline_edit']) && wp_verify_nonce($_POST['_inline_edit'], 'inlineeditnonce');
$is_polly_nonce_ok = isset( $_POST['amazon-polly-post-nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['amazon-polly-post-nonce'] ), 'amazon-polly' );
// Check if this post is saved in 'regular' way.
if ( $is_polly_nonce_ok || $is_quick_edit ) {
$is_post_available = isset( $_POST['content'] );
$is_polly_enabled = isset( $_POST['amazon_polly_enable'] );
// Check if translation deactivation flag is enabled. If yes, disable
// and delete all files.
$is_translate_deactivation_enabled = isset( $_POST['amazon_ai_deactive_translation'] );
if ( !empty($is_translate_deactivation_enabled) ) {
$common->deactive_translation_for_post($post_id);
}
$is_post_id_available = isset( $post_id );
$is_key_valid = ( get_option( 'amazon_polly_valid_keys' ) === '1' );
// If this is quick edit, we need to check other place to see if polly was already enabled for this post.
if ( $is_post_id_available && $is_quick_edit ) {
if ( 1 == get_post_meta( $post_id, 'amazon_polly_enable', true)) {
$is_polly_enabled = true;
}
}
// Input var okay.
$is_amazon_polly_voice_id_available = isset( $_POST['amazon_polly_voice_id'] );
if ( $is_post_id_available && $is_post_available && $is_polly_enabled && $is_key_valid && $is_amazon_polly_voice_id_available ) {
// Getting voice and sample rate which should be used for converting post to audio.
$voice_id = sanitize_text_field( wp_unslash( $_POST['amazon_polly_voice_id'] ) );
$sample_rate = get_option( 'amazon_polly_sample_rate' );
// Cleaning text. Includes for example removing not supported characters etc.
$clean_text = $common->clean_text( $post_id, true, false);
// Breaking text into smaller parts, which will be then send to Amazon Polly for conversion.
$sentences = $common->break_text( $clean_text );
// We will be operating on local file system for stiching files together.
$wp_filesystem = $common->prepare_wp_filesystem();
// Actual invocation of method which will call Amazon Polly API and create audio file.
$this->convert_to_audio( $post_id, $sample_rate, $voice_id, $sentences, $wp_filesystem, '' );
// Checking what was the source language of text and updating options for translate operations.
$source_language = $common->get_source_language();
update_post_meta( $post_id, 'amazon_polly_transcript_' . $source_language, $clean_text );
update_post_meta( $post_id, 'amazon_polly_transcript_source_lan', $source_language );
}
// It's possible that Amazon Polly was not enabled, then we make sure that the audio file
// for the post is deleted (if existing) and options are removed.
if ( $is_post_id_available && ! $is_polly_enabled ) {
$common->delete_post_audio( $post_id );
update_post_meta( $post_id, 'amazon_polly_enable', 0 );
update_post_meta( $post_id, 'amazon_polly_audio_location', '' );
}
}
}
/**
* Method execute Amazon Polly API and convert content which was provided to audio file.
*
* @param string $post_id ID of the posts which is being converted.
* @param string $sample_rate Sample rate for speech conversion.
* @param string $voice_id Amazon Polly voice ID.
* @param string $sentences Sentences which should be converted to audio.
* @param string $wp_filesystem Reference to WP File system variable.
* @param string $lang Language
* @since 1.0.0
*/
public function convert_to_audio( $post_id, $sample_rate, $voice_id, $sentences, $wp_filesystem, $lang ) {
// Creating new standard common object for interacting with other methods of the plugin.
$common = new AmazonAI_Common();
$common->init();
// This part will be called only in case of translate opration and converting
// text into other languages (only then the last parameter is not empty).
// The last parameter of method specify language for which we are creating audio
foreach ($common->get_all_translable_languages() as $language_code) {
if ( $language_code == $lang ) {
$voice_id = get_option( 'amazon_polly_trans_langs_' . $language_code . '_voice' );
}
}
if ( empty($voice_id) ) {
return;
}
// Just in case we check if sample rate is valid, if not we will set default value.
$sample_rate_values = array( '22050', '16000', '8000' );
if ( ! in_array( $sample_rate, $sample_rate_values, true ) ) {
$sample_rate = '22050';
}
// Preparing locations and names of temporary files which will be used.
$upload_dir = wp_upload_dir()['basedir'];
$file_prefix = 'amazon_polly_';
$file_name = $file_prefix . $post_id . $lang . '.mp3';
$file_temp_full_name = trailingslashit($upload_dir) . 'temp_' . $file_name;
$dir_final_full_name = trailingslashit($upload_dir);
if ( get_option('uploads_use_yearmonth_folders') ) {
$dir_final_full_name .= get_the_date( 'Y', $post_id ) . '/' . get_the_date( 'm', $post_id ) . "/";
}
$file_final_full_name = $dir_final_full_name . $file_name;
// Delete temporary file if already exists.
if ( $wp_filesystem->exists( $file_temp_full_name ) ) {
$wp_filesystem->delete( $file_temp_full_name );
}
// Delete final file if already exists.
if ( $wp_filesystem->exists( $file_final_full_name ) ) {
$wp_filesystem->delete( $file_final_full_name );
}
// We might be stiching multiple smaller audio files. This variable will
// be used to detact the first part.
$first_part = true;
// Iterating through each of text parts.
foreach ( $sentences as $key => $text_content ) {
// Remove all tags
$text_content = strip_tags($text_content);
// Adding breaths sounds (if enabled).
$text_content = $this->add_breaths($common, $text_content);
// If plugin SSML support option is enabled, plugin will try to decode all SSML tags.
$text_content = $this->ssml_support($common, $text_content);
// Adding special polly mark.
$text_content = $this->add_mark_tag($common, $text_content);
// Adding speak polly mark.
$text_content = $this->add_speak_tags($common, $text_content);
//Preparing lexicons which will be used create audio.
$lexicons = $common->get_lexicons();
$lexicons_array = explode( ' ', $lexicons );
//Preparing Amazon Polly client object.
$polly_client = $common->get_polly_client();
//Call Amazon Polly service.
if ( ! empty( $lexicons ) and ( count( $lexicons_array ) > 0 ) ) {
$result = $polly_client->synthesizeSpeech(
array(
'OutputFormat' => 'mp3',
'SampleRate' => $sample_rate,
'Text' => $text_content,
'TextType' => 'ssml',
'VoiceId' => $voice_id,
'LexiconNames' => $lexicons_array,
)
);
} else {
$result = $polly_client->synthesizeSpeech(
array(
'OutputFormat' => 'mp3',
'SampleRate' => $sample_rate,
'Text' => $text_content,
'TextType' => 'ssml',
'VoiceId' => $voice_id,
)
);
}
// Grab the stream and output to a file.
$contents = $result['AudioStream']->getContents();
// Save first part of the audio stream in the parial temporary file.
$wp_filesystem->put_contents( $file_temp_full_name . '_part_' . $key, $contents );
// Merge new temporary file with previous ones.
if ( $first_part ) {
$wp_filesystem->put_contents( $file_temp_full_name, $contents );
$first_part = false;
} else {
$common->remove_id3( $file_temp_full_name . '_part_' . $key );
$merged_file = $wp_filesystem->get_contents( $file_temp_full_name ) . $wp_filesystem->get_contents( $file_temp_full_name . '_part_' . $key );
$wp_filesystem->put_contents( $file_temp_full_name, $merged_file );
}
// Deleting partial audio file.
$wp_filesystem->delete( $file_temp_full_name . '_part_' . $key );
}
// Saving audio file in final destination.
$fileHandler = $common->get_file_handler();
$audio_location_link = $fileHandler->save($wp_filesystem, $file_temp_full_name, $dir_final_full_name, $file_final_full_name, $post_id, $file_name);
// This will bust the browser cache when a content revision is made.
$audio_location_link = add_query_arg( 'version', time(), $audio_location_link );
// We are using a hash of these values to improve the speed of queries.
$amazon_polly_settings_hash = md5( $voice_id . $sample_rate . $fileHandler->get_type() );
if ( $lang == '' ) {
update_post_meta( $post_id, 'amazon_polly_audio_link_location', $audio_location_link );
update_post_meta( $post_id, 'amazon_polly_audio_location', $fileHandler->get_type() );
} else {
update_post_meta( $post_id, 'amazon_polly_translation_' . $lang, '1' );
}
// Update post meta data.
update_post_meta( $post_id, 'amazon_polly_enable', 1 );
update_post_meta( $post_id, 'amazon_polly_voice_id', $voice_id );
update_post_meta( $post_id, 'amazon_polly_sample_rate', $sample_rate );
update_post_meta( $post_id, 'amazon_polly_settings_hash', $amazon_polly_settings_hash );
}
/**
* Method add SSML breaths tags if functionality is enabled.
*
* @param string $common Plugin common object.
* @param string $text_content Existing text.
* @since 2.5.0
*/
private function add_breaths($common, $text_content) {
// Depending on the plugin configuration SSML tags for automated breaths sound will be added.
$is_breaths_enabled = $common->is_auto_breaths_enabled();
if ( $is_breaths_enabled ) {
$text_content = '' . $text_content . '';
}
return $text_content;
}
/**
* Method decode SSML tags if functionality is enabled.
*
* @param string $common Plugin common object.
* @param string $text_content Existing text.
* @since 2.5.0
*/
private function ssml_support($common, $text_content) {
// If plugin SSML support option is enabled, plugin will try to decode all SSML tags.
$is_ssml_enabled = $common->is_ssml_enabled();
if ( $is_ssml_enabled ) {
$text_content = $common->decode_ssml_tags( $text_content );
}
$text_content = str_replace( '**AMAZONPOLLY*SSML*BREAK*time=***500ms***SSML**', '', $text_content );
$text_content = str_replace( '**AMAZONPOLLY*SSML*BREAK*time=***1s***SSML**', '', $text_content );
return $text_content;
}
/**
* Method add special mark tag for output text.
*
* @param string $common Plugin common object.
* @param string $text_content Existing text.
* @since 2.5.0
*/
private function add_mark_tag($common, $text_content) {
$amazon_polly_mark_value = 'wp-plugin-awslabs';
$amazon_polly_mark_value = apply_filters( 'amazon_polly_mark_value', $amazon_polly_mark_value );
$text_content = '' . $text_content . '';
return $text_content;
}
/**
* Method add speak tags for output text.
*
* @param string $common Plugin common object.
* @param string $text_content Existing text.
* @since 2.5.0
*/
private function add_speak_tags($common, $text_content) {
$text_content = '' . $text_content . '';
return $text_content;
}
/**
* Batch process the post transcriptions.
*
* @since 1.0.0
*/
public function ajax_bulk_synthesize() {
check_ajax_referer( 'pollyajaxnonce', 'nonce' );
$batch_size = 1;
$common = new AmazonAI_Common();
$common->init();
$post_types_supported = $common->get_posttypes_array();
$amazon_polly_voice_id = get_option( 'amazon_polly_voice_id' );
$amazon_polly_sample_rate = get_option( 'amazon_polly_sample_rate' );
$amazon_polly_audio_location = ( 'on' === get_option( 'amazon_polly_s3' ) ) ? 's3' : 'local';
// We are using a hash of these values to improve the speed of queries.
$amazon_polly_settings_hash = md5( $amazon_polly_voice_id . $amazon_polly_sample_rate . $amazon_polly_audio_location );
$args = array(
'posts_per_page' => $batch_size,
'post_type' => $post_types_supported,
'meta_query' => array(
'relation' => 'OR',
array(
'key' => 'amazon_polly_audio_link_location',
'compare' => 'NOT EXISTS',
),
array(
'key' => 'amazon_polly_voice_id',
'value' => $amazon_polly_voice_id,
'compare' => '!=',
),
array(
'key' => 'amazon_polly_sample_rate',
'value' => $amazon_polly_sample_rate,
'compare' => '!=',
),
array(
'key' => 'amazon_polly_audio_location',
'value' => $amazon_polly_audio_location,
'compare' => '!=',
),
),
);
$query = new WP_Query( $args );
$post_ids = wp_list_pluck( $query->posts, 'ID' );
if ( is_array( $post_ids ) && ! empty( $post_ids ) ) {
foreach ( $post_ids as $post_id ) {
$clean_text = $common->clean_text( $post_id, true, false);
$sentences = $common->break_text( $clean_text );
$wp_filesystem = $common->prepare_wp_filesystem();
$this->convert_to_audio( $post_id, $amazon_polly_sample_rate, $amazon_polly_voice_id, $sentences, $wp_filesystem, '' );
}
} else {
$step = 'done';
}
$percentage = $this->get_percentage_complete();
echo wp_json_encode(
array(
'step' => $step,
'percentage' => $percentage,
)
);
wp_die();
}
/**
* Calculate the percentage complete.
*
* @since 1.0.0
*/
private function get_percentage_complete() {
$total_posts = 0;
$common = new AmazonAI_Common();
$common->init();
$post_types_supported = $common->get_posttypes_array();
$posts_needing_translation = $this->get_num_posts_needing_transcription();
foreach ( $post_types_supported as $post_type ) {
$post_type_count = wp_count_posts( $post_type )->publish;
$total_posts += $post_type_count;
}
if ( 0 >= $total_posts || 0 >= $posts_needing_translation ) {
$percentage = 100;
} else {
$percentage = round( $posts_needing_translation / $total_posts * 100, 2 );
}
return $percentage;
}
/**
* Checks how many posts should be converted.
*
* @since 1.0.0
*/
public function get_num_posts_needing_transcription() {
$common = new AmazonAI_Common();
$common->init();
$post_types_supported = $common->get_posttypes_array();
$amazon_polly_voice_id = get_option( 'amazon_polly_voice_id' );
$amazon_polly_sample_rate = get_option( 'amazon_polly_sample_rate' );
$amazon_polly_audio_location = ( 'on' === get_option( 'amazon_polly_s3' ) ) ? 's3' : 'local';
$args = array(
'posts_per_page' => '-1',
'post_type' => $post_types_supported,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'amazon_polly_audio_link_location',
'compare' => 'EXISTS',
),
array(
'key' => 'amazon_polly_voice_id',
'value' => $amazon_polly_voice_id,
'compare' => '=',
),
array(
'key' => 'amazon_polly_sample_rate',
'value' => $amazon_polly_sample_rate,
'compare' => '=',
),
array(
'key' => 'amazon_polly_audio_location',
'value' => $amazon_polly_audio_location,
'compare' => '=',
),
),
);
$query = new WP_Query( $args );
return count( $query->posts );
}
}