Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alt text generator via attachment edit UI ✨ #33

Merged
merged 15 commits into from
Apr 14, 2024
1 change: 1 addition & 0 deletions better-file-name.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
$better_file_name_settings = new Settings();
$better_file_name_admin = new Admin( $better_file_name_settings, plugins_url( 'build', __FILE__ ) );
$better_file_name_dalle_image_generator = new Dalle_Image_Generator( $better_file_name_settings );
$better_file_name_alt_text_rest_api = new Better_File_Name_Ai\Alt_Text_Rest_Api( $better_file_name_settings );

if ( defined( 'WP_CLI' ) && WP_CLI ) {
\WP_CLI::add_command( 'better-file-name generate-alt-text', Better_File_Name_Ai\Generate_Alt_Text_Cli::class );
Expand Down
78 changes: 78 additions & 0 deletions js/media-alt-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* global betterFileName */
import domReady from '@wordpress/dom-ready';

domReady( () => {
const buttonCallback = async function _buttonCallback( event ) {
const button = event.target;
const mediaId = button.getAttribute( 'data-media-id' );
const input = document.querySelector(
'#attachment-details-two-column-alt-text'
);
button.style.display = 'none';
const spinner = button.parentNode.querySelector( '.spinner' );
const generateAltText = button.parentNode.querySelector(
'.generate-alt-text__loading'
);
spinner.style.visibility = 'visible';
generateAltText.classList.remove( 'hidden' );

const altText = input.value;
if ( betterFileName?.api ) {
try {
const result = await fetch( betterFileName.api, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': betterFileName.nonce,
},
body: JSON.stringify( {
mediaId,
altText,
} ),
} );
const data = await result.json();
if ( data.alt_text ) {
input.value = data.alt_text;
const textChangeEvent = new Event( 'change', {
bubbles: true,
cancelable: true,
} );
input.dispatchEvent( textChangeEvent );
}
} catch ( error ) {
console.error( 'Error:', error ); // eslint-disable-line no-console
} finally {
generateAltText.classList.add( 'hidden' );
spinner.style.visibility = 'hidden';
button.style.display = 'inline-block';
}
}
};

function addButtonHandler() {
const button = document.querySelector( '.generate-alt-text' );
if ( button ) {
button.addEventListener( 'click', buttonCallback );
}
}

const ModalView = wp.media.view.Modal;
wp.media.view.Modal = wp.media.view.Modal.extend( {
open() {
// Ensure that the main attachment fields are rendered.
ModalView.prototype.open.apply( this );
// debugger;
addButtonHandler();
},
} );

const EditAttachments = wp.media.view.MediaFrame.EditAttachments;
wp.media.view.MediaFrame.EditAttachments =
wp.media.view.MediaFrame.EditAttachments.extend( {
rerender() {
EditAttachments.prototype.rerender.apply( this, arguments );
// debugger;
addButtonHandler();
},
} );
} );
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@wordpress/scripts": "^26.19.0"
},
"scripts": {
"build": "wp-scripts build js/index.js --output-path=build --output-filename=index.build.js",
"build": "wp-scripts build js/* --output-path=build",
"check-engines": "wp-scripts check-engines",
"check-licenses": "wp-scripts check-licenses",
"format": "wp-scripts format '**/*.{js,jsx,ts,tsx,json,md}'",
Expand All @@ -30,7 +30,7 @@
"lint:pkg-json": "wp-scripts lint-pkg-json",
"packages-update": "wp-scripts packages-update",
"plugin-zip": "wp-scripts plugin-zip",
"start": "wp-scripts start js/index.js --output-path=build --output-filename=index.build.js",
"start": "wp-scripts start js/* --output-path=build",
"test:e2e": "wp-scripts test-e2e",
"test:unit": "wp-scripts test-unit-js"
}
Expand Down
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ Rename file idea for WordPress was originally purposed by [Pascal Birchler](http
</a>
</div>


### Additional features:
- Allows generating featured image using dall-e-2 or dall-e-3 API.

- Allows generating featured image using dall-e-2 or dall-e-3 API.

> Note: GPT-4 Vision is in preview, It is not recommended to use this plugin on a production site.

## Demo:
https://github.com/PatelUtkarsh/better-file-name-ai/assets/5015489/1f0ce636-ceeb-4e6e-918b-872d3069d40f

https://github.com/PatelUtkarsh/better-file-name-ai/assets/5015489/1f0ce636-ceeb-4e6e-918b-872d3069d40f

## Installation

Expand Down
57 changes: 56 additions & 1 deletion src/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@ public function __construct( Settings $settings, string $plugin_url ) {
if ( ! $this->settings->get_openai_api_key() ) {
return;
}

if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
return;
}

if ( $this->settings->get_rename_file() ) {
add_filter( 'wp_handle_sideload_prefilter', [ $this, 'rename_new_file' ], 9999 );
add_filter( 'wp_handle_upload_prefilter', [ $this, 'rename_new_file' ], 9999 );
}

if ( $this->settings->should_generate_alt_text() ) {
add_filter( 'wp_update_attachment_metadata', [ $this, 'update_alt_text' ], 10, 2 );
add_filter( 'wp_update_attachment_metadata', $this->update_alt_text( ... ), 10, 2 );
add_filter( 'attachment_fields_to_edit', $this->attachment_fields_to_edit( ... ), 10, 2 );
add_action( 'wp_enqueue_media', $this->enqueue_media( ... ) );
}

if ( $this->settings->should_integrate_dall_e() ) {
Expand Down Expand Up @@ -103,4 +110,52 @@ public function enqueue_scripts() {
]
);
}

