Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenAI as a Provider for Descriptive Text Generation #828

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions includes/Classifai/Features/DescriptiveTextGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Classifai\Features;

use Classifai\Providers\Azure\ComputerVision;
use Classifai\Providers\OpenAI\ChatGPT;
use Classifai\Services\ImageProcessing;
use WP_REST_Server;
use WP_REST_Request;
Expand All @@ -21,6 +22,13 @@ class DescriptiveTextGenerator extends Feature {
*/
const ID = 'feature_descriptive_text_generator';

/**
* Prompt for generating descriptive text.
*
* @var string
*/
public $prompt = 'You are an assistant that generates descriptions of images that are used on a website. You will be provided with an image and will describe the main item you see in the image, giving details but staying concise. There is no need to say "the image contains" or similar, just describe what is actually in the image. This text will be important for screen readers, so make sure it is descriptive and accurate but not overly verbose.';

/**
* Constructor.
*/
Expand All @@ -33,6 +41,7 @@ public function __construct() {
// Contains just the providers this feature supports.
$this->supported_providers = [
ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ),
ChatGPT::ID => __( 'OpenAI', 'classifai' ),
];
}

Expand Down
141 changes: 141 additions & 0 deletions includes/Classifai/Providers/OpenAI/ChatGPT.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace Classifai\Providers\OpenAI;

use Classifai\Features\ContentResizing;
use Classifai\Features\DescriptiveTextGenerator;
use Classifai\Features\ExcerptGeneration;
use Classifai\Features\TitleGeneration;
use Classifai\Providers\Provider;
Expand All @@ -14,6 +15,8 @@

use function Classifai\get_default_prompt;
use function Classifai\sanitize_number_of_responses_field;
use function Classifai\get_modified_image_source_url;
use function Classifai\get_largest_size_and_dimensions_image_url;

class ChatGPT extends Provider {

Expand Down Expand Up @@ -132,6 +135,17 @@ public function get_default_provider_settings(): array {
case TitleGeneration::ID:
$common_settings['number_of_suggestions'] = 1;
break;

case DescriptiveTextGenerator::ID:
$common_settings['prompt'] = [
[
'title' => esc_html__( 'ClassifAI default', 'classifai' ),
'prompt' => $this->feature_instance->prompt,
'original' => 1,
'default' => 1,
],
];
break;
}

return $common_settings;
Expand Down Expand Up @@ -189,6 +203,9 @@ public function rest_endpoint_callback( $post_id = 0, string $route_to_call = ''

// Handle all of our routes.
switch ( $route_to_call ) {
case 'descriptive_text':
$return = $this->generate_descriptive_text( $post_id, $args );
break;
case 'excerpt':
$return = $this->generate_excerpt( $post_id, $args );
break;
Expand All @@ -203,6 +220,130 @@ public function rest_endpoint_callback( $post_id = 0, string $route_to_call = ''
return $return;
}

/**
* Generate descriptive text of an image.
*
* @param int $post_id The attachment ID we're processing.
* @param array $args Optional arguments.
* @return string|WP_Error
*/
public function generate_descriptive_text( int $post_id = 0, array $args = [] ) {
// Check to be sure the attachment exists and is an image.
if ( ! wp_attachment_is_image( $post_id ) ) {
return new WP_Error( 'invalid', esc_html__( 'This attachment can\'t be processed.', 'classifai' ) );
}

$metadata = wp_get_attachment_metadata( $post_id );

if ( ! $metadata || ! is_array( $metadata ) ) {
return new WP_Error( 'invalid', esc_html__( 'No valid metadata found.', 'classifai' ) );
}

$image_url = get_modified_image_source_url( $post_id );

if ( empty( $image_url ) || ! filter_var( $image_url, FILTER_VALIDATE_URL ) ) {
if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
$image_url = get_largest_size_and_dimensions_image_url(
get_attached_file( $post_id ),
wp_get_attachment_url( $post_id ),
$metadata,
[ 512, 2000 ],
[ 512, 2000 ],
100 * MB_IN_BYTES
);
} else {
$image_url = wp_get_attachment_url( $post_id );
}
}

if ( empty( $image_url ) ) {
return new WP_Error( 'error', esc_html__( 'Valid image size not found. Make sure the image is bigger than 512x512px.', 'classifai' ) );
}

$feature = new DescriptiveTextGenerator();
$settings = $feature->get_settings();

// These checks (and the one above) happen in the REST permission_callback,
// but we run them again here in case this method is called directly.
if ( empty( $settings ) || ( isset( $settings[ static::ID ]['authenticated'] ) && false === $settings[ static::ID ]['authenticated'] ) || ( ! $feature->is_feature_enabled() && ( ! defined( 'WP_CLI' ) || ! WP_CLI ) ) ) {
return new WP_Error( 'not_enabled', esc_html__( 'Descriptive text generation is disabled or OpenAI authentication failed. Please check your settings.', 'classifai' ) );
}

$request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() );

/**
* Filter the prompt we will send to ChatGPT.
*
* @since 3.2.0
* @hook classifai_chatgpt_descriptive_text_prompt
*
* @param {string} $prompt Prompt we are sending to ChatGPT.
* @param {int} $post_id ID of attachment we are describing.
*
* @return {string} Prompt.
*/
$prompt = apply_filters( 'classifai_chatgpt_descriptive_text_prompt', get_default_prompt( $settings[ static::ID ]['prompt'] ?? [] ) ?? $feature->prompt, $post_id );

/**
* Filter the request body before sending to ChatGPT.
*
* @since 3.2.0
* @hook classifai_chatgpt_descriptive_text_request_body
*
* @param {array} $body Request body that will be sent to ChatGPT.
* @param {int} $post_id ID of attachment we are describing.
*
* @return {array} Request body.
*/
$body = apply_filters(
'classifai_chatgpt_descriptive_text_request_body',
[
'model' => $this->chatgpt_model,
'messages' => [
[
'role' => 'system',
'content' => $prompt,
],
[
'role' => 'user',
'content' => [
[
'type' => 'image_url',
'image_url' => [
'url' => $image_url,
'detail' => 'auto',
],
],
],
],
],
'temperature' => 0.2,
'max_tokens' => 300,
],
$post_id
);

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

// Extract out the text response, if it exists.
if ( ! is_wp_error( $response ) && ! empty( $response['choices'] ) ) {
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.
$response = sanitize_text_field( trim( $choice['message']['content'], ' "\'' ) );
}
}
}

