*/ private const BULK_QUERY_ARGS = array( 'wpai_bulk_alt_text', 'wpai_attachment_ids', '_wpai_bulk_nonce' ); // phpcs:ignore SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition -- This is used as an array const. /** * Nonce action signing the bulk action redirect. * * @since 1.4.0 * * @var string */ private const BULK_NONCE_ACTION = 'wpai_bulk_alt_text'; /** * {@inheritDoc} */ public static function get_id(): string { return 'alt-text-generation'; } /** * Tracks whether the media-focused assets have already been enqueued. * * @since 0.3.0 * * @var bool */ private bool $media_assets_enqueued = false; /** * {@inheritDoc} */ protected function load_metadata(): array { return array( 'label' => __( 'Alt Text Generation', 'ai' ), 'description' => __( 'Generates accessible alternative (alt) text for images using AI vision models, following common web accessibility guidance. Requires an AI connector that includes support for vision-based image analysis models.', 'ai' ), 'category' => Experiment_Category::EDITOR, 'capability' => 'vision', 'usage' => array( 'trigger' => 'on_demand', 'runs' => __( 'When you click Generate Alt Text or run the bulk action, and automatically when you import an image made with Image Generation', 'ai' ), 'sends' => __( 'The image, plus post content when used in the editor', 'ai' ), 'requests' => __( 'One request per image', 'ai' ), ), ); } /** * {@inheritDoc} */ public function register(): void { add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_assets' ) ); add_action( 'wp_enqueue_media', array( $this, 'enqueue_media_frame_assets' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_media_library_assets' ) ); add_action( 'add_meta_boxes_attachment', array( $this, 'setup_attachment_meta_box' ) ); add_filter( 'attachment_fields_to_edit', array( $this, 'add_button_to_media_modal' ), 10, 2 ); add_filter( 'bulk_actions-upload', array( $this, 'register_bulk_action' ) ); add_filter( 'handle_bulk_actions-upload', array( $this, 'handle_bulk_action' ), 10, 3 ); add_filter( 'removable_query_args', array( $this, 'register_removable_query_args' ) ); if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { return; } \WP_CLI::add_command( 'ai alt-text', Alt_Text_Command::class ); } /** * Registers any needed abilities. * * @since 0.3.0 */ public function register_abilities(): void { wp_register_ability( 'ai/' . self::get_id(), array( 'label' => $this->get_label(), 'description' => $this->get_description(), 'ability_class' => Alt_Text_Generation_Ability::class, ), ); } /** * Enqueues block editor assets. * * @since 0.3.0 */ public function enqueue_editor_assets(): void { Asset_Loader::enqueue_script( 'alt_text_generation', 'experiments/alt-text-generation', array( 'include_core_abilities' => true ) ); Asset_Loader::localize_script( 'alt_text_generation', 'AltTextGenerationData', array( 'enabled' => $this->is_enabled(), ) ); $this->maybe_enqueue_media_script(); $this->maybe_enqueue_media_editor_script(); } /** * Enqueues assets whenever the core media modal is registered. * * @since 0.3.0 */ public function enqueue_media_frame_assets(): void { $this->maybe_enqueue_media_script(); } /** * Conditionally enqueues assets on media-related admin screens (e.g., upload.php). * * @since 0.3.0 * * @param string $hook_suffix Current admin page hook suffix. */ public function maybe_enqueue_media_library_assets( string $hook_suffix ): void { if ( ! $this->is_enabled() ) { return; } if ( in_array( $hook_suffix, array( 'upload.php', 'media-new.php' ), true ) ) { $this->maybe_enqueue_media_script(); $this->maybe_enqueue_bulk_script(); return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'attachment' !== $screen->post_type ) { return; } $this->maybe_enqueue_media_script(); } /** * Shared helper to enqueue and localize the media UI script once per request. * * @since 0.3.0 */ private function maybe_enqueue_media_script(): void { if ( $this->media_assets_enqueued || ! $this->is_enabled() ) { return; } Asset_Loader::enqueue_script( 'alt_text_generation_media', 'experiments/alt-text-generation-media', array( 'include_core_abilities' => true ) ); Asset_Loader::localize_script( 'alt_text_generation_media', 'AltTextGenerationMediaData', array( 'enabled' => $this->is_enabled(), ) ); $this->media_assets_enqueued = true; } /** * Conditionally enqueues assets for the experimental Gutenberg media editor. * * Requires the Gutenberg media editor experiment or media modal experiment to be enabled. * * @since 1.0.0 */ private function maybe_enqueue_media_editor_script(): void { if ( ! is_plugin_active( 'gutenberg/gutenberg.php' ) ) { return; } $experiments = get_option( 'gutenberg-experiments' ); if ( ! isset( $experiments['gutenberg-media-editor'] ) && ! isset( $experiments['gutenberg-media-editor-modal'] ) ) { return; } Asset_Loader::enqueue_script( 'alt_text_generation_media_editor', 'experiments/alt-text-generation-media-editor' ); } /** * Sets up the attachment meta box. * * Adds a meta box to the attachment edit screen that contains * the Generate/Regenerate button. * * @since 0.3.0 * * @param \WP_Post $post The attachment post. */ public function setup_attachment_meta_box( \WP_Post $post ): void { if ( ! $this->is_enabled() || ! wp_attachment_is_image( $post ) ) { return; } add_meta_box( 'ai_alt_text_generation', __( 'Alt Text', 'ai' ), array( $this, 'render_attachment_meta_box' ), 'attachment', ); } /** * Renders the attachment meta box content. * * @since 0.3.0 * * @param \WP_Post $post The attachment post. */ public function render_attachment_meta_box( \WP_Post $post ): void { $button_text = empty( get_post_meta( $post->ID, '_wp_attachment_image_alt', true ) ) ? __( 'Generate', 'ai' ) : __( 'Regenerate', 'ai' ); printf( '