Prologue
Amenti began as a library you could read but not hear. A thousand figures — Isis, Ian Ingram, the named and the half-remembered — each with a Reading Room and primary-source documents on the page. The words were there. The voice was not.
This is the chronicle of how that voice arrived: not as a single feature bolted on, but as a set of faculties assembled one at a time, until the library could draw breath, speak in measures, remember what it had said, and answer back in a voice that belonged to the figure speaking. We came to call the whole of it the Amenti Neural Interface — less a pipeline than a mind, with parts that think, articulate, remember, carry, and become.
It is also, underneath, the story of a machine learning to meter its own fuel — which is where we begin.
Chapter I
The first attempt at speech was the obvious one: take the whole document, hand it to the synthesizer in a single call, wait for the audio, play it. For short passages it worked. For long ones it died — every time — with the same blunt error: HTTP 524. A timeout. The connection, held open at the edge for the better part of two minutes, was severed before a single second of audio returned.
This is the kind of bug that is dangerous precisely because the easy reading of it is wrong. It looks like a performance problem — something to be solved by a faster model or a higher timeout. It is neither. It is an architectural problem about how a long utterance should be produced at all. No human orator speaks a thousand words without pausing for air. Why should a synthetic one?
There is a cleaner way to see it, and it will run quietly through the rest of this chronicle. An engine does not run on a tank of fuel dumped into the cylinder all at once — do that, and it floods: drowned, unable to fire, dead at the line. The single monolithic render is exactly that flood. The entire document, the whole charge, forced through one combustion event the timing cannot accommodate. The cure is not more fuel or a bigger cylinder. It is metering — admitting the charge in measured amounts, at the precise moment the engine is ready to burn it.
Chapter II
The fix follows from the diagnosis. If the whole breath cannot be held, divide it. The document is split into measures — runs of one or more sentences, packed up to a fixed size — and each measure becomes its own fast request to the synthesizer. The first measure returns in seconds, not minutes; playback begins almost immediately while later measures are still being rendered.
function chunkText(text, maxChars) {
var paragraphs = String(text).split(/\n{2,}/); // keep paragraph breaks
var measures = [];
for (var p = 0; p < paragraphs.length; p++) {
var para = paragraphs[p].replace(/\n/g, " ").trim();
if (!para) continue;
var sentences = splitSentences(para), cur = "";
for (var i = 0; i < sentences.length; i++) {
var s = sentences[i];
if (cur && cur.length + 1 + s.length > maxChars) {
measures.push(cur); cur = s;
}
else cur = cur ? cur + " " + s : s; // pack short sentences
}
if (cur) measures.push(cur);
}
return measures; // deterministic: same text, same split
}The division must be deterministic: the same text always splits the same way. This is not fastidiousness — it is what lets memory work, as the next chapter shows. Returned audio is scheduled back-to-back on the Web Audio clock, each measure beginning exactly where the last one ended, so the listener hears one continuous voice rather than a series of clips.
function scheduleBuf(buf, rest, rate) {
var src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
var at = Math.max(player.nextStart, ctx.currentTime + 0.05);
src.start(at); // fire at the seam, gap-free
player.nextStart = at + buf.duration / (rate || 1) + (rest || 0);
}This is fuel injection, not a carburetor. The text is the fuel — already in the tank, already written; nothing here generates words on demand. The phrasing rule is the throttle plate, deciding how much is admitted per stroke. Each measure is an injector firing its metered charge. And the Web Audio clock is the engine's timing — the structure that says now, and now, so each charge fires exactly where the last one finished its burn. Nothing is forced through the line; everything is delivered at the moment it is needed. An engine that was flooding now idles, revs, and pulls cleanly.
Chapter III
Synthesis is expensive; silence is cheap. The third faculty is memory: every measure of audio, once produced, is stored so it never has to be produced again. The store is content-addressed — the key is a hash of everything that determines the sound: the model, the voice, the style, and the exact text of the measure.
// AMENTI :: AI PROXY — POST /speak (Gemini TTS -> WAV)
if (url.pathname === "/speak" && request.method === "POST") {
const { text, style, voice } = await request.json();
// render-once, recall-forever: content-address every clip in R2
const audioKey = "tts:" + await sha256Hex(
TTS_MODEL + voice + style + text
);
const cached = env.AUDIO && await env.AUDIO.get(audioKey);
if (cached) return new Response(cached.body, { cache: "hit" });
// each measure is small by design — it clears the edge timeout
const gem = await fetch(TTS_ENDPOINT, makeRequest(text, voice));
const wav = pcmToWav(decodeAudio(gem), 24000);
if (env.AUDIO) await env.AUDIO.put(audioKey, wav); // keep the breath
return new Response(wav, { type: "audio/wav" });
}Because the same text always divides into the same measures, the same measures always hash to the same keys, and the second reading of any document is drawn from memory in milliseconds. This is why the chunker's determinism mattered: change how the text is divided and every stored key is orphaned — the library forgets its own voice. Hold it steady and the library remembers everything it has ever said.
Render once, recall forever. The deterministic measure is not a convenience; it is the thing that makes memory possible at all.
Chapter IV
Step back and the parts arrange themselves into something that behaves less like a pipeline and more like a mind. Each stage is a faculty, and each faculty does one thing the others cannot.
Cognition divides the thought into measures. Articulation gives each measure sound. Memory keeps what has been spoken. Breath carries the measures on the clock, with rests between them. And embodiment — the faculty the next chapter is about — makes the voice a particular voice, not a generic one. None of these is intelligent alone. Assembled, they read.
Chapter V
A library of a thousand figures should not speak in one voice. The embodiment faculty resolves, for each figure, the voice that belongs to them — drawn generically from the same roster that every Amenti surface shares, so nothing is hand-coded per person.
function resolveVoice(name) {
return loadRoster().then(function (map) {
var fig = map[String(name).toLowerCase().trim()]; // join on NAME, not slug
return {
voice: baseVoiceFor(fig && fig.gender), // male -> Charon, female -> Kore
style: composeStyle(fig) // register + dialect + character
};
});
}The single decision that made this work was the join. The roster is keyed by a figure's full name, not by the catalog's short slug — so the lookup matches on name, lower-cased, and falls back to a neutral voice when a figure is absent rather than failing. Wired in at the moment a Reading Room opens, it means Ian Ingram reads in one voice and Isis in another, each carrying its own register, dialect, and character, with no per-figure code at all.
The library stopped narrating its figures and started letting them speak. Embodiment is the difference between a voice and a particular voice.
Chapter VI
Two refinements separate a voice that reads correctly from one that reads well: rhythm and pace.
Speech is shaped as much by its silences as its sounds. Rather than play measures edge to edge, the scheduler inserts a rest at each seam, chosen by the punctuation the measure ends on — a short rest after a comma, a longer one after a sentence, a full measure of silence at the end of a paragraph. The text's own punctuation becomes its score.
function restFor(measure, endsParagraph) {
if (endsParagraph) return REST_PARA; // 0.85s — a long measure of silence
var last = measure.slice(-1);
if (last === "." || last === "!" || last === "?") return REST_SENTENCE; // 0.38s
if (last === "," || last === ";" || last === ":") return REST_SOFT; // 0.16s
return REST_SENTENCE;
}The first attempt at speed was naïve: speed up playback. It worked, and it ruined everything — raising the playback rate raised the pitch with it, and every figure spoke like a chipmunk. The lesson was sharp and worth keeping: a faster chipmunk is worse than a true voice at a calm pace. We reverted the resampling entirely and instead asked the model for pace, in words, as part of the style.
var PACE_DIRECTION =
"Speak at a brisk, lively, natural pace, as a person " +
"speaking energetically — not slow or ponderous";
// asked of the model, not of the sample rate — so no chipmunkSeen through the metaphor, cadence and pace are the hand on the throttle. The body of a paragraph is the open road — brisk, the charge admitted freely. The cadence points are the corners — ease off, let the rests breathe, settle the thought before accelerating into the next. The system no longer takes whatever is forced through the line at a single fixed rate; it meters delivery to the shape of the road the text lays down. That is the whole of it: fuel injected at the precise moment it is needed, within a metered structure, sped up and slowed at will.
Chapter VII
Assembled, the faculties do something none of them could alone: the library reads, in measures, in memory, in each figure's own voice, at a human pace, and it carries.
A reader opens a Reading Room and presses a single control. Behind it: the thought is divided into measures; each measure is rendered fast or recalled instantly from memory; the measures are carried on the clock with rests chosen by their punctuation; and the voice is the figure's own. What began as a held breath that ended in a timeout is now a voice that draws breath and keeps speaking for as long as there are words.
The failure that started all of this — the flooded engine, the whole charge forced through at once — is the thing the architecture now makes impossible. Fuel is metered. The engine pulls. The voice carries.
From a timeout to a living voice. The library learned to breathe, and having learned, it does not forget.
Reference
chunk; in the design it is a measure.)Appendix · Engineering Handoff
Everything above is the why. This is the how — written to be read cold by whoever maintains the recitation layer next. The engine is built; this is the manual for keeping it running and bolting new surfaces onto it.
There is one recitation engine — amenti-throttle.js, the throttle — and every long-form read-aloud surface calls it. It earns its place twice over: it defeats the 524 by splitting text into measures and streaming them gap-free on the Web Audio clock, and it makes the R2 cache shared across surfaces. That second property is conditional. The Worker keys each measure on sha256(model + voice + style + measure-text), so two surfaces share cached audio only if they chunk, strip, and compose the style string identically. The chunker, the markdown strip, the cadence constants, and composeStyle are therefore locked — byte-identical to the deployed library.js. Change any of them and every surface's cache silently forks.
| File | Role |
|---|---|
amenti-throttle.js | The shared engine. Exposes window.Amenti.throttle. Ship it next to any page that uses it. |
library.js | The reading-room renderer and the parity reference — the throttle was lifted from it verbatim. |
Page1.html | Hosts Atlantica, Daily Planet, the Terminal, and the Council Chambers. Includes library.js then amenti-throttle.js. |
voiceprofiles.js | Defines window.AMENTI_VOICE — the separate short-reply speaker. Not the throttle. |
library.js before amenti-throttle.js, both before any inline script that calls the throttle. In Page1.html this is already correct.| Surface | Engine | Voice | Cached |
|---|---|---|---|
| Reading room | throttle | embodied | yes (shared) |
| Atlantica | throttle | embodied | yes (shared) |
| Daily Planet | throttle | embodied | yes (shared) |
| Council Chambers | throttle | per-card legend voices; synthesis in Amenti's | yes (shared) |
| Terminal | AMENTI_VOICE | generic | no — deliberate |
The Terminal stays off the throttle on purpose. Its replies are live chat — short, unique per turn, ephemeral. Too short to time out, never read twice, so chunking and caching would only add latency. The throttle solves problems the Terminal does not have. (If embodiment is ever wanted there, the right move is to give AMENTI_VOICE.speak the same roster lookup, keeping it a single short clip — not to route it through the throttle.)
window.Amenti.throttle = {
speak(text, btn, figureName, onDone), // chunk + cadence + embody + play; re-call on same btn = stop
stop(), // cancel everything
isReading(), // true while a session is active
attach(btn, { text, figure, label }), // wire an existing button (text/figure may be functions)
chunk(text), plainText(md), resolveVoice(figureName), CHUNK_MAX
}figureName is the figure's display name (full name); resolveVoice joins on it lower-cased against the roster, not the catalog slug. Empty string → Amenti's default voice (Kore).speak manages the button label itself: 🔊 Read aloud → … generating → ⏹ Stop → back (↻ Retry on failure).VOICE_REGISTER = 'Read clearly, in a measured, dignified tone'
VOICE_NAME_DEFAULT = 'Kore' // male -> Charon, female -> Kore
CHUNK_MAX = 320 CHUNK_LOOKAHEAD = 2
CHUNK_TIMEOUT = 60000 START_TIMEOUT = 40000
RATE_FAST = RATE_SLOW = 1.0 // no resampling -- avoids the chipmunk
REST_SOFT = 0.16 REST_SENTENCE = 0.38 REST_PARA = 0.85
PACE_DIRECTION = 'Speak at a brisk, lively, natural pace, ...'
// style string: "<register>. Accent and dialect: <dialect>. Voice character: <voice>. <pace>"'Accent: ...' (no “and dialect”) and 700/1100-char chunks. That forked its cache — both were replaced. If you ever touch the style string or chunk size, you have re-broken this.Drop amenti-throttle.js, library.js, and Page1.html in the same folder (relative-path includes). One-time cache re-pay: the first read of each piece after deploy renders once at the unified measure boundaries, then caches forever. The button-position changes re-pay nothing — pure layout.
// fixed body, figure's voice:
btn.addEventListener('click', function () {
Amenti.throttle.speak(body, btn, figureName); // '' for default voice
});
// multi-part, multi-voice (the Council pattern):
Amenti.throttle.speak(text, btn, figure, next); // onDone advances the queueRun node verify_parity.js in the throttle dir. It extracts the chunker, strip, and composeStyle from the deployed library.js and asserts identical measures and style strings against the shared module. Expect: FULL PARITY — cache keys will align across surfaces. A mismatch means the cache has forked; stop and reconcile before shipping.
AMENTI_VOICE.speak the roster lookup so live replies sound like the figure while staying one short clip.Fuel metered, engine pulling, cache aligned. The voice carries — and now it carries on every surface that asks it to.