Gemini-powered text-to-speech with narration intelligence.
speak takes text, runs it through Gemini to optimize it for spoken delivery (adding pacing, audio tags, emotional cues), then renders it to audio through Gemini's native TTS model. Two API calls, one command.
# Clone and symlink
git clone https://github.com/haroldyoung/speak.git
ln -s "$(pwd)/speak/speak" ~/.local/bin/speak
# Or just copy
cp speak/speak ~/.local/bin/speak
chmod +x ~/.local/bin/speakIf you have Homebrew's espeak-ng installed, it links its own speak binary into /opt/homebrew/bin/speak. If Homebrew comes before ~/.local/bin in your $PATH, running speak will execute espeak-ng instead of this driver, throwing:
Error: The specified espeak-ng voice does not exist.
To ensure speak always resolves to this persona driver across all directories:
- Shell Alias (Recommended): Add to
~/.zshrc:alias speak="$HOME/Projects/speak/speak"
- Symlink into Homebrew:
ln -sf "$HOME/Projects/speak/speak" /opt/homebrew/bin/speak
bash4+curlpython3(for JSON handling and local adapters)ffmpeg/ffprobe(audio conversion)- A Gemini API key — get one at aistudio.google.com/apikey
- (Optional) An OpenRouter API key if you want to use OpenRouter models for narration optimization.
- (Optional) For offline local Kokoro TTS:
pip3 install kokoro-onnx soundfile - (Optional) For offline local Suno Bark TTS:
pip3 install transformers torch soundfile scipy
export GEMINI_API_KEY="your-key-here"
# Optional:
export OPENROUTER_API_KEY="your-openrouter-key"speak supports 4 text-to-speech engines:
| Engine | Deployment | Audio Tags | Description |
|---|---|---|---|
gemini |
Cloud API | Yes | Google Gemini 3.1 Flash TTS — rich expressive delivery directed by Gemini |
gcloud |
Cloud API | No | Google Cloud Text-to-Speech Chirp 3 HD — studio-grade voices |
kokoro |
Offline local | No | Kokoro 82M v1.0 — ultra-fast local neural TTS with persona voice blending |
bark |
Offline local | Yes | Suno Bark — generative neural audio with expressive prosody and native audio tags |
# Speak text directly (default: gemini)
speak "Information has shape, and the shape persists across substrate."
# Speak using offline Suno Bark with native audio tags
speak -e bark "That is hilarious [laughs] I can't believe it."
# Speak using offline Kokoro TTS
speak -e kokoro "Offline speech synthesis without an API key."
# Speak clipboard contents
speak -c
# Choose a voice
speak -v Kore "This is the Kore voice."
# Skip narration optimization (raw mode)
speak -r "Already has [whispers] audio tags [pause] in it."
# Save to file instead of playing
speak -o chapter1.wav < chapter1.txt
# Pipe from stdin
cat essay.md | speak -v Charon
# Combine flags
speak -rc # raw + clipboard
speak -c -v Orus -o out.wav # clipboard, specific voice, save to file
# Pipe audio to another process
speak -r --stdout "hello" | mpv -
# Get structured JSON output (metadata + base64 audio)
speak --json "some text" | jq .duration_seconds
speak -r --json "text" | jq -r .audio_wav_base64 | base64 -d > out.wavWhen running offline models (Kokoro or Bark), speak can keep the model weights resident in Apple Silicon unified memory to eliminate cold startup delays:
speak daemon start all # Pre-load Kokoro & Bark into memory
speak daemon status # View memory residency and daemon status
speak daemon stop all # Unload models and free memory- Visual Indicator:
speakshows(daemon)in the voice badge when synthesizing with a warm resident daemon, or(standalone)if the model is being loaded from disk. - Resident Daemon Prompt: After running a local model standalone,
speakcan ask if you'd like to load the daemon in the background for faster responses. - Config Setting: Toggle this prompt on/off with:
Responding
speak set daemon_prompt false # Disable post-run prompt speak set daemon_prompt true # Enable post-run prompt (default)
neverto the interactive prompt also disables it automatically.
With the daemon active, speech synthesis starts almost instantly. If the daemon is stopped, speak automatically falls back to standard execution without errors.
For text over ~500 words, speak automatically splits at paragraph boundaries, generates audio for each chunk in parallel, and concatenates the results. This avoids API size limits and quality drift on long outputs.
# Narrate a full chapter (auto-chunks)
speak < chapter.md
# Fast mode for quicker generation on long text
speak --fast < chapter.md
# Save a narrated chapter
speak -o chapter.wav < chapter.mdIf individual chunks hit the API's content filter, speak reports which failed and concatenates the rest. The token count shown is the aggregate across all chunks.
speak is designed to be called from other scripts and tools. All formatted output goes to stderr, keeping stdout clean for data.
# --stdout writes raw WAV bytes to stdout
speak -r --stdout "alert text" | ffplay -nodisp -autoexit - 2>/dev/null
# --json returns structured metadata with embedded audio
result=$(speak --json "some text")
echo "$result" | jq '.voice, .duration_seconds, .tokens'
# JSON output shape:
# {
# "voice": "Puck",
# "duration_seconds": 5,
# "raw": false,
# "tokens": { "optimize": "142/89", "tts": "89/432" },
# "transcript": "the optimized transcript...",
# "audio_wav_base64": "UklGR..."
# }30 built-in voices with distinct character. Run speak voices to see them all:
◇ Zephyr Bright
◆ Puck Upbeat ← default
▣ Charon Informative
◈ Kore Firm
△ Fenrir Excitable
...
Change your default:
speak set voice KoreThe narration optimizer adds these automatically, but you can also use them directly with -r:
| Tag | Effect |
|---|---|
[whispers] |
Whispered delivery |
[excited] |
High energy |
[serious] |
Measured, grave |
[warmly] |
Warm tone |
[slowly] |
Reduced pace |
[pause] |
Brief silence |
[laughs] |
Laughter |
[sighs] |
Audible sigh |
[shouting] |
Raised volume |
[sarcastic] |
Sarcastic delivery |
Tags are not limited to this list. The model interprets natural language, so [like a cartoon villain] or [as if revealing a secret] also work.
Config lives at ~/.config/speak/config:
engine=gemini
persona=puck
optimizer_provider=gemini
optimizer_model=gemini-2.5-flashspeak set # view current config
speak set persona Kore # change default persona
speak set optimizer_provider openrouter # switch to OpenRouter optimizer
speak set optimizer_model openrouter/free # use free OpenRouter model- Optimize — Your text is sent to Gemini 2.5 Flash with a narration director prompt. It returns the same text with audio tags, pacing cues, and style direction added.
- Synthesize — The optimized transcript is sent to Gemini 3.1 Flash TTS, which returns raw PCM audio.
- Convert — ffmpeg wraps the PCM in a WAV container.
- Play —
afplay(macOS),paplay/aplay(Linux), ormpv.
Skip step 1 with -r if your text is already tagged or you want the model's default interpretation.
speak provides real-time visual feedback and an interactive sci-fi HUD in the terminal:
While audio is preparing, a compact, non-wrapping progress sweep reports live word count and elapsed deciseconds:
⚙ SYNTHESIZING [▱▱▰▰▱▱▱▱] 482 words (0.8s)
During playback, speak renders a borderless, adaptive teleprompter that auto-scales to any terminal width (including thin split panes) with zero scrollback leak:
All systems nominal, captain.
Visual telemetry is online and orbit is established.
Primary sensors are holding at maximum efficiency.
RIVER ● PLAY [████████░░] [━━━━━━━━━╸──────] 53% 01:23 / 02:35
- Borderless & Window-Adaptive: No rigid box frames or wrapping sidebars; scales cleanly to narrow windows and split panes.
- Colored Voice Telemetry Badge: Displays the active voice name in its deterministic 256-color badge right before
● PLAYon the bottom status line. - Steady Playback Beacon:
● PLAYstays steady throughout speech without distracting word-to-word state flickering, returning to○ IDLEonly on long silence (> 600ms). - Signal VU Meter: Acoustic amplitude bar reflecting real decibel volume in 50ms time slices.
- Progress Track: Determinate progress bar showing percentage, elapsed time, and total audio duration.
- Configurable Active Word Emphasis: Default active word styling is bold electric cyan (
emphasis=bold). Customize via:speak set emphasis bold # Bold only (default) speak set emphasis underline # Underline only speak set emphasis both # Bold + underline speak set emphasis inverse # High-contrast inverted pill
- Full Scroll View & Window View (
speak set view scroll|window):--view scroll/speak set view scroll(default): Full Scroll View using the terminal's native scrollback. As each line completes, it is permanently printed into the terminal history above the live active line and bottom telemetry bar so you can scroll up at any time.--view window/speak set view window: Fixed-l N(1 to 8 lines, default:3) in-place teleprompter window.
- Sample-Accurate Per-Clause Timing & Markdown Normalization (Kokoro):
- Automatically strips Markdown syntax (
***,**bold**,# Headings,[links](url), tables, and backticks) so TTS never reads formatting symbols ("asterisk asterisk asterisk") aloud. - Synthesizes clause-by-clause and writes
<output>.timing.jsonwith exact sample-level[start, speech_end, end]timestamps (len(audio_part) / 24000.0), locking the visual word highlight to the audio with zero cumulative drift.
- Automatically strips Markdown syntax (
- Closed Captioning / Lyric Scrolling:
-l, --lines N: Set visible caption lines inwindowmode (1 to 8, default:3).--highlight word | line: Choose between word-by-word karaoke style (default) or line-by-line illumination.--lead MS: Caption anticipation lead time in milliseconds before speech (default:65).- Monochromatic Cyan Gradient: Single hue only; the spoken line is electric cyan, stepping down in pure cyan luminance (84%, 67%, 51%, 37%) so visual attention is never distracted by conflicting colors.
- Terminal Tab Title: Updates the IDE terminal tab title via OSC (
VOICE: <VOICE>) and clears on completion.
speak includes a self-contained, zero-build 2-file Web UI sidecar (web_ui.py + static/index.html) that wraps the CLI and can be kept running persistently in the background:
speak web start # Start and keep the Web UI running on http://127.0.0.1:2420
speak web open # Start (if needed) and open http://127.0.0.1:2420 in your browser
speak web status # Check PID, memory usage, and keepalive supervisor status
speak web restart # Restart the Web UI server
speak web logs # Tail the Web UI server logs (~/.config/speak/web_ui.log)
speak web stop # Stop the Web UI server and unload keepalive
# Or start both local model daemons (Kokoro & Bark) and the Web UI together:
speak daemon start all
speak daemon status- Keepalive Persistence: On macOS,
speak web startautomatically installs and loads alaunchdLaunchAgent (~/Library/LaunchAgents/com.speak.webui.plistwithRunAtLoad=trueandKeepAlive=true) paired with an auto-restarting supervisor so the Web UI stays up across crashes and logins. - Introspection (
GET /api/schema): Parses./speak --help,voices.yaml,engines.yaml, and~/.config/speak/configin read-only mode to dynamically populate subcommands, flags, engines, and 30+ voices. - Persistent Queue (
GET /api/queue,POST /api/queue/state): Syncs directly withspeak queue(outputs/queue.json) to track unplayed/played items and remembered playback positions. - Live SSE Execution (
POST /api/run): Spawns./speakviaasyncio.create_subprocess_exec(nevershell=True) and streams real-time stdout/stderr telemetry and ANSI status updates to the browser over Server-Sent Events (text/event-stream). - Responsive 1/2/3-Column Layout & Theme Switcher: Adapts from a single-column iPhone stack (with Console at the end) up to a 3-column widescreen studio layout (Voice & Source, Player with top controls & live captions, and Queue), with instant switching between ◇ Lothal and 🕹️ 1984 Arcade themes.
| Platform | Audio player | Clipboard |
|---|---|---|
| macOS | afplay |
pbpaste |
| Linux (PulseAudio) | paplay |
xclip / xsel |
| Linux (PipeWire) | paplay |
xclip / xsel / wl-paste |
| Linux (ALSA) | aplay |
xclip / xsel |
| Any | mpv (fallback) |
— |
MIT