mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
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
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
# 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)
|
||||
```python
|
||||
# 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)
|
||||
```python
|
||||
@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)
|
||||
```python
|
||||
@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`
|
||||
Reference in New Issue
Block a user