Alt タグは、アクセスしやすい Web ページを作成する上で最もよく知られている要素ですが、残念ながら無視されたり、適切に実装されていないことがよくあります。Alt タグは、画像に追加される短いテキストの説明です。スクリーン リーダーは、Web ページの内容をユーザーに読み上げます。画像の説明は、視覚障害のあるユーザーには画像が見えないため、ページ上の画像の内容を伝えるために読み上げられるものです。
残念ながら、画像に alt タグがまったく付いていないことはよくあります。また、視覚障害のあるユーザーにとって扱いにくくなるような方法で alt タグが誤用されているのも見てきました。たとえば、「画像」や「イメージ」とだけ書かれたタグや、画像の内容とは関係なく著者が追加したかわいらしいキャプションのタグ (ブログ ページにコーヒーとラップトップの画像があり、「親愛なる日記、ゲスト ライターに選ばれたらうれしいです」というキャプションが付いているものなど) などです。また、3 行の SEO キーワードを含む alt タグも見てきました。Web サイトに何が書かれているのかを聞き取ろうとしたら、「画像」や SEO キーワードの長いリストしか聞こえないなんて想像できますか?
これは、視覚障害のあるユーザーが不適切な alt タグを上書きし、Open AI を活用して AI 生成の説明を挿入できるようにすることで、視覚障害のあるユーザーを支援するために設計された Chrome 拡張機能です。これにより、視覚障害のあるユーザーは、視覚障害のないユーザーがアクセスできる Web ページ上のすべてのコンテンツに実際にアクセスできるようになります (少なくとも、SEO キーワードの長いリストによって速度が低下することはありません)。
拡張機能だけが必要な場合は、このリポジトリをダウンロードして、README の指示に従ってください。
ただし、OpenAI を使用して Chrome 拡張機能を構築する方法のステップバイステップ ガイドに興味がある場合は、次の手順に従ってください。
まず、基本的な Chrome ボイラープレートを起動して実行してみましょう。このリポジトリをクローンし、README の指示に従ってください。
起動してインストールすると、拡張機能バーに画像アイコンが表示されます (テストを迅速に行うためにピン留めすることをお勧めします)。そのアイコンをクリックすると、「hello world」というポップアップが表示されます。
定型コードを開いて、既存のファイルを確認してみましょう。ここでは、Chrome 拡張機能の基本についても説明します。
Static/manifest.json - すべての Chrome 拡張機能には manifest.json ファイルがあります。拡張機能に関する基本情報と設定が含まれています。マニフェスト ファイルには、名前、説明、src/background.js に設定された背景ファイル、image-icon.png に設定されたアイコン (拡張機能メニューに表示される拡張機能を表すアイコン) があり、ポップアップのファイル ソースとして popup.html が設定されています。
src/background.js - マニフェストに設定された background.js ファイル。このファイル内のコードはバックグラウンドで実行され、拡張機能の機能をトリガーするイベントを監視します。
src/content.js - Web ページのコンテキストで実行されるスクリプト、または Web ページを変更するスクリプトは、コンテンツ スクリプトに配置されます。
src/popup.js、static/popup.css、static/popup.html - これらのファイルは、拡張機能アイコンをクリックしたときに表示されるポップアップを制御します。
基本的な設定を始めましょう。static/manifest.json を開き、名前と説明を「Screen Reader Image Description Generator」(または任意の名前) に変更します。
コンテンツスクリプトを使用してWebページとの対話を有効にする
この拡張機能は、ユーザーがアクセスしている Web サイトの alt タグを上書きします。つまり、ページの HTML にアクセスする必要があります。Chrome 拡張機能でこれを行うには、コンテンツ スクリプトを使用します。コンテンツ スクリプトは、src/content.js ファイルにあります。
コンテンツ スクリプトを挿入する最も簡単な方法は、js ファイルへの参照を含む「scripts」フィールドをマニフェストに追加することです。この方法でコンテンツ スクリプトを設定すると、拡張機能が読み込まれるたびにリンクされたスクリプトが実行されます。ただし、このケースでは、ユーザーが拡張機能を開いたときに拡張機能が自動的に実行されることは望ましくありません。一部の Web サイトでは、画像に適切な alt タグが設定されているため、ユーザーが必要だと判断したときにのみコードを実行する必要があります。
ポップアップにボタンを追加し、コンテンツ スクリプトにコンソール ログを追加します。これにより、ユーザーがボタンをクリックすると、コンテンツ スクリプトが読み込まれ、Chrome コンソールにステートメントが印刷されて、そのことが確認できます。
ポップアップ.html
<button id="generate-alt-tags-button">Generate image descriptions</button>
src/content.js
console.log('hello console')
ポップアップ上のボタンクリックをコンテンツ スクリプトに接続するには、popup.js と background.js の両方が必要です。
popup.jsでは、DOMからボタンを取得し、イベントリスナーを追加します。ユーザーがそのボタンをクリックすると、コンテンツスクリプトを挿入する必要があることを示すメッセージを送信します。メッセージの名前は「injectContentScript」にします。
const generateAltTagButton = document.body.querySelector('#generate-alt-tags-button'); generateAltTagButton.addEventListener('click', async () => { chrome.runtime.sendMessage({action: 'injectContentScript'}) });
background.js には、イベントを監視してそれに反応するコードがあります。ここでは、イベント リスナーを設定しており、受信したメッセージが 'injectContentScript' の場合、アクティブなタブ (ユーザーの現在の Web ページ) でコンテンツ スクリプトを実行します。
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'injectContentScript') { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { chrome.scripting.executeScript({ target: { tabId: tabs[0].id }, files: ['content.js'] }); }); } });
これを設定する最後の手順は、マニフェストに「activeTab」と「scripting」の権限を追加することです。「scripting」権限は、コンテンツ スクリプトを実行するために必要です。また、スクリプトを挿入するページの権限も追加する必要があります。この場合、スクリプトをユーザーの現在の Web サイト (つまりアクティブ タブ) に挿入します。これが activeTab 権限で許可されるものです。
manifest.json の場合:
"permissions": [ "activeTab", "scripting" ],
この時点で、拡張機能を正しく実行するために、Chrome から拡張機能を削除して再読み込みする必要がある場合があります。拡張機能が実行されると、Chrome コンソールにコンソール ログが表示されます。
現段階でリポジトリの動作するコードの github リンクはこちらです。
ページ画像を収集し、テスト用の alt タグを挿入する
次のステップは、コンテンツ スクリプト ファイルを使用してページ上のすべての画像を取得し、その情報を API 呼び出しで送信して画像の説明を取得することです。また、説明があると便利な画像のみを呼び出すようにする必要があります。画像の中には、純粋に装飾的なものであり、説明でスクリーン リーダーの速度を低下させる必要のないものもあります。たとえば、検索バーに「検索」というラベルと虫眼鏡アイコンの両方があるとします。画像の alt タグが空の文字列に設定されている場合、または aria-hidden が true に設定されている場合、その画像はスクリーン リーダーに含める必要がないため、説明の生成をスキップできます。
まず、content.js でページ上のすべての画像を収集します。正しく動作していることをすぐに確認できるように、console.log を追加します。
const images = document.querySelectorAll("img"); console.log(images)
次に、画像をループして、alt タグを生成する必要がある画像をチェックします。これには、alt タグがないすべての画像、空の文字列ではない alt タグを持つ画像、aria-hidden 属性によってスクリーン リーダーから明示的に非表示にされていない画像が含まれます。
for (let image of images) { const imageHasAltTag = image.hasAttribute('alt'); const imageAltTagIsEmptyString = image.hasAttribute('alt') && image.alt === ""; const isAriaHidden = image.ariaHidden ?? false; if (!imageHasAltTag || !imageAltTagIsEmptyString || !isAriaHidden) { // this is an image we want to generate an alt tag for! } }
次に、alt タグを設定するためのテスト文字列を追加して、OpenAI 呼び出しに進む前に、それらを設定するための機能的な方法があることを確認できます。content.js は次のようになります。
function scanPhotos() { const images = document.querySelectorAll("img"); console.log(images) for (let image of images) { const imageHasAltTag = image.hasAttribute('alt'); const imageAltTagIsEmptyString = image.hasAttribute('alt') && image.alt === ""; const isAriaHidden = image.ariaHidden ?? false; if (!imageHasAltTag || !imageAltTagIsEmptyString || !isAriaHidden) { image.alt = 'Test Alt Text' } } } scanPhotos()
この時点で、Chrome 開発ツールの Elements を開いて画像をクリックすると、alt タグとして「Test Alt Text」が設定されていることがわかります。
この段階でコードが保存されている作業リポジトリはここにあります。
OpenAIをインストールして画像の説明を生成する
OpenAI を使用するには、OpenAI キーを生成し、アカウントにクレジットを追加する必要があります。OpenAI キーを生成するには:
このキーを保存します。また、非公開にしておいてください。パブリック Git リポジトリにプッシュしないでください。
さて、リポジトリに戻って、まず OpenAi をインストールします。プロジェクト ディレクトリ内のターミナルで、次を実行します。
npm install openai
次に、content.js で、ファイルの先頭に次のコードを追加し、1 行目に OpenAI キーを貼り付けて、OpenAI をインポートします。
const openAiSecretKey = 'YOUR_KEY_GOES_HERE' import OpenAI from "openai"; const openai = new OpenAI({ apiKey: openAiSecretKey, dangerouslyAllowBrowser: true });
「DangerouslyAllowBrowser」を使用すると、ブラウザからキーを使用して呼び出しを行うことができます。一般的に、これは安全ではない方法です。このプロジェクトはローカルでのみ実行しているため、バックエンドの取得を設定するのではなく、このままにしておきます。他のプロジェクトで OpenAI を使用する場合は、キーを秘密に保つことに関するベストプラクティスに従ってください。
ここで、OpenAI に画像の説明を生成させるための呼び出しを追加します。チャット補完エンドポイントの作成を呼び出します (チャット補完エンドポイントの OpenAI ドキュメント)。
独自のプロンプトを作成し、画像の src URL も渡す必要があります ( AI プロンプト エンジニアリングの詳細)。プロンプトは自由に変更できます。OpenAI が長い説明を返していたため、説明を 20 作品に制限することにしました。さらに、Yelp や Facebook のロゴなどのロゴを完全に説明していることに気付きました (つまり、「中に白い小文字の f がある大きな青いボックス」)。これは役に立ちませんでした。インフォグラフィックの場合は、文字数制限を無視して、画像テキスト全体を共有するようお願いします。
以下は、最初の AI 応答の内容を返し、エラーを「handleError」関数に渡す完全な呼び出しです。呼び出しが成功したかどうかのフィードバックをより迅速に取得できるように、各応答の console.log を含めました。
async function generateDescription(imageSrcUrl) { const response = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "user", content: [ { type: "text", text: "Describe this image in 20 words or less. If the image looks like the logo of a large company, just say the company name and then the word logo. If the image has text, share the text. If the image has text and it is more than 20 words, ignore the earlier instruction to limit the words and share the full text."}, { type: "image_url", image_url: { "url": imageSrcUrl, }, }, ], }, ], }).catch(handleError); console.log(response) if (response) { return response.choices[0].message.content;} } function handleError(err) { console.log(err); }
前に書いた if ステートメントにこの関数の呼び出しを追加します (この非同期呼び出しを含めるには、 scanImages 関数の先頭に async キーワードも追加する必要があります)。
const imageDescription = await generateDescription(image.src) if (!imageDescription) { return; } image.alt = imageDescription
現時点での完全な content.js とリポジトリへのリンクは次のとおりです。
UIの構築
次に、ユーザーがタグを生成するためのボタンをクリックした後に何が起きているのかがわかるように UI を構築します。タグの読み込みには数秒かかるため、動作中であることをユーザーに知らせるために「読み込み中」メッセージを表示します。さらに、正常に完了したか、エラーが発生したかをユーザーに知らせます。シンプルにするために、HTML に一般的なユーザー メッセージ div を配置し、拡張機能で何が起こっているかに基づいて、popup.js を使用して適切なメッセージを動的にユーザーに挿入します。
Chrome 拡張機能の設定方法では、コンテンツ スクリプト (content.js) は popup.js から分離されており、一般的な JavaScript ファイルのように変数を共有することはできません。コンテンツ スクリプトがポップアップにタグが読み込まれていること、または正常に読み込まれたことを知らせる方法は、メッセージ パッシングです。ユーザーが元のボタンをクリックしたときにバックグラウンド ワーカーにコンテンツ スクリプトを挿入するように知らせるときに、メッセージ パッシングをすでに使用しました。
まず、HTML で、ボタンの下に 'user-message' という ID を持つ div を追加します。また、最初のメッセージの説明を少し追加しました。
<div id="user-message"> <img src="image-icon.png" width="40" class="icon" alt=""/> This extension uses OpenAI to generate alternative image descriptions for screen readers. </div>
次に、popup.js に、拡張機能の状態の更新を含む可能性のある送信されたメッセージをリッスンするリスナーを追加します。また、コンテンツ スクリプトから返される状態の結果に基づいて挿入する HTML も記述します。
const userMessage = document.body.querySelector('#user-message'); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { renderUI(message.action) } ); function renderUI(extensionState) { generateAltTagButton.disabled=true; if (extensionState === 'loading') { userMessage.innerHTML = '<img src="loading-icon.png" width="50" class="icon" alt=""/> New image descriptions are loading... <br> <br>Please wait. We will update you when the descriptions have loaded.' } else if (extensionState === 'success') { userMessage.innerHTML = '<img src="success-icon.png" width="50" class="icon" alt=""/> New image descriptions have been loaded! <br> <br> If you would like to return to the original image descriptions set by the web page author, please refresh the page.' } else if (extensionState === 'errorGeneric') { userMessage.innerHTML = '<img src="error-icon.png" width="50" class="icon"alt=""/> There was an error generating new image descriptions. <br> <br> Please refresh the page and try again.' } else if (extensionState === 'errorAuthentication') { userMessage.innerHTML = '<img src="error-icon.png" width="50" class="icon"alt=""/> There was an error generating new image descriptions. <br> <br> Your OpenAI key is not valid. Please double check your key and try again.' } else if (extensionState === 'errorMaxQuota') { userMessage.innerHTML = '<img src="error-icon.png" width="50" class="icon"alt=""/> There was an error generating new image descriptions. <br> <br> You\'ve either used up your current OpenAI plan and need to add more credit, or you\'ve made too many requests too quickly. Please check your plan, add funds if needed, or slow down the requests.' } }
コンテンツ スクリプト内で、「extensionState」という新しい変数を定義します。この変数は、「initial」(拡張機能は読み込まれていますが、まだ何も起こっていません)、「loading」、「success」、または「error」(OpenAI エラー メッセージに基づいて、他のエラー状態も追加します) のいずれかになります。また、拡張機能の状態変数を更新し、状態が変化するたびに popup.js にメッセージを送信します。
let extensionState = 'initial';
エラー ハンドラは次のようになります。
function handleError(err) { if (JSON.stringify(err).includes('401')) { extensionState = 'errorAuthentication' chrome.runtime.sendMessage({action: extensionState}) } else if (JSON.stringify(err).includes('429')) { extensionState = 'errorMaxQuota' chrome.runtime.sendMessage({action: extensionState}) } else { extensionState = 'errorGeneric' chrome.runtime.sendMessage({action: extensionState}) } console.log(err); }
また、scanPhotos 関数内では、関数の開始時に状態を「ロード中」に設定し、エラーなしで完全に実行された場合は「成功」に設定します。
async function scanPhotos() { extensionState = 'loading' chrome.runtime.sendMessage({action: extensionState}) const images = document.querySelectorAll("img"); for (let image of images) { const imageHasAltTag = image.hasAttribute('alt'); const imageAltTagIsEmptyString = image.hasAttribute('alt') && image.alt === ""; const isAriaHidden = image.ariaHidden ?? false; if (!imageHasAltTag || !imageAltTagIsEmptyString || !isAriaHidden) { const imageDescription = await generateDescription(image.src) if (!imageDescription) { return; } image.alt = imageDescription } } extensionState = 'success' chrome.runtime.sendMessage({action: extensionState}) }
わかりにくいポップアップの動作を修正 - ポップアップが閉じて再度開いたときに拡張機能の状態が維持される
この時点で、alt タグを生成し、成功メッセージを取得し、ポップアップを閉じて再度開くと、ユーザーに新しい alt タグを生成するように促す最初のメッセージが表示されることに気付くかもしれません。生成された alt タグがコード内にあるにもかかわらずです。
Chrome では、拡張機能のポップアップを開くたびに、まったく新しいポップアップが表示されます。拡張機能によって以前に実行された内容や、コンテンツ スクリプトで実行されている内容は記憶されません。ただし、ポップアップが開かれたときに拡張機能の状態を呼び出して確認することで、新しく開かれたポップアップが拡張機能の正確な状態をレンダリングしていることを確認できます。そのためには、ポップアップに別のメッセージを渡し、今度は拡張機能の状態を要求します。また、そのメッセージをリッスンして現在の状態を返すメッセージ リスナーを content.js に追加します。
ポップアップ
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { chrome.tabs.sendMessage(tabs[0].id, {action: "getExtensionState"}, function(response) { // if the content script hasn't been injected, then the code in that script hasn't been run, and we'll get an error or no response if (chrome.runtime.lastError || !response) { return; } else if (response) { // if the code in content script HAS been injected, we'll get a response which tells us what state the code is at (loading, success, error, etc) renderUI(response.extensionState) } }); });
コンテンツ
chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) { if (request.action === "getExtensionState") sendResponse({extensionState}); });
コンテンツ スクリプトが一度も実行されていない場合 (つまり、ユーザーが alt タグを生成するボタンをクリックしたことがない場合)、拡張機能の状態変数やイベント リスナーは存在しません。この場合、Chrome は応答としてランタイム エラーを返します。そのため、エラーのチェックを組み込み、エラーを受け取った場合はデフォルトの UI をそのままにしておきます。
拡張機能のアクセシビリティ - aria-live、色のコントラスト、閉じるボタン
この拡張機能はスクリーン リーダーを使用するユーザー向けに設計されているため、スクリーン リーダーで実際に使用できることを確認する必要があります。今がスクリーン リーダーをオンにして、すべてが正常に動作するかどうかを確認する良いタイミングです。
アクセシビリティのために、いくつかクリーンアップしたい点があります。まず、すべてのテキストのコントラスト レベルが十分に高いことを確認します。ボタンについては、背景を #0250C5 に、フォントを白太字に設定することにしました。これはコントラスト比が 7.1 で、AA レベルと AAA レベルの両方で WCAG に準拠しています。WebAimコントラスト チェッカーで、使用したい色のコントラスト比を確認できます。
次に、スクリーン リーダーを使用しているときに、ユーザー メッセージが読み込み中、成功、またはエラー メッセージに変わったときに、スクリーン リーダーが自動的に更新内容を読み上げないことに気付きました。これを修正するには、aria-live という HTML 属性を使用します。aria-live を使用すると、開発者はスクリーン リーダーに変更内容をユーザーに更新するように通知できます。aria-live は assertive または political に設定できます。assertive に設定すると、スクリーン リーダー キューに読み取り待ちの項目が他にあるかどうかに関係なく、更新内容がすぐに読み上げられます。polite に設定すると、スクリーン リーダーが読み上げているすべての項目の最後に更新内容が読み上げられます。このケースでは、できるだけ早くユーザーに更新内容を伝えたいと考えています。そのため、user-message 要素の親要素である popup-container にその属性を追加します。
<div class="popup-container" aria-live="assertive">
最後に、スクリーン リーダーを使用すると、ポップアップを閉じる簡単な方法がないことに気が付きました。マウスを使用する場合は、ポップアップの外側の任意の場所をクリックするだけでポップアップが閉じますが、キーボードを使用してポップアップを閉じる方法がわかりません。そのため、ポップアップの下部に「閉じる」ボタンを追加して、ユーザーが簡単にポップアップを閉じて Web ページに戻れるようにします。
popup.html に以下を追加します。
<div> <button id="close-button">Close</button> </div>
popup.js では、onclick に close 関数を追加します。
const closeButton = document.body.querySelector('#close-button'); closeButton.addEventListener('click', async () => { window.close() });
以上です。ご質問やご提案がございましたら、お気軽にお問い合わせください。