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, an animated cyber-sweep reports live elapsed deciseconds across the pipeline phases:
⚙ SYNTHESIZING [▰▰▱▱▱▱▱▱] phonemization & acoustic modeling (0.4s)...
⚙ SYNTHESIZING [▱▱▰▰▱▱▱▱] neural token generation (1.2s)...
⚙ SYNTHESIZING [▱▱▱▱▱▰▰▱] waveform synthesis & rendering (2.1s)...
During playback, speak renders a borderless, adaptive teleprompter that auto-scales to any terminal width (including thin split panes) with zero scrollback leak:
VOICE: RIVER
All systems nominal, captain.
Visual telemetry is online and orbit is established.
Primary sensors are holding at maximum efficiency.
● TALK [████████░░] [━━━━━━━━━╸──────] 53% 01:23 / 02:35
- Borderless & Window-Adaptive: No rigid box frames or wrapping sidebars; scales cleanly to narrow windows and split panes.
- Vocal Rhythm Beacon:
○ IDLEduring pauses,● TALK(solid green) on normal speech,● ACTIVE(bright green) on loud vocal stress points. - Signal VU Meter: Acoustic amplitude bar reflecting real decibel volume in 50ms time slices.
- Progress Track: Determinate progress bar showing playback position against total audio duration.
- Closed Captioning / Lyric Scrolling: 1 to 8 lines of text with vertical breathing room (default:
3). Centered on the active line.-l, --lines N: Set visible caption lines (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).- Prosodic Pause-Aligned Sync: Lines break at natural punctuation and breath pauses (
, . ! ? ; —) so audio pauses coincide with line breaks rather than falling mid-line. - Word-Anchored Phonetic Timing: Estimates syllable counts and punctuation pauses per word, calibrated to audio speech onset to eliminate cumulative drift.
- Monochromatic Cyan Gradient: Single hue only; the spoken line is electric bold cyan, stepping down in pure cyan luminance (84%, 67%, 51%, 37%) so visual attention is never distracted by conflicting colors.
- Beacon Hysteresis & Smoothing: 350ms hold window prevents rapid flickering to
IDLEbetween spoken words.
- Terminal Tab Title: Updates the IDE terminal tab title via OSC (
VOICE: <VOICE>) and clears on completion.
| 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