Files
abogen/docs/developer-guide.md
T
Artem Akymenko 096ea58d74 docs: rewrite developer-guide as architectural reference
- Remove implementation details that will rot (code templates, tutorials)
- Keep only stable contracts: ownership, lifecycle, protocols, error semantics
- 270 lines → reference that only changes when architecture changes
2026-07-12 16:20:28 +03:00

11 KiB

TTS Plugin Architecture — Architectural Reference

This document describes the stable architectural contracts of the TTS Plugin Architecture. It documents invariants that only change when the architecture itself changes.


1. Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                        Host Application                         │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────────────┐  │
│  │ Plugin      │  │ HostContext  │  │ Plugin Discovery       │  │
│  │ Manager     │──│ (config_dir, │  │ (plugin directories)   │  │
│  │             │  │  logger,     │  │                        │  │
│  │ - discover  │  │  http_client)│  └────────────────────────┘  │
│  │ - validate  │  └──────────────┘            │                 │
│  │ - activate  │                              ▼                 │
│  │ - dispose   │  ┌─────────────────────────────────────────┐   │
│  └──────┬──────┘  │              Plugin Package             │   │
│         │         │  ┌──────────────┐ ┌─────────────────┐   │   │
│         ▼         │  │ PLUGIN_      │ │ MODEL_          │   │   │
│  ┌────────────┐   │  │ MANIFEST     │ │ REQUIREMENTS    │   │   │
│  │   Engine   │◄──┤  │ create_engine│ │                 │   │   │
│  └──────┬─────┘   │  └──────────────┘ └─────────────────┘   │   │
│         │         └─────────────────────────────────────────┘   │
│         │ createSession()                                       │
│         ▼                                                       │
│  ┌─────────────┐                                                │
│  │EngineSession│                                                │
│  └──────┬──────┘                                                │
│         │ synthesize()                                          │
│         ▼                                                       │
│  ┌────────────────┐                                             │
│  │SynthesizedAudio│                                             │
│  └────────────────┘                                             │
└─────────────────────────────────────────────────────────────────┘

Core Components

Component Responsibility
PluginManifest Static metadata: id, name, version, api_version, capabilities, engine manifest
EngineManifest Voice sources, parameters, audio formats
HostContext Minimal host services: config_dir, logger, http_client
Engine Stateless factory for sessions; thread-safe createSession()
EngineSession Owns mutable execution state; not thread-safe
PluginManager Discovers, validates, and manages plugin lifecycle
Capabilities Optional interfaces: VoiceLister, PreviewGenerator, StreamingSynthesizer, CancelableSession

2. Ownership Model

Engine Ownership

PluginManager.create_engine() → Engine
  • PluginManager creates and caches engines
  • Caller receives Engine instance
  • Caller must dispose all sessions before disposing engine
  • Engine.dispose() releases engine resources
  • After Engine.dispose(): all methods except dispose() raise EngineError

Session Ownership

Engine.createSession() → EngineSession
  • Engine creates session
  • Ownership transfers to caller immediately
  • Caller is responsible for session.dispose()
  • Engine does NOT track sessions — no registry, no callbacks
  • After session.dispose(): all methods except dispose() raise EngineError

Disposal Order (Invariant)

# Correct
engine = manager.create_engine("id")
session = engine.createSession()
try:
    audio = session.synthesize(request)
finally:
    session.dispose()      # 1. Sessions FIRST
engine.dispose()           # 2. Then engine

# INCORRECT — violates contract (undefined behavior)
engine.dispose()
session.synthesize(request)  # EngineError

3. Lifecycle State Machine

DISCOVERY
  PluginManager.discover(plugin_dirs)
  → Loads PLUGIN_MANIFEST, MODEL_REQUIREMENTS
  → Validates api_version (major must match)
  → Validates declared capabilities are implemented

MODEL_DOWNLOAD (if MODEL_REQUIREMENTS non-empty)
  Host reads MODEL_REQUIREMENTS
  Downloads/caches models
  Resolves model_path

ACTIVATION
  create_engine(context, model_path, config)
  → Atomic: succeeds fully or raises EngineError
  → Returns Engine

SESSION_CREATION
  engine.createSession() → EngineSession
  → Ownership transfers to caller
  → Raises EngineError on failure
  → Never returns partial session

SYNTHESIS
  session.synthesize(request)
  → Returns SynthesizedAudio
  → Raises EngineError on failure
  → Session remains usable after error

SESSION_DISPOSAL
  session.dispose()
  → Idempotent, never raises
  → After: all methods raise EngineError