return $response;
}

/**
* Generate an excerpt using ChatGPT.
*
Expand Down
1 change: 1 addition & 0 deletions includes/Classifai/Services/ImageProcessing.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public static function get_service_providers(): array {
'classifai_image_processing_service_providers',
[
'Classifai\Providers\Azure\ComputerVision',
'Classifai\Providers\OpenAI\ChatGPT',
'Classifai\Providers\OpenAI\DallE',
]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const ContentResizingSettings = () => {
>
<PromptRepeater
prompts={ featureSettings.condense_text_prompt }
setPromts={ ( prompts ) => {
setPrompts={ ( prompts ) => {
setFeatureSettings( {
condense_text_prompt: prompts,
} );
Expand All @@ -47,7 +47,7 @@ export const ContentResizingSettings = () => {
>
<PromptRepeater
prompts={ featureSettings.expand_text_prompt }
setPromts={ ( prompts ) => {
setPrompts={ ( prompts ) => {
setFeatureSettings( {
expand_text_prompt: prompts,
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const ExcerptGenerationSettings = () => {
);
const { excerptPostTypesOptions } = usePostTypes();
const { setFeatureSettings } = useDispatch( STORE_NAME );
const setPromts = ( prompts ) => {
const setPrompts = ( prompts ) => {
setFeatureSettings( {
generate_excerpt_prompt: prompts,
} );
Expand All @@ -46,7 +46,7 @@ export const ExcerptGenerationSettings = () => {
>
<PromptRepeater
prompts={ featureSettings.generate_excerpt_prompt }
setPromts={ setPromts }
setPrompts={ setPrompts }
/>
</SettingsRow>
<SettingsRow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ import { __ } from '@wordpress/i18n';
export const PromptRepeater = ( props ) => {
const [ showConfirmDialog, setShowConfirmDialog ] = useState( false );
const [ activeIndex, setActiveIndex ] = useState( null );
const { prompts = [], setPromts } = props;
const { prompts = [], setPrompts } = props;

const placeholder =
prompts?.filter( ( prompt ) => prompt.original )[ 0 ]?.prompt || '';

// Add a new prompt.
const addPrompt = () => {
setPromts( [
setPrompts( [
...prompts,
{ default: 0, original: 0, prompt: '', title: '' },
] );
Expand All @@ -42,7 +42,7 @@ export const PromptRepeater = ( props ) => {
if ( prompt[ 0 ]?.default ) {
prompts[ 0 ].default = 1;
}
setPromts( [ ...prompts ] );
setPrompts( [ ...prompts ] );
};

// Update prompt.
Expand All @@ -60,7 +60,7 @@ export const PromptRepeater = ( props ) => {
...prompts[ index ],
...changes,
};
setPromts( [ ...prompts ] );
setPrompts( [ ...prompts ] );
};

// Confirm dialog to remove prompt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const TitleGenerationSettings = () => {
select( STORE_NAME ).getFeatureSettings()
);
const { setFeatureSettings } = useDispatch( STORE_NAME );
const setPromts = ( prompts ) => {
const setPrompts = ( prompts ) => {
setFeatureSettings( {
generate_title_prompt: prompts,
} );
Expand All @@ -39,7 +39,7 @@ export const TitleGenerationSettings = () => {
>
<PromptRepeater
prompts={ featureSettings.generate_title_prompt }
setPromts={ setPromts }
setPrompts={ setPrompts }
/>
</SettingsRow>
);
Expand Down
22 changes: 22 additions & 0 deletions src/js/settings/components/provider-settings/openai-chatgpt.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { __ } from '@wordpress/i18n';
import { SettingsRow } from '../settings-row';
import { STORE_NAME } from '../../data/store';
import { useFeatureContext } from '../feature-settings/context';
import { PromptRepeater } from '../feature-additional-settings/prompt-repeater';

/**
* Component for OpenAI ChatGPT Provider settings.
Expand All @@ -32,6 +33,11 @@ export const OpenAIChatGPTSettings = ( { isConfigured = false } ) => {
);
const { setProviderSettings } = useDispatch( STORE_NAME );
const onChange = ( data ) => setProviderSettings( providerName, data );
const setPrompts = ( prompts ) => {
setProviderSettings( providerName, {
prompt: prompts,
} );
};

const Description = () => (
<>
Expand Down Expand Up @@ -82,6 +88,22 @@ export const OpenAIChatGPTSettings = ( { isConfigured = false } ) => {
/>
</SettingsRow>
) }
{ [ 'feature_descriptive_text_generator' ].includes(
featureName
) && (
<SettingsRow
label={ __( 'Prompt', 'classifai' ) }
description={ __(
'Add a custom prompt, if desired.',
'classifai'
) }
>
<PromptRepeater
prompts={ providerSettings.prompt }
setPrompts={ setPrompts }
/>
</SettingsRow>
) }
</>
);
};
34 changes: 0 additions & 34 deletions tests/Classifai/Providers/Azure/ComputerVisionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,40 +60,6 @@ public function test_smart_crop_image() {
remove_filter( 'classifai_should_smart_crop_image', '__return_true' );
}

/**
* Ensure that settings returns default settings array if the `classifai_computer_vision` is not set.
*/
public function test_no_computer_vision_option_set() {
delete_option( 'classifai_computer_vision' );

$defaults = [];

$expected = array_merge(
$defaults,
[
'status' => '0',
'roles' => [],
'users' => [],
'user_based_opt_out' => 'no',
'descriptive_text_fields' => [
'alt' => 'alt',
'caption' => 0,
'description' => 0,
],
'provider' => 'ms_computer_vision',
'ms_computer_vision' => [
'endpoint_url' => '',
'api_key' => '',
'authenticated' => false,
'descriptive_confidence_threshold' => 55,
],
]
);
$settings = ( new \Classifai\Features\DescriptiveTextGenerator() )->get_settings();

$this->assertSame( $expected, $settings );
}

/**
* Ensure that attachment meta is being set.
*/
Expand Down
Loading
Loading