/**
* Add custom field to media attachment
*
* @param array $form_fields Form fields.
* @param object $post WP_Post object.
*
* @return array
*/
public function attachment_fields_to_edit( $form_fields, $post ) {
if ( ! str_starts_with( $post->post_mime_type, 'image' ) ) {
return $form_fields;
}

$form_fields['alt-text-generator'] = [
'input' => 'html',
'html' => sprintf( '<button class="button generate-alt-text" data-media-id="%d">%s</button><span class="generate-alt-text__loading hidden">%s</span><span class="spinner"></span>', $post->ID, __( 'Generate alt text', 'better-file-name' ), esc_html__( 'Generating alt text...', 'better-file-name' ) ),
'label' => '',
];

return $form_fields;
}

public function enqueue_media() {
$version_file = __DIR__ . '/../build/media-alt-text.asset.php';
if ( ! file_exists( $version_file ) ) {
return;
}
$version = include $version_file;
wp_enqueue_script(
'better-file-name-ai-media',
$this->plugin_url . '/media-alt-text.js',
$version['dependencies'],
$version['version'],
[
'in_footer' => true,
]
);

wp_localize_script(
'better-file-name-ai-media',
'betterFileName',
[
'api' => rest_url( 'better-file-name/v1/alt-text-generator' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
]
);
}
}
72 changes: 72 additions & 0 deletions src/Alt_Text_Rest_Api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
declare( strict_types=1 );

namespace Better_File_Name_Ai;

use WP_REST_Response;

class Alt_Text_Rest_Api {

public Settings $setting;

public function __construct( Settings $setting ) {
$this->setting = $setting;
add_action( 'rest_api_init', [ $this, 'register_routes' ] );
}

public function register_routes(): void {
register_rest_route(
'better-file-name/v1',
'/alt-text-generator',
[
'methods' => 'POST',
'callback' => $this->generate_image_alt_text( ... ),
'permission_callback' => function () {
return current_user_can( 'upload_files' );
},
'args' => [
'mediaId' => [
'required' => true,
'validate_callback' => $this->validate_attachment_id( ... ),
],
],
]
);
}

public function validate_attachment_id( $param, $_request, $_key ): bool {
unset( $_request, $_key );
if ( ! is_numeric( $param ) ) {
return false;
}

return get_post_type( $param ) === 'attachment';
}

public function generate_image_alt_text( $request ): WP_REST_Response {

if ( ! $this->setting->get_openai_api_key() ) {
return new WP_REST_Response( [ 'error' => 'OpenAI API key not found' ], 404 );
}

$open_ai_wrapper = new Openai_Wrapper( $this->setting->get_openai_api_key(), $this->setting->get_dell_e_version() );

$post_id = $request->get_param( 'mediaId' );

$file_path = new File_Path();
$file_path = $file_path->get_image_path( $post_id );
if ( $file_path === null ) {
return new WP_REST_Response( [ 'error' => 'Attachment file not found.' ], 404 );
}
try {
if ( ! empty( $file_path ) ) {
$text = $open_ai_wrapper->get_alt_text( $file_path );
return new WP_REST_Response( [ 'alt_text' => $text ], 200 );
} else {
return new WP_REST_Response( [ 'error' => 'File path not found.' ], 404 );
}
} catch ( \Exception $e ) {
return new WP_REST_Response( [ 'error' => $e->getMessage() ], 500 );
}
}
}
38 changes: 38 additions & 0 deletions src/File_Path.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
declare( strict_types=1 );