DEACTIVATION
  engine.dispose()
  → Caller MUST dispose all sessions first
  → Idempotent, never raises
  → After: all methods raise EngineError

4. Protocol Contracts

Engine (Protocol)

@runtime_checkable
class Engine(Protocol):
    def createSession(self) -> EngineSession:
        """Create a new session. Thread-safe. Transfers ownership."""
        ...

    def dispose(self) -> None:
        """Release engine resources.
        Caller must dispose all sessions first.
        Idempotent, never raises.
        After: all methods except dispose() raise EngineError."""
        ...

EngineSession (Protocol)

@runtime_checkable
class EngineSession(Protocol):
    def synthesize(self, request: SynthesisRequest) -> SynthesizedAudio:
        """Synthesize audio.
        Returns SynthesizedAudio or raises EngineError.
        Session remains usable after error."""
        ...

    def dispose(self) -> None:
        """Release session resources.
        Idempotent, never raises.
        After: all methods except dispose() raise EngineError."""
        ...

Capability Protocols (Optional)

  • VoiceLister: listVoices(source_id: str) -> list[VoiceManifest]
  • PreviewGenerator: generatePreview(voice: VoiceSelection, text: str) -> SynthesizedAudio
  • StreamingSynthesizer: synthesizeStream(request: SynthesisRequest) -> Iterator[bytes]
  • CancelableSession: cancel() -> None (causes in-flight synthesize to raise CancelledError)

5. Error Semantics

EngineError (base)
├── ModelNotFoundError      # Required model not found
├── ModelLoadError          # Model failed to load
├── NetworkError            # Network operation failed
├── InvalidInputError       # Request validation failed
├── ConfigurationError      # Invalid configuration
├── CancelledError          # Operation cancelled via CancelableSession
└── InternalError           # Unexpected internal failure

When Each Is Raised

Error Raised By Conditions
ModelNotFoundError create_engine() Required model not found at model_path
ModelLoadError create_engine() Model exists but fails to load
NetworkError synthesize(), create_engine() Network call fails (cloud engines)
InvalidInputError synthesize() Request validation fails (empty text, invalid voice, etc.)
ConfigurationError create_engine() Config values invalid for this engine
CancelledError synthesize(), synthesizeStream() CancelableSession.cancel() called
InternalError Any Unexpected internal failure (bug)

Dispose Contract

  • dispose() is idempotent and never raises
  • After dispose(): all methods except dispose() raise EngineError
  • Engine: caller must dispose all sessions first; violating this is undefined behavior

6. Capabilities

Capability Interface Enables
voice_list VoiceLister listVoices(source_id) — enumerate available voices
preview PreviewGenerator generatePreview(voice, text) — preview without session
streaming StreamingSynthesizer synthesizeStream(request) — chunked audio output
cancel CancelableSession cancel() — interrupt in-flight synthesis

Plugins declare capabilities in PluginManifest.capabilities. Host validates at load time.


7. Contract Tests

Location: tests/contracts/

Purpose: Verify every plugin satisfies the architectural contracts.

Guarantees:

  • Required exports exist (PLUGIN_MANIFEST, MODEL_REQUIREMENTS, create_engine)
  • create_engine is atomic
  • Engine.createSession() transfers ownership, never returns partial
  • dispose() is idempotent on Engine and EngineSession
  • After dispose(), methods raise EngineError
  • synthesize() raises typed EngineError subtypes, session remains usable
  • Declared capabilities are actually implemented
  • Plugin loader validates manifest, api_version, capabilities

Run: pytest tests/contracts/ -v


8. Behavioral Tests

Location: tests/test_behavioral_regression.py

Purpose: Verify user-facing behavior via public API only (create_pipeline, Engine, EngineSession, PluginManager).

Scope:

  • Synthesis with various inputs (short, long, empty, unicode, mixed scripts)
  • Voice selection and listing
  • Parameter handling (speed, etc.)
  • Error scenarios (unknown plugin, disposal, etc.)
  • Resource cleanup (dispose idempotency, no leaks)
  • Pipeline utility (create_pipeline)

Run: pytest tests/test_behavioral_regression.py -v


9. Reference

  • Architecture Spec: docs/architecture-final-v2.md
  • Amendment (lang_code): docs/architecture-amendment-001.md
  • Migration Roadmap: docs/migration-roadmap.md
  • Plugin Examples: plugins/kokoro/, plugins/supertonic/
  • Protocol Definitions: abogen/tts_plugin/engine.py, abogen/tts_plugin/capabilities.py