Practical Integration

Auto-Tagging and Organizing the Media Library With AI

⏱ 16 min

A media library that’s grown for years without a tagging discipline is a familiar problem: hundreds or thousands of images, most untagged, most missing alt text, no practical way to find “the ones with a red background” without opening each one. Lesson 2 built the pattern for describing and tagging a single image. This lesson runs that pattern across an existing library, and, just as important, builds in a human review step before any suggestion actually changes a live attachment. AI suggesting tags for a site’s entire media library without a human ever looking at the suggestions first is how mislabeled, unreviewed metadata quietly creeps into search and filtering across a whole site.

What you'll learn in this lesson
Querying existing, untagged media
WP_Query against post_type => "attachment" with a meta_query for missing alt text.
Generating suggestions without applying them
Storing AI output as a separate, pending suggestion rather than overwriting real metadata directly.
A minimal review screen
Listing pending suggestions with an approve/reject action per image.
Applying approved suggestions
Only writing to _wp_attachment_image_alt and taxonomy terms after a human says yes.
Prerequisites

Lesson 2 of this course for the underlying with_file() plus generate_text() pattern this lesson runs at scale. Basic familiarity with WP_Query and custom admin pages is assumed rather than re-taught here.

Step 1: Finding media that actually needs attention

Querying every attachment on a large site and reprocessing all of them wastes calls on images that are already tagged. Target the ones missing alt text first:

// File: my-plugin/class-media-auto-tag.php
function my_plugin_get_untagged_images( int $limit = 20 ): array {
	return get_posts(
		array(
			'post_type'      => 'attachment',
			'post_mime_type' => 'image',
			'post_status'    => 'inherit',
			'posts_per_page' => $limit,
			'meta_query'     => array(
				array(
					'key'     => '_wp_attachment_image_alt',
					'compare' => 'NOT EXISTS',
				),
			),
		)
	);
}

NOT EXISTS catches images that have never had alt text set at all, which is usually the bulk of a neglected library. A separate pass with 'compare' => '=' and an empty string value catches attachments where the meta key exists but was saved blank, worth running as a follow-up query if the first pass doesn’t cover everything you expected.

Step 2: Generating a suggestion, not a final answer

Reuse Lesson 2’s description-and-tags pattern, but store the result as a pending suggestion rather than writing straight to _wp_attachment_image_alt:

// File: my-plugin/class-media-auto-tag.php
use WordPress\AiClient\Files\DTO\File;

function my_plugin_suggest_tags_for_attachment( int $attachment_id ): void {
	$path = get_attached_file( $attachment_id );
	$mime = get_post_mime_type( $attachment_id );

	if ( ! $path || ! file_exists( $path ) ) {
		return;
	}

	$file = new File( $path, $mime );

	$response = wp_ai_client_prompt()
		->with_text(
			'Respond with JSON only: {"alt_text": "one plain sentence", ' .
			'"tags": ["3 to 6 short lowercase keywords"]}'
		)
		->with_file( $file )
		->generate_text();

	$parsed = json_decode( $response, true );

	if ( ! is_array( $parsed ) || empty( $parsed['alt_text'] ) ) {
		return;
	}

	update_post_meta( $attachment_id, '_my_plugin_suggested_alt', sanitize_text_field( $parsed['alt_text'] ) );
	update_post_meta( $attachment_id, '_my_plugin_suggested_tags', wp_json_encode( $parsed['tags'] ?? array() ) );
}

Notice _my_plugin_suggested_alt, not _wp_attachment_image_alt. Nothing a visitor or editor currently sees changes yet, the suggestion sits in its own meta keys until someone reviews it.

Step 3: A minimal review list

A single admin page listing pending suggestions with an approve/reject action per image is enough, this doesn’t need to be elaborate:

// File: my-plugin/class-media-auto-tag.php
add_action( 'admin_menu', function () {
	add_media_page(
		__( 'AI Tag Suggestions', 'my-plugin' ),
		__( 'AI Tag Suggestions', 'my-plugin' ),
		'upload_files',
		'my-plugin-tag-suggestions',
		'my_plugin_render_suggestions_page'
	);
} );

