One new file, no code: a design-exploration note for #93, which asks not for an integration but for a recommendation — what of the game's generated prose to voice, when (on-demand vs pre-generated), which vendor, and the cost/latency/storage tradeoffs. The note lands on a lean v1: voice the epilogue first, on-demand then cached, using OpenAI gpt-4o-mini-tts.
What makes the note worth reading top to bottom is that it's grounded in the code — and in grounding it, it corrects four of the issue's own claims. This guide walks those four, each next to the source that settles it. Read it and you can vouch that the recommendation rests on what the code actually does.
The TL;DR table and the first load-bearing decision.
23| **Voice the epilogue** (on-demand → cache) | **Opt-in "listen"** on the per-turn Chronicle | Auto-voicing the Chronicle every turn (the cost + latency trap) |
24| **A TTS lane** through the AI seam (cost on the same `[ai]` line) | **Pre-generate** the 10 curated objective briefs | Per-figure character voices (charming, but multiplies calls + complexity) |
25| **Content-hash audio cache** (Supabase Storage bucket) | User-selectable narrator voice (ties to [#90](https://github.com/mseeks/revisionist/issues/90)) | Voicing timeline headlines/details (too granular for the value) |
26| **Moderate the epilogue text before voicing** (closes an existing gap) | Carry cached audio with a **shared run** ([#88](https://github.com/mseeks/revisionist/issues/88)/[#89](https://github.com/mseeks/revisionist/issues/89)) | A commissioned/cloned bespoke Chronicler voice |
90| **Epilogue** | ⭐ **on-demand → cache** | Once per run, read at the verdict pause. The share artifact and the paid surface. Highest value, lowest pressure. **The MVP.** |
91| Chronicle (per-turn) | **opt-in "listen" button** | Re-narrates the *whole* timeline every turn and is regenerated each turn — voicing it blindly is expensive *and* slow on every turn, and it can't be voiced "newest-only." Make it a button on the current telling. |
92| Objective brief | **pre-generate the 10 curated; on-demand for composed** | The curated pool is fixed and enumerable ([`objectives.ts`](../../server/utils/objectives.ts), 10 entries) — render them once at build/seed time and ship the audio. Live-composed briefs voice on demand, then cache. |
93| Figure replies | **defer past v1** | Ambitious (a distinct voice per figure — `figureName` is attached, so it's *possible*), but it multiplies calls and cost and adds real complexity. |
94| Timeline headline/detail | **skip v1** | Short, frequent, low marginal charm for the call volume. |
153| Commercial use | **unrestricted**, you own output, no attribution, caching OK | **paid plan required** (free = non-commercial + attribution) | standard GCP terms, you own output |
154| In the repo already? | **yes — `openai` SDK + `OPENAI_API_KEY` are wired** | no (new SDK + new paid account + new key) | no (new GCP dep + service account) |
155| Best for | the steerable narrator, **least ops** | a bespoke premium Chronicler voice | a free tier + clean ownership |
The note's central move — "voice the epilogue first" — works because the code already draws the seam. The Chronicler and the epilogue are two distinct AI tasks in the routing table, not one prose layer in two moods. The epilogue's route even carries an in-code annotation that's the whole argument for voicing it first: it's the share artifact and the paid surface.
epilogue is a member of the task enum, with its own route and that telling comment.
323description:'The real recorded historical event this dispatch is trying to influence, and the exact real-world date it occurred (e.g. "the Hindenburg disaster at Lakehurst — May 6, 1937")'
328description:'"too-late" if the event\'s real date above is earlier than the stated moment the dispatch arrives — its chance already spent; otherwise "in-time"'
342description:'"builds" if this dispatch meets a condition/price the figure just revealed, deliberately exploits a change already on the record, or coheres with and escalates the run\'s strategy; "resets" if it abandons that thread and starts cold; "neutral" otherwise'
The issue says TTS "fits as a new task/lane through the same seam, instrumented the same way." Almost. Every model call does route through a per-task gateway that stamps one [ai] cost line and records the spend — that part is true, and it's the reason the note insists TTS cost must show up there too.
emit writes the one [ai] line per call and records its dollar cost to the spend cap.
But the transport is text-in / JSON-schema-out. TTS is text-in / audio-out on a different OpenAI endpoint, reached through the sameopenai client the repo already builds. So a TTS lane is a small sibling transport that reuses this plumbing, plus a per-character rate-card entry — because today's cost function is token-only.
getOpenAIClient — already wired from OPENAI_API_KEY; OpenAI TTS reuses it, no new credential.
The most consequential correction. The issue claims voicing "adds no new moderation surface" because the prose "already passes the moderation seam." For the epilogue — the MVP — that's false. A turn moderates exactly four model outputs: the figure's reply, their action, and the timeline headline + detail.
moderateAction screens those four fields — and only those.
The Chronicle and the epilogue are not in that list. Their endpoint fires the Chronicler and returns the prose untouched — no moderation call anywhere in it.
The chronicle/epilogue path: synthesize prose, return it. No moderateAction.
The issue calls Supabase Storage "already in the stack" and proposes it as the audio cache's home. Supabase is in the stack — for auth and Postgres. The config wires a URL, a key, and a redirect; nothing configures or touches a storage bucket anywhere in the repo.
Supabase: auth config only. The private runtimeConfig holds the model keys, no storage.
For a cached surface, latency stops being decisive — you generate once and replay from cache forever. That pivot (away from the issue's latency-first framing) is what tilts the pick toward OpenAI and reframes the fallback as a quality choice, not a latency one.
OpenAI gpt-4o-mini-tts
ElevenLabs
Google Chirp 3 HD
In the repo already?
yes — SDK + key wired
no (new SDK + paid acct)
no (new GCP dep)
Steerable narrator
yes (instructions)
no (Flash); v3 is offline
Gemini TTS yes
Commercial terms
own output, no attribution
paid plan required
own output (GCP)
Best for
the v1 pick, least ops
a bespoke premium voice
a free tier
Vendor figures re-verified against live official sources on 2026-06-17.
The restated v1: surface, vendor, seam, cache, safety.
23| **Voice the epilogue** (on-demand → cache) | **Opt-in "listen"** on the per-turn Chronicle | Auto-voicing the Chronicle every turn (the cost + latency trap) |
24| **A TTS lane** through the AI seam (cost on the same `[ai]` line) | **Pre-generate** the 10 curated objective briefs | Per-figure character voices (charming, but multiplies calls + complexity) |
25| **Content-hash audio cache** (Supabase Storage bucket) | User-selectable narrator voice (ties to [#90](https://github.com/mseeks/revisionist/issues/90)) | Voicing timeline headlines/details (too granular for the value) |
26| **Moderate the epilogue text before voicing** (closes an existing gap) | Carry cached audio with a **shared run** ([#88](https://github.com/mseeks/revisionist/issues/88)/[#89](https://github.com/mseeks/revisionist/issues/89)) | A commissioned/cloned bespoke Chronicler voice |
90| **Epilogue** | ⭐ **on-demand → cache** | Once per run, read at the verdict pause. The share artifact and the paid surface. Highest value, lowest pressure. **The MVP.** |
91| Chronicle (per-turn) | **opt-in "listen" button** | Re-narrates the *whole* timeline every turn and is regenerated each turn — voicing it blindly is expensive *and* slow on every turn, and it can't be voiced "newest-only." Make it a button on the current telling. |
92| Objective brief | **pre-generate the 10 curated; on-demand for composed** | The curated pool is fixed and enumerable ([`objectives.ts`](../../server/utils/objectives.ts), 10 entries) — render them once at build/seed time and ship the audio. Live-composed briefs voice on demand, then cache. |
93| Figure replies | **defer past v1** | Ambitious (a distinct voice per figure — `figureName` is attached, so it's *possible*), but it multiplies calls and cost and adds real complexity. |
94| Timeline headline/detail | **skip v1** | Short, frequent, low marginal charm for the call volume. |
153| Commercial use | **unrestricted**, you own output, no attribution, caching OK | **paid plan required** (free = non-commercial + attribution) | standard GCP terms, you own output |
154| In the repo already? | **yes — `openai` SDK + `OPENAI_API_KEY` are wired** | no (new SDK + new paid account + new key) | no (new GCP dep + service account) |
155| Best for | the steerable narrator, **least ops** | a bespoke premium Chronicler voice | a free tier + clean ownership |
A design exploration's job is to end with the implementation work teed up. The note closes with five ready-to-file issues in dependency order — the TTS lane first, the voiced epilogue (with its moderation step) on top, then the cache, the pre-generated briefs, and the opt-in Chronicle.
23| **Voice the epilogue** (on-demand → cache) | **Opt-in "listen"** on the per-turn Chronicle | Auto-voicing the Chronicle every turn (the cost + latency trap) |
24| **A TTS lane** through the AI seam (cost on the same `[ai]` line) | **Pre-generate** the 10 curated objective briefs | Per-figure character voices (charming, but multiplies calls + complexity) |
25| **Content-hash audio cache** (Supabase Storage bucket) | User-selectable narrator voice (ties to [#90](https://github.com/mseeks/revisionist/issues/90)) | Voicing timeline headlines/details (too granular for the value) |
26| **Moderate the epilogue text before voicing** (closes an existing gap) | Carry cached audio with a **shared run** ([#88](https://github.com/mseeks/revisionist/issues/88)/[#89](https://github.com/mseeks/revisionist/issues/89)) | A commissioned/cloned bespoke Chronicler voice |
90| **Epilogue** | ⭐ **on-demand → cache** | Once per run, read at the verdict pause. The share artifact and the paid surface. Highest value, lowest pressure. **The MVP.** |
91| Chronicle (per-turn) | **opt-in "listen" button** | Re-narrates the *whole* timeline every turn and is regenerated each turn — voicing it blindly is expensive *and* slow on every turn, and it can't be voiced "newest-only." Make it a button on the current telling. |
92| Objective brief | **pre-generate the 10 curated; on-demand for composed** | The curated pool is fixed and enumerable ([`objectives.ts`](../../server/utils/objectives.ts), 10 entries) — render them once at build/seed time and ship the audio. Live-composed briefs voice on demand, then cache. |
93| Figure replies | **defer past v1** | Ambitious (a distinct voice per figure — `figureName` is attached, so it's *possible*), but it multiplies calls and cost and adds real complexity. |
94| Timeline headline/detail | **skip v1** | Short, frequent, low marginal charm for the call volume. |
153| Commercial use | **unrestricted**, you own output, no attribution, caching OK | **paid plan required** (free = non-commercial + attribution) | standard GCP terms, you own output |
154| In the repo already? | **yes — `openai` SDK + `OPENAI_API_KEY` are wired** | no (new SDK + new paid account + new key) | no (new GCP dep + service account) |
155| Best for | the steerable narrator, **least ops** | a bespoke premium Chronicler voice | a free tier + clean ownership |