Jekyll2024-01-24T18:00:27+00:00https://blog.monotonous.org/feed/atom/index.xmlmonotonous.orgEitan's PitchEitan IsaacsonIntroducing Spiel2024-01-10T00:00:00+00:002024-01-10T00:00:00+00:00https://blog.monotonous.org/2024/01/10/Introducing-Spiel<h2 id="a-new-speech-api-and-framework">A New Speech API and Framework</h2>
<p><img src="/assets/uploads/2024/01/spiel-logo.svg" alt="Spiel Logo" style="max-width: 30vw; float: right; margin-left: 1rem;" /></p>
<p>I wrote the beginning of what I hope will be an <a href="https://github.com/eeejay/spiel">appealing speech API for desktop Linux and beyond</a>. It consists of two parts, a <a href="https://eeejay.github.io/spiel/generated-org.freedesktop.Speech.Provider.html">speech provider interface specification</a> and a <a href="https://eeejay.github.io/spiel/">client library</a>. My hope is that the simplicity of the design and its leverage of existing free desktop technologies will make adoption of this API easy.</p>
<p>Of course, Linux already has a speech framework in the form of <a href="https://github.com/brailcom/speechd">Speech Dispatcher</a>. I believe there have been a handful of technologies and recent developments in the free desktop space that offer a unique opportunity to build something truly special. They include:</p>
<h3 id="d-bus">D-Bus</h3>
<p>D-Bus came about several years after Speech Dispatcher. It is worth pausing and thinking about the different architectural similarities between a local speech service and a desktop IPC bus. The problems that Speech Dispatcher tackles, such as auto-spawning, wire protocols, IPC transports, session persistence, modularity, and others have been generalized by D-Bus.</p>
<p>Instead of a specialized module for Speech Dispatcher, what if speech engines just exposed an interface on the session bus? With a service file they can automatically spawn and go away as needed.</p>
<h3 id="flatpak-and-snap">Flatpak (and Snap??)</h3>
<p>Flatpak offers a standardized packaging format that can encapsulate complex setups into a sandboxed installation with little to no thoughts of the <a href="https://en.wikipedia.org/wiki/Dependency_hell">dependency hell</a> Linux users have grown accustomed to. One neat feature in Flatpaks is that they support exposing fully sandboxed D-Bus services, such as a speech engine. Flatpaks offer an out-of-band distribution model that sidesteps the limitations and fragmentation of traditional distro package streams. Flatpak repositories like <a href="https://flathub.org">Flathub</a> are the perfect vehicle for speech engines because of the mix of proprietary and peculiar licenses that are often associated with them, for example…</p>
<h3 id="neural-text-to-speech">Neural text to speech</h3>
<p>I have always been frustrated with the lack of naturally sounding speech synthesis in free software. It always seemed that the game was rigged and only the big tech platforms would be able to afford to distribute nice sounding voices. This is all quickly changing with a flurry of <a href="https://github.com/rhasspy/piper/tree/master" title="Piper">new</a> <a href="https://github.com/MycroftAI/mimic3/tree/master" title="Mimic3">speech</a> <a href="https://github.com/coqui-ai/TTS" title="Coqui TTS">systems</a> covering many languages. It is very exciting to see this happening, it seems like there is a new innovation on this front every day. Because of the size of some of the speech models, and because of the eclectic copyright associated with them we can’t expect distros to preinstall them, Flatpaks and Neural speech systems are a perfect match for this purpose.</p>
<h3 id="talking-apps-that-arent-screen-readers">Talking apps that aren’t screen readers</h3>
<p>In recent years we have seen many new applications of speech synthesis entering the mainstream - navigation apps, e-book readers, personal assistants and smart speakers. When Speech Dispatcher was first designed, its primary audience was blind Linux users. As the use cases have ballooned so has the demand for a more generalized framework that will cater to a diverse set of users.</p>
<p>There is <a href="https://ssir.org/articles/entry/the_curb_cut_effect">precedent for technology that was designed for disabled people becoming mainstream</a>. Everyone benefits when a niche technology becomes conventional, especially those who depend on it most.</p>
<h2 id="questions-and-answers">Questions and Answers</h2>
<p>I’m sure you have questions, I have some answers. So now we will play our two roles, you the perplexed skeptic, unsure about why another software stack is needed, and me - a benevolent guide who can anticipate your questions.</p>
<h3 id="why-are-you-starting-from-scratch-cant-you-improve-speech-dispatcher">Why are you starting from scratch? Can’t you improve Speech Dispatcher?</h3>
<p>Speech Dispatcher is over 20 years old. Of course, that isn’t a reason to replace it. After all, some of your <a href="https://www.vim.org/">favorite</a> <a href="https://www.mozilla.org/">apps</a> are even older. Perhaps there is room for incremental improvements in Speech Dispatcher. But, as I wrote above, I believe there are several developments in recent years that offer an opportunity for a clean slate.</p>
<h3 id="i-love-espeak-what-is-all-this-talk-about-naturally-sounding-voices">I love eSpeak, what is all this talk about “naturally sounding” voices?</h3>
<p>eSpeak isn’t going anywhere. It has a permissible license, is very responsive, and is ergonomic for screen reader users who <a href="https://www.youtube.com/watch?v=DFkmRewclaE">consume speech at high rates</a> for long periods of time. We will have an eSpeak speech provider in this new framework.</p>
<p>Many other users, who rely on speech for narration or virtual assistants will prefer a more natural voice. The goal is to make those speech engines available and easy to install.</p>
<h3 id="i-know-for-a-fact-that-you-can-use-insert-speech-engine-with-speech-dispatcher">I know for a fact that you can use /insert speech engine/ with Speech Dispatcher</h3>
<p>It is true that with enough effort you can plug anything into Speech Dispatcher.</p>
<p>Speech Dispatcher depends on a fraught set of configuration files, scripts, executables and shared libraries. A user who wants to use a synthesis engine other than the default bundled one in their distro needs to open a terminal, carefully place resources in the right place and edit configuration files.</p>
<h3 id="what-plan-do-you-have-to-migrate-all-the-current-applications-that-rely-on-speech-dispatcher">What plan do you have to migrate all the current applications that rely on Speech Dispatcher?</h3>
<p>I don’t. Both APIs can coexist. I’m not a contributor or maintainer of Speech Dispatcher. There might always be a need for the unique features in Speech Dispatcher, and it might have another 20 years of service ahead.</p>
<h3 id="i-couldnt-help-but-notice-you-chose-to-write-libspiel-in-c-instead-of-a-modern-memory-safe-language-with-a-strong-ownership-model-like-rust">I couldn’t help but notice you chose to write libspiel in C instead of a modern memory safe language with a strong ownership model like Rust.</h3>
<p>Yes.</p>EitanA New Speech API and Framework I wrote the beginning of what I hope will be an appealing speech API for desktop Linux and beyond. It consists of two parts, a speech provider interface specification and a client library. My hope is that the simplicity of the design and its leverage of existing free desktop technologies will make adoption of this API easy. Of course, Linux already has a speech framework in the form of Speech Dispatcher. I believe there have been a handful of technologies and recent developments in the free desktop space that offer a unique opportunity to build something truly special. They include: D-Bus D-Bus came about several years after Speech Dispatcher. It is worth pausing and thinking about the different architectural similarities between a local speech service and a desktop IPC bus. The problems that Speech Dispatcher tackles, such as auto-spawning, wire protocols, IPC transports, session persistence, modularity, and others have been generalized by D-Bus. Instead of a specialized module for Speech Dispatcher, what if speech engines just exposed an interface on the session bus? With a service file they can automatically spawn and go away as needed. Flatpak (and Snap??) Flatpak offers a standardized packaging format that can encapsulate complex setups into a sandboxed installation with little to no thoughts of the dependency hell Linux users have grown accustomed to. One neat feature in Flatpaks is that they support exposing fully sandboxed D-Bus services, such as a speech engine. Flatpaks offer an out-of-band distribution model that sidesteps the limitations and fragmentation of traditional distro package streams. Flatpak repositories like Flathub are the perfect vehicle for speech engines because of the mix of proprietary and peculiar licenses that are often associated with them, for example… Neural text to speech I have always been frustrated with the lack of naturally sounding speech synthesis in free software. It always seemed that the game was rigged and only the big tech platforms would be able to afford to distribute nice sounding voices. This is all quickly changing with a flurry of new speech systems covering many languages. It is very exciting to see this happening, it seems like there is a new innovation on this front every day. Because of the size of some of the speech models, and because of the eclectic copyright associated with them we can’t expect distros to preinstall them, Flatpaks and Neural speech systems are a perfect match for this purpose. Talking apps that aren’t screen readers In recent years we have seen many new applications of speech synthesis entering the mainstream - navigation apps, e-book readers, personal assistants and smart speakers. When Speech Dispatcher was first designed, its primary audience was blind Linux users. As the use cases have ballooned so has the demand for a more generalized framework that will cater to a diverse set of users. There is precedent for technology that was designed for disabled people becoming mainstream. Everyone benefits when a niche technology becomes conventional, especially those who depend on it most. Questions and Answers I’m sure you have questions, I have some answers. So now we will play our two roles, you the perplexed skeptic, unsure about why another software stack is needed, and me - a benevolent guide who can anticipate your questions. Why are you starting from scratch? Can’t you improve Speech Dispatcher? Speech Dispatcher is over 20 years old. Of course, that isn’t a reason to replace it. After all, some of your favorite apps are even older. Perhaps there is room for incremental improvements in Speech Dispatcher. But, as I wrote above, I believe there are several developments in recent years that offer an opportunity for a clean slate. I love eSpeak, what is all this talk about “naturally sounding” voices? eSpeak isn’t going anywhere. It has a permissible license, is very responsive, and is ergonomic for screen reader users who consume speech at high rates for long periods of time. We will have an eSpeak speech provider in this new framework. Many other users, who rely on speech for narration or virtual assistants will prefer a more natural voice. The goal is to make those speech engines available and easy to install. I know for a fact that you can use /insert speech engine/ with Speech Dispatcher It is true that with enough effort you can plug anything into Speech Dispatcher. Speech Dispatcher depends on a fraught set of configuration files, scripts, executables and shared libraries. A user who wants to use a synthesis engine other than the default bundled one in their distro needs to open a terminal, carefully place resources in the right place and edit configuration files. What plan do you have to migrate all the current applications that rely on Speech Dispatcher? I don’t. Both APIs can coexist. I’m not a contributor or maintainer of Speech Dispatcher. There might always be a need for the unique features in Speech Dispatcher, and it might have another 20 years of service ahead. I couldn’t help but notice you chose to write libspiel in C instead of a modern memory safe language with a strong ownership model like Rust. Yes.speechSynthesis.getVoices()2021-11-15T00:00:00+00:002021-11-15T00:00:00+00:00https://blog.monotonous.org/2021/11/15/speechSynthesis-getVoices<p>Half of the DOM <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API">Web Speech API</a> deals with speech synthesis. There is a method called <code class="language-plaintext highlighter-rouge">speechSynthesis.getVoices</code> that returns a list of all the supported voices in the given browser. Your website can use it to choose a nice voice to use, or present a menu to the user for them to choose.</p>
<p>The one tricky thing about the <code class="language-plaintext highlighter-rouge">getVoices()</code> method is that the underlying implementation will usually not have a list of voices ready when first called. Since speech synthesis is not a commonly used API, most browsers will initialize their speech synthesis lazily in the background when a <code class="language-plaintext highlighter-rouge">speechSynthesis</code> method is first called. If that method is <code class="language-plaintext highlighter-rouge">getVoices()</code> the first time it is called it will return an empty list. So what will conventional wisdom have you do? Something like this:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">getVoices</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">voices</span> <span class="o">=</span> <span class="nx">speechSynthesis</span><span class="p">.</span><span class="nx">getVoices</span><span class="p">();</span>
<span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="nx">voices</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">voices</span> <span class="o">=</span> <span class="nx">speechSynthesis</span><span class="p">.</span><span class="nx">getVoices</span><span class="p">()</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">voices</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If synthesis is indeed not initialized and first returns an empty list, the page will hang in an infinite CPU-bound loop. This is because the loop is monopolizing the main thread and not allowing synthesis to initialize. Also, an empty voice list is a valid value! For example, Chrome does not have speech synthesis enabled on Linux and will always return an empty list.</p>
<p>So, to get this working we need to not block the main thread by making asynchronous calls to <code class="language-plaintext highlighter-rouge">getVoices</code>, we should also have a limit on how many times we attempt to call <code class="language-plaintext highlighter-rouge">getVoices()</code> before giving up, in the case where there are indeed no voices:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">getVoices</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">voices</span> <span class="o">=</span> <span class="nx">speechSynthesis</span><span class="p">.</span><span class="nx">getVoices</span><span class="p">();</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">attempts</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">attempts</span> <span class="o"><</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">attempts</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">voices</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">await</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">requestAnimationFrame</span><span class="p">(</span><span class="nx">r</span><span class="p">));</span>
<span class="nx">voices</span> <span class="o">=</span> <span class="nx">speechSynthesis</span><span class="p">.</span><span class="nx">getVoices</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">voices</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>But that method still polls, which isn’t great and is needlessly wasteful. There is another way to do it. You could rely on the <code class="language-plaintext highlighter-rouge">voiceschanged</code> DOM event that will be fired once synthesis voices become available. We will also add a timeout to that so our async method returns even if the browser never fires that event.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">async</span> <span class="kd">function</span> <span class="nx">getVoices</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">GET_VOICES_TIMEOUT</span> <span class="o">=</span> <span class="mi">2000</span><span class="p">;</span> <span class="c1">// two second timeout</span>
<span class="kd">let</span> <span class="nx">voices</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">speechSynthesis</span><span class="p">.</span><span class="nx">getVoices</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">voices</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">voices</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">voiceschanged</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span>
<span class="nx">r</span> <span class="o">=></span> <span class="nx">speechSynthesis</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span>
<span class="dl">"</span><span class="s2">voiceschanged</span><span class="dl">"</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="p">{</span> <span class="na">once</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}));</span>
<span class="kd">let</span> <span class="nx">timeout</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">setTimeout</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span> <span class="nx">GET_VOICES_TIMEOUT</span><span class="p">));</span>
<span class="c1">// whatever happens first, a voiceschanged event or a timeout.</span>
<span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">race</span><span class="p">([</span><span class="nx">voiceschanged</span><span class="p">,</span> <span class="nx">timeout</span><span class="p">]);</span>
<span class="k">return</span> <span class="nb">window</span><span class="p">.</span><span class="nx">speechSynthesis</span><span class="p">.</span><span class="nx">getVoices</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>
<p>You’re welcome, Internet!</p>EitanHalf of the DOM Web Speech API deals with speech synthesis. There is a method called speechSynthesis.getVoices that returns a list of all the supported voices in the given browser. Your website can use it to choose a nice voice to use, or present a menu to the user for them to choose. The one tricky thing about the getVoices() method is that the underlying implementation will usually not have a list of voices ready when first called. Since speech synthesis is not a commonly used API, most browsers will initialize their speech synthesis lazily in the background when a speechSynthesis method is first called. If that method is getVoices() the first time it is called it will return an empty list. So what will conventional wisdom have you do? Something like this: function getVoices() { let voices = speechSynthesis.getVoices(); while (!voices.length) { voices = speechSynthesis.getVoices() } return voices; } If synthesis is indeed not initialized and first returns an empty list, the page will hang in an infinite CPU-bound loop. This is because the loop is monopolizing the main thread and not allowing synthesis to initialize. Also, an empty voice list is a valid value! For example, Chrome does not have speech synthesis enabled on Linux and will always return an empty list. So, to get this working we need to not block the main thread by making asynchronous calls to getVoices, we should also have a limit on how many times we attempt to call getVoices() before giving up, in the case where there are indeed no voices: async function getVoices() { let voices = speechSynthesis.getVoices(); for (let attempts = 0; attempts < 100; attempts++) { if (voices.length) { break; } await new Promise(r => requestAnimationFrame(r)); voices = speechSynthesis.getVoices(); } return voices; } But that method still polls, which isn’t great and is needlessly wasteful. There is another way to do it. You could rely on the voiceschanged DOM event that will be fired once synthesis voices become available. We will also add a timeout to that so our async method returns even if the browser never fires that event. async function getVoices() { const GET_VOICES_TIMEOUT = 2000; // two second timeout let voices = window.speechSynthesis.getVoices(); if (voices.length) { return voices; } let voiceschanged = new Promise( r => speechSynthesis.addEventListener( "voiceschanged", r, { once: true })); let timeout = new Promise(r => setTimeout(r, GET_VOICES_TIMEOUT)); // whatever happens first, a voiceschanged event or a timeout. await Promise.race([voiceschanged, timeout]); return window.speechSynthesis.getVoices(); } You’re welcome, Internet!HTML AQI Gauge2021-08-26T00:00:00+00:002021-08-26T00:00:00+00:00https://blog.monotonous.org/2021/08/26/html-aqi-meter<p>I needed a meter to tell me what the air quality is like outside. Now I know!</p>
<p>If you need one as well, or if you are looking for an accessible gauge for anything else, here you go.</p>
<p>You can also <a href="https://codepen.io/eeejay/pen/GREJWga">mess with it on Codepen</a>.</p>
<script>
function setDial(aqi) {
let angle = getAQIDialAngle(aqi);
let [bg, white] = getAQIColor(aqi);
let meter = document.querySelector(".gauge > div[role=meter]");
let dial = meter.querySelector(".dial");
meter.setAttribute("aria-valuenow", aqi);
meter.setAttribute("aria-valuetext", aqi);
dial.querySelector(".aqi-num").textContent = aqi;
dial.querySelector(".arrow").style.transform = `rotate(${angle - 90}deg)`;
dial.style.backgroundColor = bg;
dial.classList.toggle("white", white);
}
function getAQIDialAngle(aqi) {
if (aqi >= 301) {
return Math.min((aqi - 301) / 200 * 30 + 150, 180);
} else if (aqi >= 201) {
return (aqi - 201) / 100 * 30 + 120;
} else if (aqi >= 151) {
return (aqi - 151) / 50 * 30 + 90;
} else if (aqi >= 101) {
return (aqi - 101) / 50 * 30 + 60;
} else if (aqi >= 51) {
return (aqi - 51) / 50 * 30 + 30;
} else if (aqi >= 0) {
return aqi / 50 * 30;
} else {
return 0;
}
}
function getAQIColor(aqi) {
function combineColors(c1, c2, bias) {
return c1.map((c, i) => ((c * (1 - bias)) + (c2[i] * bias)));
}
function stringifyColor(c) {
return `rgb(${c})`;
}
function calculateColors(c1, c2, bias) {
let bg = combineColors(c1, c2, bias);
let white = ((bg[0] * 299) + (bg[1] * 587) + (bg[2] * 114)) / 1000 < 128;
return [stringifyColor(bg), white];
}
const aqiColorMap = [
[0, [0, 255, 0]],
[50, [255, 255, 0]],
[100, [255, 126, 0]],
[150, [255, 0, 0]],
[200, [143, 63, 151]],
[300, [126, 0, 35]]
];
for (let i in aqiColorMap) {
let [target, color] = aqiColorMap[i];
if (target > aqi) {
if (i == 0) {
return calculateColors(color, color, 1);
}
let [prevTarget, prevColor] = aqiColorMap[i - 1];
return calculateColors(prevColor, color, (aqi - prevTarget) / (target - prevTarget));
}
}
let [, color] = aqiColorMap[aqiColorMap.length - 1];
return calculateColors(color, color, 1);
}
</script>
<style>
.gauge {
display: inline-block;
}
.gauge > div[role=meter] {
border-radius: 8rem 8rem 0 0;
border: 1px solid black;
width: 16rem;
height: 8rem;
background-color: black;
position: relative;
overflow: hidden;
background: conic-gradient(from 0deg at 50% 100%,
red 30deg, #8f3f97 30deg 60deg,
#7e0023 60deg 91deg, transparent 91deg 269deg,
#00e400 269deg 300deg, #ffff00 300deg 330deg,
#ff7e00 330deg 360deg);
}
.gauge > label {
font-size: 14px;
text-align: center;
background-color: black;
padding: .2rem 0;
display: block;
}
.dial {
background-color: #00ff00;
transition: background-color 1s, color .25s;
border-radius: 10rem 10rem 0 0;
width: 70%;
height: 70%;
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
position: absolute;
bottom: 0;
left: 15%;
z-index: 2;
overflow: hidden;
box-shadow: 0px 0px 0px 1rem #000;
border-bottom: none;
box-sizing: border-box;
color: #000;
}
.dial.white {
color: #fff;
}
.dial > span {
text-align: center;
font-family: sans-serif;
}
.dial > .arrow {
position: absolute;
left: calc(50% - .25rem);
bottom: 0;
width: .5rem;
height: calc(100% + 1px);
background-color: transparent;
transform-origin: bottom center;
transform: rotate(-90deg);
transition: transform 1s;
}
.dial > .arrow:after {
content: "";
border-left: .5rem solid transparent;
border-right: .5rem solid transparent;
border-top: .5rem solid #000;
position: absolute;
left: calc(50% - .5rem);
top: 0;
width: 0;
height: 0;
transition: border-color .25s;
}
.dial.white > .arrow:after {
border-top-color: #fff;
}
.aqi-num {
font-weight: bold;
font-size: 120%;
margin-top: 1.25rem;
}
@media (forced-colors: active) {
.dial {
border: 2px solid black;
border-bottom: none;
}
.gauge > div[role=meter] {
border-width: 2px;
border-bottom: none;
}
.gauge > label {
border: 2px solid black;
}
}
</style>
<div>
<label for="set-aqi">Set AQI:</label><input type="range" id="set-aqi" min="0" max="500" value="10" />
</div>
<div class="gauge">
<div role="meter" aria-valuemin="0" aria-valuemax="500" aria-labelledby="meter-label">
<div class="dial"><span class="aqi-num"></span><span>AQI</span><div class="arrow"></div></div>
</div>
<label class="label" id="meter-label">10 Minute Average US EPA PM2.5 AQI</label>
</div>
<script>
let range = document.getElementById("set-aqi");
setDial(range.value);
range.addEventListener("change", evt => {
setDial(evt.target.value);
});
</script>EitanI needed a meter to tell me what the air quality is like outside. Now I know! If you need one as well, or if you are looking for an accessible gauge for anything else, here you go. You can also mess with it on Codepen.This is how I surf the Internet2020-07-21T00:00:00+00:002020-07-21T00:00:00+00:00https://blog.monotonous.org/2020/07/21/this-is-how-i-surf-the-internet<p><img src="/assets/uploads/2020/07/newtabs.png" alt="tab bar with a bunch of new tabs" /></p>
<p>Aside from a handful of pinned tabs, I open a new tab for anything I need to do: search the web, file a bug, look up documentation, check on the news, the weather, you get the idea. I am also addicted to Firefox’s new tab page, so I’ll often open a new tab out of boredom to let Pocket suggest an article for me. I hardly ever look at the same tab twice. If I need to get to something, it is never worth digging through all those tabs, I’ll just type what I am looking for in a new tab, and hope for a good suggestion from the awesomebar. After a couple of days I’ll have hundreds of tabs open. I declare “tab bankruptcy”, I purge them all, and start over.</p>
<p>A while ago I made an addon for myself. It was essentially a tab FIFO. It would only allow 10 tabs to be open at a time. If an 11th tab was created, the least recently activated tab would be closed.</p>
<p><img src="/assets/uploads/2020/07/throttle-tabs-popup.png" alt="Throttle Tabs popup" style="max-width: 250px; float: right; margin-left: 1rem;" /></p>
<p>I came to think what if I am not the only person who abuses tabs in this way? What if there are other poor souls out there with hundreds or even thousands of open tabs. Are they waiting for Marie Kondo to hold their hand while they deliberate each tab before they discard it?</p>
<p>So I decided to polish my addon a bit, give it a UI, and put it up on AMO. Since users might not trust an addon that automatically closes tabs, I decided to add an “overflow” feature which is essentially tab purgatory. Instead of having the addon auto-close the tab, it hides it. The tab is still accessible via the addon’s popup, Firefox’s “Hidden Tabs” submenu, or through tab search in the awesomebar. The overflow can be capped too so it can permanently discard old tabs after a given limit.</p>
<p>It’s called Throttle Tabs, it has a cute logo (which I now realize looks like a tab with a poop emoji on it), and <a href="https://addons.mozilla.org/en-US/firefox/addon/throttle-tabs/">you can get it on addons.mozilla.org</a>.</p>EitanAside from a handful of pinned tabs, I open a new tab for anything I need to do: search the web, file a bug, look up documentation, check on the news, the weather, you get the idea. I am also addicted to Firefox’s new tab page, so I’ll often open a new tab out of boredom to let Pocket suggest an article for me. I hardly ever look at the same tab twice. If I need to get to something, it is never worth digging through all those tabs, I’ll just type what I am looking for in a new tab, and hope for a good suggestion from the awesomebar. After a couple of days I’ll have hundreds of tabs open. I declare “tab bankruptcy”, I purge them all, and start over. A while ago I made an addon for myself. It was essentially a tab FIFO. It would only allow 10 tabs to be open at a time. If an 11th tab was created, the least recently activated tab would be closed. I came to think what if I am not the only person who abuses tabs in this way? What if there are other poor souls out there with hundreds or even thousands of open tabs. Are they waiting for Marie Kondo to hold their hand while they deliberate each tab before they discard it? So I decided to polish my addon a bit, give it a UI, and put it up on AMO. Since users might not trust an addon that automatically closes tabs, I decided to add an “overflow” feature which is essentially tab purgatory. Instead of having the addon auto-close the tab, it hides it. The tab is still accessible via the addon’s popup, Firefox’s “Hidden Tabs” submenu, or through tab search in the awesomebar. The overflow can be capped too so it can permanently discard old tabs after a given limit.Revamping Firefox’s Reader Mode this Summer2019-08-06T00:00:00+00:002019-08-06T00:00:00+00:00https://blog.monotonous.org/2019/08/06/revamping-reader-view-mode<p><em>This is cross-posted from a <a href="https://medium.com/@akshithashetty84/revamping-firefoxs-reader-mode-this-summer-fa1b287879cf">Medium article by Akshitha Shetty</a>, a Summer of Code student I have been mentoring. It’s been a pleasure and I wish her luck in her next endeavor!</em></p>
<p>For me, getting all set to read a book would mean spending hours hopping between stores to find the right lighting and mood to get started. But with Firefox’s Reader Mode it’s now much more convenient to get reading on the go. And this summer, I have been fortunate to shift roles from a user to a developer for the Reader Mode . As I write this blog, I have completed two months as a Google Summer of Code student developer with Mozilla. It has been a really enriching experience and thus I would like to share some glimpses of the project and my journey so far.</p>
<h2 id="motivation-behind-choosing-this-organization-and-project">Motivation behind choosing this organization and project</h2>
<p>I began as an open-source contributor to Mozilla early this year. What really impressed me was how open and welcoming Mozillians were. Open-source contribution can be really intimidating at first. But in my case, the kind of documentation and direction that Mozilla provided helped me steer in the right direction really swiftly. Above all, it’s the underlying principle of the organization — “people first” that truly resonated with me. On going through the project idea list, the “Firefox Reader Mode Revamp” was of great interest to me. It was one of the projects where I would be directly enhancing the user-experience for Firefox users and also learning a lot more about user-experience and accessibility in the process.</p>
<h2 id="redesign-of-the-reader-mode-in-making">Redesign of the Reader mode in making</h2>
<p>The new design of the reader mode has the following features -</p>
<ol>
<li>A vertical toolbar is to replaced by a horizontal toolbar so that it is the sync with the other toolbars present in Firefox.</li>
<li>The toolbar is now being designed so that it complies with the Photon Design System (the latest design guidelines proposed by the organization).</li>
<li>The accessibility of the Reader Mode is being improved by making it keyboard friendly.</li>
</ol>
<figure>
<img src="/assets/uploads/2019/08/readerview-redesign.png" alt="" />
<figcaption>Mock-up for Reader Mode Redesign</figcaption>
</figure>
<p>Thanks to Abraham Wallin for designing the new UI for the Reader mode.</p>
<h2 id="get-set-code">Get Set Code</h2>
<p>Once the design was ready, I began with the coding of the UI. I thoroughly enjoyed the process and learnt a lot from the challenges I faced during this process. One of the challenges I faced during this phase was to make the toolbar adjust it’s width as per the content width of the main page. This required me to refactor certain portions of the existing code base as well make sure the newly coded toolbar follows the same.</p>
<h2 id="to-sum-it-all-up">To Sum it all up</h2>
<p>All in all, it has been a really exciting process. I would like to thank my mentor — <a href="https://blog.monotonous.org">Eitan Isaacson</a> for putting in the time and effort to mentor this project. Also I would like to thank — Gijs Kruitbosch and Yura Zenevich for reviewing my code at various points of time.</p>
<p>I hope this gets you excited to see the Reader Mode in its all new look ! Stay tuned for my next blog where I will be revealing the Revamped Reader Mode into action.</p>EitanThis is cross-posted from a Medium article by Akshitha Shetty, a Summer of Code student I have been mentoring. It’s been a pleasure and I wish her luck in her next endeavor! For me, getting all set to read a book would mean spending hours hopping between stores to find the right lighting and mood to get started. But with Firefox’s Reader Mode it’s now much more convenient to get reading on the go. And this summer, I have been fortunate to shift roles from a user to a developer for the Reader Mode . As I write this blog, I have completed two months as a Google Summer of Code student developer with Mozilla. It has been a really enriching experience and thus I would like to share some glimpses of the project and my journey so far. Motivation behind choosing this organization and project I began as an open-source contributor to Mozilla early this year. What really impressed me was how open and welcoming Mozillians were. Open-source contribution can be really intimidating at first. But in my case, the kind of documentation and direction that Mozilla provided helped me steer in the right direction really swiftly. Above all, it’s the underlying principle of the organization — “people first” that truly resonated with me. On going through the project idea list, the “Firefox Reader Mode Revamp” was of great interest to me. It was one of the projects where I would be directly enhancing the user-experience for Firefox users and also learning a lot more about user-experience and accessibility in the process. Redesign of the Reader mode in making The new design of the reader mode has the following features - A vertical toolbar is to replaced by a horizontal toolbar so that it is the sync with the other toolbars present in Firefox. The toolbar is now being designed so that it complies with the Photon Design System (the latest design guidelines proposed by the organization). The accessibility of the Reader Mode is being improved by making it keyboard friendly. Mock-up for Reader Mode Redesign Thanks to Abraham Wallin for designing the new UI for the Reader mode. Get Set Code Once the design was ready, I began with the coding of the UI. I thoroughly enjoyed the process and learnt a lot from the challenges I faced during this process. One of the challenges I faced during this phase was to make the toolbar adjust it’s width as per the content width of the main page. This required me to refactor certain portions of the existing code base as well make sure the newly coded toolbar follows the same. To Sum it all up All in all, it has been a really exciting process. I would like to thank my mentor — Eitan Isaacson for putting in the time and effort to mentor this project. Also I would like to thank — Gijs Kruitbosch and Yura Zenevich for reviewing my code at various points of time. I hope this gets you excited to see the Reader Mode in its all new look ! Stay tuned for my next blog where I will be revealing the Revamped Reader Mode into action.HTML Text Snippet Extension2019-07-30T00:00:00+00:002019-07-30T00:00:00+00:00https://blog.monotonous.org/2019/07/30/html-snippet-extension<p>I often need to quickly test a snippet of HTML, mostly to see how it interacts with our accessibility APIs.</p>
<p>Instead of creating some throwaway HTML file each time, I find it easier to paste in the HTML in devtools, or even make a data URI.</p>
<p>Last week I spent an hour creating an extension that allows you to just paste some HTML into the address bar and have it rendered immediately.</p>
<p>You just need to prefix it with the <code class="language-plaintext highlighter-rouge">html</code> keyword, and you’re good to go. Like this <code class="language-plaintext highlighter-rouge">html <h1>Hello, World!</h1></code>.</p>
<p>You can <a href="https://github.com/eeejay/quicksnippet/releases">download it from github</a>.</p>
<p>There might be other extensions or ways of doing this, but it was a quick little project.</p>EitanI often need to quickly test a snippet of HTML, mostly to see how it interacts with our accessibility APIs. Instead of creating some throwaway HTML file each time, I find it easier to paste in the HTML in devtools, or even make a data URI. Last week I spent an hour creating an extension that allows you to just paste some HTML into the address bar and have it rendered immediately. You just need to prefix it with the html keyword, and you’re good to go. Like this html <h1>Hello, World!</h1>. You can download it from github. There might be other extensions or ways of doing this, but it was a quick little project.How To Tweak Firefox's User Interface2017-11-15T16:07:50+00:002017-11-15T16:07:50+00:00https://blog.monotonous.org/2017/11/15/how-to-tweak-firefoxs-user-interface<p>(<strong>Warning:</strong> Modifying <code class="language-plaintext highlighter-rouge">userChrome.css</code> is not guaranteed to work between versions of Firefox and may lead to hard-to-diagnose bugs. Use at your own risk!)<br />
Firefox Quantum has made a clean break from Firefox’s legacy addons. Hooray!<br />
A casualty of this change is the ability to have addons that fundamentally alter Firefox’s user interface. This can be a problem if you depended on this for accessibility needs. Say, you had an addon that enlarged the fonts in Firefox’s chrome.<br />
Luckily, not all is lost. With some CSS knowledge, you can customize the Firefox user interface as much as you need. Simply drop some CSS rules into $PROFILE/chrome/userChrome.css.<br />
Here is an example rule that employs large yellow on black text:</p>
<pre><span style="color:#af5f00;">*</span> <span style="color:#06989a;">{</span>
<span style="color:#4e9a06;">font-size-adjust</span>: <span style="color:#cc0000;">0.75</span> <span style="color:#75507b;">!important</span>;
<span style="color:#4e9a06;">background-color</span>: <span style="color:#cc0000;">#000</span> <span style="color:#75507b;">!important</span>;
<span style="color:#4e9a06;">color</span>: <span style="color:#cc0000;">yellow</span> <span style="color:#75507b;">!important</span>;
<span style="color:#06989a;">}</span>
</pre>
<p>The effect on Firefox will be dramatic:</p>
<figure id="attachment_1338" style="width: 660px" class="wp-caption aligncenter"><img class="size-large wp-image-1338" src="/assets/uploads/2017/11/screenshot-from-2017-11-15-16-01-53.png?w=660" alt="" width="660" height="469" srcset="/assets/uploads/2017/11/screenshot-from-2017-11-15-16-01-53.png 2560w, /assets/uploads/2017/11/screenshot-from-2017-11-15-16-01-53-300x213.png 300w, /assets/uploads/2017/11/screenshot-from-2017-11-15-16-01-53-768x546.png 768w, /assets/uploads/2017/11/screenshot-from-2017-11-15-16-01-53-1024x728.png 1024w" sizes="(max-width: 660px) 100vw, 660px" /><figcaption class="wp-caption-text">Restylized user interface with yellow on black text</figcaption></figure>
<p>Note, this <strong>will</strong> break things, and it will not be perfect. Before using this kind of solution check what accessibility features your platform provides.</p>Eitan(Warning: Modifying userChrome.css is not guaranteed to work between versions of Firefox and may lead to hard-to-diagnose bugs. Use at your own risk!) Firefox Quantum has made a clean break from Firefox’s legacy addons. Hooray! A casualty of this change is the ability to have addons that fundamentally alter Firefox’s user interface. This can be a problem if you depended on this for accessibility needs. Say, you had an addon that enlarged the fonts in Firefox’s chrome. Luckily, not all is lost. With some CSS knowledge, you can customize the Firefox user interface as much as you need. Simply drop some CSS rules into $PROFILE/chrome/userChrome.css. Here is an example rule that employs large yellow on black text: * { font-size-adjust: 0.75 !important; background-color: #000 !important; color: yellow !important; } The effect on Firefox will be dramatic: Restylized user interface with yellow on black text Note, this will break things, and it will not be perfect. Before using this kind of solution check what accessibility features your platform provides.Phoropter: A Vision Simulator2017-11-06T07:38:53+00:002017-11-06T07:38:53+00:00https://blog.monotonous.org/2017/11/06/phoropter-a-vision-simulator<p>After porting <a href="http://blog.monotonous.org/2017/10/13/nocoffee-visual-impairment-simulator/">Aaron’s NoCoffee extension</a> to Firefox, I thought it would be neat to make a camera version of that. Something you can carry around with you, and take snapshots of websites, signs, or print material. You can then easily share the issues you see around you.<br />
I’m calling it Phoropter, and you can <a href="https://eeejay.github.io/phoropter">see it here</a> (best viewed with Chrome or Firefox on Android).<br />
I could imagine this is what Pokémon Go is like if instead of creatures you collected mediocre designs.<br />
Say you are looking at a London Underground map, and you notice the legend is completely color reliant. Looking through Phoropter you will see what the legend would look like to someone with <a href="http://www.color-blindness.com/protanopia-red-green-color-blindness/">protanopia</a>, red-green color blindness.<br />
<img class="wp-image-1283 aligncenter" src="/assets/uploads/2017/11/screenshot-nov-2-2017-2_10_50-pm1.png" alt="Screenshot (Nov 2, 2017 2_10_50 PM)(1)" width="334" height="593" srcset="/assets/uploads/2017/11/screenshot-nov-2-2017-2_10_50-pm1.png 1080w, /assets/uploads/2017/11/screenshot-nov-2-2017-2_10_50-pm1-169x300.png 169w, /assets/uploads/2017/11/screenshot-nov-2-2017-2_10_50-pm1-768x1365.png 768w, /assets/uploads/2017/11/screenshot-nov-2-2017-2_10_50-pm1-576x1024.png 576w" sizes="(max-width: 334px) 100vw, 334px" /><br />
You can then grab a snapshot with the camera icon and get a side-by-side photo that shows the difference in perception. You can now alert the transit authorities, or at least shame them on Twitter.<br />
<img class="alignnone size-full wp-image-1312" src="/assets/uploads/2017/11/image3.jpg" alt="A side-by-side snapshot of the London Tube's legend with typical vision on the left and protonopia on the right" width="960" height="690" srcset="/assets/uploads/2017/11/image3.jpg 960w, /assets/uploads/2017/11/image3-300x216.jpg 300w, /assets/uploads/2017/11/image3-768x552.jpg 768w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><br />
Once you get into it, it’s quite addicting. No design is above scrutiny.<br />
<img class="alignnone wp-image-1315 size-full" src="/assets/uploads/2017/11/image2.jpg" alt="A page from a workbook displayed side-by-side with typical and green-red blindness." width="960" height="690" srcset="/assets/uploads/2017/11/image2.jpg 960w, /assets/uploads/2017/11/image2-300x216.jpg 300w, /assets/uploads/2017/11/image2-768x552.jpg 768w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><br />
I started this project thinking I can pull it off with CSS filters on a video element, but it turns out that is way to slow. So I ended up using WebGL via <a href="http://evanw.github.io/glfx.js/">glfx.js</a>. Tried to make is as progressive as possible, you can add it to your home screen. I won’t bore you with the details, check out the <a href="https://github.com/eeejay/phoropter">source</a> when you have a chance.<br />
There are still many more filters I can add later. In the meantime, open this in your mobile browser and,<br />
<a href="https://eeejay.github.io/phoropter"><img class="size-full wp-image-1322 aligncenter" src="/assets/uploads/2017/11/cooltext264671948409237.png" alt="Collect Them All!" width="633" height="115" srcset="/assets/uploads/2017/11/cooltext264671948409237.png 633w, /assets/uploads/2017/11/cooltext264671948409237-300x55.png 300w" sizes="(max-width: 633px) 100vw, 633px" /></a></p>EitanAfter porting Aaron’s NoCoffee extension to Firefox, I thought it would be neat to make a camera version of that. Something you can carry around with you, and take snapshots of websites, signs, or print material. You can then easily share the issues you see around you. I’m calling it Phoropter, and you can see it here (best viewed with Chrome or Firefox on Android). I could imagine this is what Pokémon Go is like if instead of creatures you collected mediocre designs. Say you are looking at a London Underground map, and you notice the legend is completely color reliant. Looking through Phoropter you will see what the legend would look like to someone with protanopia, red-green color blindness. You can then grab a snapshot with the camera icon and get a side-by-side photo that shows the difference in perception. You can now alert the transit authorities, or at least shame them on Twitter. Once you get into it, it’s quite addicting. No design is above scrutiny. I started this project thinking I can pull it off with CSS filters on a video element, but it turns out that is way to slow. So I ended up using WebGL via glfx.js. Tried to make is as progressive as possible, you can add it to your home screen. I won’t bore you with the details, check out the source when you have a chance. There are still many more filters I can add later. In the meantime, open this in your mobile browser and,NoCoffee: Visual Impairment Simulator2017-10-13T09:37:49+00:002017-10-13T09:37:49+00:00https://blog.monotonous.org/2017/10/13/nocoffee-visual-impairment-simulator<p>Four years ago, on a snowy February day, Aaron Leventhal huddled in his unheated home and <a href="https://accessgarage.wordpress.com/2013/02/09/458/">created</a> a Chrome extension called NoCoffee. This extension allows users to experience web content through different lenses of visual impairments<em>.<br />
I recently ran across this extension again, and thought it is high-time we ported it to Firefox. Firefox’s support of WebExtension standards means that this should be trivial. It was! With Aaron’s permission, I <a href="https://github.com/eeejay/NoCoffee">posted the source to github</a> and did some tweaking and cleanup.<br />
You can now <a href="https://addons.mozilla.org/en-US/firefox/addon/nocoffee/">try out the extension in Firefox</a>!<br />
_</em> Not medically or scientifically accurate._</p>EitanFour years ago, on a snowy February day, Aaron Leventhal huddled in his unheated home and created a Chrome extension called NoCoffee. This extension allows users to experience web content through different lenses of visual impairments. I recently ran across this extension again, and thought it is high-time we ported it to Firefox. Firefox’s support of WebExtension standards means that this should be trivial. It was! With Aaron’s permission, I posted the source to github and did some tweaking and cleanup. You can now try out the extension in Firefox! _ Not medically or scientifically accurate._Firefox's Accessibility Preferences2017-07-21T11:21:48+00:002017-07-21T11:21:48+00:00https://blog.monotonous.org/2017/07/21/firefoxs-accessibility-preferences<p>If you use Firefox Nightly, you may notice that there is no more <em>Accessibility</em> section in the preferences screen, this change will arrive in Firefox 56 as part of a preferences reorg. This is good news!<br />
<img class="aligncenter wp-image-1255 size-large" src="/assets/uploads/2017/07/screenshot-from-2017-07-21-10-40-52.png?w=660" alt="Screenshot of the new "Browsing" section, which includes scrolling options as well as search while you type and cursor keys navigation." width="660" height="147" srcset="/assets/uploads/2017/07/screenshot-from-2017-07-21-10-40-52.png 1654w, /assets/uploads/2017/07/screenshot-from-2017-07-21-10-40-52-300x67.png 300w, /assets/uploads/2017/07/screenshot-from-2017-07-21-10-40-52-768x171.png 768w, /assets/uploads/2017/07/screenshot-from-2017-07-21-10-40-52-1024x228.png 1024w" sizes="(max-width: 660px) 100vw, 660px" /><br />
Cursor browsing and search while you type, are still available under the <em>Browsing</em> section, as these options offer convenience for everybody, regardless of disability. Users should now be able to find an option under an appropriate feature section, or search for it in the far upper corner. This is a positive trend, that I hope will continue as we imagine our users more broadly with a diverse set of use-cases, that include, but are not exclusive to disability.<br />
Thanks to everyone who made this happen!</p>EitanIf you use Firefox Nightly, you may notice that there is no more Accessibility section in the preferences screen, this change will arrive in Firefox 56 as part of a preferences reorg. This is good news! Cursor browsing and search while you type, are still available under the Browsing section, as these options offer convenience for everybody, regardless of disability. Users should now be able to find an option under an appropriate feature section, or search for it in the far upper corner. This is a positive trend, that I hope will continue as we imagine our users more broadly with a diverse set of use-cases, that include, but are not exclusive to disability. Thanks to everyone who made this happen!