Project structure and design decisions
The task-scoped Deck, observer, presenter, and Scout framework boundaries are specified in SpeakEasy conversation topology.
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)
│ ├── adapters/ # Normalized TTS adapter layer
│ │ ├── types.ts # TTSAdapter, TTSRequest, TTSResult
│ │ ├── registry.ts # createAdapterRegistry(), PROVIDER_ORDER
│ │ ├── audio.ts # Shared afplay playback
│ │ └── request.ts # ProviderConfig → TTSRequest helper
│ ├── 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 │
│ adapters: Map<TTSProviderId, TTSAdapter> │
│ cache: TTSCache │
│ queue: Array<{text, options}> │
├─────────────────────────────────────────────────────────┤
│ speak(text, options) → Promise<void> │
│ stopSpeaking() → void │
│ getCacheStats() → CacheStats │
└─────────────────────────────────────────────────────────┘
src/adapters/types.ts)All providers implement the normalized adapter contract:
interface TTSAdapter {
readonly id: TTSProviderId;
readonly capabilities: TTSAdapterCapabilities;
validate(): boolean;
synthesize(request: TTSRequest): Promise<TTSResult>;
formatError(error: unknown): string;
}
Legacy Provider methods (speak(), generateAudio()) remain on built-in classes for backward compatibility but delegate to synthesize().
User Request
│
▼
┌─────────────┐
│ SpeakEasy │
│ .speak() │
└──────┬──────┘
│
▼
┌─────────────┐ Cache Hit? ┌─────────────┐
│ TTSCache │ ───────Yes────▶ │ afplay │
│ lookup │ │ (playback) │
└──────┬──────┘ └─────────────┘
│ No
▼
┌─────────────┐
│ TTSAdapter │
│.synthesize()│
└──────┬──────┘
│
▼
┌─────────────┐
│ Save to │
│ Cache │
└──────┬──────┘
│
▼
┌─────────────┐
│ playTTSResult│
│ (afplay) │
└─────────────┘
To add a bespoke backend, see Custom Providers.
| 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|instructions)
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() │
└─────────────┘
TTSAdapter.synthesize(), making it easy to add new backendsafplay for audio playback, ensuring consistent volume control~/.config/speakeasy/ prevents API key sprawl