function my_plugin_render_suggestions_page(): void {
	$pending = get_posts(
		array(
			'post_type'      => 'attachment',
			'posts_per_page' => 50,
			'meta_query'     => array(
				array( 'key' => '_my_plugin_suggested_alt', 'compare' => 'EXISTS' ),
			),
		)
	);

	echo '<div class="wrap"><h1>' . esc_html__( 'AI Tag Suggestions', 'my-plugin' ) . '</h1>';

	foreach ( $pending as $attachment ) {
		$alt  = get_post_meta( $attachment->ID, '_my_plugin_suggested_alt', true );
		$tags = json_decode( get_post_meta( $attachment->ID, '_my_plugin_suggested_tags', true ), true ) ?: array();

		echo '<div style="display:flex;gap:16px;align-items:center;border-bottom:1px solid #ccc;padding:12px 0;">';
		echo wp_get_attachment_image( $attachment->ID, array( 80, 80 ) );
		echo '<div><strong>' . esc_html( $alt ) . '</strong><br>' . esc_html( implode( ', ', $tags ) ) . '</div>';
		echo '<form method="post">';
		wp_nonce_field( 'my_plugin_apply_suggestion_' . $attachment->ID );
		echo '<input type="hidden" name="attachment_id" value="' . esc_attr( $attachment->ID ) . '">';
		echo '<button class="button button-primary" name="my_plugin_action" value="approve">' . esc_html__( 'Approve', 'my-plugin' ) . '</button> ';
		echo '<button class="button" name="my_plugin_action" value="reject">' . esc_html__( 'Reject', 'my-plugin' ) . '</button>';
		echo '</form></div>';
	}

	echo '</div>';
}

This is deliberately plain HTML rather than a polished data table, the point is having a real place a human looks before anything is applied, not building a finished UI component.

Step 4: Applying only what’s approved

// File: my-plugin/class-media-auto-tag.php
add_action( 'admin_init', function () {
	if ( empty( $_POST['my_plugin_action'] ) || empty( $_POST['attachment_id'] ) ) {
		return;
	}

	$attachment_id = absint( $_POST['attachment_id'] );

	check_admin_referer( 'my_plugin_apply_suggestion_' . $attachment_id );

	if ( ! current_user_can( 'upload_files' ) ) {
		return;
	}

	if ( 'approve' === $_POST['my_plugin_action'] ) {
		$alt  = get_post_meta( $attachment_id, '_my_plugin_suggested_alt', true );
		$tags = json_decode( get_post_meta( $attachment_id, '_my_plugin_suggested_tags', true ), true ) ?: array();

		update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt );
		wp_set_object_terms( $attachment_id, array_map( 'sanitize_text_field', $tags ), 'post_tag', true );
	}

	delete_post_meta( $attachment_id, '_my_plugin_suggested_alt' );
	delete_post_meta( $attachment_id, '_my_plugin_suggested_tags' );
} );

Rejecting or approving both clear the pending suggestion meta, the difference is only whether the real alt text and tags get written first. Nothing touches _wp_attachment_image_alt outside this one approval branch.

Why not just apply suggestions automatically

It would be less code to skip the pending-suggestion step and write straight to _wp_attachment_image_alt from Step 2. The reason not to: a vision-adjacent pattern built on a general-purpose method (Lesson 2’s honest caveat still applies here) will occasionally misread an image, and alt text is an accessibility feature, wrong alt text actively hurts screen reader users rather than just looking sloppy. A one-click human review step is a small cost for avoiding that.

Test it: generate and review a real suggestion

wp-env run cli wp eval '
$images = my_plugin_get_untagged_images( 1 );
if ( $images ) {
	my_plugin_suggest_tags_for_attachment( $images[0]->ID );
	echo "Suggested for attachment " . $images[0]->ID . "\n";
}
'

Then visit Media > AI Tag Suggestions in wp-admin, you should see that image with a suggested alt text and tag list, and working Approve / Reject buttons.

Don't run this against thousands of images in one request

The single-attachment function above is the building block, running it across an entire library needs the batching approach the next lesson builds, calling it in a tight loop over a large get_posts() result inside one page load will time out well before finishing.

Recap

Auto-tagging at library scale reuses Lesson 2’s with_file() plus generate_text() pattern one attachment at a time, but stores the result as a separate, clearly-named pending suggestion rather than overwriting real metadata directly. A minimal admin review screen lets a human approve or reject each suggestion, and only an approval writes to _wp_attachment_image_alt and the post_tag taxonomy. That review step matters specifically because the underlying vision pattern is a workaround, not a guaranteed-accurate dedicated feature.

Resources & further reading

← Generating Video Summaries From Transcripts Building a Bulk Media Analysis Tool →