← Back to Home

Architecture

Project structure and design decisions

v0.2.19

Documentation

Architecture

The task-scoped Deck, observer, presenter, and Scout framework boundaries are specified in SpeakEasy conversation topology.

Overview

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.

Directory Structure

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

Core Components

SpeakEasy Class (src/index.ts)

The main orchestrator that:

  • Loads global config from ~/.config/speakeasy/settings.json
  • Initializes all provider instances
  • Manages the speech queue with priority support
  • Handles caching via TTSCache
  • Provides fallback logic when providers fail
┌─────────────────────────────────────────────────────────┐
│                      SpeakEasy                          │
├─────────────────────────────────────────────────────────┤
│  config: SpeakEasyConfig                                │
│  adapters: Map<TTSProviderId, TTSAdapter>               │
│  cache: TTSCache                                        │
│  queue: Array<{text, options}>                          │
├─────────────────────────────────────────────────────────┤
│  speak(text, options) → Promise<void>                   │
│  stopSpeaking() → void                                  │
│  getCacheStats() → CacheStats                           │
└─────────────────────────────────────────────────────────┘

TTSAdapter interface (src/adapters/types.ts)

All providers implement the normalized adapter contract:

typescript
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().

Provider flow

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 Implementations

ProviderAPI EndpointModelDefault Voice
SystemmacOS sayN/ASamantha
OpenAIapi.openai.com/v1/audio/speechtts-1nova
ElevenLabsapi.elevenlabs.io/v1/text-to-speech/{voiceId}eleven_monolingual_v1EXAVITQu4vr4xnSDxMaL
Groqapi.groq.com/openai/v1/audio/speechcanopylabs/orpheus-v1-englishtara
GeminiGoogle Generative AI SDKgemini-2.5-flash-preview-ttsPuck

Caching System

The TTSCache class provides:

  • Built-in SQLite metadata storage (node:sqlite)
  • File-based audio storage in /tmp/speakeasy-cache/
  • UUID v5 keys based on text + provider + voice + rate + instructions (when applicable)
  • Configurable TTL and max size
  • Automatic cleanup of expired entries
Cache Key = UUID5(text|provider|voice|rate|instructions)

Configuration Hierarchy

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)

CLI Architecture

The CLI (src/bin/speakeasy-cli.ts) uses Commander.js:

speakeasy "text" [options]
     │
     ▼
┌─────────────┐
│  Commander  │
│   parsing   │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  Build      │
│  config     │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  SpeakEasy  │
│  instance   │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│   .speak()  │
└─────────────┘

Key Design Decisions

  1. Adapter abstraction: All providers implement TTSAdapter.synthesize(), making it easy to add new backends
  2. Fallback chain: If a provider fails, automatically try the next in the fallback order
  3. Cache by default: API providers cache automatically to reduce costs and latency
  4. macOS native: Uses afplay for audio playback, ensuring consistent volume control
  5. Global config: Single config file at ~/.config/speakeasy/ prevents API key sprawl