תגיות Alt הן ההיבט הידוע ביותר בבניית דפי אינטרנט נגישים, אך למרבה הצער, לעתים קרובות הם מוזנחים או מיושמים בצורה גרועה. תגיות Alt הן תיאורי טקסט קצרים שנוספו לתמונות. קוראי מסך קוראים למשתמשים את התוכן של דף אינטרנט, ותיאורי התמונות הם מה שהם קוראים כדי להעביר את מה שיש בתמונות בדף למשתמשים לקויי ראייה, מכיוון שהם לא יכולים לראות אותם.
למרבה הצער, זה נפוץ שלתמונות חסרות לחלוטין תגיות alt. ראיתי גם שימוש לרעה בתגי אלט בדרכים שמקשות על משתמש לקוי ראייה, כגון תגיות שכתובות רק "תמונה" או "תמונה", תגיות שהן כיתובים חמודים שהמחבר הוסיף ללא כל התייחסות למה שיש בתמונה ( כלומר, תמונה של קפה ומחשב נייד בדף בלוגים, עם הכיתוב "יומן יקר, אשמח להיבחר ככותב אורח"). ראיתי גם תגיות alt הכוללות 3 שורות של מילות מפתח SEO. האם אתה יכול לדמיין לנסות להקשיב למה שקיים באתר רק כדי לשמוע "תמונה של תמונה" או רשימה ארוכה של מילות מפתח SEO?
זהו תוסף Chrome שנועד להעצים משתמשים לקויי ראייה בכך שהוא מאפשר להם להחליף תגיות alt רעות, ולמנף בינה מלאכותית פתוחה כדי להוסיף תיאורים שנוצרו בינה מלאכותית. זה מאפשר למשתמש לקוי ראייה לגשת למעשה לכל התוכן בדף אינטרנט שמשתמש חסר ראייה יכול לגשת אליו (או לפחות לא להאט על ידי רשימה ארוכה של מילות מפתח SEO).
אם אתה רק רוצה את התוסף, אתה מוזמן להוריד את הריפו הזה ולעקוב אחר ההוראות ב-README.
עם זאת, אם אתה מעוניין במדריך שלב אחר שלב כיצד לבנות תוסף Chrome עם OpenAI, להלן הסבר.
ראשית, בואו נפעיל את לוח הדוד הבסיסי של Chrome. שכפל מאגר זה ופעל לפי ההוראות ב-README:
לאחר שתעלה את זה והתקנת אותו, אמור להיות לך סמל תמונה בסרגל ההרחבה שלך (אני ממליץ להצמיד אותו כדי להפוך את הבדיקה למהירה יותר), וכשתלחץ עליו, אמור לראות חלון קופץ עם "שלום עולם".
בואו נפתח את קוד ה-boilerplate, ונעבור על הקבצים הקיימים. זה יכסה גם כמה יסודות של תוסף Chrome:
Static/manifest.json - לכל סיומת Chrome יש קובץ manifest.json. זה כולל מידע בסיסי והגדרה על התוסף. בקובץ המניפסט שלנו, יש לנו שם, תיאור, קובץ רקע שהוגדר ל-src/background.js, סמל שהוגדר ל-image-icon.png (זהו הסמל שיופיע המייצג את הסיומת בתפריט ההרחבות), והוא מגדיר popup.html כמקור הקובץ עבור החלון הקופץ שלנו.
src/background.js - קובץ background.js שהוגדר במניפסט שלנו. הקוד בקובץ זה יפעל ברקע ויעקוב אחר אירועים שמפעילים פונקציונליות בתוסף.
src/content.js - כל סקריפט שמופעל בהקשר של דף האינטרנט או משנה את דף האינטרנט מוכנס לסקריפט תוכן.
src/popup.js, static/popup.css ו-static/popup.html - קבצים אלה שולטים בחלון הקופץ שאתה רואה כשאתה לוחץ על סמל ההרחבה
בוא נתקין כמה יסודות - פתח את static/manifest.json ונשנה את השם והתיאור ל"מחולל תיאור תמונה של קורא מסך" (או מה שתעדיף).
אפשר אינטראקציה עם דפי אינטרנט באמצעות סקריפט תוכן
התוסף שלנו הולך לדרוס תגיות alt באתר שבו המשתמש נמצא, מה שאומר שאנחנו צריכים גישה לדף html. הדרך לעשות זאת ב-Chrome Extensions היא באמצעות סקריפטים של תוכן. סקריפט התוכן שלנו יהיה בקובץ src/content.js שלנו.
הדרך הפשוטה ביותר להחדיר סקריפט תוכן היא על ידי הוספת שדה "סקריפטים" למניפסט עם הפניה לקובץ js. כאשר אתה מגדיר סקריפט תוכן בצורה זו, הסקריפט המקושר יופעל בכל פעם שהתוסף נטען. עם זאת, במקרה שלנו, איננו רוצים שהתוסף שלנו יפעל אוטומטית כאשר משתמש פותח את התוסף. בחלק מהאתרים יש תגיות alt בסדר גמור על תמונות, אז אנחנו רוצים להפעיל את הקוד רק כשהמשתמש מחליט שזה נחוץ.
אנו הולכים להוסיף כפתור בפופאפ שלנו ויומן מסוף בסקריפט התוכן שלנו, כך שכאשר המשתמש לוחץ על הכפתור, סקריפט התוכן נטען, ונוכל לאשר זאת על ידי ראיית ההצהרה שלנו מודפסת במסוף Chrome.
Popup.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', היא תפעיל את סקריפט התוכן בכרטיסייה הפעילה (דף האינטרנט הנוכחי של המשתמש).
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" למניפסט שלנו. הרשאת "סקריפטים" נדרשת כדי להפעיל כל סקריפט תוכן. אנחנו צריכים גם להוסיף הרשאות עבור הדפים שאליהם אנו מחדירים את הסקריפט. במקרה זה, אנו נזריק את הסקריפט לאתר הנוכחי של המשתמש, הלא הוא הכרטיסייה הפעילה שלו, וזה מה שהרשאת ActiveTab מאפשרת.
ב-manifes.json:
"permissions": [ "activeTab", "scripting" ],
בשלב זה, ייתכן שיהיה עליך להסיר את התוסף מ-Chrome ולטעון אותו מחדש כדי שהוא יפעל כהלכה. ברגע שהוא פועל, אנו אמורים לראות את יומן המסוף שלנו בקונסולת Chrome שלנו.
הנה קישור github לקוד עבודה עבור ה-repo בשלב זה .
איסוף תמונות עמוד והכנסת תגיות alt-test לבדיקה
הצעד הבא שלנו הוא להשתמש בקובץ סקריפט התוכן שלנו כדי לתפוס את כל התמונות בדף, אז יש לנו את המידע הזה מוכן לשלוח קריאות 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 dev tools Elements, לחץ על תמונה, אנו אמורים לראות את "Test Alt Text" מוגדר כתג alt.
מאגר עבודה עבור המקום בו נמצא הקוד בשלב זה נמצא כאן.
התקן את OpenAI והפק תיאורי תמונה
כדי להשתמש ב-OpenAI, תצטרך ליצור מפתח OpenAI וגם להוסיף אשראי לחשבונך. כדי ליצור מפתח OpenAI:
שמור את המפתח הזה. כמו כן, שמרו על פרטיות - הקפידו לא לדחוף אותו לשום מאגר Git ציבורי.
כעת, בחזרה ב-repo שלנו, ראשית אנו רוצים להתקין את OpenAi. בטרמינל שבתוך ספריית הפרויקט, הפעל:
npm install openai
כעת ב-content.js, נייבא את OpenAI על ידי הוספת קוד זה בראש הקובץ, כאשר מפתח ה-OpenAI שלך מודבק בשורה 1:
const openAiSecretKey = 'YOUR_KEY_GOES_HERE' import OpenAI from "openai"; const openai = new OpenAI({ apiKey: openAiSecretKey, dangerouslyAllowBrowser: true });
"DangerouslyAllowBrowser" מאפשר לבצע את השיחה עם המפתח שלך מהדפדפן. בדרך כלל, זהו נוהג לא בטוח. מכיוון שאנו מפעילים את הפרויקט הזה רק באופן מקומי, נשאיר אותו כך, במקום להגדיר אחזור אחורי. אם אתה כן משתמש ב-OpenAI בפרויקטים אחרים, ודא שאתה פועל לפי שיטות עבודה מומלצות בנוגע לשמירה על סוד המפתח.
כעת אנו מוסיפים את הקריאה שלנו לאפשר ל-OpenAI ליצור תיאורי תמונה. אנו קוראים לנקודת הקצה יצירת השלמת צ'אט ( מסמכי OpenAI עבור נקודת הקצה של השלמת צ'אט ).
עלינו לכתוב הנחיה משלנו וגם להעביר את כתובת ה-src של התמונה ( מידע נוסף על הנדסת הנחיות בינה מלאכותית ). אתה יכול להתאים את ההנחיה כרצונך. בחרתי להגביל את התיאורים ל-20 עבודות כי OpenAI החזירה תיאורים ארוכים. בנוסף, שמתי לב שהוא מתאר באופן מלא סמלי לוגו כמו Yelp או לוגו של פייסבוק (כלומר, 'קופסה כחולה גדולה עם אותיות קטנות F בפנים'), אשר לא הועילו. במקרה ומדובר באינפוגרפיקה, אני מבקש להתעלם ממגבלת המילים ולחלוק את טקסט התמונה המלא.
הנה הקריאה המלאה, שמחזירה את התוכן של תגובת הבינה המלאכותית הראשונה וגם מעבירה את השגיאה לפונקציית "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 כדי לכלול את הקריאה האסינכרונית הזו):
const imageDescription = await generateDescription(image.src) if (!imageDescription) { return; } image.alt = imageDescription
הנה קישור ל-content.js המלא ול-repo בשלב זה.
בניית ממשק המשתמש
לאחר מכן, אנו רוצים לבנות את ממשק המשתמש שלנו כך שהמשתמש יידע מה קורה לאחר שהוא לוחץ על הכפתור כדי ליצור את התגים. זה לוקח כמה שניות לטעינת התגים, אז אנחנו רוצים הודעת 'טעינה' כדי שהמשתמש יידע שזה עובד. בנוסף, אנחנו רוצים ליידע אותם שזה הצליח, או אם יש שגיאה. על מנת לשמור על פשטות, יהיה לנו הודעת משתמש כללית div ב-html, ולאחר מכן נשתמש ב-popup.js כדי להכניס באופן דינמי את ההודעה המתאימה למשתמש בהתבסס על מה שקורה בתוסף.
באופן שבו הרחבות Chrome מוגדרות, סקריפט התוכן שלנו (content.js) מופרד מה-popup.js שלנו, והם לא מסוגלים לשתף משתנים כמו קבצי JavaScript טיפוסיים. הדרך שבה סקריפט התוכן יכול ליידע את הקופץ שהתגים נטענים, או נטענים בהצלחה, היא באמצעות העברת הודעות. כבר השתמשנו בהעברת הודעות כאשר הודענו לעובד הרקע להחדיר את סקריפט התוכן כאשר משתמש לחץ על הכפתור המקורי.
ראשית, ב-html שלנו, נוסיף div עם המזהה 'user-message' מתחת לכפתור שלנו. הוספתי עוד קצת תיאור עבור ההודעה הראשונית, גם כן.
<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', שיכול להיות 'ראשוני' (התוסף נטען אבל עדיין לא קרה כלום), 'טעינה', 'הצלחה' או 'שגיאה' (אנחנו יוסיף גם כמה מצבי שגיאה אחרים בהתבסס על הודעות שגיאה של 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 שלנו שמאזין לאותה הודעה ושולח בחזרה את המצב הנוכחי.
popup.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) } }); });
content.js
chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) { if (request.action === "getExtensionState") sendResponse({extensionState}); });
אם סקריפט התוכן מעולם לא הופעל (המכונה גם המשתמש מעולם לא לחץ על הכפתור כדי ליצור תגיות alt), לא יהיה משתנה מצב הרחבה או מאזין אירועים. במקרה זה, כרום יחזיר שגיאת זמן ריצה בתגובה. אז אנחנו כוללים בדיקה לאיתור שגיאה, ואם נקבל אחת, השאר את ממשק המשתמש המוגדר כברירת מחדל כפי שהוא.
נגישות הרחבה - aria-live, ניגודיות צבע וכפתור סגירה
התוסף הזה מיועד לאנשים שמשתמשים בקוראי מסך, אז עכשיו עלינו לוודא שהוא באמת שמיש עם קורא מסך! עכשיו זה זמן טוב להפעיל את קורא המסך ולראות אם הכל עובד כמו שצריך.
יש כמה דברים שאנחנו רוצים לנקות בשביל הנגישות. קודם כל, אנחנו רוצים לוודא שכל הטקסט הוא ברמת ניגודיות גבוהה מספיק. עבור הכפתור, החלטתי להגדיר את הרקע ל-#0250C5 ואת הגופן למודגש לבן. יש לזה יחס ניגודיות של 7.1 והוא תואם ל-WCAG ברמת AA וגם ברמת AAA. אתה יכול לבדוק יחסי ניגודיות עבור כל הצבעים שתרצה להשתמש כאן ב-WebAim Contrast Checker.
שנית, בעת השימוש בקורא המסך שלי, אני שם לב שקורא המסך אינו קורא אוטומטית את העדכונים כאשר הודעת המשתמש משתנה להודעת טעינה, הצלחה או שגיאה. על מנת לתקן זאת, נשתמש בתכונת html הנקראת aria-live. Aria-live מאפשרת למפתחים ליידע את קוראי המסך לעדכן משתמשים בשינויים. אתה יכול להגדיר את aria-live לאסרטיבי או מנומס - אם הוא מוגדר לאסרטיבי, העדכונים ייקראו מיד, ללא קשר אם יש פריטים אחרים שמחכים לקריאה בתור קורא המסך. אם הוא מוגדר למנומס, העדכון ייקרא בסוף כל מה שקורא המסך נמצא בתהליך הקריאה. במקרה שלנו, אנו רוצים לעדכן את המשתמש בהקדם האפשרי. אז ב-popup-container, אלמנט האב של אלמנט ה-user-message שלנו, נוסיף את התכונה הזו.
<div class="popup-container" aria-live="assertive">
לבסוף, באמצעות קורא המסך, אני שם לב שאין דרך קלה לסגור את החלון הקופץ. בעת שימוש בעכבר, אתה פשוט לוחץ בכל מקום מחוץ לחלון הקופץ והוא נסגר, אבל אני לא ממש מצליח להבין איך לסגור אותו באמצעות המקלדת. אז נוסיף כפתור 'סגור' בתחתית החלון הקופץ, כך שהמשתמשים יוכלו לסגור אותו בקלות ולחזור לדף האינטרנט.
ב-popup.html, אנו מוסיפים:
<div> <button id="close-button">Close</button> </div>
ב-popup.js, אנו מוסיפים את פונקציית הסגירה ל-onclick:
const closeButton = document.body.querySelector('#close-button'); closeButton.addEventListener('click', async () => { window.close() });
וזהו! אם יש לך שאלות או הצעות, אנא צור קשר.