diff --git a/README.md b/README.md index a85785026..be24b38a5 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,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), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service) or [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) * Generate titles from post content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service) or [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) * Expand or condense text content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service) or [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) -* 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 new images on demand to use in-content or as a featured image using [OpenAI's DALL·E 3 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) * Moderate incoming comments for sensitive content using [OpenAI's Moderation API](https://platform.openai.com/docs/guides/moderation) * 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) @@ -438,7 +438,7 @@ Note that [Azure AI Vision](https://docs.microsoft.com/en-us/azure/cognitive-ser ### 2. Configure OpenAI API Keys under Tools > ClassifAI > Image Processing > Image Generation -* Select **OpenAI DALL-E** in the provider dropdown. +* Select **OpenAI DALL·E 3** in the provider dropdown. * Enter your API Key copied from the above step into the `API Key` field. ### 3. Enable specific Image Processing features diff --git a/includes/Classifai/Features/Feature.php b/includes/Classifai/Features/Feature.php index 8ea69e51a..b01c23d6d 100644 --- a/includes/Classifai/Features/Feature.php +++ b/includes/Classifai/Features/Feature.php @@ -1179,11 +1179,13 @@ function ( $role ) { __( 'Provider', 'classifai' ) => $feature_settings['provider'], ]; - if ( method_exists( $provider, 'get_debug_information' ) ) { + if ( $provider && method_exists( $provider, 'get_debug_information' ) ) { $all_debug_info = array_merge( $common_debug_info, $provider->get_debug_information() ); + } else { + $all_debug_info = $common_debug_info; } /** diff --git a/includes/Classifai/Features/ImageGeneration.php b/includes/Classifai/Features/ImageGeneration.php index b617dc596..940ff1bd2 100644 --- a/includes/Classifai/Features/ImageGeneration.php +++ b/includes/Classifai/Features/ImageGeneration.php @@ -33,7 +33,7 @@ public function __construct() { // Contains just the providers this feature supports. $this->supported_providers = [ - DallE::ID => __( 'OpenAI Dall-E', 'classifai' ), + DallE::ID => __( 'OpenAI DALL·E 3', 'classifai' ), ]; } diff --git a/includes/Classifai/Providers/OpenAI/DallE.php b/includes/Classifai/Providers/OpenAI/DallE.php index 73bae1b7e..0f1021ba6 100644 --- a/includes/Classifai/Providers/OpenAI/DallE.php +++ b/includes/Classifai/Providers/OpenAI/DallE.php @@ -18,18 +18,18 @@ class DallE extends Provider { const ID = 'openai_dalle'; /** - * OpenAI DALL·E URL + * OpenAI DALL·E URL. * * @var string */ protected $dalle_url = 'https://api.openai.com/v1/images/generations'; /** - * Maximum number of characters a prompt can have + * Maximum number of characters a prompt can have. * * @var int */ - public $max_prompt_chars = 1000; + public $max_prompt_chars = 4000; /** * OpenAI DALL·E constructor. @@ -64,14 +64,14 @@ public function register_endpoints() { 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this->feature_instance, 'rest_endpoint_callback' ], 'args' => [ - 'prompt' => [ + 'prompt' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', 'description' => esc_html__( 'Prompt used to generate an image', 'classifai' ), ], - 'n' => [ + 'n' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 10, @@ -79,18 +79,38 @@ public function register_endpoints() { 'validate_callback' => 'rest_validate_request_arg', 'description' => esc_html__( 'Number of images to generate', 'classifai' ), ], - 'size' => [ + 'quality' => [ + 'type' => 'string', + 'enum' => [ + 'standard', + 'hd', + ], + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'Quality of generated image', 'classifai' ), + ], + 'size' => [ 'type' => 'string', 'enum' => [ - '256x256', - '512x512', '1024x1024', + '1792x1024', + '1024x1792', ], 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', 'description' => esc_html__( 'Size of generated image', 'classifai' ), ], - 'format' => [ + 'style' => [ + 'type' => 'string', + 'enum' => [ + 'vivid', + 'natural', + ], + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'Style of generated image', 'classifai' ), + ], + 'format' => [ 'type' => 'string', 'enum' => [ 'url', @@ -123,7 +143,7 @@ public function render_provider_fields() { 'label_for' => 'api_key', 'input_type' => 'password', 'default_value' => $settings['api_key'], - 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, 'description' => sprintf( wp_kses( /* translators: %1$s is replaced with the OpenAI sign up URL */ @@ -141,7 +161,7 @@ public function render_provider_fields() { ); add_settings_field( - static::ID . 'number_of_images', + static::ID . '_number_of_images', esc_html__( 'Number of images', 'classifai' ), [ $this->feature_instance, 'render_select' ], $this->feature_instance->get_option_name(), @@ -152,12 +172,31 @@ public function render_provider_fields() { 'options' => array_combine( range( 1, 10 ), range( 1, 10 ) ), 'default_value' => $settings['number_of_images'], 'description' => __( 'Number of images that will be generated in one request. Note that each image will incur separate costs.', 'classifai' ), - 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, ] ); add_settings_field( - static::ID . 'image_size', + static::ID . '_quality', + esc_html__( 'Image quality', 'classifai' ), + [ $this->feature_instance, 'render_select' ], + $this->feature_instance->get_option_name(), + $this->feature_instance->get_option_name() . '_section', + [ + 'option_index' => static::ID, + 'label_for' => 'quality', + 'options' => [ + 'standard' => __( 'Standard', 'classifai' ), + 'hd' => __( 'High Definition', 'classifai' ), + ], + 'default_value' => $settings['quality'], + 'description' => __( 'The quality of the image that will be generated. High Definition creates images with finer details and greater consistency across the image but costs more.', 'classifai' ), + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, + ] + ); + + add_settings_field( + static::ID . '_image_size', esc_html__( 'Image size', 'classifai' ), [ $this->feature_instance, 'render_select' ], $this->feature_instance->get_option_name(), @@ -166,13 +205,32 @@ public function render_provider_fields() { 'option_index' => static::ID, 'label_for' => 'image_size', 'options' => [ - '256x256' => '256x256', - '512x512' => '512x512', - '1024x1024' => '1024x1024', + '1024x1024' => '1024x1024 (square)', + '1792x1024' => '1792x1024 (landscape)', + '1024x1792' => '1024x1792 (portrait)', ], 'default_value' => $settings['image_size'], - 'description' => __( 'Size of generated images.', 'classifai' ), - 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. + 'description' => __( 'Size of generated images. Larger sizes cost more.', 'classifai' ), + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, + ] + ); + + add_settings_field( + static::ID . '_style', + esc_html__( 'Image style', 'classifai' ), + [ $this->feature_instance, 'render_select' ], + $this->feature_instance->get_option_name(), + $this->feature_instance->get_option_name() . '_section', + [ + 'option_index' => static::ID, + 'label_for' => 'style', + 'options' => [ + 'vivid' => __( 'Vivid', 'classifai' ), + 'natural' => __( 'Natural', 'classifai' ), + ], + 'default_value' => $settings['style'], + 'description' => __( 'The style of the generated images. Vivid causes more hyper-real and dramatic images. Natural causes more natural, less hyper-real looking images.', 'classifai' ), + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, ] ); } @@ -194,7 +252,9 @@ public function get_default_provider_settings(): array { $common_settings, [ 'number_of_images' => 1, - 'image_size' => '256x256', + 'quality' => 'standard', + 'image_size' => '1024x1024', + 'style' => 'vivid', ] ); } @@ -217,8 +277,22 @@ public function sanitize_settings( array $new_settings ): array { if ( $this->feature_instance instanceof ImageGeneration ) { $new_settings[ static::ID ]['number_of_images'] = absint( $new_settings[ static::ID ]['number_of_images'] ?? $settings[ static::ID ]['number_of_images'] ); - if ( in_array( $new_settings[ static::ID ]['image_size'], [ '256x256', '512x512', '1024x1024' ], true ) ) { - $new_settings[ static::ID ]['image_size'] = sanitize_text_field( $new_settings[ static::ID ]['image_size'] ?? $settings[ static::ID ]['image_size'] ); + if ( in_array( $new_settings[ static::ID ]['quality'], [ 'standard', 'hd' ], true ) ) { + $new_settings[ static::ID ]['quality'] = sanitize_text_field( $new_settings[ static::ID ]['quality'] ); + } else { + $new_settings[ static::ID ]['quality'] = $settings[ static::ID ]['quality']; + } + + if ( in_array( $new_settings[ static::ID ]['image_size'], [ '1024x1024', '1792x1024', '1024x1792' ], true ) ) { + $new_settings[ static::ID ]['image_size'] = sanitize_text_field( $new_settings[ static::ID ]['image_size'] ); + } else { + $new_settings[ static::ID ]['image_size'] = $settings[ static::ID ]['image_size']; + } + + if ( in_array( $new_settings[ static::ID ]['style'], [ 'vivid', 'natural' ], true ) ) { + $new_settings[ static::ID ]['style'] = sanitize_text_field( $new_settings[ static::ID ]['style'] ); + } else { + $new_settings[ static::ID ]['style'] = $settings[ static::ID ]['style']; } } @@ -264,12 +338,19 @@ public function generate_image( string $prompt = '', array $args = [] ) { $args = wp_parse_args( array_filter( $args ), [ - 'num' => $settings['number_of_images'] ?? 1, - 'size' => $settings['image_size'] ?? '1024x1024', - 'format' => 'url', + 'num' => $settings['number_of_images'] ?? 1, + 'quality' => $settings['quality'] ?? 'standard', + 'size' => $settings['image_size'] ?? '1024x1024', + 'style' => $settings['style'] ?? 'vivid', + 'format' => 'url', ] ); + // Force proper image size for those that had been using DALL·E 2 and haven't updated settings. + if ( ! in_array( $args['size'], [ '1024x1024', '1792x1024', '1024x1792' ], true ) ) { + $args['size'] = '1024x1024'; + } + if ( ! $image_generation->is_feature_enabled() ) { return new WP_Error( 'not_enabled', esc_html__( 'Image generation is disabled or OpenAI authentication failed. Please check your settings.', 'classifai' ) ); } @@ -307,40 +388,47 @@ public function generate_image( string $prompt = '', array $args = [] ) { 'classifai_dalle_request_body', [ 'prompt' => sanitize_text_field( $prompt ), - 'n' => absint( $args['num'] ), - 'size' => sanitize_text_field( $args['size'] ), + 'model' => 'dall-e-3', + 'n' => 1, + 'quality' => sanitize_text_field( $args['quality'] ), 'response_format' => sanitize_text_field( $args['format'] ), + 'size' => sanitize_text_field( $args['size'] ), + 'style' => sanitize_text_field( $args['style'] ), ] ); - // Make our API request. - $response = $request->post( - $this->dalle_url, - [ - 'body' => wp_json_encode( $body ), - ] - ); + $responses = []; - set_transient( 'classifai_openai_dalle_latest_response', $response, DAY_IN_SECONDS * 30 ); + // DALL·E 3 doesn't support multiple images in a single request so make one request per image. + for ( $i = 0; $i < $args['num']; $i++ ) { + $responses[] = $request->post( + $this->dalle_url, + [ + 'body' => wp_json_encode( $body ), + ] + ); + } + + set_transient( 'classifai_openai_dalle_latest_response', $responses[ array_key_last( $responses ) ], DAY_IN_SECONDS * 30 ); - // Extract out the image response, if it exists. - if ( ! is_wp_error( $response ) && ! empty( $response['data'] ) ) { - $cleaned_response = []; + $cleaned_responses = []; - foreach ( $response['data'] as $data ) { - if ( ! empty( $data[ $args['format'] ] ) ) { - if ( 'url' === $args['format'] ) { - $cleaned_response[] = [ 'url' => esc_url_raw( $data[ $args['format'] ] ) ]; - } else { - $cleaned_response[] = [ 'url' => $data[ $args['format'] ] ]; + foreach ( $responses as $response ) { + // Extract out the image response, if it exists. + if ( ! is_wp_error( $response ) && ! empty( $response['data'] ) ) { + foreach ( $response['data'] as $data ) { + if ( ! empty( $data[ $args['format'] ] ) ) { + if ( 'url' === $args['format'] ) { + $cleaned_responses[] = [ 'url' => esc_url_raw( $data[ $args['format'] ] ) ]; + } else { + $cleaned_responses[] = [ 'url' => $data[ $args['format'] ] ]; + } } } } - - $response = $cleaned_response; } - return $response; + return $cleaned_responses; } /** @@ -354,7 +442,10 @@ public function get_debug_information(): array { $debug_info = []; if ( $this->feature_instance instanceof ImageGeneration ) { - $debug_info[ __( 'Number of images', 'classifai' ) ] = $provider_settings['number_of_images']; + $debug_info[ __( 'Number of images', 'classifai' ) ] = $provider_settings['number_of_images'] ?? 1; + $debug_info[ __( 'Quality', 'classifai' ) ] = $provider_settings['quality'] ?? 'standard'; + $debug_info[ __( 'Size', 'classifai' ) ] = $provider_settings['image_size'] ?? '1024x1024'; + $debug_info[ __( 'Style', 'classifai' ) ] = $provider_settings['style'] ?? 'vivid'; $debug_info[ __( 'Latest response:', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_dalle_latest_response' ) ); } diff --git a/readme.txt b/readme.txt index 685c1d7ac..5dc3c7c98 100644 --- a/readme.txt +++ b/readme.txt @@ -21,7 +21,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), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service) or [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) * Generate titles from post content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service) or [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) * Expand or condense text content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service) or [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) -* 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 new images on demand to use in-content or as a featured image using [OpenAI's DALL·E 3 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) * Classify post content using [IBM Watson's Natural Language Understanding API](https://www.ibm.com/watson/services/natural-language-understanding/) and [OpenAI's Embedding API](https://platform.openai.com/docs/guides/embeddings) diff --git a/tests/cypress/integration/image-processing/image-generation-openai-dalle.test.js b/tests/cypress/integration/image-processing/image-generation-openai-dalle.test.js index 0e0f8eed9..3f676e1ee 100644 --- a/tests/cypress/integration/image-processing/image-generation-openai-dalle.test.js +++ b/tests/cypress/integration/image-processing/image-generation-openai-dalle.test.js @@ -5,6 +5,7 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_generation' ); cy.get( '#status' ).check(); + cy.get( '#provider' ).select( 'openai_dalle' ); cy.get( '#submit' ).click(); cy.optInAllFeatures(); } ); @@ -19,13 +20,23 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { ); cy.get( '#api_key' ).clear().type( 'password' ); + cy.get( + 'select[name="classifai_feature_image_generation[openai_dalle][number_of_images]"]' + ).select( '2' ); + cy.get( + 'select[name="classifai_feature_image_generation[openai_dalle][quality]"]' + ).select( 'hd' ); + cy.get( + 'select[name="classifai_feature_image_generation[openai_dalle][image_size]"]' + ).select( '1024x1792' ); + cy.get( + 'select[name="classifai_feature_image_generation[openai_dalle][style]"]' + ).select( 'natural' ); - cy.get( '#status' ).check(); cy.get( '#classifai_feature_image_generation_roles_administrator' ).check(); - cy.get( '#number_of_images' ).select( '2' ); - cy.get( '#image_size' ).select( '512x512' ); + cy.get( '#submit' ).click(); } ); diff --git a/tests/test-plugin/dalle.json b/tests/test-plugin/dalle.json index 71771e388..cc5056612 100644 --- a/tests/test-plugin/dalle.json +++ b/tests/test-plugin/dalle.json @@ -1,9 +1,6 @@ { "created": 1589478378, "data": [ - { - "b64_json": "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=" - }, { "b64_json": "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=" }