Project structure and design decisions
SpeakEasy is a unified text-to-speech library that abstracts multiple TTS providers behind a common interface. It runs on macOS and uses native audio playback.
speakeasy/
├── src/
│ ├── index.ts # Main SpeakEasy class, exports
│ ├── types.ts # TypeScript interfaces
│ ├── cache.ts # SQLite audio cache via node:sqlite (TTSCache)
│ ├── providers/ # TTS provider implementations
│ │ ├── system.ts # macOS `say` command
│ │ ├── openai.ts # OpenAI TTS API
│ │ ├── elevenlabs.ts # ElevenLabs API
│ │ ├── groq.ts # Groq Orpheus TTS
│ │ └── gemini.ts # Google Gemini TTS
│ ├── bin/
│ │ └── speakeasy-cli.ts # CLI entry point
│ └── cli/
│ ├── config.ts # Config file management
│ ├── constants.ts # Default values
│ ├── doctor.ts # Health check logic
│ └── ui.ts # CLI output formatting
├── docs/ # Documentation
├── dist/ # Compiled output (gitignored)
└── dewey.config.ts # Dewey documentation config
src/index.ts)The main orchestrator that:
~/.config/speakeasy/settings.json┌─────────────────────────────────────────────────────────┐
│ SpeakEasy │
├─────────────────────────────────────────────────────────┤
│ config: SpeakEasyConfig │
│ providers: Map<string, Provider> │
│ cache: TTSCache │
│ queue: Array<{text, options}> │
├─────────────────────────────────────────────────────────┤
│ speak(text, options) → Promise<void> │
│ stopSpeaking() → void │
│ getCacheStats() → CacheStats │
└─────────────────────────────────────────────────────────┘
src/types.ts)All providers implement:
interface Provider {
speak(config: ProviderConfig): Promise<void>;
validateConfig(): boolean;
getErrorMessage(error: any): string;
}
User Request
│
▼
┌─────────────┐
│ SpeakEasy │
│ .speak() │
└──────┬──────┘
│
▼
┌─────────────┐ Cache Hit? ┌─────────────┐
│ TTSCache │ ───────Yes────▶ │ afplay │
│ lookup │ │ (playback) │
└──────┬──────┘ └─────────────┘
│ No
▼
┌─────────────┐
│ Provider │
│ .speak() or │
│ .generate() │
└──────┬──────┘
│
▼
┌─────────────┐
│ Save to │
│ Cache │
└──────┬──────┘
│
▼
┌─────────────┐
│ afplay │
│ (playback) │
└─────────────┘
| Provider | API Endpoint | Model | Default Voice |
|---|---|---|---|
| System | macOS say | N/A | Samantha |
| OpenAI | api.openai.com/v1/audio/speech | tts-1 | nova |
| ElevenLabs | api.elevenlabs.io/v1/text-to-speech/{voiceId} | eleven_monolingual_v1 | EXAVITQu4vr4xnSDxMaL |
| Groq | api.groq.com/openai/v1/audio/speech | canopylabs/orpheus-v1-english | tara |
| Gemini | Google Generative AI SDK | gemini-2.5-flash-preview-tts | Puck |
The TTSCache class provides:
node:sqlite)/tmp/speakeasy-cache/Cache Key = UUID5(text|provider|voice|rate)
1. Direct parameters (highest priority)
↓
2. Environment variables (OPENAI_API_KEY, etc.)
↓
3. Global config (~/.config/speakeasy/settings.json)
↓
4. Built-in defaults (lowest priority)
The CLI (src/bin/speakeasy-cli.ts) uses Commander.js:
speakeasy "text" [options]
│
▼
┌─────────────┐
│ Commander │
│ parsing │
└──────┬──────┘
│
▼
┌─────────────┐
│ Build │
│ config │
└──────┬──────┘
│
▼
┌─────────────┐
│ SpeakEasy │
│ instance │
└──────┬──────┘
│
▼
┌─────────────┐
│ .speak() │
└─────────────┘
afplay for audio playback, ensuring consistent volume control~/.config/speakeasy/ prevents API key sprawl