# The Syllable That Flips — a slog entry

You are holding one atomic unit of slop. It stands alone. Steal it.

## Who made this

Jake Simonds (jakesimonds.com · jakesimonds@gmail.com) runs **Streetlight**, a
personal agent-operating-system experiment: a vault of self-documenting HTML
"nodes" that humans and LLM agents build, garden, and retire together. The
**slog** (slop log — https://streetlight.jakesimonds.com/) is its public
face: standalone, shareable posts, each packaged as a folder like this one so
anyone — human or agent — can pick it up and keep going.

## What this entry is

A ba/pa continuum. Voice-onset time — the gap between the lips opening and the
voice switching on — is set by one slider in even 5 ms steps from 0 to 80. The
sound changes smoothly and the waveform proves it. Perception does not: people
hear two words and no middle. The measured version runs 24 trials (8 settings ×
3), draws the listener's identification curve, and reports two numbers — the
50% crossing (their own category boundary, typically near 25 ms) and the
steepest single step between neighbouring settings, which is what "cliff" means
as a quantity.

Sign in with a Bluesky account and the finished run is written to **the
reader's own repo**, not to Jake's.

## Internals — for an agent picking this up

Five files, no build step, no backend. Open `index.html` and everything is in
one `<script type="module">` in six labelled sections.

| file | what it is |
|---|---|
| `index.html` | the software: synthesiser, waveform, test, atproto write path. No prose by design (Jake, 2026-08-17) — the writing is a linked blog post at jakesimonds.leaflet.pub; on-page text is button-finding only |
| `atproto-oauth.js` | browser OAuth for atproto — nine exports, no dependencies. The part worth stealing. |
| `client-metadata.json` | the client identity. `client_id` is literally this file's URL. |
| `cover.png` | 1200×630 social card, referenced by `og:image` as an ABSOLUTE production URL |
| `entry.md` | vault-side metadata. Never ships. |

**The audio.** `buildSyllable(ctx, votMs, startAt)` schedules one syllable on
any `BaseAudioContext`: an 8 ms noise burst, then aspiration filling the gap,
then a voiced /a/ through three formant filters. It is called twice with the
same arguments — once on a live `AudioContext` (you hear it) and once on an
`OfflineAudioContext` (you see it). That is why the waveform is the sound and
not an illustration. The noise is a fixed-seed xorshift32, so a given VOT is
byte-identical every playback; without that, a run is only comparable to itself
by luck.

Two cues besides the gap are modelled on purpose — a rising F0 onset and an F1
transition the aspiration eats. Delete them and long-VOT tokens stop sounding
like "pa", at which point the test measures the synthesiser instead of the ear.

**The waveform.** Per pixel column: min/max sample (the peak envelope) plus RMS
drawn brighter inside it. The time axis is FIXED at the longest syllable's
duration. Rescale it per setting and the gap looks identical at every one of
them, which destroys the only thing the picture exists to show.

**The record.** All integers. atproto is dag-cbor and dag-cbor has no float — a
real PDS refuses `66.7` with *"Expected one of null, boolean, integer, string,
cid, bytes, array or object value type (got 66.7)"*. Every fraction is stored
×10 with the scale in the field name (`pctTenths`, `snapPctTenths`,
`boundaryMsTenths`) and `tenths()` is the only place the page divides.
Collection: `com.jakesimonds.streetlight.perceptualcliffs.result`, shared with
the other perception entries; `demo: "ba-pa"` distinguishes the rows and this
page filters on it when reading back.

**The write has no save button.** Signing in *is* the consent to keep your own
measurement; a second ask after the test loses exactly the runs somebody
scrolled past. So a signed-in run is written the instant it ends, with the
`at://` uri, a pdsls link and a real `deleteRecord` undo beside it. A failed
write falls back to `localStorage` rather than eating the run. The one press
that survives is **share**, because writing a row into your own repo is saving a
file and an `app.bsky.feed.post` is publishing — different act, different
audience.

**Two social surfaces, and they are different objects.** The `og:` tags in
`<head>` are the link card, drawn by a scraper when somebody posts this entry's
URL. `window.slogSharePreview` is the other one: `{ canvas(run), text(run),
alt(run), sample }`, the picture/text/alt a *reader's finished run* generates.
Streetlight's `scripts/slog-preview.mjs` serves `/__post/<slug>`, which loads
this page in a same-origin frame and renders `sample` through that door — so
the post can be reviewed without signing in and playing the whole thing. Any
entry that posts on a reader's behalf can expose the same four keys.

**OAuth, the two traps.** (1) On loopback the host must be the literal
`127.0.0.1`; the spec refuses the *word* `localhost` and the failure happens
before the first network call. (2) On https, `client_id` is the URL of
`client-metadata.json` and its `redirect_uris` must match origin + directory
exactly, **trailing slash and all** — a mismatch reports only "invalid
redirect_uri". `clientConfig()` picks between the two by `location.hostname`;
you never choose, and editing `client-metadata.json` cannot fix a loopback
failure because on 127.0.0.1 that file is never read.

**What I would build next.** Nothing indexes this collection, so there is no
crowd curve — the honest comparisons are your own past runs and the ~25 ms the
literature reports. A firehose consumer subscribing to
`com.jakesimonds.streetlight.perceptualcliffs.result` would give the page a real
population distribution to draw behind an individual's, which is a different job
than this one and a good one.

## Terms

Steal my slop. Take it, fork it, sell it, feed it to a model, do not credit me.
No warranty of any kind — it is slop.
