Skip to content

Commit

Permalink
Merge pull request #532 from 10up/feat/488
Browse files Browse the repository at this point in the history
feat/488: Ability to resize post content using OpenAI's ChatGPT
  • Loading branch information
dkotter authored Sep 1, 2023
2 parents f4e7815 + b0cc25a commit d4711b3
Show file tree
Hide file tree
Showing 14 changed files with 1,098 additions and 24 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"Backbone": "readonly",
"_": "readonly",
"File": "readonly",
"Headers": "readonly"
"Headers": "readonly",
"requestAnimationFrame": "readonly"
},
"extends": ["plugin:@wordpress/eslint-plugin/recommended"],
"ignorePatterns": ["*.json"]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
matrix:
core:
- {name: 'WP latest', version: 'latest'}
- {name: 'WP minimum', version: 'WordPress/WordPress#5.7'}
- {name: 'WP minimum', version: 'WordPress/WordPress#5.8'}
- {name: 'WP trunk', version: 'WordPress/WordPress#master'}
steps:
- name: Checkout
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro

* Generate a summary of post content and store it as an excerpt using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat)
* Generate titles from post content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat)
* Expand or condense text content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat)
* Generate new images on demand to use in-content or as a featured image using [OpenAI's DALL·E API](https://platform.openai.com/docs/guides/images)
* Generate transcripts of audio files using [OpenAI's Whisper API](https://platform.openai.com/docs/guides/speech-to-text)
* Convert text content into audio and output a "read-to-me" feature on the front-end to play this audio using [Microsoft Azure's Text to Speech API](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/text-to-speech)
Expand All @@ -52,9 +53,9 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro
| :-: | :-: | :-: |
| ![Screenshot of ClassifAI post tagging](assets/img/screenshot-1.png "Example of a Block Editor post with Watson Categories, Keywords, Concepts, and Entities.") | ![Screenshot of ClassifAI recommended content](assets/img/screenshot-2.png "Example of a Recommended Content Block with Azure AI Personalizer.") | ![Screenshot of ClassifAI excerpt generation](assets/img/screenshot-7.png "Example of automatic excerpt generation with OpenAI.") |

| Audio Transcripts | Title Generation | Text to Speech |
| :-: | :-: | :-: |
| ![Screenshot of ClassifAI audio transcript generation](assets/img/screenshot-9.png "Example of automatic audio transcript generation with OpenAI.") | ![Screenshot of ClassifAI title generation](assets/img/screenshot-10.png "Example of automatic title generation with OpenAI.") | ![Screenshot of ClassifAI text to speech generation](assets/img/screenshot-11.png "Example of automatic text to speech generation with Azure.") |
| Audio Transcripts | Title Generation | Expand or Condense Text | Text to Speech |
| :-: | :-: | :-: | :-: |
| ![Screenshot of ClassifAI audio transcript generation](assets/img/screenshot-9.png "Example of automatic audio transcript generation with OpenAI.") | ![Screenshot of ClassifAI title generation](assets/img/screenshot-10.png "Example of automatic title generation with OpenAI.") | ![Screenshot of ClassifAI expand/condense text feature](assets/img/screenshot-12.png "Example of expanding or condensing text with OpenAI.") | ![Screenshot of ClassifAI text to speech generation](assets/img/screenshot-11.png "Example of automatic text to speech generation with Azure.") |

### Image Processing

Expand Down Expand Up @@ -219,6 +220,7 @@ IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understand

* Choose to add the ability to generate excerpts.
* Choose to add the ability to generate titles.
* Choose to add the ability to resize content.
* Set the other options as needed.
* Save changes and ensure a success message is shown. An error will show if API authentication fails.

Expand All @@ -230,6 +232,10 @@ IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understand
* To test title generation, edit (or create) an item that supports titles.
* Ensure this item has content saved.
* Open the Summary panel in the sidebar and click on `Generate titles`.
* To test content resizing, edit (or create) an item. Note: only the block editor is supported.
* Add a paragraph block with some content.
* With this block selected, select the AI icon in the toolbar and choose to either expand or condense the text.
* In the modal that pops up, select one of the options.

## Set Up Language Processing (via OpenAI Embeddings)

Expand Down
Binary file added assets/img/screenshot-12.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion classifai.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Update URI: https://classifaiplugin.com
* Description: Enhance your WordPress content with Artificial Intelligence and Machine Learning services.
* Version: 2.3.0-dev
* Requires at least: 5.7
* Requires at least: 5.8
* Requires PHP: 7.4
* Author: 10up
* Author URI: https://10up.com
Expand Down
235 changes: 218 additions & 17 deletions includes/Classifai/Providers/OpenAI/ChatGPT.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ public function __construct( $service ) {
'title' => __( 'OpenAI ChatGPT', 'classifai' ),
'fields' => array( 'api-key' ),
'features' => array(
'enable_excerpt' => __( 'Excerpt generation', 'classifai' ),
'enable_titles' => __( 'Title generation', 'classifai' ),
'enable_excerpt' => __( 'Excerpt generation', 'classifai' ),
'enable_titles' => __( 'Title generation', 'classifai' ),
'enable_resize_content' => __( 'Content resizing', 'classifai' ),
),
);
}
Expand All @@ -72,8 +73,9 @@ public function is_feature_enabled( string $feature = '' ) {
$feature_roles = [];

$role_keys = [
'enable_excerpt' => 'roles',
'enable_titles' => 'title_roles',
'enable_excerpt' => 'roles',
'enable_titles' => 'title_roles',
'enable_resize_content' => 'resize_content_roles',
];

if ( isset( $role_keys[ $feature ] ) ) {
Expand Down Expand Up @@ -108,7 +110,7 @@ public function is_feature_enabled( string $feature = '' ) {
* This only fires if can_register returns true.
*/
public function register() {
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
add_action( 'enqueue_block_assets', [ $this, 'enqueue_editor_assets' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
add_action( 'edit_form_before_permalink', [ $this, 'register_generated_titles_template' ] );
}
Expand Down Expand Up @@ -172,6 +174,24 @@ public function enqueue_editor_assets() {
'before'
);
}

if ( $this->is_feature_enabled( 'enable_resize_content' ) ) {
wp_enqueue_script(
'classifai-content-resizing-plugin-js',
CLASSIFAI_PLUGIN_URL . 'dist/content-resizing-plugin.js',
get_asset_info( 'content-resizing-plugin', 'dependencies' ),
get_asset_info( 'content-resizing-plugin', 'version' ),
true
);

wp_enqueue_style(
'classifai-content-resizing-plugin-css',
CLASSIFAI_PLUGIN_URL . 'dist/content-resizing-plugin.css',
[],
CLASSIFAI_PLUGIN_VERSION,
'all'
);
}
}

/**
Expand All @@ -184,10 +204,8 @@ public function enqueue_admin_assets( $hook_suffix = '' ) {
return;
}

$screen = get_current_screen();
$settings = $this->get_settings();
$user_roles = wp_get_current_user()->roles ?? [];
$title_roles = $settings['title_roles'] ?? [];
$screen = get_current_screen();
$settings = $this->get_settings();

// Load the assets for the classic editor.
if ( $screen && ! $screen->is_block_editor() ) {
Expand Down Expand Up @@ -410,6 +428,60 @@ public function setup_fields_sections() {
'description' => __( 'Number of titles that will be generated in one request.', 'classifai' ),
]
);

// Add resizing content fields.
add_settings_section(
$this->get_option_name() . '_resize_content_settings',
esc_html__( 'Resizing content settings', 'classifai' ),
'',
$this->get_option_name()
);

add_settings_field(
'enable-resize-content',
esc_html__( 'Enable content resizing', 'classifai' ),
[ $this, 'render_input' ],
$this->get_option_name(),
$this->get_option_name() . '_resize_content_settings',
[
'label_for' => 'enable_resize_content',
'input_type' => 'checkbox',
'default_value' => $default_settings['enable_resize_content'],
'description' => __( '"Shrink content" and "Grow content" menu items will be added to the paragraph block\'s toolbar menu.', 'classifai' ),
]
);

$content_resize_roles = $roles;

unset( $content_resize_roles['contributor'], $content_resize_roles['subscriber'] );

add_settings_field(
'resize-content-roles',
esc_html__( 'Allowed roles', 'classifai' ),
[ $this, 'render_checkbox_group' ],
$this->get_option_name(),
$this->get_option_name() . '_resize_content_settings',
[
'label_for' => 'resize_content_roles',
'options' => $content_resize_roles,
'default_values' => $default_settings['resize_content_roles'],
'description' => __( 'Choose which roles are allowed to resize content.', 'classifai' ),
]
);

add_settings_field(
'number-resize-content',
esc_html__( 'Number of suggestions', 'classifai' ),
[ $this, 'render_select' ],
$this->get_option_name(),
$this->get_option_name() . '_resize_content_settings',
[
'label_for' => 'number_resize_content',
'options' => array_combine( range( 1, 10 ), range( 1, 10 ) ),
'default_value' => $default_settings['number_resize_content'],
'description' => __( 'Number of suggestions that will be generated in one request.', 'classifai' ),
]
);
}

/**
Expand Down Expand Up @@ -462,6 +534,24 @@ public function sanitize_settings( $settings ) {
$new_settings['number_titles'] = 1;
}

if ( empty( $settings['enable_resize_content'] ) || 1 !== (int) $settings['enable_resize_content'] ) {
$new_settings['enable_resize_content'] = 'no';
} else {
$new_settings['enable_resize_content'] = '1';
}

if ( isset( $settings['resize_content_roles'] ) && is_array( $settings['resize_content_roles'] ) ) {
$new_settings['resize_content_roles'] = array_map( 'sanitize_text_field', $settings['resize_content_roles'] );
} else {
$new_settings['resize_content_roles'] = array_keys( get_editable_roles() ?? [] );
}

if ( isset( $settings['number_resize_content'] ) && is_numeric( $settings['number_resize_content'] ) && (int) $settings['number_resize_content'] >= 1 && (int) $settings['number_resize_content'] <= 10 ) {
$new_settings['number_resize_content'] = absint( $settings['number_resize_content'] );
} else {
$new_settings['number_resize_content'] = 1;
}

return $new_settings;
}

Expand All @@ -484,14 +574,17 @@ public function get_default_settings() {
$editable_roles = get_editable_roles() ?? [];

return [
'authenticated' => false,
'api_key' => '',
'enable_excerpt' => false,
'roles' => array_keys( $editable_roles ),
'length' => (int) apply_filters( 'excerpt_length', 55 ),
'enable_titles' => false,
'title_roles' => array_keys( $editable_roles ),
'number_titles' => 1,
'authenticated' => false,
'api_key' => '',
'enable_excerpt' => false,
'roles' => array_keys( $editable_roles ),
'length' => (int) apply_filters( 'excerpt_length', 55 ),
'enable_titles' => false,
'title_roles' => array_keys( $editable_roles ),
'number_titles' => 1,
'enable_resize_content' => false,
'resize_content_roles' => array_keys( $editable_roles ),
'number_resize_content' => 1,
];
}

Expand Down Expand Up @@ -519,6 +612,8 @@ public function get_provider_debug_information( $settings = null, $configured =
__( 'Generate titles', 'classifai' ) => $enable_titles ? __( 'yes', 'classifai' ) : __( 'no', 'classifai' ),
__( 'Allowed roles (titles)', 'classifai' ) => implode( ', ', $settings['title_roles'] ?? [] ),
__( 'Number of titles', 'classifai' ) => absint( $settings['number_titles'] ?? 1 ),
__( 'Allowed roles (resize)', 'classifai' ) => implode( ', ', $settings['resize_content_roles'] ?? [] ),
__( 'Number of suggestions', 'classifai' ) => absint( $settings['number_resize_content'] ?? 1 ),
__( 'Latest response', 'classifai' ) => $this->get_formatted_latest_response( get_transient( 'classifai_openai_chatgpt_latest_response' ) ),
];
}
Expand Down Expand Up @@ -548,6 +643,9 @@ public function rest_endpoint_callback( $post_id = 0, $route_to_call = '', $args
case 'title':
$return = $this->generate_titles( $post_id, $args );
break;
case 'resize_content':
$return = $this->resize_content( $post_id, $args );
break;
}

return $return;
Expand Down Expand Up @@ -755,6 +853,109 @@ public function generate_titles( int $post_id = 0, array $args = [] ) {
return $return;
}

/**
* Resizes content.
*
* @param int $post_id The Post Id we're processing
* @param array $args Arguments passed in.
* @return string|WP_Error
*/
public function resize_content( int $post_id, array $args = array() ) {
if ( ! $post_id || ! get_post( $post_id ) ) {
return new WP_Error( 'post_id_required', esc_html__( 'Post ID is required to resize content.', 'classifai' ) );
}

$settings = $this->get_settings();
$args = wp_parse_args(
array_filter( $args ),
[
'num' => $settings['number_resize_content'] ?? 1,
]
);

$request = new APIRequest( $settings['api_key'] ?? '' );

if ( 'shrink' === $args['resize_type'] ) {
$prompt = 'Decrease the content length no more than 2 to 4 sentences.';
} else {
$prompt = 'Increase the content length no more than 2 to 4 sentences.';
}

/**
* Filter the resize prompt we will send to ChatGPT.
*
* @since 2.3.0
*
* @param {string} $prompt Resize prompt we are sending to ChatGPT. Gets added as a system prompt.
* @param {int} $post_id ID of post.
* @param {array} $args Arguments passed to endpoint.
*
* @return {string} Prompt.
*/
$prompt = apply_filters( 'classifai_chatgpt_' . $args['resize_type'] . '_content_prompt', $prompt, $post_id, $args );

/**
* Filter the resize request body before sending to ChatGPT.
*
* @since 2.3.0
* @hook classifai_chatgpt_resize_content_request_body
*
* @param {array} $body Request body that will be sent to ChatGPT.
* @param {int} $post_id ID of post.
*
* @return {array} Request body.
*/
$body = apply_filters(
'classifai_chatgpt_resize_content_request_body',
[
'model' => $this->chatgpt_model,
'messages' => [
[
'role' => 'system',
'content' => $prompt,
],
[
'role' => 'user',
'content' => esc_html( $args['content'] ),
],
],
'temperature' => 0.9,
'n' => absint( $args['num'] ),
],
$post_id
);

// Make our API request.
$response = $request->post(
$this->chatgpt_url,
[
'body' => wp_json_encode( $body ),
]
);

set_transient( 'classifai_openai_chatgpt_latest_response', $response, DAY_IN_SECONDS * 30 );

if ( is_wp_error( $response ) ) {
return $response;
}

if ( empty( $response['choices'] ) ) {
return new WP_Error( 'no_choices', esc_html__( 'No choices were returned from OpenAI.', 'classifai' ) );
}

// Extract out the text response.
$return = [];

foreach ( $response['choices'] as $choice ) {
if ( isset( $choice['message'], $choice['message']['content'] ) ) {
// ChatGPT often adds quotes to strings, so remove those as well as extra spaces.
$return[] = sanitize_text_field( trim( $choice['message']['content'], ' "\'' ) );
}
}

return $return;
}

/**
* Get our content, trimming if needed.
*
Expand Down
Loading

0 comments on commit d4711b3

Please sign in to comment.