namespace Better_File_Name_Ai;

class File_Path {

private string $base_dir;

public function __construct( private readonly string $use_path = '' ) {
$uploads = wp_get_upload_dir();
$this->base_dir = $uploads['basedir'];
}

public function get_image_path( $post_id ): ?string {
if ( wp_get_environment_type() !== 'production' || $this->use_path === 'local' ) {
$attachment_data = wp_get_attachment_metadata( $post_id );
if ( ! isset( $attachment_data['file'] ) ) {
return null;
}
$file_path = str_replace( basename( $attachment_data['file'] ), '', $attachment_data['file'] );
if ( isset( $attachment_data['sizes']['large'] ) ) {
$file_path = $this->base_dir . DIRECTORY_SEPARATOR . $file_path . $attachment_data['sizes']['large']['file'];
} else {
$file_path = $this->base_dir . DIRECTORY_SEPARATOR . $attachment_data['file'];
}
} else {
[ $file_path ] = image_downsize( $post_id, 'thumbnail' );
if ( ! $file_path ) {
[ $file_path ] = image_downsize( $post_id, 'large' );
}
if ( ! $file_path ) {
$file_path = wp_get_attachment_url( $post_id );
}
}
return $file_path;
}
}
26 changes: 3 additions & 23 deletions src/Generate_Alt_Text_Cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,36 +56,16 @@ public function __invoke( array $args, array $assoc_args ) {
WP_CLI::log( 'Found ' . count( $attachment_ids ) . ' attachments without alt text.' );

$attachment_ids_chunks = array_chunk( $attachment_ids, 50 );
$uploads = wp_get_upload_dir();
$base_dir = $uploads['basedir'];
unset( $attachment_ids, $query, $uploads );
unset( $attachment_ids, $query );

$open_ai_wrapper = new Openai_Wrapper( $setting->get_openai_api_key(), $setting->get_dell_e_version() );

$file_path = new File_Path( $use );
$generated_alt_text_count = 0;
foreach ( $attachment_ids_chunks as $attachment_ids ) {
foreach ( $attachment_ids as $post_id ) {
WP_CLI::log( 'Processing attachment ID: ' . $post_id );
if ( wp_get_environment_type() !== 'production' || $use === 'data' ) {
$attachment_data = wp_get_attachment_metadata( $post_id );
if ( ! isset( $attachment_data['file'] ) ) {
continue;
}
$file_path = str_replace( basename( $attachment_data['file'] ), '', $attachment_data['file'] );
if ( isset( $attachment_data['sizes']['large'] ) ) {
$file_path = $base_dir . DIRECTORY_SEPARATOR . $file_path . $attachment_data['sizes']['large']['file'];
} else {
$file_path = $base_dir . DIRECTORY_SEPARATOR . $file_path . $attachment_data['file'];
}
} else {
[ $file_path ] = image_downsize( $post_id, 'thumbnail' );
if ( ! $file_path ) {
[ $file_path ] = image_downsize( $post_id, 'large' );
}
if ( ! $file_path ) {
$file_path = wp_get_attachment_url( $post_id );
}
}
$file_path = $file_path->get_image_path( $post_id );
if ( ! $dry_run && $setting->get_openai_api_key() ) {
try {
if ( ! empty( $file_path ) ) {
Expand Down
2 changes: 1 addition & 1 deletion src/Openai_Wrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function request( string $path, string $prompt ): string {
'method' => 'POST',
'headers' => $headers,
'body' => wp_json_encode( $data ),
'timeout' => 10,
'timeout' => defined( 'WP_CLI' ) && WP_CLI ? 30 : 15,
]
);

Expand Down
Loading