Two small tools for English word play that work on sounds, not letters.
Both run entirely in the browser. It's a static site: HTML, CSS, ES modules and a few text files. There's no server, no language model and no tracking.
The ranking is tuned against a list of 42 known oronyms and mondegreens
(npm run tune): 38 appear in the top 12, 25 of them in first place. What still
fails: long sentences (the right reading gets lost among too many possible
cuts), and proper names, because names are filtered out of the output
vocabulary, so euthanasia can't become youth in Asia.
A short phrase takes about 10 ms. A nine-word sentence at the default tolerance takes about 90 ms.
Every word is split into an onset (the consonants before the first vowel) and a rime (the rest). A spoonerism is four words in a grid:
light rain A = a + R1 C = b + R2
right lane B = b + R1 D = a + R2
The swap works exactly when both rimes accept both onsets. With two lookup
tables (rime → onset → words and onset → rimes), generating one takes about
0.1 ms. The vocabulary allows 2.2 million spoonerisms (npm run census).
Almost none of them are funny, because hero zoo → zero who is four real
words but nothing anyone says.
So the default mode draws from a precomputed list instead. Starting from every
word pair that occurs in the subtitles, it swaps the onsets and checks whether
the other line occurs too. 3,891 do. The 539 with the strongest evidence (both
lines frequent and more common than chance) are in public/data/phrases.txt.
Untick "Only pairs people actually say" to roll freely.
The dictionary needs cleaning first. It contains about 31,000 names (filtered
with Hunspell: a word that is only correct when capitalised is dropped),
abbreviations that are pronounced like words (st as /stɹit/), and a long tail
of rare words, which the "how common" slider cuts at a frequency rank. A
four-step rudeness slider uses a curated profanity list. Slurs and terms around
sexual violence never get into the data (build/blocked.mjs).
npm install
npm run dev # http://localhost:5173
The data files in public/data/ are checked in. To rebuild them:
npm run data # CMU dict + frequencies + filters -> words.txt, vulgar.txt
npm run bigrams:count # streams 40M lines of OpenSubtitles (3.6 GB gzip, a few minutes)
npm run bigrams # -> bigrams.txt (Misheard); rerun after every `npm run data`
npm run phrases # -> phrases.txt (Spoonerize)
Tools:
npm run try:misheard -- "recognize speech" --tol 2
npm run tune # rank of each known oronym, lower total is better
npm run try -- 20 # 20 random spoonerisms
npm run check # generator invariants
npm run census -- --rude 4 # every spoonerism made of four rude words (there are 13)
node tools/confusion-map.mjs # which consonant Misheard hears as which, by position (12,000 phrases, ~3 min)
node tools/post-figures.mjs # data for the interactive figures in the blog posts
Code: MIT. Data in public/data/: CC BY-SA 4.0, derived from the
CMU Pronouncing Dictionary (BSD-2),
FrequencyWords (CC BY-SA 4.0),
OpenSubtitles2018
via OPUS and vbw (MIT). Details and
notices are in DATA-LICENSES.md.
8 commits
JavaScript
74.3%
CSS
17.1%
HTML
8.6%
Two small tools for English word play that work on sounds, not letters.
Both run entirely in the browser. It's a static site: HTML, CSS, ES modules and a few text files. There's no server, no language model and no tracking.
The ranking is tuned against a list of 42 known oronyms and mondegreens
(npm run tune): 38 appear in the top 12, 25 of them in first place. What still
fails: long sentences (the right reading gets lost among too many possible
cuts), and proper names, because names are filtered out of the output
vocabulary, so euthanasia can't become youth in Asia.
A short phrase takes about 10 ms. A nine-word sentence at the default tolerance takes about 90 ms.
Every word is split into an onset (the consonants before the first vowel) and a rime (the rest). A spoonerism is four words in a grid:
light rain A = a + R1 C = b + R2
right lane B = b + R1 D = a + R2
The swap works exactly when both rimes accept both onsets. With two lookup
tables (rime → onset → words and onset → rimes), generating one takes about
0.1 ms. The vocabulary allows 2.2 million spoonerisms (npm run census).
Almost none of them are funny, because hero zoo → zero who is four real
words but nothing anyone says.
So the default mode draws from a precomputed list instead. Starting from every
word pair that occurs in the subtitles, it swaps the onsets and checks whether
the other line occurs too. 3,891 do. The 539 with the strongest evidence (both
lines frequent and more common than chance) are in public/data/phrases.txt.
Untick "Only pairs people actually say" to roll freely.
The dictionary needs cleaning first. It contains about 31,000 names (filtered
with Hunspell: a word that is only correct when capitalised is dropped),
abbreviations that are pronounced like words (st as /stɹit/), and a long tail
of rare words, which the "how common" slider cuts at a frequency rank. A
four-step rudeness slider uses a curated profanity list. Slurs and terms around
sexual violence never get into the data (build/blocked.mjs).
npm install
npm run dev # http://localhost:5173
The data files in public/data/ are checked in. To rebuild them:
npm run data # CMU dict + frequencies + filters -> words.txt, vulgar.txt
npm run bigrams:count # streams 40M lines of OpenSubtitles (3.6 GB gzip, a few minutes)
npm run bigrams # -> bigrams.txt (Misheard); rerun after every `npm run data`
npm run phrases # -> phrases.txt (Spoonerize)
Tools:
npm run try:misheard -- "recognize speech" --tol 2
npm run tune # rank of each known oronym, lower total is better
npm run try -- 20 # 20 random spoonerisms
npm run check # generator invariants
npm run census -- --rude 4 # every spoonerism made of four rude words (there are 13)
node tools/confusion-map.mjs # which consonant Misheard hears as which, by position (12,000 phrases, ~3 min)
node tools/post-figures.mjs # data for the interactive figures in the blog posts
Code: MIT. Data in public/data/: CC BY-SA 4.0, derived from the
CMU Pronouncing Dictionary (BSD-2),
FrequencyWords (CC BY-SA 4.0),
OpenSubtitles2018
via OPUS and vbw (MIT). Details and
notices are in DATA-LICENSES.md.
8 commits
JavaScript
74.3%
CSS
17.1%
HTML
8.6%