← All changes
|
includes/extensions/AI_SEO_Tools/Post_Generator_Module.php
+91
-58
51.1.76
→
51.1.83
View file →
| @@ -2,9 +2,9 @@ | ||
| 2 | 2 | /** |
| 3 | 3 | * Post Generator module for AI SEO Tools. |
| 4 | 4 | * |
| 5 | 5 | * Generates full blog posts (title, HTML content, excerpt, tags) |
| 6 | - * via OpenAI and optionally generates a matching featured image. | |
| 6 | + * via the configured AI provider and optionally generates a matching featured image. | |
| 7 | 7 | * |
| 8 | 8 | * @package King_Addons |
| 9 | 9 | */ |
| 10 | 10 | |
| @@ -38,9 +38,9 @@ | ||
| 38 | 38 | wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403); |
| 39 | 39 | } |
| 40 | 40 | |
| 41 | 41 | $ka_ai_opts = get_option('king_addons_ai_options', []); |
| 42 | - if (empty($ka_ai_opts['openai_api_key'])) { | |
| 42 | + if ('' === \King_Addons\AI_Provider::getApiKey()) { | |
| 43 | 43 | wp_send_json_error(['message' => esc_html__('OpenAI API key is not set. Please add it in AI Settings.', 'king-addons'), 'code' => 'no_api_key'], 400); |
| 44 | 44 | } |
| 45 | 45 | |
| 46 | 46 | $description = sanitize_textarea_field(wp_unslash($_POST['description'] ?? '')); |
| @@ -53,11 +53,13 @@ | ||
| 53 | 53 | : 'medium'; |
| 54 | 54 | $category_raw = sanitize_text_field(wp_unslash($_POST['category_id'] ?? 'auto')); |
| 55 | 55 | $category_id = ($category_raw === 'auto' || $category_raw === '0') ? $category_raw : (string) absint($category_raw); |
| 56 | 56 | $gen_image = !empty($_POST['generate_image']) && king_addons_freemius()->can_use_premium_code(); |
| 57 | - $image_model = in_array($_POST['image_model'] ?? 'dall-e-3', ['dall-e-3', 'gpt-image-1'], true) | |
| 58 | - ? sanitize_key(wp_unslash($_POST['image_model'])) | |
| 59 | - : 'dall-e-3'; | |
| 57 | + $posted_model = sanitize_text_field(wp_unslash($_POST['image_model'] ?? '')); | |
| 58 | + $allowed_models = wp_list_pluck(\King_Addons\AI_Provider::getModelsFor('image'), 'id'); | |
| 59 | + $image_model = in_array($posted_model, $allowed_models, true) | |
| 60 | + ? $posted_model | |
| 61 | + : \King_Addons\AI_Provider::getImageModel(); | |
| 60 | 62 | $image_quality = sanitize_key(wp_unslash($_POST['image_quality'] ?? 'standard')); |
| 61 | 63 | $image_size = sanitize_key(wp_unslash($_POST['image_size'] ?? '1024x1024')); |
| 62 | 64 | |
| 63 | 65 | if ($description === '') { |
| @@ -173,9 +175,9 @@ | ||
| 173 | 175 | ), |
| 174 | 176 | ]; |
| 175 | 177 | update_option(self::BULK_OPTION_PROGRESS, $progress, false); |
| 176 | 178 | |
| 177 | - // 1. Generate content via OpenAI. | |
| 179 | + // 1. Generate content via the configured AI provider. | |
| 178 | 180 | $post_data = $this->generate_post_content( |
| 179 | 181 | (string) ($settings['description'] ?? ''), |
| 180 | 182 | $idx, |
| 181 | 183 | (string) ($settings['length'] ?? 'medium'), |
| @@ -231,9 +233,9 @@ | ||
| 231 | 233 | if (!empty($settings['generate_image'])) { |
| 232 | 234 | $attach_id = $this->generate_and_attach_image( |
| 233 | 235 | $post_data['title'], |
| 234 | 236 | (string) ($settings['description'] ?? ''), |
| 235 | - (string) ($settings['image_model'] ?? 'dall-e-3'), | |
| 237 | + (string) ($settings['image_model'] ?: \King_Addons\AI_Provider::getImageModel()), | |
| 236 | 238 | (string) ($settings['image_quality'] ?? 'standard'), |
| 237 | 239 | (string) ($settings['image_size'] ?? '1024x1024'), |
| 238 | 240 | $post_id |
| 239 | 241 | ); |
| @@ -273,9 +275,9 @@ | ||
| 273 | 275 | delete_transient(self::BULK_OPTION_LOCK); |
| 274 | 276 | } |
| 275 | 277 | |
| 276 | 278 | /** |
| 277 | - * Call OpenAI Chat Completions to generate post fields as JSON. | |
| 279 | + * Call the provider's Chat Completions endpoint to generate post fields as JSON. | |
| 278 | 280 | * |
| 279 | 281 | * @param string $description User-supplied topic/description. |
| 280 | 282 | * @param int $post_num Index within the batch (for uniqueness). |
| 281 | 283 | * @return array|\WP_Error Associative array with keys: title, content, excerpt, tags. |
| @@ -280,9 +282,9 @@ | ||
| 280 | 282 | * @param int $post_num Index within the batch (for uniqueness). |
| 281 | 283 | * @return array|\WP_Error Associative array with keys: title, content, excerpt, tags. |
| 282 | 284 | */ |
| 283 | 285 | /** |
| 284 | - * Build the OpenAI prompt string (same logic as generate_post_content) without making an API call. | |
| 286 | + * Build the prompt string (same logic as generate_post_content) without making an API call. | |
| 285 | 287 | * Used to expose the current prompt in the status response. |
| 286 | 288 | */ |
| 287 | 289 | private function build_prompt_preview(string $description, int $post_num, string $length = 'medium', string $category_mode = 'auto'): string |
| 288 | 290 | { |
| @@ -313,13 +315,17 @@ | ||
| 313 | 315 | |
| 314 | 316 | private function generate_post_content(string $description, int $post_num, string $length = 'medium', string $category_mode = 'auto') |
| 315 | 317 | { |
| 316 | 318 | $options = get_option('king_addons_ai_options', []); |
| 317 | - $api_key = $options['openai_api_key'] ?? ''; | |
| 318 | - $model = $options['openai_model'] ?? 'gpt-4o-mini'; | |
| 319 | + $api_key = \King_Addons\AI_Provider::getApiKey(); | |
| 320 | + $model = \King_Addons\AI_Provider::getTextModel(); | |
| 319 | 321 | |
| 320 | 322 | if ($api_key === '') { |
| 321 | - return new \WP_Error('missing_api_key', esc_html__('OpenAI API key is missing.', 'king-addons')); | |
| 323 | + return new \WP_Error('missing_api_key', sprintf( | |
| 324 | + /* translators: %s: provider name */ | |
| 325 | + esc_html__('%s API key is missing.', 'king-addons'), | |
| 326 | + \King_Addons\AI_Provider::getLabel() | |
| 327 | + )); | |
| 322 | 328 | } |
| 323 | 329 | |
| 324 | 330 | $word_targets = ['short' => 300, 'medium' => 600, 'long' => 1200]; |
| 325 | 331 | $word_count = $word_targets[$length] ?? 600; |
| @@ -347,18 +353,15 @@ | ||
| 347 | 353 | |
| 348 | 354 | $max_tokens_map = ['short' => 700, 'medium' => 1200, 'long' => 2400]; |
| 349 | 355 | $max_tokens = $max_tokens_map[$length] ?? 1200; |
| 350 | 356 | |
| 351 | - $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [ | |
| 352 | - 'headers' => [ | |
| 353 | - 'Authorization' => 'Bearer ' . $api_key, | |
| 354 | - 'Content-Type' => 'application/json', | |
| 355 | - ], | |
| 356 | - 'body' => wp_json_encode([ | |
| 357 | + $response = wp_remote_post(\King_Addons\AI_Provider::getChatEndpoint(), [ | |
| 358 | + 'headers' => \King_Addons\AI_Provider::getHeaders(), | |
| 359 | + 'body' => wp_json_encode(\King_Addons\AI_Provider::prepareChatPayload([ | |
| 357 | 360 | 'model' => $model, |
| 358 | 361 | 'messages' => [['role' => 'user', 'content' => $prompt]], |
| 359 | 362 | 'max_tokens' => $max_tokens, |
| 360 | - ]), | |
| 363 | + ])), | |
| 361 | 364 | 'timeout' => 120, |
| 362 | 365 | 'data_format' => 'body', |
| 363 | 366 | ]); |
| 364 | 367 | |
| @@ -369,9 +372,9 @@ | ||
| 369 | 372 | $code = wp_remote_retrieve_response_code($response); |
| 370 | 373 | $body = json_decode(wp_remote_retrieve_body($response), true); |
| 371 | 374 | |
| 372 | 375 | if ($code !== 200 || empty($body['choices'][0]['message']['content'])) { |
| 373 | - $api_error = $body['error']['message'] ?? esc_html__('API request failed.', 'king-addons'); | |
| 376 | + $api_error = \King_Addons\AI_Provider::extractErrorMessage($body, esc_html__('API request failed.', 'king-addons')); | |
| 374 | 377 | return new \WP_Error('api_error', $api_error); |
| 375 | 378 | } |
| 376 | 379 | |
| 377 | 380 | $raw = trim((string) $body['choices'][0]['message']['content']); |
| @@ -396,13 +399,13 @@ | ||
| 396 | 399 | ]; |
| 397 | 400 | } |
| 398 | 401 | |
| 399 | 402 | /** |
| 400 | - * Generate an image via OpenAI Images API and attach it to a post. | |
| 403 | + * Generate an image via the provider's Images API and attach it to a post. | |
| 401 | 404 | * |
| 402 | 405 | * @param string $title Post title (used as image prompt context). |
| 403 | 406 | * @param string $description Overall topic description. |
| 404 | - * @param string $model 'dall-e-3' or 'gpt-image-1'. | |
| 407 | + * @param string $model Image model id of the configured AI provider. | |
| 405 | 408 | * @param string $quality Quality setting for the selected model. |
| 406 | 409 | * @param string $size Image dimensions string. |
| 407 | 410 | * @param int $post_id Post to attach the image to. |
| 408 | 411 | * @return int|\WP_Error Attachment ID on success, WP_Error on failure. |
| @@ -408,37 +411,48 @@ | ||
| 408 | 411 | * @return int|\WP_Error Attachment ID on success, WP_Error on failure. |
| 409 | 412 | */ |
| 410 | 413 | private function generate_and_attach_image(string $title, string $description, string $model, string $quality, string $size, int $post_id) |
| 411 | 414 | { |
| 412 | - $options = get_option('king_addons_ai_options', []); | |
| 413 | - $api_key = $options['openai_api_key'] ?? ''; | |
| 415 | + $api_key = \King_Addons\AI_Provider::getApiKey(); | |
| 414 | 416 | |
| 415 | 417 | if ($api_key === '') { |
| 416 | - return new \WP_Error('missing_api_key', esc_html__('OpenAI API key is missing.', 'king-addons')); | |
| 418 | + return new \WP_Error('missing_api_key', sprintf( | |
| 419 | + /* translators: %s: provider name */ | |
| 420 | + esc_html__('%s API key is missing.', 'king-addons'), | |
| 421 | + \King_Addons\AI_Provider::getLabel() | |
| 422 | + )); | |
| 417 | 423 | } |
| 418 | 424 | |
| 419 | 425 | $prompt = 'Professional blog featured image for an article titled: "' . $title . '". Topic: ' . $description . '. Photorealistic style, no text overlays, no watermarks.'; |
| 420 | 426 | |
| 421 | - $body = ['model' => $model, 'prompt' => $prompt, 'size' => $size]; | |
| 427 | + // OpenAI's image models take vendor specific size/quality options; | |
| 428 | + // OpenRouter fans out to many vendors that do not share them. | |
| 429 | + $body = ['model' => $model, 'prompt' => $prompt]; | |
| 422 | 430 | |
| 423 | - if ($model === 'dall-e-3') { | |
| 424 | - $body['n'] = 1; | |
| 425 | - $body['quality'] = ($quality === 'hd') ? 'hd' : 'standard'; | |
| 426 | - } elseif ($model === 'gpt-image-1') { | |
| 427 | - $body['quality'] = in_array($quality, ['low', 'medium', 'high', 'auto'], true) ? $quality : 'auto'; | |
| 431 | + if (\King_Addons\AI_Provider::isOpenRouter()) { | |
| 432 | + $body['n'] = 1; | |
| 433 | + } else { | |
| 434 | + $body['size'] = $size; | |
| 435 | + | |
| 436 | + if ($model === 'dall-e-3') { | |
| 437 | + $body['n'] = 1; | |
| 438 | + $body['quality'] = ($quality === 'hd') ? 'hd' : 'standard'; | |
| 439 | + } elseif ($model === 'gpt-image-1') { | |
| 440 | + $body['quality'] = in_array($quality, ['low', 'medium', 'high', 'auto'], true) ? $quality : 'auto'; | |
| 441 | + } | |
| 428 | 442 | } |
| 429 | 443 | |
| 430 | - $response = wp_remote_post('https://api.openai.com/v1/images/generations', [ | |
| 431 | - 'headers' => [ | |
| 432 | - 'Authorization' => 'Bearer ' . $api_key, | |
| 433 | - 'Content-Type' => 'application/json', | |
| 434 | - ], | |
| 435 | - 'body' => wp_json_encode($body), | |
| 436 | - 'timeout' => 120, | |
| 437 | - ]); | |
| 444 | + $data = \King_Addons\AI_Provider::decodeResponse( | |
| 445 | + wp_remote_post(\King_Addons\AI_Provider::getImagesEndpoint(), [ | |
| 446 | + 'headers' => \King_Addons\AI_Provider::getHeaders(), | |
| 447 | + 'body' => wp_json_encode($body), | |
| 448 | + 'timeout' => 120, | |
| 449 | + ]), | |
| 450 | + esc_html__('Image generation failed.', 'king-addons') | |
| 451 | + ); | |
| 438 | 452 | |
| 439 | - if (is_wp_error($response)) { | |
| 440 | - return $response; | |
| 453 | + if (is_wp_error($data)) { | |
| 454 | + return $data; | |
| 441 | 455 | } |
| 442 | 456 | |
| 443 | 457 | require_once ABSPATH . 'wp-admin/includes/image.php'; |
| 444 | 458 | require_once ABSPATH . 'wp-admin/includes/file.php'; |
| @@ -443,41 +457,60 @@ | ||
| 443 | 457 | require_once ABSPATH . 'wp-admin/includes/image.php'; |
| 444 | 458 | require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 445 | 459 | require_once ABSPATH . 'wp-admin/includes/media.php'; |
| 446 | 460 | |
| 447 | - $code = wp_remote_retrieve_response_code($response); | |
| 448 | - $data = json_decode(wp_remote_retrieve_body($response), true); | |
| 461 | + // An image comes back either as a hosted URL or as inline base64. | |
| 462 | + $item = (isset($data['data'][0]) && is_array($data['data'][0])) ? $data['data'][0] : []; | |
| 463 | + $base64 = ''; | |
| 464 | + $remote_url = ''; | |
| 465 | + $mime = 'image/png'; | |
| 449 | 466 | |
| 450 | - if ($model === 'gpt-image-1') { | |
| 451 | - $b64 = $data['data'][0]['b64_json'] ?? ''; | |
| 452 | - if ($b64 === '') { | |
| 453 | - $err = $data['error']['message'] ?? esc_html__('No image data returned.', 'king-addons'); | |
| 454 | - return new \WP_Error('api_error', $err); | |
| 467 | + if (!empty($item['b64_json']) && is_string($item['b64_json'])) { | |
| 468 | + $base64 = $item['b64_json']; | |
| 469 | + if (!empty($item['media_type']) && is_string($item['media_type'])) { | |
| 470 | + $mime = $item['media_type']; | |
| 455 | 471 | } |
| 472 | + } elseif (!empty($item['url']) && is_string($item['url'])) { | |
| 473 | + $remote_url = $item['url']; | |
| 474 | + } elseif (!empty($item['image_url']['url']) && is_string($item['image_url']['url'])) { | |
| 475 | + $remote_url = $item['image_url']['url']; | |
| 476 | + } | |
| 456 | 477 | |
| 457 | - $bytes = base64_decode($b64); | |
| 478 | + if ($remote_url !== '' && strpos($remote_url, 'data:') === 0 && preg_match('#^data:([^;,]+);base64,(.+)$#s', $remote_url, $matches)) { | |
| 479 | + $mime = $matches[1]; | |
| 480 | + $base64 = $matches[2]; | |
| 481 | + $remote_url = ''; | |
| 482 | + } | |
| 483 | + | |
| 484 | + if ($base64 === '' && $remote_url === '') { | |
| 485 | + return new \WP_Error('api_error', esc_html__('No image data returned.', 'king-addons')); | |
| 486 | + } | |
| 487 | + | |
| 488 | + if ($base64 !== '') { | |
| 489 | + $bytes = base64_decode($base64, true); | |
| 458 | 490 | if (!$bytes) { |
| 459 | 491 | return new \WP_Error('decode_error', esc_html__('Failed to decode image data.', 'king-addons')); |
| 460 | 492 | } |
| 461 | 493 | |
| 462 | - $tmp = wp_tempnam('postgen.png'); | |
| 494 | + $type_map = ['image/jpeg' => 'jpg', 'image/jpg' => 'jpg', 'image/webp' => 'webp', 'image/gif' => 'gif']; | |
| 495 | + $extension = $type_map[$mime] ?? 'png'; | |
| 496 | + $name = substr(sanitize_file_name($title), 0, 80); | |
| 497 | + if ($name === '') { | |
| 498 | + $name = 'postgen'; | |
| 499 | + } | |
| 500 | + | |
| 501 | + $tmp = wp_tempnam($name . '.' . $extension); | |
| 463 | 502 | if (!$tmp || !file_put_contents($tmp, $bytes)) { |
| 464 | 503 | return new \WP_Error('write_error', esc_html__('Failed to write temp image file.', 'king-addons')); |
| 465 | 504 | } |
| 466 | 505 | |
| 467 | 506 | return media_handle_sideload([ |
| 468 | - 'name' => substr(sanitize_file_name($title), 0, 80) . '.png', | |
| 507 | + 'name' => $name . '.' . $extension, | |
| 469 | 508 | 'tmp_name' => $tmp, |
| 470 | 509 | ], $post_id, $title); |
| 471 | 510 | } |
| 472 | 511 | |
| 473 | - // DALL·E 3 (URL-based). | |
| 474 | - if ($code !== 200 || empty($data['data'][0]['url'])) { | |
| 475 | - $err = $data['error']['message'] ?? esc_html__('Image generation failed.', 'king-addons'); | |
| 476 | - return new \WP_Error('api_error', $err); | |
| 477 | - } | |
| 478 | - | |
| 479 | - return media_sideload_image(esc_url_raw($data['data'][0]['url']), $post_id, $title, 'id'); | |
| 512 | + return media_sideload_image(esc_url_raw($remote_url), $post_id, $title, 'id'); | |
| 480 | 513 | } |
| 481 | 514 | |
| 482 | 515 | /** |
| 483 | 516 | * Kick the batch processor if cron missed its schedule or went stale. |