mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
- 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
11 KiB
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
Engineinstance - Caller must dispose all sessions before disposing engine
- Engine.dispose() releases engine resources
- After
Engine.dispose(): all methods exceptdispose()raiseEngineError
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 exceptdispose()raiseEngineError
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 raiseCancelledError)
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 exceptdispose()raiseEngineError - 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_engineis atomicEngine.createSession()transfers ownership, never returns partialdispose()is idempotent on Engine and EngineSession- After
dispose(), methods raiseEngineError synthesize()raises typedEngineErrorsubtypes, 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