← Back to Projects

Workout Story Generator

An audio-first workout companion where you are the protagonist. Exercises become obstacles in the plot, and the rest between sets is when the story advances. Being rebuilt to run entirely on-device.

Python LLM Kokoro Chatterbox Silero VAD Moonshine On-device
Workout Story Generator

Exercise is repetitive and the gap between sets is dead time — long enough to reach for your phone, short enough that the context switch costs you the workout. This turns that gap into the reward: an audio story where you are the protagonist, your exercises are the obstacles the character has to physically overcome, and the rest between sets is when the plot moves.

No screen, no typing. A narrator describes the world in third person; a sidekick talks directly to you and tells you when to move.

The 2023 interface: exercise/rep/set table, a free-text story premise, and a choice between an interactive mode that lets you steer the plot and an auto mode that runs itself.

The design that holds up

Built in 2023 against gpt-3.5-turbo. Three parts of it aged well:

Exercise → narrative motion. A single function maps a real movement to a story beat — pushing a jammed door, hauling a lever, holding still while something passes. The system prompt carries the mapping examples: hip hinge with weighted bar → lifting a heavy lever; holding a straight body position → aiming a rifle, hiding without movement. The exercise isn’t narrated over, it’s what the character is doing.

Quotation marks as the voice router. The prompt forbids quoting the main character and requires quoting the sidekick. That one constraint makes speaker assignment a string operation rather than a classification problem: quoted text goes to the sidekick voice, everything else to the narrator.

Synthesis runs ahead of playback. Cue lines for the whole session are rendered up front, and a worker pool feeds a playback queue, so speech is never waiting on the model mid-set.

What’s being rebuilt, and why

Every model-facing layer is dead. Coqui TTS is archived, pyttsx3 over espeak-ng is robotic, and the OpenAI dependency means it stops working on a gym wifi dead spot. The rewrite targets on-device: a local LLM, Silero VAD, Moonshine for speech recognition, and a modern neural TTS.

The speech work so far is measured rather than guessed — real-time factor is synthesis seconds per second of audio, on desktop CPU:

engineRTFnotes
Piper (medium voice)0.01855× real-time, 61 MB per voice
Kokoro0.128× real-time, Apache 2.0, no emotion control
Chatterbox1.8slower than real-time, but has an explicit emotion knob

Two findings changed the plan:

Licences decide more than quality does. Piper is the fastest option and its engine is GPL-3.0 — which conflicts with App Store distribution terms, the same incompatibility that pushed VLC off the store. Its voices are also licensed separately from the engine, and the obvious-sounding ones (ryan, hfc_male, hfc_female) are CC BY-NC-SA — non-commercial. Kokoro is Apache 2.0 end to end with no per-voice minefield.

Pre-generation buys expressiveness. Because the episode is rendered before the workout rather than during it, a model that runs at 1.8× slower than real-time is still perfectly usable — nobody is waiting on it at 5am. So the narrator uses fast Kokoro for the bulk of the words, while the sidekick, whose lines are short and few, uses a slower model that can actually be told to sound excited.

The cast came out of a listening pass over all 28 English Kokoro voices: am_michael narrates, am_fenrir is the sidekick. The first sidekick pick was rejected as too flat — an action-fantasy sidekick who sounds bored is worse than no sidekick.

Intensity has to track the passage. A single fixed emotion setting is wrong: the same excitement that works for “Ready? Push! Ten big shoves!” is exhausting over a minute of rest-period exposition. Chatterbox’s exaggeration parameter is mapped to the story beat instead — 0.5 for exposition between sets, 0.8 as the next challenge builds, 1.2 for the call to action. That assignment is deterministic rather than guessed, because the workout structure already says which beat you are in. Synthesis cost is flat across levels, so the variation is free.

Status

In progress, and honest about it. The 2023 version runs; the local rewrite is at the stage of choosing the speech stack. Current state and open questions are logged in docs/TTS_BENCH.md in the repo.

Unverified / open items

  • Nothing has been auditioned over Bluetooth, which is the only listening context that matters. A2DP re-encodes, and voice separation obvious on desktop speakers can muddy over SBC.
  • No phone-side measurement at all. Every RTF above is desktop CPU; phone silicon is slower and thermally limited across a 45-minute session.
  • The conversational sidekick is unresolved. Opening a Bluetooth headset mic forces the link from A2DP to HFP and collapses audio bandwidth, so live back-and-forth may have to become phone-mic push-to-talk, or branch selection between sets.
  • Local LLM quality for long-form serialized narrative is untested; only licences have been checked so far.