Teg Alt ialah aspek yang paling terkenal dalam membina halaman web yang boleh diakses, tetapi malangnya ia sering diabaikan atau tidak dilaksanakan dengan baik. Tag Alt ialah penerangan teks ringkas yang ditambahkan pada imej. Pembaca skrin membaca kandungan halaman web kepada pengguna, dan perihalan imej ialah apa yang mereka baca untuk menyampaikan perkara yang terdapat dalam imej pada halaman itu kepada pengguna cacat penglihatan, kerana mereka tidak dapat melihatnya.
Malangnya, adalah perkara biasa untuk imej kehilangan sebarang teg alt sepenuhnya. Saya juga telah melihat teg alt disalahgunakan dalam cara yang menyukarkan pengguna cacat penglihatan, seperti teg yang hanya menyebut "gambar" atau "imej", teg yang merupakan kapsyen comel yang ditambahkan oleh pengarang tanpa merujuk kepada perkara yang terdapat dalam imej ( iaitu, imej kopi dan komputer riba pada halaman blog, dengan kapsyen "diari sayang, saya ingin dipilih sebagai penulis jemputan"). Saya juga telah melihat tag alt yang merangkumi 3 baris kata kunci SEO. Bolehkah anda bayangkan cuba mendengar apa yang ada di tapak web hanya untuk mendengar "imej gambar" atau senarai panjang kata kunci SEO?
Ini ialah sambungan Chrome yang direka bentuk untuk memperkasakan pengguna cacat penglihatan dengan membenarkan mereka menulis ganti teg alt yang buruk dan memanfaatkan Open AI untuk memasukkan perihalan yang dijana AI. Ini membolehkan pengguna cacat penglihatan untuk benar-benar mengakses semua kandungan pada halaman web yang boleh diakses oleh pengguna tidak cacat penglihatan (atau sekurang-kurangnya tidak diperlahankan oleh senarai panjang kata kunci SEO).
Jika anda hanya mahu sambungan, anda dialu-alukan untuk memuat turun repo ini dan ikut arahan dalam README.
Walau bagaimanapun, jika anda berminat dengan panduan langkah demi langkah tentang cara membina sambungan Chrome dengan OpenAI, berikut adalah panduannya.
Mula-mula, mari sediakan pelat dandang Chrome asas dan berjalan. Klon repositori ini dan ikut arahan dalam README:
Sebaik sahaja anda memasangnya dan memasangnya, anda sepatutnya mempunyai ikon imej dalam bar sambungan anda (saya cadangkan menyematkannya untuk membuat ujian lebih cepat), dan apabila anda mengklik padanya, akan melihat pop timbul dengan "hello world."
Mari buka kod boilerplate, dan lihat fail sedia ada. Ini juga akan merangkumi beberapa asas Sambungan Chrome:
Static/manifest.json - Setiap sambungan Chrome mempunyai fail manifest.json. Ia termasuk maklumat asas dan persediaan tentang sambungan. Dalam fail Manifes kami, kami mempunyai nama, perihalan, fail latar belakang ditetapkan kepada src/background.js, ikon ditetapkan kepada image-icon.png (ini ialah ikon yang akan muncul mewakili sambungan pada menu sambungan), dan ia menetapkan popup.html sebagai sumber fail untuk popup kami.
src/background.js - Fail background.js yang disediakan dalam manifes kami. Kod dalam fail ini akan dijalankan di latar belakang dan memantau peristiwa yang mencetuskan fungsi dalam sambungan.
src/content.js - Sebarang skrip yang dijalankan dalam konteks halaman web atau mengubah suai halaman web akan dimasukkan ke dalam skrip kandungan.
src/popup.js, static/popup.css dan static/popup.html - Fail ini mengawal pop timbul yang anda lihat apabila anda mengklik ikon sambungan
Mari sediakan beberapa asas - buka static/manifest.json dan tukar nama dan perihalan kepada "Penjana Penerangan Imej Pembaca Skrin" (atau apa sahaja yang anda suka).
Dayakan berinteraksi dengan halaman web menggunakan skrip kandungan
Sambungan kami akan menimpa teg alt pada tapak web yang digunakan pengguna, yang bermaksud kami memerlukan akses kepada html halaman. Cara untuk melakukan ini dalam Sambungan Chrome adalah melalui skrip kandungan. Skrip kandungan kami akan berada dalam fail src/content.js kami.
Cara paling mudah untuk menyuntik skrip kandungan ialah dengan menambahkan medan "skrip" pada manifes dengan rujukan kepada fail js. Apabila anda menyediakan skrip kandungan dengan cara ini, skrip yang dipautkan akan dijalankan apabila sambungan dimuatkan. Walau bagaimanapun, dalam kes kami, kami tidak mahu sambungan kami dijalankan secara automatik apabila pengguna membuka sambungan. Sesetengah tapak web mempunyai teg alt yang sangat baik ditetapkan pada imej, jadi kami hanya mahu menjalankan kod tersebut apabila pengguna memutuskan ia perlu.
Kami akan menambah butang dalam pop timbul kami dan log konsol dalam skrip kandungan kami, supaya apabila pengguna mengklik butang, skrip kandungan dimuatkan dan kami boleh mengesahkannya dengan melihat penyata kami dicetak dalam konsol Chrome.
Popup.html
<button id="generate-alt-tags-button">Generate image descriptions</button>
src/content.js
console.log('hello console')
Cara untuk menyambung klik butang itu pada pop timbul ke skrip kandungan melibatkan kedua-dua popup.js dan background.js.
Dalam popup.js, kami akan mengambil butang daripada DOM dan menambah pendengar acara. Apabila pengguna mengklik butang itu, kami akan menghantar mesej yang menandakan skrip kandungan harus disuntik. Kami akan menamakan mesej "injectContentScript"
const generateAltTagButton = document.body.querySelector('#generate-alt-tags-button'); generateAltTagButton.addEventListener('click', async () => { chrome.runtime.sendMessage({action: 'injectContentScript'}) });
Dalam background.js, kami mempunyai kod yang memantau acara dan bertindak balas terhadapnya. Di sini, kami menyediakan pendengar acara dan jika mesej yang diterima ialah 'injectContentScript', ia akan melaksanakan skrip kandungan dalam tab aktif (halaman web semasa pengguna).
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'] }); }); } });
Langkah terakhir untuk menyediakan ini ialah menambahkan kebenaran "activeTab" dan "skrip" pada manifes kami. Kebenaran "skrip" diperlukan untuk menjalankan sebarang skrip kandungan. Kami juga perlu menambah kebenaran untuk halaman yang kami masukkan skrip ke dalamnya. Dalam kes ini, kami akan menyuntik skrip ke dalam tapak web semasa pengguna, aka tab aktif mereka, dan itulah yang dibenarkan oleh kebenaran ActiveTab.
Dalam manifest.json:
"permissions": [ "activeTab", "scripting" ],
Pada ketika ini, anda mungkin perlu mengalih keluar pelanjutan daripada Chrome dan memuatkannya semula supaya ia berjalan dengan betul. Sebaik sahaja ia berjalan, kami akan melihat log konsol kami dalam konsol Chrome kami.
Berikut ialah pautan github untuk kod kerja untuk repo pada peringkat ini .
Mengumpul imej halaman dan memasukkan teg alt ujian
Langkah seterusnya kami ialah menggunakan fail skrip kandungan kami untuk mengambil semua imej pada halaman, jadi kami mempunyai maklumat itu sedia untuk dihantar dalam panggilan API kami untuk mendapatkan penerangan imej. Kami juga ingin memastikan bahawa kami hanya membuat panggilan untuk imej yang mempunyai huraian berguna. Sesetengah imej adalah hiasan semata-mata dan tidak perlu memperlahankan pembaca skrin dengan penerangannya. Contohnya, jika anda mempunyai bar carian yang mempunyai kedua-dua label "carian" dan ikon kaca pembesar. Jika imej mempunyai teg alt ditetapkan kepada rentetan kosong, atau mempunyai aria-tersembunyi ditetapkan kepada benar, ini bermakna imej itu tidak perlu disertakan dalam pembaca skrin dan kita boleh melangkau menjana penerangan untuknya.
Jadi pertama, dalam content.js, kami akan mengumpulkan semua imej pada halaman. Saya menambah console.log supaya saya boleh mengesahkan dengan cepat ia berfungsi dengan betul:
const images = document.querySelectorAll("img"); console.log(images)
Kemudian kami akan mengulangi imej, dan menyemak imej yang kami perlukan untuk menjana teg alt. Itu termasuk semua imej yang tidak mempunyai teg alt dan imej yang mempunyai teg alt yang bukan rentetan kosong dan imej yang tidak disembunyikan secara eksplisit daripada pembaca skrin dengan atribut tersembunyi aria.
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! } }
Kemudian kami boleh menambah rentetan ujian untuk menetapkan tag alt, jadi kami boleh mengesahkan kami mempunyai cara yang berfungsi untuk menetapkannya sebelum kami beralih ke panggilan OpenAI kami. content.js kami kini kelihatan seperti:
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()
Pada ketika ini, jika kita membuka Elemen alat pembangun Chrome, klik pada imej, kita akan melihat "Test Alt Text" ditetapkan sebagai teg alt.
Repo berfungsi untuk tempat kod berada pada peringkat ini ada di sini.
Pasang OpenAI dan jana penerangan imej
Untuk menggunakan OpenAI, anda perlu menjana kunci OpenAI dan juga menambah kredit pada akaun anda. Untuk menjana kunci OpenAI:
Simpan kunci ini. Selain itu, pastikan ia tertutup - pastikan anda tidak menolaknya ke mana-mana repo git awam.
Sekarang, kembali ke repo kami, mula-mula kami mahu memasang OpenAi. Di terminal di dalam direktori projek, jalankan:
npm install openai
Sekarang dalam content.js, kami akan mengimport OpenAI dengan menambahkan kod ini di bahagian atas fail, dengan kunci OpenAI anda ditampal pada baris 1:
const openAiSecretKey = 'YOUR_KEY_GOES_HERE' import OpenAI from "openai"; const openai = new OpenAI({ apiKey: openAiSecretKey, dangerouslyAllowBrowser: true });
"DangerouslyAllowBrowser" membenarkan panggilan dibuat dengan kunci anda daripada penyemak imbas. Secara amnya, ini adalah amalan yang tidak selamat. Memandangkan kami hanya menjalankan projek ini secara tempatan, kami akan membiarkannya seperti ini, dan bukannya menyediakan pengambilan bahagian belakang. Jika anda menggunakan OpenAI dalam projek lain, pastikan anda mengikuti amalan terbaik mengenai merahsiakan kunci.
Kini kami menambah panggilan kami untuk membolehkan OpenAI menjana penerangan imej. Kami memanggil titik akhir pelengkapan buat sembang ( dokumen OpenAI untuk titik akhir pelengkapan sembang ).
Kita perlu menulis gesaan kita sendiri dan juga memasukkan URL src imej ( maklumat lanjut tentang kejuruteraan gesaan AI ). Anda boleh menyesuaikan gesaan seperti yang anda mahu. Saya memilih untuk mengehadkan huraian kepada 20 karya kerana OpenAI memulangkan huraian yang panjang. Selain itu, saya perhatikan ia menerangkan sepenuhnya logo seperti Yelp atau logo Facebook (iaitu, 'kotak biru besar dengan huruf kecil putih f di dalam'), yang tidak membantu. Sekiranya ia adalah maklumat grafik, saya meminta had perkataan diabaikan dan teks imej penuh dikongsi.
Berikut ialah panggilan penuh, yang mengembalikan kandungan respons AI pertama dan juga menghantar ralat ke dalam fungsi "handleError". Saya telah menyertakan console.log bagi setiap respons supaya kami boleh mendapatkan maklum balas yang lebih cepat sama ada panggilan berjaya atau tidak:
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); }
Kami menambah panggilan ke fungsi ini ke dalam pernyataan if yang kami tulis sebelum ini (kami juga perlu menambah kata kunci tak segerak pada permulaan fungsi scanImages untuk memasukkan panggilan tak segerak ini):
const imageDescription = await generateDescription(image.src) if (!imageDescription) { return; } image.alt = imageDescription
Berikut ialah pautan ke content.js penuh dan repo pada ketika ini.
Membina UI
Seterusnya, kami ingin membina UI kami supaya pengguna mengetahui perkara yang berlaku selepas mereka mengklik butang untuk menjana teg. Ia mengambil masa beberapa saat untuk teg dimuatkan, jadi kami mahukan mesej 'memuatkan' supaya pengguna tahu ia berfungsi. Selain itu, kami ingin memberitahu mereka bahawa ia berjaya, atau jika terdapat ralat. Untuk memastikan perkara mudah, kami akan mempunyai div mesej pengguna umum dalam html, dan kemudian menggunakan popup.js untuk memasukkan mesej yang sesuai secara dinamik kepada pengguna berdasarkan apa yang berlaku dalam sambungan.
Cara pelanjutan Chrome disediakan, skrip kandungan kami (content.js) diasingkan daripada popup.js kami dan ia tidak dapat berkongsi pembolehubah seperti mana fail JavaScript biasa. Cara skrip kandungan boleh memberitahu pop timbul bahawa teg sedang dimuatkan, atau berjaya dimuatkan, adalah melalui penghantaran mesej. Kami telah menggunakan penghantaran mesej apabila kami memberitahu pekerja latar belakang untuk menyuntik skrip kandungan apabila pengguna mengklik pada butang asal.
Pertama, dalam html kami, kami akan menambah div dengan id 'mesej pengguna' di bawah butang kami. Saya telah menambah sedikit lagi penerangan untuk mesej awal juga.
<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>
Kemudian, dalam popup.js kami, kami akan menambah pendengar yang mendengar sebarang mesej yang dihantar yang mungkin mengandungi kemas kini kepada keadaan sambungan. Kami juga akan menulis beberapa html untuk disuntik berdasarkan apa jua keputusan keadaan yang kami perolehi daripada skrip kandungan.
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.' } }
Dalam skrip kandungan kami, kami akan mentakrifkan pembolehubah baharu yang dipanggil 'extensionState', yang boleh sama ada 'awal' (sambungan dimuatkan tetapi tiada apa yang berlaku lagi), 'memuatkan', 'berjaya' atau 'ralat' (kami akan menambah beberapa keadaan ralat lain juga berdasarkan mesej ralat OpenAI). Kami juga akan mengemas kini pembolehubah keadaan sambungan dan menghantar mesej ke popup.js setiap kali keadaan berubah.
let extensionState = 'initial';
Pengendali ralat kami untuk menjadi:
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); }
Dan dalam fungsi scanPhotos kami, kami menetapkan keadaan untuk 'memuatkan' pada permulaan fungsi, dan kepada 'berjaya' jika ia berjalan sepenuhnya tanpa ralat.
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}) }
Membetulkan gelagat pop timbul yang mengelirukan - keadaan sambungan berterusan apabila pop timbul ditutup dan dibuka semula
Anda mungkin perasan pada ketika ini bahawa jika anda menjana teg alt, mendapat mesej kejayaan dan menutup serta membuka semula tetingkap timbul, ia akan memaparkan mesej awal yang menggesa pengguna menjana teg alt baharu. Walaupun tag alt yang dihasilkan berada dalam kod sekarang!
Dalam Chrome, setiap kali anda membuka pop timbul sambungan, ia adalah pop timbul serba baharu. Ia tidak akan mengingati apa-apa yang telah dilakukan sebelum ini oleh pelanjutan, atau perkara yang dijalankan dalam skrip kandungan. Walau bagaimanapun, kami boleh memastikan mana-mana pop timbul yang baru dibuka menunjukkan keadaan tepat sambungan dengan memintanya memanggil dan menyemak keadaan sambungan apabila ia dibuka. Untuk berbuat demikian, kami akan menghantar mesej pop timbul yang lain, kali ini meminta keadaan sambungan dan kami akan menambah pendengar mesej dalam content.js kami yang mendengar mesej itu dan menghantar semula keadaan semasa.
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}); });
Jika skrip kandungan tidak pernah dijalankan (aka pengguna tidak pernah mengklik butang untuk menjana teg alt), tidak akan ada pembolehubah keadaan sambungan atau pendengar acara. Dalam keadaan ini, chrome akan mengembalikan ralat masa jalan sebagai tindak balas. Jadi kami menyertakan semakan untuk ralat, dan jika kami menerimanya, biarkan UI lalai seperti sedia ada.
Kebolehaksesan sambungan - aria-live, kontras warna dan butang tutup
Sambungan ini direka untuk orang yang menggunakan pembaca skrin, jadi sekarang kita perlu memastikan ia benar-benar boleh digunakan dengan pembaca skrin! Sekarang adalah masa yang baik untuk menghidupkan pembaca skrin anda dan melihat sama ada semuanya berfungsi dengan baik.
Terdapat beberapa perkara yang ingin kami bersihkan untuk kebolehaksesan. Pertama sekali, kami ingin memastikan semua teks adalah tahap kontras yang cukup tinggi. Untuk butang, saya telah memutuskan untuk menetapkan latar belakang kepada #0250C5 dan fon kepada putih tebal. Ini mempunyai nisbah kontras 7.1 dan mematuhi WCAG pada kedua-dua tahap AA dan AAA. Anda boleh menyemak nisbah kontras untuk sebarang warna yang anda ingin gunakan di sini di WebAim Contrast Checker.
Kedua, apabila menggunakan pembaca skrin saya, saya mendapati bahawa pembaca skrin tidak membaca kemas kini secara automatik apabila mesej pengguna berubah kepada pemuatan, kejayaan atau mesej ralat. Untuk membetulkannya, kami akan menggunakan atribut html yang dipanggil aria-live. Aria-live membenarkan pembangun untuk memberitahu pembaca skrin untuk mengemas kini pengguna perubahan. Anda boleh menetapkan aria-live kepada sama ada tegas atau sopan - jika ia ditetapkan kepada asertif, kemas kini akan dibaca serta-merta, tidak kira jika terdapat item lain menunggu untuk dibaca dalam baris gilir pembaca skrin. Jika ia ditetapkan kepada sopan, kemas kini akan dibaca pada penghujung semua pembaca skrin sedang dalam proses membaca. Dalam kes kami, kami ingin mengemas kini pengguna secepat mungkin. Jadi dalam bekas pop timbul, elemen induk elemen mesej pengguna kami, kami akan menambah atribut itu.
<div class="popup-container" aria-live="assertive">
Akhir sekali, menggunakan pembaca skrin, saya perasan tidak ada cara mudah untuk menutup pop timbul. Apabila menggunakan tetikus, anda hanya klik di mana-mana di luar pop timbul dan ia ditutup, tetapi saya tidak dapat mengetahui cara menutupnya menggunakan papan kekunci. Jadi kami akan menambah butang 'tutup' di bahagian bawah pop timbul, supaya pengguna boleh menutupnya dengan mudah dan kembali ke halaman web.
Dalam popup.html, kami menambah:
<div> <button id="close-button">Close</button> </div>
Dalam popup.js, kami menambah fungsi tutup pada onclick:
const closeButton = document.body.querySelector('#close-button'); closeButton.addEventListener('click', async () => { window.close() });
Dan itu sahaja! Jika anda mempunyai sebarang soalan atau cadangan, sila hubungi.