Skip to content

Commit

Permalink
Merge pull request #6 from mslinnea/add-alt-text
Browse files Browse the repository at this point in the history
Add alt text generation
  • Loading branch information
mslinnea authored Nov 17, 2024
2 parents 1bd1565 + bc5fa05 commit 4b05dfa
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ be compatible with all AI services supported by the [AI Services Plugin](https:/

- Generate Meta Description
- Generate Meta Keywords
- Generate Image Alt Text via Image Block toolbar button

# Demo with OpenAI gpt-3.5-turbo model

Expand Down
10 changes: 7 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
"dependencies": {
"@alleyinteractive/block-editor-tools": "^0.5.1",
"@wordpress/api-fetch": "^7.0.1",
"@wordpress/block-editor": "^14.7.0",
"@wordpress/components": "^28.0.3",
"@wordpress/data": "^10.0.2",
"@wordpress/editor": "^14.0.8",
"@wordpress/hooks": "^4.11.0",
"@wordpress/i18n": "^5.11.0",
"@wordpress/plugins": "^7.0.3",
"mustache": "^4.2.0",
"react": "^18.3.1",
Expand Down
69 changes: 69 additions & 0 deletions src/components/altText/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { __ } from '@wordpress/i18n';
import { getMimeType, getBase64Image } from '../../helpers/image-helpers';

const { enums, helpers } = window.aiServices.ai;
const AI_CAPABILITIES = [enums.AiCapability.MULTIMODAL_INPUT, enums.AiCapability.TEXT_GENERATION];

export default async function generateAltText(props, setInProgress, service) {
const { attributes, setAttributes } = props;

// Check if AI service is available
if (!service) {
// eslint-disable-next-line no-console
console.error('AI service is not available.');
return;
}

// Check if image URL is available
if (!attributes.url) {
// eslint-disable-next-line no-console
console.error('Image URL is missing.');
return;
}

setInProgress(true);

// Fetch the image data and prepare the request
try {
const mimeType = getMimeType(attributes.url);
const base64Image = await getBase64Image(attributes.url);

// Call the AI service to generate alt text
const candidates = await service.generateText(
{
role: enums.ContentRole.USER,
parts: [
{
text: __(
'Create a brief description of what the following image shows, suitable as alternative text for screen readers.',
'ai-seo-tools',
),
},
{
inlineData: {
mimeType,
data: base64Image,
},
},
],
},
{
feature: 'ai-seo-tools-alt-text',
capabilities: AI_CAPABILITIES,
},
);

// Extract the generated alt text
const alt = helpers
.getTextFromContents(helpers.getCandidateContents(candidates))
.replaceAll('\n\n\n\n', '\n\n');

// Set the alt text attribute
setAttributes({ alt });
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error generating alt text:', error);
} finally {
setInProgress(false);
}
}
23 changes: 23 additions & 0 deletions src/components/blockControls/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { addFilter } from '@wordpress/hooks';
import ImageControls from '../imageControls';

// eslint-disable-next-line func-names
const addToolbar = (BlockEdit) => function (props) {
// eslint-disable-next-line react/prop-types
const { name } = props;
if (name === 'core/image') {
return (
<>
<BlockEdit {...props} />
<ImageControls {...props} />
</>
);
}
return <BlockEdit {...props} />;
};

addFilter(
'editor.BlockEdit',
'ai-seo-tools/add-block-toolbar',
addToolbar,
);
35 changes: 35 additions & 0 deletions src/components/imageControls/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { __ } from '@wordpress/i18n';
import { ToolbarButton } from '@wordpress/components';
import { useState } from 'react';
import { BlockControls } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import generateAltText from '../altText/index';

const { enums, store: aiStore } = window.aiServices.ai;
const AI_CAPABILITIES = [enums.AiCapability.MULTIMODAL_INPUT, enums.AiCapability.TEXT_GENERATION];

export default function ImageControls(props) {
const [inProgress, setInProgress] = useState(false);
const service = useSelect((select) => select(aiStore)
.getAvailableService(
{ capabilities: AI_CAPABILITIES },
));

const handleClick = async () => {
setInProgress(true);
await generateAltText(props, setInProgress, service);
setInProgress(false);
};

return (
<BlockControls group="inline">
<ToolbarButton
label={__('Write Alt Text', 'ai-seo-tools')}
icon="universal-access-alt"
showTooltip
disabled={inProgress}
onClick={handleClick}
/>
</BlockControls>
);
}
File renamed without changes.
30 changes: 30 additions & 0 deletions src/helpers/image-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export function getMimeType(url) {
const extension = url.split('.').pop().toLowerCase();
switch (extension) {
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'avif':
return 'image/avif';
case 'webp':
return 'image/webp';
case 'jpg':
case 'jpeg':
default:
return 'image/jpeg';
}
}

export async function getBase64Image(url) {
const data = await fetch(url);
const blob = await data.blob();
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const base64data = reader.result;
resolve(base64data);
};
});
}
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import './components/settingsScreen';
import './components/settingsPanel';
import './components/blockControls';

0 comments on commit 4b05dfa

Please sign in to comment.