| @@ -16,8 +16,9 @@ | ||
| 16 | 16 | use WPDeveloper\BetterDocs\AI\ProviderFactory; |
| 17 | 17 | use WPDeveloper\BetterDocs\AI\ModelRegistry; |
| 18 | 18 | use WPDeveloper\BetterDocs\Utils\AIUsage; |
| 19 | 19 | use WPDeveloper\BetterDocs\REST\AIEdit; |
| 20 | + use WPDeveloper\BetterDocs\REST\WriteWithAI as RESTWriteWithAI; | |
| 20 | 21 | |
| 21 | 22 | class WriteWithAI extends Base { |
| 22 | 23 | |
| 23 | 24 | public $settings; |
| @@ -100,8 +101,15 @@ | ||
| 100 | 101 | 'platform_label' => isset( $platform_labels[ $active_platform ] ) ? $platform_labels[ $active_platform ] : ucfirst( (string) $active_platform ), |
| 101 | 102 | 'model' => $active_model, |
| 102 | 103 | 'model_label' => isset( $model_labels[ $active_model ] ) ? $model_labels[ $active_model ] : $active_model, |
| 103 | 104 | 'max_token' => (int) $this->settings->get( 'ai_autowrite_max_token', 2500 ), |
| 105 | + // Attachment limits, resolved server-side so the modal can refuse | |
| 106 | + // an oversize file before uploading it — and so it advertises the | |
| 107 | + // host's real ceiling rather than ours when the host is smaller. | |
| 108 | + 'max_upload_bytes' => RESTWriteWithAI::upload_cap( 'file' ), | |
| 109 | + 'max_media_bytes' => RESTWriteWithAI::upload_cap( 'media' ), | |
| 110 | + 'media_exts' => RESTWriteWithAI::media_exts(), | |
| 111 | + 'supports_transcription' => $this->platform_supports( 'transcription', $active_platform ), | |
| 104 | 112 | 'settings_url' => esc_url( admin_url( 'admin.php?page=betterdocs-settings#betterdocs-ai' ) ), |
| 105 | 113 | 'woo_active' => class_exists( 'WooCommerce' ), |
| 106 | 114 | 'is_multilingual_active' => Helper::is_multilingual_active(), |
| 107 | 115 | 'language_options' => Helper::get_active_languages(), |
| @@ -460,9 +468,9 @@ | ||
| 460 | 468 | $factory = new ProviderFactory( $this->settings ); |
| 461 | 469 | $platform = $factory->active_platform(); |
| 462 | 470 | $model = $factory->active_model( $platform ); |
| 463 | 471 | |
| 464 | - if ( ! $this->platform_supports_vision( $platform, $model ) ) { | |
| 472 | + if ( ! $this->platform_supports( 'vision', $platform, $model ) ) { | |
| 465 | 473 | return new \WP_Error( |
| 466 | 474 | 'ai_no_vision', |
| 467 | 475 | sprintf( |
| 468 | 476 | /* translators: 1: AI platform id, 2: model name. */ |
| @@ -497,8 +505,78 @@ | ||
| 497 | 505 | return $result['content']; |
| 498 | 506 | } catch ( \Exception $error ) { |
| 499 | 507 | return new \WP_Error( 'ai_vision_failed', 'Error: ' . $error->getMessage() ); |
| 500 | 508 | } |
| 509 | + } | |
| 510 | + | |
| 511 | + /** | |
| 512 | + * Transcribe an uploaded recording to plain text. | |
| 513 | + * | |
| 514 | + * Deliberately not a doc-generation call: this returns the raw transcript so | |
| 515 | + * the author can correct it, and generation then runs through the ordinary | |
| 516 | + * grounded from-source path. That keeps one generation path in the plugin | |
| 517 | + * instead of a second, media-shaped one. | |
| 518 | + * | |
| 519 | + * @since 4.9.4 | |
| 520 | + * | |
| 521 | + * @param array $file `[ 'path', 'filename', 'mime' ]` — a PHP temp upload. | |
| 522 | + * @return string|\WP_Error Transcript text. | |
| 523 | + */ | |
| 524 | + public function transcribe( $file ) { | |
| 525 | + $factory = new ProviderFactory( $this->settings ); | |
| 526 | + $platform = $factory->active_platform(); | |
| 527 | + | |
| 528 | + if ( ! $this->platform_supports( 'transcription', $platform ) ) { | |
| 529 | + return new \WP_Error( | |
| 530 | + 'ai_no_transcription', | |
| 531 | + sprintf( | |
| 532 | + /* translators: %s: AI platform id, e.g. "claude". */ | |
| 533 | + __( 'The configured AI platform (%s) can\'t read audio or video. Switch to OpenAI or Google Gemini in BetterDocs → Settings → AI Content Suite, or upload a text file instead.', 'betterdocs' ), | |
| 534 | + $platform | |
| 535 | + ) | |
| 536 | + ); | |
| 537 | + } | |
| 538 | + | |
| 539 | + // 300s, matching every other AI call here: a 25 MB recording can take a | |
| 540 | + // minute or more to come back, well past the 50s provider default. | |
| 541 | + $options = $this->ai_chat_options(); | |
| 542 | + $options['model'] = ModelRegistry::transcription_model( $platform ); | |
| 543 | + | |
| 544 | + try { | |
| 545 | + return $factory->make()->transcribe( $file, $options ); | |
| 546 | + } catch ( \Exception $error ) { | |
| 547 | + return new \WP_Error( 'ai_transcribe_failed', 'Error: ' . $error->getMessage() ); | |
| 548 | + } | |
| 549 | + } | |
| 550 | + | |
| 551 | + /** | |
| 552 | + * Whether a platform + model can handle a given input capability. | |
| 553 | + * | |
| 554 | + * One entry point for every "can this provider read that file?" question, so | |
| 555 | + * a new capability is a case here rather than another bespoke check beside | |
| 556 | + * the call site. | |
| 557 | + * | |
| 558 | + * @since 4.9.4 | |
| 559 | + * | |
| 560 | + * @param string $capability `vision` | `transcription` | |
| 561 | + * @param string $platform | |
| 562 | + * @param string $model Chat model. Ignored for transcription, which uses | |
| 563 | + * its own model (see ModelRegistry). | |
| 564 | + * @return bool | |
| 565 | + */ | |
| 566 | + public function platform_supports( $capability, $platform, $model = '' ) { | |
| 567 | + switch ( $capability ) { | |
| 568 | + case 'vision': | |
| 569 | + return $this->platform_supports_vision( $platform, $model ); | |
| 570 | + | |
| 571 | + case 'transcription': | |
| 572 | + // Presence in the transcription map is the whole test — the chat | |
| 573 | + // model is irrelevant, since transcription runs as its own call | |
| 574 | + // with its own model before any doc is written. | |
| 575 | + return '' !== ModelRegistry::transcription_model( $platform ); | |
| 576 | + } | |
| 577 | + | |
| 578 | + return false; | |
| 501 | 579 | } |
| 502 | 580 | |
| 503 | 581 | /** |
| 504 | 582 | * Whether the active platform + model can accept image input in the OpenAI |