| @@ -434,8 +434,103 @@ | ||
| 434 | 434 | return 'Error: ' . $error->getMessage(); |
| 435 | 435 | } |
| 436 | 436 | } |
| 437 | 437 | |
| 438 | + /** | |
| 439 | + * Generate documentation from an uploaded image using a vision-capable model. | |
| 440 | + * | |
| 441 | + * Mirrors generate_openai_response() but sends the picture alongside the text | |
| 442 | + * prompt as an OpenAI-format multimodal user message | |
| 443 | + * (`content: [ {type:text}, {type:image_url} ]`). The OpenAI-compatible | |
| 444 | + * provider forwards that message array to the wire verbatim, so no provider | |
| 445 | + * change is needed. Only OpenAI vision models are wired — Claude and Gemini | |
| 446 | + * use a different image envelope, so they are refused with a clear error | |
| 447 | + * instead of being sent a payload they would reject. | |
| 448 | + * | |
| 449 | + * @param string $prompt Composed instruction prompt. | |
| 450 | + * @param array $image { data_uri:string, mime:string }. | |
| 451 | + * @param int|null $max_tokens Optional token cap. | |
| 452 | + * @param array $extra_system Extra system messages (instruction sets). | |
| 453 | + * @return string|\WP_Error Generated content, or WP_Error on guard/failure. | |
| 454 | + */ | |
| 455 | + public function generate_vision_response( $prompt, $image, $max_tokens = null, $extra_system = array() ) { | |
| 456 | + if ( empty( $image['data_uri'] ) ) { | |
| 457 | + return new \WP_Error( 'ai_vision_no_image', __( 'No image data to send to the AI.', 'betterdocs' ) ); | |
| 458 | + } | |
| 459 | + | |
| 460 | + $factory = new ProviderFactory( $this->settings ); | |
| 461 | + $platform = $factory->active_platform(); | |
| 462 | + $model = $factory->active_model( $platform ); | |
| 463 | + | |
| 464 | + if ( ! $this->platform_supports_vision( $platform, $model ) ) { | |
| 465 | + return new \WP_Error( | |
| 466 | + 'ai_no_vision', | |
| 467 | + sprintf( | |
| 468 | + /* translators: 1: AI platform id, 2: model name. */ | |
| 469 | + __( 'The configured AI model (%1$s / %2$s) can\'t read images. Switch to an OpenAI vision model such as GPT-4o or GPT-4o mini in BetterDocs → Settings → AI Content Suite, or upload a PDF/DOCX/TXT instead.', 'betterdocs' ), | |
| 470 | + $platform, | |
| 471 | + '' !== (string) $model ? $model : 'default' | |
| 472 | + ) | |
| 473 | + ); | |
| 474 | + } | |
| 475 | + | |
| 476 | + try { | |
| 477 | + $messages = array_merge( | |
| 478 | + array( array( 'role' => 'system', 'content' => $this->get_system_prompt() ) ), | |
| 479 | + $this->normalize_extra_system( $extra_system ), | |
| 480 | + array( | |
| 481 | + array( | |
| 482 | + 'role' => 'user', | |
| 483 | + 'content' => array( | |
| 484 | + array( 'type' => 'text', 'text' => (string) $prompt ), | |
| 485 | + array( 'type' => 'image_url', 'image_url' => array( 'url' => (string) $image['data_uri'] ) ), | |
| 486 | + ), | |
| 487 | + ), | |
| 488 | + ) | |
| 489 | + ); | |
| 490 | + | |
| 491 | + $result = $factory->make()->chat( $messages, $this->ai_chat_options( $max_tokens ) ); | |
| 492 | + | |
| 493 | + if ( is_wp_error( $result ) ) { | |
| 494 | + return $result; | |
| 495 | + } | |
| 496 | + | |
| 497 | + return $result['content']; | |
| 498 | + } catch ( \Exception $error ) { | |
| 499 | + return new \WP_Error( 'ai_vision_failed', 'Error: ' . $error->getMessage() ); | |
| 500 | + } | |
| 501 | + } | |
| 502 | + | |
| 503 | + /** | |
| 504 | + * Whether the active platform + model can accept image input in the OpenAI | |
| 505 | + * multimodal format. Deliberately conservative: only OpenAI vision model | |
| 506 | + * families qualify, because Claude and Gemini require a different image | |
| 507 | + * envelope this path does not build. gpt-3.5 (text-only) is excluded. | |
| 508 | + * | |
| 509 | + * @param string $platform | |
| 510 | + * @param string $model | |
| 511 | + * @return bool | |
| 512 | + */ | |
| 513 | + protected function platform_supports_vision( $platform, $model ) { | |
| 514 | + if ( 'openai' !== $platform ) { | |
| 515 | + return false; | |
| 516 | + } | |
| 517 | + | |
| 518 | + $model = strtolower( (string) $model ); | |
| 519 | + | |
| 520 | + if ( '' === $model || false !== strpos( $model, 'gpt-3.5' ) ) { | |
| 521 | + return false; | |
| 522 | + } | |
| 523 | + | |
| 524 | + foreach ( array( 'gpt-4o', 'gpt-4.1', 'gpt-4-turbo', 'gpt-4-vision', 'chatgpt-4o', 'gpt-5', 'o1', 'o3', 'o4' ) as $family ) { | |
| 525 | + if ( false !== strpos( $model, $family ) ) { | |
| 526 | + return true; | |
| 527 | + } | |
| 528 | + } | |
| 529 | + | |
| 530 | + return false; | |
| 531 | + } | |
| 532 | + | |
| 438 | 533 | public function get_outline_system_prompt() { |
| 439 | 534 | $prompt = <<<'PROMPT' |
| 440 | 535 | You are a Senior Technical Writer. Produce a documentation OUTLINE only — not the full article. |
| 441 | 536 | |