StoryMatrix connects seven adapter categories—audio, image, language model, music, sound effects, timeline, and text-to-speech—to replaceable ports; montage remains a service layer.
🗣️ TTS adapters
| Provider | Category | Adapter class · file | Port implemented | Auth env var | Base URL / model default | Cost | Selected by (config key = value) | Mock counterpart |
|---|---|---|---|---|---|---|---|---|
| Piper | TTS | PiperAdapter · infrastructure/adapters/tts/piper.py | TTSService | — | SERVICES__PIPER__PIPER_BINARY = piper | Local | providers.strategy.tts_provider = piper | MockTTSAdapter |
| ElevenLabs | TTS | ElevenLabsAdapter · infrastructure/adapters/tts/elevenlabs.py | TTSService | PROVIDERS__ELEVENLABS__API_KEY | Provider config selects its model strategy | Paid API | providers.strategy.tts_provider = elevenlabs | MockTTSAdapter |
| PlayAI | TTS | PlayAIAdapter · infrastructure/adapters/tts/playai.py | TTSService | PROVIDERS__PLAYAI__API_KEY | https://api.play.ht/api/v2 | Paid API | Constructed in the TTS services dictionary when local-only is false, but omitted from the orchestrator provider list ([piper, elevenlabs, mock]) | MockTTSAdapter |
| Coqui | TTS | CoquiTTSAdapter · infrastructure/adapters/tts/coqui_adapter.py | TTSAdapter | No auth env var; receives services.coqui | tts_models/multilingual/multi-dataset/xtts_v2 from CoquiTTSConfig | Local model | Constructed in the TTS services dictionary, but omitted from the orchestrator provider list ([piper, elevenlabs, mock]) | MockTTSAdapter |
| Mock | TTS | MockTTSAdapter · infrastructure/adapters/tts/mock_tts_adapter.py | TTSService | — | Synthetic output | No external cost | providers.strategy.tts_provider = mock | — |
🚀 Open-source TTS ecosystem watch — 2026-08-04
The table above is the implemented StoryMatrix inventory. The projects below are a dated research watchlist, not installed adapters. “Open source” here refers to the code and/or model terms stated by each project; code, weights, voices, and generated-output terms must all be checked before deployment.
| Project and current milestone | License posture | Officially documented innovation | CPU-only Debian fit | StoryMatrix decision |
|---|---|---|---|---|
OHF Piper v1.6.0 (2026-07-23) | GPL-3.0 engine; verify each voice/model | Fast, fully local neural TTS; CLI, Python, and C++ interfaces; current release adds the Nakdimon Hebrew phonemizer | Proven baseline; x86-64 manylinux wheels and low operational overhead | Keep as the offline production default; document the move from the archived MIT-era repository to the maintained GPL fork |
Chatterbox family / package v0.1.2 (2025-06-13) | MIT code; verify model-card/weight terms; generated audio is watermarked | Nano 110M targets on-device use and claims 3× real time on eight CPU cores; Turbo adds one-step decoding and paralinguistic tags; Multilingual V3 covers 23+ languages | Highest-priority bake-off after Piper | Prototype a Nano adapter for expressive speech and cloning only after memory, latency, weight-license, and watermark acceptance checks |
| Kokoro 82M | Apache-2.0 code and Apache-licensed weights | Compact 82M-parameter, 24 kHz multilingual open-weight model | High-priority bake-off; small model, but its reference runtime imports PyTorch | Benchmark quality, cold start, peak RAM, and real-time factor against Piper |
| MeloTTS | MIT | Multilingual synthesis, including Chinese–English mixing; project explicitly documents real-time CPU inference | Strong CPU fallback candidate | Benchmark as a simple multilingual local adapter; it is not the cloning choice |
| OpenVoice V2 (2024-04) | MIT; project states free commercial use | Zero-shot cross-lingual cloning plus granular voice-style control across six natively supported languages | Benchmark first; moderate local stack with no official CPU throughput claim | Evaluate when cloning/style control is required; keep outside the default path until CPU evidence exists |
F5-TTS 1.1.22 (2026-07-23) | MIT | Flow-matching Diffusion Transformer, zero-shot reference cloning, chunked inference, multi-style and multi-speaker generation | Research only; official performance evidence is GPU-centric and the PyTorch model is substantially heavier than Piper/Kokoro | Quality benchmark, not a CPU production default |
| Fun-CosyVoice 3.0 0.5B (2025-12 model milestone) | Apache-2.0 | Nine languages plus dialects, cross-lingual zero-shot cloning, pronunciation inpainting, instruction control, and bi-streaming text-in/audio-out with claimed latency as low as 150 ms | Research only; optimized paths use GPU-oriented vLLM/Triton components | Track streaming and directing features; require a native CPU benchmark before adapter work |
| Qwen3-TTS 0.6B/1.7B (2026-01-22) | Apache-2.0 | Ten languages, three-second cloning, natural-language voice design and prosody control, and hybrid streaming with claimed first-packet latency as low as 97 ms | 0.6B watch candidate; official setup recommends GPU/FlashAttention 2 | Benchmark only after a supported CPU path is demonstrated |
| Spark-TTS 0.5B (2025-03) | Apache-2.0 | Single-stream speech tokens over Qwen2.5, Chinese/English zero-shot cross-lingual cloning, code-switching, and gender/pitch/rate control | Research only; published throughput is GPU-based | Track for controllable bilingual narration, not the CPU default |
| Orpheus 3B multilingual preview (2025-04) | Apache-2.0 | Llama-based zero-shot cloning, emotion tags, and streaming; the project links a no-GPU llama.cpp route | CPU-capable but heavy at 3B parameters | Experimental long-form quality bake-off only |
| VoxCPM2 2B (2026-04) | Apache-2.0 | Tokenizer-free diffusion-autoregressive design, 30 languages, 48 kHz output, voice design, controllable cloning, and streaming | Not a strict CPU default; reference requirements specify CUDA, though an on-device llama.cpp-omni path exists | Watch the CPU runtime; do not import its CUDA-first stack |
| StyleTTS2 | MIT code, but inference uses a GPL dependency; pretrained models impose disclosure/voice-permission conditions | Style diffusion, speech-language-model adversarial training, and zero-shot speaker adaptation | Possible but operationally risky; project notes CPU inference avoids one GPU artifact | Keep off the production shortlist until dependency and model-use obligations are accepted |
| Fish Audio S2 Pro | Not OSI-open for production: its Research License requires a separate written commercial license and attribution | 4B Dual-AR model, 80+ languages, natural-language emotion control, multi-speaker/multi-turn synthesis, streaming, and rapid cloning | Reject for this deployment; CUDA/SGLang/H200-oriented and too large for the strict CPU target | Research reference only; do not describe it as a deployable FOSS provider |
CPU adoption order
- Operate now: OHF Piper.
- Benchmark next: Chatterbox Nano, Kokoro 82M, MeloTTS, then OpenVoice V2.
- Track for future quality/streaming work: F5-TTS, CosyVoice 3.0, Qwen3-TTS 0.6B, Spark-TTS, Orpheus, and VoxCPM2.
- Do not adopt without an explicit policy change: StyleTTS2 under its full dependency/model obligations, or Fish Audio under its restrictive commercial terms.
Every bake-off must run on the production-class CPU host and record real-time factor, peak resident memory, cold-start time, language/voice quality, model size, license provenance, and generated-output obligations. GPU latency numbers are not CPU evidence, and a permissive code repository does not automatically make its model weights or voices permissive.
🔎 Audio adapters
| Provider | Category | Adapter class · file | Port implemented | Auth env var | Base URL / model default | Cost | Selected by (config key = value) | Mock counterpart |
|---|---|---|---|---|---|---|---|---|
| Chroma | Audio | ChromaAudioSearchAdapter · infrastructure/adapters/audio/chroma_search.py | AudioSearchService | — | Chroma DB ./data/chroma_db; all-MiniLM-L6-v2 model | Local | Audio-search wiring selects Chroma when local-only and low-resource flags are false | MockAudioSearchService |
The adapter inventory contains 21 concrete classes across these seven directories. Three declared support classes are not adapters, and montage implementations live under infrastructure/services/.
🧠 LLM adapters
| Provider | Category | Adapter class · file | Port implemented | Auth env var | Base URL / model default | Cost | Selected by (config key = value) | Mock counterpart |
|---|---|---|---|---|---|---|---|---|
| OpenRouter | LLM | OpenRouterAdapter · infrastructure/adapters/llm/openrouter.py | LLMService, StoryPlannerService, StoryDataService, AudioDirectionService | PROVIDERS__OPENROUTER__API_KEY | https://openrouter.ai/api/v1; configured model | Paid API | The PROVIDERS__OPENROUTER__API_KEY environment variable is set and APP__DEV_LOCAL_ONLY is false; the selector then uses providers.strategy.llm_provider | MockLLMService |
| Ollama | LLM | OllamaService · infrastructure/adapters/llm/ollama.py | LLMService | — | http://localhost:57707; model mistral:latest | Local | OllamaService is not constructed by ServiceContainer; no providers.story_data_provider branch routes it | MockStoryDataService |
| Query enhancer | LLM | OpenRouterQueryEnhancer · infrastructure/adapters/llm/query_enhancer.py | LLMQueryEnhancer | OpenRouter credential | OpenRouter configuration | Paid API | The container wires ConcreteLLMQueryEnhancer, not this adapter class | MockLLMService through the enhancer |
| Story data | LLM | LLMStoryDataAdapter · infrastructure/adapters/llm/llm_story_data_adapter.py | StoryDataService | Provider-specific | Wraps an injected LLMService | Provider-dependent | Wrapper class exists, but no providers.story_data_provider selector or DI branch constructs it | MockStoryDataService |
🔊 SFX adapters
| Provider | Category | Adapter class · file | Port implemented | Auth env var | Base URL / model default | Cost | Selected by (config key = value) | Mock counterpart |
|---|---|---|---|---|---|---|---|---|
| Local library | SFX | LocalSFXAdapter · infrastructure/adapters/sfx/local.py | SFXService | — | Configured local SFX library | Local | providers.strategy.sfx_provider = local | MockSfxAdapter |
| Freesound | SFX | FreesoundSFXAdapter · infrastructure/adapters/sfx/freesound.py | SFXService | PROVIDERS__FREESOUND__API_KEY | https://freesound.org/apiv2 | External API | providers.strategy.sfx_provider = freesound | MockSfxAdapter |
| ElevenLabs | SFX | ElevenLabsSFXAdapter · infrastructure/adapters/sfx/elevenlabs.py | SFXService | PROVIDERS__ELEVENLABS__API_KEY | ElevenLabs SFX endpoint | Paid API | providers.strategy.sfx_provider = elevenlabs | MockSfxAdapter |
🎼 Music adapters
| Provider | Category | Adapter class · file | Port implemented | Auth env var | Base URL / model default | Cost | Selected by (config key = value) | Mock counterpart |
|---|---|---|---|---|---|---|---|---|
| Local library | Music | LocalMusicAdapter · infrastructure/adapters/music/local.py | MusicDiscoveryService | — | Configured local asset library | Local | providers.strategy.music_provider = local | MockMusicAdapter |
| MusicGen | Music | MusicGenAdapter · infrastructure/adapters/music/musicgen_adapter.py | MusicDiscoveryService | MUSICGEN_API_KEY when configured | Local MusicGen model | Compute-only | providers.strategy.music_provider = musicgen | MockMusicAdapter |
| Mock | Music | MockMusicAdapter · infrastructure/adapters/music/mock.py | MusicDiscoveryService | — | Synthetic output | No external cost | providers.strategy.music_provider = mock | — |
🖼️ Image adapters
| Provider | Category | Adapter class · file | Port implemented | Auth env var | Base URL / model default | Cost | Selected by (config key = value) | Mock counterpart |
|---|---|---|---|---|---|---|---|---|
| DALL·E | Image | DalleAdapter · infrastructure/adapters/image/dalle_adapter.py | ImageGenerationService | DALLE_API_KEY | https://api.openai.com/v1; dall-e-3 | Paid API | providers.strategy.image_provider = dalle | PillowAdapter in local-only mode |
| Midjourney | Image | MidjourneyAdapter · infrastructure/adapters/image/midjourney.py | ImageGenerationService | PROVIDERS__MIDJOURNEY__API_KEY | <host>:<port> placeholder from MidjourneyConfig.api_url | Paid or hosted API | providers.strategy.image_provider = midjourney | PillowAdapter in local-only mode |
| Pillow | Image | PillowAdapter · infrastructure/adapters/image/pillow_adapter.py | ImageGenerationService | — | Local image generation | Local | providers.strategy.image_provider = pillow | — |
🎞️ Timeline adapters
| Provider | Category | Adapter class · file | Port implemented | Auth env var | Base URL / model default | Cost | Selected by (config key = value) | Mock counterpart |
|---|---|---|---|---|---|---|---|---|
| Agentic LLM | Timeline | LLMAgenticTimelineAdapter · infrastructure/adapters/timeline/llm_agentic_timeline_adapter.py | AgenticTimelineService, AudioDirectionService | Provider-specific | Configured agentic provider | Provider-dependent | providers.strategy.agentic_timeline_provider = openrouter or mock is wired; openai and anthropic are accepted configuration values but unwired selector keys and therefore raise a selector-key error | MockAgenticTimelineAdapter |
| Mock agentic | Timeline | MockAgenticTimelineAdapter · infrastructure/adapters/timeline/mock_agentic_timeline_adapter.py | AgenticTimelineService | — | Synthetic timeline | No external cost | providers.strategy.agentic_timeline_provider = mock | — |
🎬 Montage services
Montage is not an adapter category. FFmpegMontageService and MontageServiceImpl implement MontageService under infrastructure/services/ffmpeg_montage.py and infrastructure/services/montage.py; the container factories ffmpeg_montage_service_factory and pydub_montage_service_factory select those service implementations.
🔌 Dependency-injection wiring
ServiceContainer in infrastructure/container.py exposes factories and selectors rather than importing every implementation eagerly. openrouter_adapter_factory creates OpenRouterAdapter; _create_dalle_adapter, _create_elevenlabs_adapter, _create_piper_adapter, _create_coqui_adapter, _create_elevenlabs_sfx_adapter, and _create_mock_sfx_adapter create their named providers. ffmpeg_montage_service_factory and pydub_montage_service_factory create the montage implementations; _create_chroma_audio_adapter creates semantic audio search; _get_sfx_providers and _get_music_providers build ordered provider lists.
| Service | Branch condition in ServiceContainer |
|---|---|
| Image generation | APP__DEV_LOCAL_ONLY or APP__LOW_RESOURCE_MODE truthy forces pillow; otherwise PROVIDERS__STRATEGY__IMAGE_PROVIDER or providers.strategy.image_provider, default pillow. |
| TTS list | Local-only or low-resource returns [piper, mock]; otherwise [piper, elevenlabs, mock]. PlayAI and Coqui are constructed in the services dictionary but excluded from this orchestrator list. |
| SFX | Local-only or low-resource returns [local, mock]; otherwise the strategy env var or configured providers.strategy.sfx_provider is used. ProviderStrategy.sfx_provider defaults to local; the freesound fallback applies only when the strategy is absent entirely. |
| LLM | Local-only or missing PROVIDERS__OPENROUTER__API_KEY returns MockLLMService; otherwise the configured OpenRouter selector. |
| Music | Local-only or low-resource selects local; otherwise the strategy env var or configured strategy, default local. |
| Audio search | Local-only or low-resource creates MockAudioSearchService; otherwise Chroma is created lazily. |
🧪 What APP__DEV_LOCAL_ONLY=true swaps
| Real adapter or service | Local-only substitute |
|---|---|
OpenRouterAdapter | MockLLMService |
ElevenLabsAdapter, PlayAIAdapter, and cloud TTS selection | PiperAdapter plus MockTTSAdapter |
FreesoundSFXAdapter and ElevenLabsSFXAdapter | MockSfxAdapter |
MusicGenAdapter | LocalMusicAdapter |
DalleAdapter and MidjourneyAdapter | PillowAdapter |
| Chroma audio search | MockAudioSearchService |
| Agentic external timeline provider | The timeline adapter does not switch because of local-only mode; LLMAgenticTimelineAdapter remains selected for openrouter, while MockAgenticTimelineAdapter is selected only when providers.strategy.agentic_timeline_provider = mock. Local-only changes the injected LLM service to MockLLMService. |
🔁 Retry, backoff, and caching
Adapters pass retry and timeout settings to their clients where the provider exposes them. OpenRouterAdapter uses configured timeout_seconds and max_retries; MidjourneyAdapter reads both from its provider config; montage and local adapters rely on local process or library errors rather than HTTP retry. SFX and TTS adapters surface provider failures through their async service methods, while the SFX resolver controls local-first ordering before external lookup. Backoff is provider-client behavior; no shared adapter-level exponential-backoff policy appears in the adapter modules.
infrastructure/services/api_cache.py defines a @cached(ttl, namespace) decorator, including an example voice lookup, but no adapter imports or invokes the decorator in this checkout. The cache decorator is therefore currently unused by integration wiring.
| Adapter | Retry / backoff / cache behavior |
|---|---|
PiperAdapter | Synthesis passes configured timeout_seconds to the async subprocess executor; the config default and SERVICES__PIPER__TIMEOUT_SECONDS determine that value. The 10s timeout is only for the Piper installation probe; voice-index data is cached in memory; no HTTP retry. |
ElevenLabsAdapter | Client timeout is 60s; streaming collects provider output; no shared backoff or adapter cache. |
PlayAIAdapter | HTTP client timeout comes from provider settings; provider errors surface; no shared cache. |
CoquiTTSAdapter | Local model/API errors surface; no shared retry or cache. |
MockTTSAdapter | Local deterministic response; no retry, backoff, or cache. |
ChromaAudioSearchAdapter | Local vector search; no network retry or shared cache. |
OpenRouterAdapter | Configured max_retries and timeout are passed to the client; structured calls use client retry behavior; no shared cache. |
OllamaService | Configured timeout bounds the local HTTP call; no adapter retry or shared cache. |
LLMStoryDataAdapter | Delegates to the selected LLMService; retry belongs to that service. |
OpenRouterQueryEnhancer | Delegates to the selected LLM; retry belongs to that service. |
LocalSFXAdapter | Local search and file operations have no HTTP retry or shared cache. |
FreesoundSFXAdapter | Timeout is 30s; no retry is implemented; no shared cache. |
ElevenLabsSFXAdapter | Timeout is 60s with bounded async waiting; no shared cache. |
LocalMusicAdapter | Semantic search falls back to keyword search; no network retry or shared cache. |
MusicGenAdapter | Local model generation has a 120s timeout; no shared retry or cache. |
MockMusicAdapter | Local silent-WAV generation; no retry, backoff, or cache. |
DalleAdapter | Each request permits max_retries + 1 attempts with linear sleep between attempts; no shared cache. |
MidjourneyAdapter | Configured submission retries and polling bounds apply; no shared cache. |
PillowAdapter | Local PIL operations surface errors; no retry, backoff, or cache. |
LLMAgenticTimelineAdapter | Retry behavior belongs to its injected LLM service; no shared cache. |
MockAgenticTimelineAdapter | Deterministic local timeline; no retry, backoff, or cache. |
🧰 External binaries
PiperAdapter resolves its executable through SERVICES__PIPER__PIPER_BINARY, whose default is piper. Montage resolves ffmpeg and ffprobe through PathsSettings.ffmpeg and PathsSettings.ffprobe, whose defaults are /usr/bin/ffmpeg and /usr/bin/ffprobe. Keep these endpoint and binary values in backticks when copying them into commands or configuration.
MidjourneyConfig.api_url defaults to a host-specific value and is published as the <host>:<port> placeholder: override it in deployment configuration rather than depending on a machine-specific endpoint. OllamaProviderConfig.base_url is the real code default http://localhost:57707; OllamaService is not wired by ServiceContainer.
⚙️ Configuration explains the full settings tree, and 🔑 Environment Variables lists the complete environment catalogue.