Az Alt címkék az akadálymentes weboldalak készítésének legismertebb elemei, de sajnos gyakran figyelmen kívül hagyják vagy rosszul alkalmazzák őket. Az Alt címkék a képekhez hozzáadott rövid szöveges leírások. A képernyőolvasók egy weboldal tartalmát olvassák fel a felhasználóknak, a képleírásokat pedig azért olvassák el, hogy közöljék az oldalon lévő képek tartalmát a látássérült felhasználókkal, mivel ők nem látják azokat.
Sajnos gyakran előfordul, hogy a képekről teljesen hiányzik az alt tag. Láttam olyan alt címkéket is visszaélni, amelyek megnehezítik a látássérült felhasználók dolgát, például olyan címkéket, amelyek csak azt mondják, hogy „kép” vagy „kép”, olyan címkéket, amelyek aranyos feliratok, amelyeket a szerző a képen látható tartalomra való hivatkozás nélkül adott hozzá ( azaz egy kávé és egy laptop képe egy blogoldalon, „kedves naplóm, örülnék, ha vendégírónak választanék” felirattal. Láttam olyan alt címkéket is, amelyek 3 sor SEO kulcsszót tartalmaznak. El tudja képzelni, hogy megpróbálja meghallgatni, mi található egy webhelyen, csak azért, hogy hallja a „kép képe” vagy a SEO kulcsszavak hosszú listáját?
Ez egy Chrome-bővítmény, amelyet arra terveztek, hogy a látássérült felhasználókat feljogosítsa azáltal, hogy felülírják a rossz alt címkéket, és kihasználják az Open AI-t a mesterséges intelligencia által generált leírások beszúrásához. Ez lehetővé teszi a látássérült felhasználó számára, hogy ténylegesen hozzáférjen a weboldal minden tartalmához, amelyhez egy látássérült felhasználó hozzáfér (vagy legalábbis nem lassítja le a SEO kulcsszavak hosszú listája).
Ha csak a bővítményt szeretné, töltse le ezt a repot , és kövesse a README utasításait.
Ha azonban lépésről lépésre szeretné megismerni a Chrome-bővítmény OpenAI-val történő létrehozását, az alábbiakban bemutatjuk.
Először is állítsunk be és működtessünk egy alapvető Chrome-mintát. Klónozza ezt a tárolót, és kövesse a README utasításait:
Miután elkészítette és telepítette, egy képikonnak kell lennie a bővítménysávban (javaslom, hogy rögzítse a tesztelés gyorsabbá tétele érdekében), és amikor rákattint, megjelenik egy felugró ablak a „hello world” felirattal.
Nyissuk meg az alapkódot, és járjuk végig a meglévő fájlokat. Ez a Chrome-bővítmény néhány alapját is lefedi:
Static/manifest.json – Minden Chrome-bővítmény rendelkezik manifest.json fájllal. Tartalmazza az alapvető információkat és a kiterjesztés beállítását. A Manifest fájlunkban van egy név, leírás, egy háttérfájl src/background.js, egy ikon pedig image-icon.png (ez az ikon, amely a kiterjesztést képviseli a kiterjesztések menüben), és beállítja a popup.html fájl forrását az előugró ablakunkhoz.
src/background.js – A jegyzékünkben beállított background.js fájl. Az ebben a fájlban található kód a háttérben fut, és figyeli azokat az eseményeket, amelyek működést váltanak ki a bővítményben.
src/content.js – Minden szkript, amely a weboldal kontextusában fut, vagy módosítja a weboldalt, tartalomszkriptbe kerül.
src/popup.js, static/popup.css és static/popup.html – Ezek a fájlok szabályozzák a kiterjesztés ikonjára kattintva megjelenő felugró ablakot
Nézzünk néhány alapvető beállítást – nyissa meg a static/manifest.json fájlt, és módosítsa a nevet és a leírást „Screen Reader Image Description Generator”-ra (vagy arra, amit szeretne).
A weblapokkal való interakció engedélyezése tartalomszkript segítségével
Bővítményünk felül fogja írni az alt címkéket azon a webhelyen, amelyen a felhasználó tartózkodik, ami azt jelenti, hogy hozzá kell férnünk a html oldalhoz. Ezt a Chrome-bővítményekben tartalomszkriptek segítségével teheti meg. A tartalomszkriptünk az src/content.js fájlunkban lesz.
A tartalomszkript beszúrásának legegyszerűbb módja egy „scripts” mező hozzáadása a jegyzékhez, amely egy js-fájlra hivatkozik. Ha így állít be egy tartalomszkriptet, a csatolt szkript a bővítmény betöltésekor futni fog. A mi esetünkben azonban nem akarjuk, hogy a bővítményünk automatikusan fusson, amikor a felhasználó megnyitja a bővítményt. Egyes webhelyeken tökéletesen finom alt címkék vannak beállítva a képeken, ezért csak akkor akarjuk futtatni a kódot, ha a felhasználó úgy dönt, hogy szükséges.
Hozzáadunk egy gombot a felugró ablakunkhoz és egy konzolnaplót a tartalomszkriptünkhöz, így amikor a felhasználó a gombra kattint, a tartalomszkript betöltődik, és ezt a Chrome konzolon kinyomtatott nyilatkozatunk megtekintésével megerősíthetjük.
Popup.html
<button id="generate-alt-tags-button">Generate image descriptions</button>
src/content.js
console.log('hello console')
A felugró ablakra kattintva a gombbal kapcsolódhat a tartalomszkripthez a popup.js és a background.js kóddal.
A popup.js fájlban megragadjuk a DOM gombot, és hozzáadunk egy eseményfigyelőt. Amikor a felhasználó rákattint erre a gombra, üzenetet küldünk, amely jelzi, hogy a tartalomszkriptet be kell adni. Az üzenet elnevezése „injectContentScript”
const generateAltTagButton = document.body.querySelector('#generate-alt-tags-button'); generateAltTagButton.addEventListener('click', async () => { chrome.runtime.sendMessage({action: 'injectContentScript'}) });
A background.js-ben van egy kód, amely figyeli az eseményeket és reagál rájuk. Itt egy eseményfigyelőt állítunk be, és ha a kapott üzenet „injectContentScript”, akkor az aktív lapon (a felhasználó aktuális weboldalán) végrehajtja a tartalomszkriptet.
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'] }); }); } });
A beállítás utolsó lépése az „activeTab” és a „scripting” engedélyek hozzáadása a jegyzékünkhöz. Bármely tartalomszkript futtatásához „scripting” engedély szükséges. Azokhoz az oldalakhoz is engedélyeket kell adnunk, amelyekbe beillesztjük a szkriptet. Ebben az esetben a szkriptet beillesztjük a felhasználó aktuális webhelyére, más néven az aktív lapjára, és ezt az activeTab engedély lehetővé teszi.
A manifest.json fájlban:
"permissions": [ "activeTab", "scripting" ],
Ezen a ponton előfordulhat, hogy el kell távolítania a bővítményt a Chrome-ból, és újra be kell töltenie a megfelelő működéshez. Miután fut, látnunk kell a konzolnaplónkat a Chrome-konzolunkban.
Itt van egy github hivatkozás a repo működési kódjához ebben a szakaszban .
Oldalképek összegyűjtése és teszt alt címkék beszúrása
Következő lépésünk az, hogy a tartalomszkriptfájlunk segítségével megragadjuk az oldalon lévő összes képet, így készen állunk arra, hogy elküldjük az API-hívásainkat, hogy képleírásokat kapjunk. Arról is szeretnénk gondoskodni, hogy csak olyan képeket hívjunk le, amelyekhez hasznos a leírás. Egyes képek pusztán dekoratívak, és nem kell lelassítaniuk a képernyőolvasókat a leírásukkal. Például, ha van egy keresősávja, amelyen a „keresés” címke és egy nagyító ikon is szerepel. Ha egy kép alt címkéje üres karakterláncra van állítva, vagy az aria-hidden értéke igaz, az azt jelenti, hogy a képet nem kell bevenni a képernyőolvasóba, és kihagyhatjuk a leírás létrehozását.
Tehát először a content.js fájlban összegyűjtjük az oldalon található összes képet. Hozzáadom a console.log fájlt, hogy gyorsan megbizonyosodhasson arról, hogy megfelelően működik:
const images = document.querySelectorAll("img"); console.log(images)
Ezután végigfutjuk a képeket, és ellenőrizzük, hogy vannak-e olyan képek, amelyekhez alt címkét kell generálnunk. Ez magában foglalja az összes olyan képet, amely nem rendelkezik alt címkével, és azokat a képeket, amelyek alt címkével nem egy üres karakterláncot tartalmaznak, valamint azokat a képeket, amelyeket nem rejtettek el kifejezetten az aria-hidden attribútummal rendelkező képernyőolvasók elől.
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! } }
Ezután hozzáadhatunk egy tesztkarakterláncot az alt címkék beállításához, így megbizonyosodhatunk arról, hogy van egy funkcionális mód ezek beállítására, mielőtt továbblépnénk az OpenAI-hívásainkra. A content.js fájl most így néz ki:
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()
Ezen a ponton, ha megnyitjuk a Chrome fejlesztői eszközök elemeit, és rákattintunk egy képre, a „Test Alt Text”-et kell látni alt tagként.
Itt található egy működő repó arról, ahol a kód ebben a szakaszban van.
Telepítse az OpenAI-t, és készítsen képleírásokat
Az OpenAI használatához létre kell hoznia egy OpenAI kulcsot, és jóvá kell hagynia a fiókját. OpenAI-kulcs létrehozása:
Mentse el ezt a kulcsot. Ezenkívül tartsa privátban – ügyeljen arra, hogy ne helyezze be nyilvános git-repókba.
Most, visszatérve a tárhelyünkhöz, először telepíteni akarjuk az OpenAi-t. A projektkönyvtárban lévő terminálban futtassa:
npm install openai
Most a content.js-ben az OpenAI-t úgy fogjuk importálni, hogy hozzáadjuk ezt a kódot a fájl tetejéhez, és az OpenAI-kulcsot beillesztjük az 1. sorba:
const openAiSecretKey = 'YOUR_KEY_GOES_HERE' import OpenAI from "openai"; const openai = new OpenAI({ apiKey: openAiSecretKey, dangerouslyAllowBrowser: true });
A „DangerouslyAllowBrowser” lehetővé teszi a hívás kezdeményezését a böngészőből származó kulcsával. Általában ez egy nem biztonságos gyakorlat. Mivel ezt a projektet csak lokálisan futtatjuk, így hagyjuk, ahelyett, hogy háttérlekérdezést hoznánk létre. Ha más projektekben is használja az OpenAI-t, ügyeljen arra, hogy kövesse a kulcstitok megőrzésére vonatkozó bevált módszereket.
Most hozzáadjuk a felhívásunkat, hogy az OpenAI képleírásokat generáljon. A csevegés befejezésének végpontját hívjuk meg ( OpenAI-dokumentumok a csevegés befejezési végpontjához ).
Meg kell írnunk a saját promptunkat, és át kell adnunk a kép src URL-jét is ( további információ az AI prompt tervezésről ). Az üzenetet tetszés szerint módosíthatja. Úgy döntöttem, hogy 20 műre korlátozom a leírásokat, mert az OpenAI hosszú leírásokat adott vissza. Ezenkívül észrevettem, hogy teljes mértékben leírja az olyan logókat, mint a Yelp vagy a Facebook logók (azaz „egy nagy kék doboz fehér kisbetűs f betűvel”), amelyek nem voltak hasznosak. Ha infografikáról van szó, kérem a szókorlát figyelmen kívül hagyását és a teljes képszöveg megosztását.
Íme a teljes hívás, amely visszaadja az első AI-válasz tartalmát, és a hibát is átadja egy „handleError” függvénynek. Minden válaszhoz mellékeltem egy console.log fájlt, hogy gyorsabban kaphassunk visszajelzést arról, hogy a hívás sikeres-e vagy sem:
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); }
Ennek a függvénynek a hívását adjuk hozzá a korábban írt if utasításhoz (a scanImages függvény elejére egy aszinkron kulcsszót is fel kell adnunk, hogy belefoglaljuk ezt az aszinkron hívást):
const imageDescription = await generateDescription(image.src) if (!imageDescription) { return; } image.alt = imageDescription
Itt van egy link a teljes content.js-hez és a repóhoz.
A felhasználói felület kiépítése
Ezt követően szeretnénk felépíteni a felhasználói felületünket, hogy a felhasználó tudja, mi történik, miután rákattint a gombra a címkék generálásához. Néhány másodpercet vesz igénybe a címkék betöltése, ezért szeretnénk egy „betöltés” üzenetet, hogy a felhasználó tudja, hogy működik. Ezenkívül szeretnénk tájékoztatni őket arról, hogy ez sikeres volt, vagy ha hiba történt. Az egyszerűség érdekében a html-ben lesz egy általános felhasználói üzenet div, majd a popup.js segítségével dinamikusan beillesztjük a megfelelő üzenetet a felhasználóhoz a bővítményben történt események alapján.
A Chrome-bővítmények beállításának módja, a tartalomszkriptünk (content.js) elkülönül a popup.js-től, és nem tudják megosztani a változókat a tipikus JavaScript-fájlokhoz hasonlóan. A tartalomszkript üzenetátadáson keresztül tudja tudatni a felugró ablakkal, hogy a címkék betöltődnek vagy sikeresen betöltődnek. Már használtuk az üzenettovábbítást, amikor tudattuk a háttérmunkással, hogy beadja a tartalomszkriptet, amikor a felhasználó az eredeti gombra kattintott.
Először a html-ünkben adjunk hozzá egy div-t a „felhasználói üzenet” azonosítójával a gombunk alá. A kezdeti üzenethez is adtam egy kis leírást.
<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>
Ezután a popup.js fájlban hozzáadunk egy figyelőt, amely figyel minden olyan küldött üzenetre, amely a bővítmény állapotának frissítését tartalmazhatja. Írunk majd néhány html-t is a beszúráshoz attól függően, hogy milyen állapotot kapunk a tartalomszkriptből.
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.' } }
A tartalomszkriptünkön belül meghatározunk egy új „extensionState” változót, amely lehet „kezdeti” (a kiterjesztés betöltődött, de még nem történt semmi), „betöltés”, „siker” vagy „hiba” (mi más hibaállapotokat is hozzáad az OpenAI hibaüzenetek alapján). A bővítmény állapotváltozóját is frissítjük, és minden alkalommal üzenetet küldünk a popup.js fájlnak, amikor az állapot megváltozik.
let extensionState = 'initial';
Hibakezelőnk a következő:
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); }
A scanPhotos függvényünkön belül pedig a funkció elején a „betöltés” állapotot állítjuk be, és a „sikeres” állapotot, ha teljesen hiba nélkül fut.
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}) }
Zavarba ejtő előugró ablakok viselkedésének javítása – a kiterjesztés továbbra is fennáll, amikor az előugró ablakok bezáródnak és újra megnyílnak
Ezen a ponton észreveheti, hogy ha alt címkéket generál, sikerüzenetet kap, majd bezárja, majd újra megnyitja az előugró ablakot, akkor megjelenik a kezdeti üzenet, amely új alt címkék létrehozására kéri a felhasználót. Annak ellenére, hogy a generált alt címkék most benne vannak a kódban!
A Chrome-ban minden alkalommal, amikor megnyit egy bővítmény felugró ablakot, az egy teljesen új előugró ablak. Nem fog emlékezni semmire, amit a bővítmény korábban tett, vagy arra, ami a tartalomszkriptben fut. Biztosíthatjuk azonban, hogy minden újonnan megnyílt előugró ablak a bővítmény pontos állapotát jelenítse meg, ha meghívjuk, és megnyitásakor ellenőrizzük a mellékállomás állapotát. Ehhez egy felugró üzenetet küldünk, amely ezúttal a kiterjesztés állapotát kéri, és hozzáadunk egy üzenetfigyelőt a content.js fájlhoz, amely meghallgatja az üzenetet, és visszaküldi az aktuális állapotot.
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}); });
Ha a tartalomszkript még soha nem futott (más néven a felhasználó soha nem kattintott a gombra az alt címkék generálásához), akkor nem lesz kiterjesztési állapotváltozó vagy eseményfigyelő. Ebben az esetben a chrome válaszként futásidejű hibát ad vissza. Tehát beiktatjuk a hibaellenőrzést, és ha ilyet kapunk, hagyjuk az alapértelmezett felhasználói felületet.
A bővítmények hozzáférhetősége – aria-live, színkontraszt és bezárás gomb
Ez a bővítmény a képernyőolvasókat használók számára készült, ezért most meg kell győződnünk arról, hogy valóban használható-e képernyőolvasóval! Itt az ideje, hogy bekapcsolja képernyőolvasóját, és ellenőrizze, hogy minden jól működik-e.
Néhány dolgot szeretnénk megtisztítani az akadálymentesítés érdekében. Mindenekelőtt meg akarunk győződni arról, hogy minden szöveg elég magas kontrasztszintű. A gombnál úgy döntöttem, hogy a hátteret #0250C5-re, a betűtípust pedig fehér félkövérre állítom. Ennek kontrasztaránya 7,1, és AA és AAA szinten is WCAG-kompatibilis. Itt ellenőrizheti a használni kívánt szín kontrasztarányát a WebAim Contrast Checkerben.
Másodszor, a képernyőolvasó használatakor azt veszem észre, hogy a képernyőolvasó nem olvassa ki automatikusan a frissítéseket, amikor a felhasználói üzenet betöltési, siker- vagy hibaüzenetté változik. Ennek javítására az aria-live nevű html attribútumot fogjuk használni. Az Aria-live lehetővé teszi a fejlesztők számára, hogy értesítsék a képernyőolvasókat, hogy frissítsék a felhasználókat a változásokról. Az aria-live-t beállíthatja asszertívre vagy udvariasra – ha asszertívre van állítva, a frissítések azonnal beolvasásra kerülnek, függetlenül attól, hogy vannak-e más elemek, amelyek beolvasásra várnak a képernyőolvasó sorában. Ha udvariasra van állítva, a frissítés a képernyőolvasó olvasási folyamatának végén kerül beolvasásra. Esetünkben a lehető leghamarabb frissíteni szeretnénk a felhasználót. Tehát a popup-containerben, a felhasználói üzenet elemünk szülőelemében hozzáadjuk ezt az attribútumot.
<div class="popup-container" aria-live="assertive">
Végül, a képernyőolvasó használatával azt vettem észre, hogy nincs egyszerű módja a felugró ablak bezárásának. Egér használatakor egyszerűen kattintson bárhová a felugró ablakon kívül, és az bezárul, de nem tudom pontosan, hogyan lehet bezárni a billentyűzet segítségével. Így a felugró ablak aljára egy „bezárás” gombot adunk, így a felhasználók könnyen bezárhatják és visszatérhetnek a weboldalra.
A popup.html-ben hozzáadjuk:
<div> <button id="close-button">Close</button> </div>
A popup.js fájlban hozzáadjuk a close függvényt az onclickhez:
const closeButton = document.body.querySelector('#close-button'); closeButton.addEventListener('click', async () => { window.close() });
És ennyi! Ha bármilyen kérdése vagy javaslata van, forduljon bizalommal.