Skip to content

Commit afc7cb4

Browse files
jeffpauldkotterafercia
authored
Merge pull request #374 from dkotter/fix/alt-text-instructions
Adjust alt text generation to more closely match the W3C Alt decision tree Co-authored-by: dkotter <dkotter@git.wordpress.org> Co-authored-by: afercia <afercia@git.wordpress.org>
2 parents ffd1eb4 + 9bcac1e commit afc7cb4

9 files changed

Lines changed: 221 additions & 46 deletions

File tree

includes/Abilities/Image/Alt_Text_Generation.php

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/**
1919
* Alt text generation WordPress Ability.
2020
*
21-
* Uses AI vision models to generate descriptive alt text for images.
21+
* Uses AI vision models to propose alt text aligned with WCAG-oriented practice.
2222
*
2323
* @since 0.3.0
2424
*/
@@ -33,6 +33,15 @@ class Alt_Text_Generation extends Abstract_Ability {
3333
*/
3434
protected const MAX_ALT_TEXT_LENGTH = 125;
3535

36+
/**
37+
* Model output token that means the correct alternative text is empty (alt="").
38+
*
39+
* @since x.x.x
40+
*
41+
* @var string
42+
*/
43+
private const DECORATIVE_ALT_TOKEN = '[[DECORATIVE_ALT]]';
44+
3645
/**
3746
* {@inheritDoc}
3847
*
@@ -57,6 +66,10 @@ protected function input_schema(): array {
5766
'sanitize_callback' => 'sanitize_textarea_field',
5867
'description' => esc_html__( 'Optional context about the image or surrounding content to improve alt text relevance.', 'ai' ),
5968
),
69+
'image_meta' => array(
70+
'type' => 'string',
71+
'description' => esc_html__( 'Structured metadata about how the image block is used, such as whether it is linked.', 'ai' ),
72+
),
6073
),
6174
);
6275
}
@@ -70,9 +83,13 @@ protected function output_schema(): array {
7083
return array(
7184
'type' => 'object',
7285
'properties' => array(
73-
'alt_text' => array(
86+
'alt_text' => array(
7487
'type' => 'string',
75-
'description' => esc_html__( 'Generated alt text for the image.', 'ai' ),
88+
'description' => esc_html__( 'Generated alternative text for the image; may be empty when alt="" is correct.', 'ai' ),
89+
),
90+
'is_decorative' => array(
91+
'type' => 'boolean',
92+
'description' => esc_html__( 'Whether the image was determined to be decorative.', 'ai' ),
7693
),
7794
),
7895
);
@@ -91,6 +108,7 @@ protected function execute_callback( $input ) {
91108
'attachment_id' => null,
92109
'image_url' => null,
93110
'context' => '',
111+
'image_meta' => '',
94112
),
95113
);
96114

@@ -102,16 +120,21 @@ protected function execute_callback( $input ) {
102120
}
103121

104122
// Generate the alt text.
105-
$result = $this->generate_alt_text( $image_reference, normalize_content( $args['context'] ) );
123+
$result = $this->generate_alt_text(
124+
$image_reference,
125+
normalize_content( $args['context'] ),
126+
sanitize_textarea_field( $args['image_meta'] )
127+
);
106128

107129
if ( is_wp_error( $result ) ) {
108130
return $result;
109131
}
110132

111-
if ( empty( $result ) ) {
112-
return new WP_Error(
113-
'no_results',
114-
esc_html__( 'No alt text was generated.', 'ai' )
133+
// Detect the decorative token from the AI response.
134+
if ( 0 === strcasecmp( trim( $result ), self::DECORATIVE_ALT_TOKEN ) ) {
135+
return array(
136+
'alt_text' => '',
137+
'is_decorative' => true,
115138
);
116139
}
117140

@@ -185,10 +208,11 @@ protected function get_image_reference( array $args ) {
185208
*
186209
* @param array{reference: string} $image_reference Prepared image reference containing a data URI.
187210
* @param string $context Optional context to improve alt text relevance.
211+
* @param string $image_meta Optional metadata about how the image block is used.
188212
* @return string|\WP_Error The generated alt text or WP_Error on failure.
189213
*/
190-
protected function generate_alt_text( array $image_reference, string $context = '' ) {
191-
$result = wp_ai_client_prompt( $this->build_prompt( $context ) )
214+
protected function generate_alt_text( array $image_reference, string $context = '', string $image_meta = '' ) {
215+
$result = wp_ai_client_prompt( $this->build_prompt( $context, $image_meta ) )
192216
->with_file( $image_reference['reference'] )
193217
->using_system_instruction( $this->get_system_instruction( 'alt-text-system-instruction.php' ) )
194218
->using_temperature( 0.3 )
@@ -368,12 +392,18 @@ protected function normalize_upload_url( string $url ): string {
368392
*
369393
* @since 0.3.0
370394
*
371-
* @param string $context Optional context about the image.
395+
* @param string $context Optional context about the image.
396+
* @param string $image_meta Optional metadata about how the image block is used.
372397
* @return string The prompt for the AI.
373398
*/
374-
protected function build_prompt( string $context = '' ): string {
399+
protected function build_prompt( string $context = '', string $image_meta = '' ): string {
375400
$prompt = __( 'Generate alt text for this image.', 'ai' );
376401

402+
// If we have image block usage metadata, add it to the prompt.
403+
if ( ! empty( $image_meta ) ) {
404+
$prompt .= "\n\n<image-meta>" . $image_meta . '</image-meta>';
405+
}
406+
377407
// If we have additional context, add it to the prompt.
378408
if ( ! empty( $context ) ) {
379409
$prompt .= "\n\n<additional-context>" . $context . '</additional-context>';

includes/Abilities/Image/alt-text-system-instruction.php

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,35 @@
1212

1313
// phpcs:ignore Squiz.PHP.Heredoc.NotAllowed
1414
return <<<'INSTRUCTION'
15-
You are an accessibility expert that generates alt text for images on websites.
15+
You are an accessibility expert that proposes alternative (alt) text for HTML images. Your output must follow the same decisions authors make with the W3C "An alt Decision Tree" (decorative vs functional vs informative vs complex images).
1616
17-
Goal: Analyze the provided image and generate concise, descriptive alt text that accurately describes the image content for users who cannot see it. The alt text should be optimized for screen readers and accessibility compliance. If additional context is provided, use it to generate a more relevant alt text.
17+
Core rule: Alt text is not always a description of what the picture looks like. It must convey the information or purpose that the image serves in this specific context. If the image disappeared, what would be lost for someone who cannot see it—that is what belongs in alt text (or in empty alt when nothing should be announced).
1818
19-
Requirements for the alt text:
19+
Follow this order:
2020
21-
- Be concise: Keep it under 125 characters when possible
22-
- Be descriptive: Describe what is visually present in the image
23-
- Be objective: Describe what you see, not interpretations or assumptions
24-
- Avoid redundancy: Do not start with "Image of", "Picture of", or "Photo of"
25-
- Include relevant details: People, objects, actions, colors, and context when meaningful
26-
- Consider context: If context is provided, ensure the alt text is relevant to the surrounding content
27-
- Plain text only: No markdown, quotes, or special formatting
21+
1) Decorative or redundant?
22+
- Purely decorative (flourish, spacer, visual-only styling) OR the same information is already in adjacent text (including visible link text in the same link as the image).
23+
- Output: respond with exactly this token and nothing else: [[DECORATIVE_ALT]]
24+
- Do not describe the image for decorative/redundant cases.
2825
29-
For images containing text, include the text in your description if it's essential to understanding the image.
26+
2) Functional (image is a control or the main content of a link or button)?
27+
- Examples: linked image with no other text in the link; icon-only button; logo linking home.
28+
- Output: short text that describes the action or destination (where the link goes, what happens when activated)—not the photo or illustration subject.
29+
- If `<image-meta>` gives a URL or destination name, base the alt on that purpose. Do not substitute a visual description of the image.
3030
31-
Respond with only the alt text, nothing else.
31+
3) Informative (image adds meaning that is not covered by nearby text)?
32+
- Output: concise objective description of the information the image communicates (people, objects, setting, actions) relevant to context.
33+
- Do not start with "Image of", "Picture of", or "Photo of".
34+
- If the image contains essential text, include that text in the alt (or summarize if it is very long, and note that a longer text alternative may be needed elsewhere).
35+
- If `<additional-context>` is provided, use it to understand the purpose, subject, and relevance of the image within the article. Be sure to describe only information not already conveyed in nearby text
36+
37+
4) Complex (chart, diagram, infographic, detailed map)?
38+
- Output: a short summary of the main point; do not paste entire data sets into alt text. If context implies a longer description exists or should exist on the page, you may mention that the full explanation is in surrounding content.
39+
40+
General requirements:
41+
- Prefer under 125 characters when possible (except when essential text in the image requires more).
42+
- Plain text only: no markdown, quotes, or labels—except the exact token [[DECORATIVE_ALT]] when appropriate.
43+
- Use any `<image-meta>` section and `<additional-context>` to decide role and wording; they override guessing from pixels alone.
44+
45+
Respond with only the alt text string, or exactly [[DECORATIVE_ALT]] for empty alternative text—nothing else.
3246
INSTRUCTION;

includes/Abilities/Review_Notes/system-instruction.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
The review types to perform for each block are provided in <review-types> tags.
6363
6464
**core/image**
65-
- accessibility: The content in <block-content> is the alt text for the image. Ensure it isn't empty and is descriptive. Flag missing or generic alt text (e.g. "image", "photo", file name)
65+
- accessibility: The content in <block-content> is the alt text for the image. Empty alt can be correct for decorative images; flag missing or poor alt when the image appears informative, functional (e.g. linked), or redundant with adjacent text. Flag generic alt text (e.g. "image", "photo", file name) when it fails to convey purpose or content
6666
- Skip readability, grammar, and seo for image blocks
6767
6868
**core/heading**

includes/Experiments/Alt_Text_Generation/Alt_Text_Generation.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
/**
2222
* Alt text generation experiment.
2323
*
24-
* Generates descriptive alt text for images using AI vision models.
24+
* Generates accessible alternative text for images using AI vision models.
2525
*
2626
* @since 0.3.0
2727
*/
@@ -48,7 +48,7 @@ public static function get_id(): string {
4848
protected function load_metadata(): array {
4949
return array(
5050
'label' => __( 'Alt Text Generation', 'ai' ),
51-
'description' => __( 'Generates descriptive alt text for images using AI vision models.', 'ai' ),
51+
'description' => __( 'Generates accessible alternative (alt) text for images using AI vision models, following common web accessibility guidance.', 'ai' ),
5252
'category' => Experiment_Category::EDITOR,
5353
);
5454
}

src/experiments/alt-text-generation/components/AltTextControls.tsx

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
/**
66
* WordPress dependencies
77
*/
8-
import { Button, TextareaControl, Spinner } from '@wordpress/components';
8+
import {
9+
Button,
10+
TextareaControl,
11+
Spinner,
12+
Notice,
13+
} from '@wordpress/components';
914
import { InspectorControls } from '@wordpress/block-editor';
1015
import { useState } from '@wordpress/element';
1116
import { __ } from '@wordpress/i18n';
@@ -57,6 +62,7 @@ export function AltTextControls( {
5762

5863
const [ isGenerating, setIsGenerating ] = useState< boolean >( false );
5964
const [ generatedAlt, setGeneratedAlt ] = useState< string | null >( null );
65+
const [ isDecorative, setIsDecorative ] = useState< boolean >( false );
6066

6167
// Don't show controls if there's no image.
6268
if ( ! attachmentId && ! imageUrl ) {
@@ -72,6 +78,7 @@ export function AltTextControls( {
7278
const handleGenerate = async () => {
7379
setIsGenerating( true );
7480
setGeneratedAlt( null );
81+
setIsDecorative( false );
7582

7683
// Clear any previous notices.
7784
( dispatch( noticesStore ) as any ).removeNotice(
@@ -84,9 +91,24 @@ export function AltTextControls( {
8491
attachmentId,
8592
imageUrl,
8693
content,
87-
clientId
94+
clientId,
95+
{
96+
linkDestination: attributes?.linkDestination,
97+
href: attributes?.href,
98+
linkTarget: attributes?.linkTarget,
99+
caption:
100+
typeof attributes?.caption === 'string'
101+
? attributes.caption
102+
: ( attributes?.caption as any )?.text,
103+
}
88104
);
89-
setGeneratedAlt( result );
105+
106+
if ( result.is_decorative ) {
107+
setIsDecorative( true );
108+
setGeneratedAlt( '' );
109+
} else {
110+
setGeneratedAlt( result.alt_text );
111+
}
90112
} catch ( err: any ) {
91113
const errorMessage =
92114
err?.message ||
@@ -107,17 +129,21 @@ export function AltTextControls( {
107129
* Applies the generated alt text to the image block.
108130
*/
109131
const handleApply = () => {
110-
if ( generatedAlt ) {
132+
if ( isDecorative ) {
133+
setAttributes( { alt: '' } );
134+
} else if ( generatedAlt ) {
111135
setAttributes( { alt: generatedAlt } );
112-
setGeneratedAlt( null );
113136
}
137+
setGeneratedAlt( null );
138+
setIsDecorative( false );
114139
};
115140

116141
/**
117142
* Dismisses the generated alt text suggestion.
118143
*/
119144
const handleDismiss = () => {
120145
setGeneratedAlt( null );
146+
setIsDecorative( false );
121147
};
122148

123149
return (
@@ -127,7 +153,7 @@ export function AltTextControls( {
127153
style={ { padding: '0 16px' } }
128154
>
129155
{ /* Generated alt text preview */ }
130-
{ hasGeneratedAlt && (
156+
{ hasGeneratedAlt && ! isDecorative && (
131157
<div style={ { marginBottom: '12px' } }>
132158
<TextareaControl
133159
label={ __( 'Generated Alt Text', 'ai' ) }
@@ -157,8 +183,37 @@ export function AltTextControls( {
157183
</div>
158184
) }
159185

186+
{ /* Decorative image notice */ }
187+
{ isDecorative && (
188+
<div style={ { marginBottom: '12px' } }>
189+
<Notice status="info" isDismissible={ false }>
190+
{ __(
191+
'This image appears to be decorative. Applying will set an empty alt attribute, which tells screen readers to skip it.',
192+
'ai'
193+
) }
194+
</Notice>
195+
<div
196+
style={ {
197+
display: 'flex',
198+
gap: '8px',
199+
marginTop: '8px',
200+
} }
201+
>
202+
<Button variant="primary" onClick={ handleApply }>
203+
{ __( 'Apply', 'ai' ) }
204+
</Button>
205+
<Button
206+
variant="secondary"
207+
onClick={ handleDismiss }
208+
>
209+
{ __( 'Dismiss', 'ai' ) }
210+
</Button>
211+
</div>
212+
</div>
213+
) }
214+
160215
{ /* Generate button */ }
161-
{ ! hasGeneratedAlt && (
216+
{ ! hasGeneratedAlt && ! isDecorative && (
162217
<Button
163218
variant="secondary"
164219
onClick={ handleGenerate }

src/experiments/alt-text-generation/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface AltTextGenerationAbilityInput {
99
attachment_id?: number;
1010
image_url?: string;
1111
context?: string;
12+
image_meta?: string;
1213
[ key: string ]: string | number | undefined;
1314
}
1415

src/experiments/image-generation/functions/upload-image.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,18 @@ export async function uploadImage(
6262
if ( isAltTextEnabled ) {
6363
try {
6464
onProgress?.( __( 'Generating alt text…', 'ai' ) );
65-
params.alt_text = await generateAltText(
65+
const altResult = await generateAltText(
6666
undefined,
6767
`data:image/png;base64,${ image.data }`
6868
);
69+
params.alt_text = altResult.alt_text;
6970
} catch ( error ) {
7071
params.alt_text = prompt;
7172
}
7273
}
7374

7475
// Set our image title to be a trimmed version of the alt text.
75-
params.title = trimText( params.alt_text );
76+
params.title = trimText( params.alt_text ?? '' );
7677

7778
onProgress?.( __( 'Importing image…', 'ai' ) );
7879

0 commit comments

Comments
 (0)