⚙️ Configuration
This page answers how StoryMatrix builds StoryMatrixConfig and resolves each setting.
🧭 Configuration tree
StoryMatrixConfig has four top-level sections: app, paths, services, and providers (src/storymatrix/config/models.py:StoryMatrixConfig). The tables list every field in those sections; nested model fields use dotted keys.
🧰 app
| Key | Type | Default | Environment variable |
|---|---|---|---|
app.app_name | str | StoryMatrix | APP__APP_NAME |
app.app_version | str | 0.1.0 | APP__APP_VERSION |
app.log_level | str | safe-log-level factory result | APP__LOG_LEVEL |
app.log_dir | Path | <repo-root>/logs | APP__LOG_DIR |
app.output_dir | Path | <repo-root>/out | APP__OUTPUT_DIR |
app.temp_dir | Path | <repo-root>/temp | APP__TEMP_DIR |
app.cache_dir | Path | <repo-root>/cache | APP__CACHE_DIR |
app.media_dir | Path | <repo-root>/media | APP__MEDIA_DIR |
app.silence_dir | Path | <repo-root>/assets/silence | APP__SILENCE_DIR |
app.data_dir | Path | <repo-root>/data | APP__DATA_DIR |
app.dev_local_only | bool | false | APP__DEV_LOCAL_ONLY |
app.keep_temp_files | bool | false | APP__KEEP_TEMP_FILES |
app.low_resource_mode | bool | false | APP__LOW_RESOURCE_MODE |
app.max_tts_concurrency | int | 2 | APP__MAX_TTS_CONCURRENCY |
out/ is the application output root (src/storymatrix/config/models.py:AppSettings).
🛣️ paths
| Key | Type | Default | Environment variable |
|---|---|---|---|
paths.ffmpeg | FilePath | /usr/bin/ffmpeg | none; PathsSettings is a BaseModel |
paths.ffprobe | FilePath | /usr/bin/ffprobe | none; PathsSettings is a BaseModel |
(src/storymatrix/config/models.py:PathsSettings)
🧱 services
| Key | Type | Default | Environment variable |
|---|---|---|---|
services.api.host | str | 0.0.0.0 | SERVICES__API__HOST |
services.api.port | int | 8000 | SERVICES__API__PORT |
services.api.public_base_url | HttpUrl | None | null | SERVICES__API__PUBLIC_BASE_URL |
services.api.auth_enabled | bool | false | SERVICES__API__AUTH_ENABLED |
services.api.api_key | SecretStr | None | unset | SERVICES__API__API_KEY |
services.api.rate_limit_enabled | bool | false | SERVICES__API__RATE_LIMIT_ENABLED |
services.api.rate_limit_requests | int | 100 | SERVICES__API__RATE_LIMIT_REQUESTS |
services.api.cors_enabled | bool | true | SERVICES__API__CORS_ENABLED |
services.web.host | str | 0.0.0.0 | SERVICES__WEB__HOST |
services.web.port | int | 8080 | SERVICES__WEB__PORT |
services.celery.broker_host | str | localhost | SERVICES__CELERY__BROKER_HOST |
services.celery.broker_port | int | 6379 | SERVICES__CELERY__BROKER_PORT |
services.celery.result_backend_host | str | localhost | SERVICES__CELERY__RESULT_BACKEND_HOST |
services.celery.result_backend_port | int | 6379 | SERVICES__CELERY__RESULT_BACKEND_PORT |
services.celery.broker_url | str | None | None (validator derives effective redis://localhost:6379/0) | SERVICES__CELERY__BROKER_URL |
services.celery.result_backend | str | None | None (validator derives effective redis://localhost:6379/0) | SERVICES__CELERY__RESULT_BACKEND |
services.celery.eager_mode | bool | false | SERVICES__CELERY__EAGER_MODE |
services.chroma.host | str | localhost | SERVICES__CHROMA__HOST |
services.chroma.port | int | 8000 | SERVICES__CHROMA__PORT |
services.chroma.collection_name | str | story_content | SERVICES__CHROMA__COLLECTION_NAME |
services.database.url | str | sqlite:///data/storymatrix.db | SERVICES__DATABASE__URL |
services.database.user | str | storymatrix | SERVICES__DATABASE__USER |
services.database.password | SecretStr | configured secret | SERVICES__DATABASE__PASSWORD |
services.database.host | str | localhost | SERVICES__DATABASE__HOST |
services.database.port | int | 57757 | SERVICES__DATABASE__PORT |
services.database.dbname | str | storymatrix | SERVICES__DATABASE__DBNAME |
services.database.data_dir | str | data | SERVICES__DATABASE__DATA_DIR |
services.local_assets.min_similarity_score | float | 0.35 | SERVICES__LOCAL_ASSETS__MIN_SIMILARITY_SCORE |
services.local_assets.sfx_library_path | Path | <repo-root>/assets/sfx | SERVICES__LOCAL_ASSETS__SFX_LIBRARY_PATH |
services.local_assets.music_library_path | Path | <repo-root>/assets/music | SERVICES__LOCAL_ASSETS__MUSIC_LIBRARY_PATH |
services.local_assets.voice_data_path | Path | <repo-root>/models/voices | SERVICES__LOCAL_ASSETS__VOICE_DATA_PATH |
services.piper.piper_binary | str | piper | SERVICES__PIPER__PIPER_BINARY |
services.piper.voices_dir | str | ./models/voices/piper | SERVICES__PIPER__VOICES_DIR |
services.piper.models_dir | str | ./models/voices/piper | SERVICES__PIPER__MODELS_DIR |
services.piper.default_voice | str | en_US-ljspeech-high | SERVICES__PIPER__DEFAULT_VOICE |
services.piper.quality | str | high | SERVICES__PIPER__QUALITY |
services.piper.speed | float | 1.0 | SERVICES__PIPER__SPEED |
services.piper.noise_scale | float | 0.667 | SERVICES__PIPER__NOISE_SCALE |
services.piper.noise_w | float | 0.8 | SERVICES__PIPER__NOISE_W |
services.piper.timeout_seconds | int | 120 | SERVICES__PIPER__TIMEOUT_SECONDS |
services.piper.enable_gpu | bool | false | SERVICES__PIPER__ENABLE_GPU |
services.piper.auto_download | bool | false | SERVICES__PIPER__AUTO_DOWNLOAD |
services.sentence_transformer.model_name | str | all-MiniLM-L6-v2 | SERVICES__SENTENCE_TRANSFORMER__MODEL_NAME |
services.sentence_transformer.cache_folder | str | None | null | SERVICES__SENTENCE_TRANSFORMER__CACHE_FOLDER |
services.coqui.model_name | str | tts_models/multilingual/multi-dataset/xtts_v2 | none; CoquiTTSConfig is a BaseModel |
services.coqui.use_gpu | bool | false | none |
services.coqui.default_speaker | str | None | null | none |
services.coqui.default_language | str | en | none |
services.coqui.default_speaker_wav | str | None | null | none |
services.coqui.speaker_wav_search_dirs | list[str] | None | null | none |
services.coqui.speaker_samples | dict[str, str] | None | null | none |
services.montage.target_loudness_dbfs | float | -20.0 | SERVICES__MONTAGE__TARGET_LOUDNESS_DBFS |
services.montage.implementation | str | ffmpeg | SERVICES__MONTAGE__IMPLEMENTATION |
services.montage.crossfade_duration_ms | int | 500 | SERVICES__MONTAGE__CROSSFADE_DURATION_MS |
services.montage.mastering | MasteringConfig | enabled with model defaults | none; nested BaseModel |
services.montage.mastering.enabled | bool | true | none; nested BaseModel |
services.montage.mastering.loudness_target | float | -20.0 | none |
services.montage.mastering.loudness_range | float | 7.0 | none |
services.montage.mastering.true_peak | float | -2.0 | none |
services.montage.mastering.compressor_threshold | float | -20.0 | none |
services.montage.mastering.compressor_ratio | float | 4.0 | none |
services.montage.mastering.compressor_attack | float | 0.005 | none |
services.montage.mastering.compressor_release | float | 0.1 | none |
services.montage.mastering.limiter_threshold | float | -1.0 | none |
services.montage.music_crossfade_enabled | bool | false | SERVICES__MONTAGE__MUSIC_CROSSFADE_ENABLED |
services.montage.music_crossfade_ms | int | 1000 | SERVICES__MONTAGE__MUSIC_CROSSFADE_MS |
services.mixing_profiles | MixingProfiles | model populated from MixingProfiles.PROFILES | none; MixingProfiles is a BaseModel |
services.mixing_profiles.narration | MixingProfileSettings | profile-dependent | none; named field |
services.mixing_profiles.dialogue | MixingProfileSettings | profile-dependent | none; named field |
services.mixing_profiles.music | MixingProfileSettings | profile-dependent | none; named field |
services.mixing_profiles.sfx | MixingProfileSettings | profile-dependent | none; named field |
services.mixing_profiles.transition | MixingProfileSettings | profile-dependent | none; named field |
services.media_repository.storage_root | Path | <repo-root>/media | none; FileSystemMediaAssetRepositoryConfig is a BaseModel |
services.media_repository.db_path | Path | <repo-root>/data/storymatrix.db | none |
services.artifacts.enabled | bool | true | none; ArtifactsConfig is a BaseModel |
services.llm.open_router.timeout | int | 30 | SERVICES__LLM__OPEN_ROUTER__TIMEOUT |
(src/storymatrix/config/models.py:ServicesSettings)
🔌 providers
| Key | Type | Default | Environment variable |
|---|---|---|---|
providers.elevenlabs.api_key | SecretStr | None | unset | PROVIDERS__ELEVENLABS__API_KEY |
providers.elevenlabs.default_voice | str | 21m00Tcm4TlvDq8ikWAM | PROVIDERS__ELEVENLABS__DEFAULT_VOICE |
providers.elevenlabs.model_strategy | enum | eleven_multilingual_v2 | PROVIDERS__ELEVENLABS__MODEL_STRATEGY |
providers.elevenlabs.model_id | str | None | null | PROVIDERS__ELEVENLABS__MODEL_ID |
providers.elevenlabs.stability | float | 0.5 | PROVIDERS__ELEVENLABS__STABILITY |
providers.elevenlabs.similarity_boost | float | 0.8 | PROVIDERS__ELEVENLABS__SIMILARITY_BOOST |
providers.elevenlabs.style | float | 0.0 | PROVIDERS__ELEVENLABS__STYLE |
providers.elevenlabs.use_speaker_boost | bool | true | PROVIDERS__ELEVENLABS__USE_SPEAKER_BOOST |
providers.elevenlabs.speed | float | 1.0 | PROVIDERS__ELEVENLABS__SPEED |
providers.elevenlabs.v3_audio_tags_enabled | bool | true | PROVIDERS__ELEVENLABS__V3_AUDIO_TAGS_ENABLED |
providers.elevenlabs.v3_auto_enhance_prompts | bool | true | PROVIDERS__ELEVENLABS__V3_AUTO_ENHANCE_PROMPTS |
providers.elevenlabs.output_format | str | mp3_44100_128 | PROVIDERS__ELEVENLABS__OUTPUT_FORMAT |
providers.elevenlabs.sfx_enabled | bool | true | PROVIDERS__ELEVENLABS__SFX_ENABLED |
providers.elevenlabs.sfx_duration_seconds | int | 10 | PROVIDERS__ELEVENLABS__SFX_DURATION_SECONDS |
providers.elevenlabs.sfx_prompt_influence | float | 0.3 | PROVIDERS__ELEVENLABS__SFX_PROMPT_INFLUENCE |
providers.elevenlabs.pronunciation_dictionary_ids | list[str] | [] | PROVIDERS__ELEVENLABS__PRONUNCIATION_DICTIONARY_IDS |
providers.elevenlabs.optimize_streaming_latency | int | 0 | PROVIDERS__ELEVENLABS__OPTIMIZE_STREAMING_LATENCY |
providers.elevenlabs.apply_text_normalization | str | auto | PROVIDERS__ELEVENLABS__APPLY_TEXT_NORMALIZATION |
providers.elevenlabs.max_retries | int | 3 | PROVIDERS__ELEVENLABS__MAX_RETRIES |
providers.elevenlabs.timeout_seconds | int | 60 | PROVIDERS__ELEVENLABS__TIMEOUT_SECONDS |
providers.freesound.api_key | SecretStr | None | unset | PROVIDERS__FREESOUND__API_KEY |
providers.openrouter.api_key | SecretStr | None | unset | PROVIDERS__OPENROUTER__API_KEY |
providers.openrouter.base_url | HttpUrl | https://openrouter.ai/api/v1 | PROVIDERS__OPENROUTER__BASE_URL |
providers.openrouter.default_model | str | google/gemini-2.5-flash-lite | PROVIDERS__OPENROUTER__DEFAULT_MODEL |
providers.openrouter.http_referer | str | https://noflippinway.lol | PROVIDERS__OPENROUTER__HTTP_REFERER |
providers.openrouter.x_title | str | NoLoL | PROVIDERS__OPENROUTER__X_TITLE |
providers.openrouter.temperature | float | 0.7 | PROVIDERS__OPENROUTER__TEMPERATURE |
providers.openrouter.max_tokens | int | 4096 | PROVIDERS__OPENROUTER__MAX_TOKENS |
providers.openrouter.max_retries | int | 3 | PROVIDERS__OPENROUTER__MAX_RETRIES |
providers.openrouter.timeout_seconds | int | 120 | PROVIDERS__OPENROUTER__TIMEOUT_SECONDS |
providers.dalle.api_key | SecretStr | None | unset | DALLE_API_KEY |
providers.dalle.base_url | HttpUrl | https://api.openai.com/v1 | DALLE_BASE_URL |
providers.dalle.model | str | dall-e-3 | DALLE_MODEL |
providers.dalle.organization_id | str | None | null | DALLE_ORGANIZATION_ID |
providers.dalle.max_retries | int | 3 | DALLE_MAX_RETRIES |
providers.dalle.timeout_seconds | int | 120 | DALLE_TIMEOUT_SECONDS |
providers.midjourney.api_url | str | host-specific default | PROVIDERS__MIDJOURNEY__API_URL |
providers.midjourney.api_key | SecretStr | None | unset | PROVIDERS__MIDJOURNEY__API_KEY |
providers.midjourney.timeout_seconds | int | 240 | PROVIDERS__MIDJOURNEY__TIMEOUT_SECONDS |
providers.midjourney.max_retries | int | 4 | PROVIDERS__MIDJOURNEY__MAX_RETRIES |
providers.midjourney.bot_type | str | MID_JOURNEY | PROVIDERS__MIDJOURNEY__BOT_TYPE |
providers.midjourney.modes | list[str] | [FAST] | PROVIDERS__MIDJOURNEY__MODES |
providers.midjourney.poll_interval_seconds | int | 15 | PROVIDERS__MIDJOURNEY__POLL_INTERVAL_SECONDS |
providers.midjourney.queue_backoff_base_seconds | int | 15 | PROVIDERS__MIDJOURNEY__QUEUE_BACKOFF_BASE_SECONDS |
providers.midjourney.queue_backoff_max_seconds | int | 25 | PROVIDERS__MIDJOURNEY__QUEUE_BACKOFF_MAX_SECONDS |
providers.midjourney.submit_concurrency | int | 1 | PROVIDERS__MIDJOURNEY__SUBMIT_CONCURRENCY |
providers.midjourney.upsample_enabled | bool | false | PROVIDERS__MIDJOURNEY__UPSAMPLE_ENABLED |
providers.midjourney.upsample_count | int | 0 | PROVIDERS__MIDJOURNEY__UPSAMPLE_COUNT |
providers.midjourney.upsample_indices | list[int] | [] | PROVIDERS__MIDJOURNEY__UPSAMPLE_INDICES |
providers.playai.api_key | SecretStr | None | unset | PROVIDERS__PLAYAI__API_KEY |
providers.playai.user_id | str | None | null | PROVIDERS__PLAYAI__USER_ID |
providers.playai.base_url | AnyUrl | https://api.play.ht/api/v2 | PROVIDERS__PLAYAI__BASE_URL |
providers.playai.default_voice | str | s3://voice-cloning-zero-shot/d9ff78ba-d016-47f6-b046-526a0004622e/alice/manifest.json | PROVIDERS__PLAYAI__DEFAULT_VOICE |
providers.playai.quality | str | high | PROVIDERS__PLAYAI__QUALITY |
providers.playai.stream_enabled | bool | true | PROVIDERS__PLAYAI__STREAM_ENABLED |
providers.playai.timeout_seconds | int | 60 | PROVIDERS__PLAYAI__TIMEOUT_SECONDS |
providers.playai.max_retries | int | 3 | PROVIDERS__PLAYAI__MAX_RETRIES |
providers.playai.output_format | str | mp3 | PROVIDERS__PLAYAI__OUTPUT_FORMAT |
providers.musicgen.api_key | SecretStr | None | unset | MUSICGEN_API_KEY |
providers.musicgen.model_id | str | facebook/musicgen-small | MUSICGEN_MODEL_ID |
providers.musicgen.timeout_seconds | int | 120 | MUSICGEN_TIMEOUT_SECONDS |
providers.pillow.font_path | str | assets/fonts/Roboto-Regular.ttf | none; PillowConfig is a BaseModel |
providers.pillow.default_width | int | 1024 | none |
providers.pillow.default_height | int | 1024 | none |
providers.ollama.base_url | HttpUrl | http://localhost:57707 | none; OllamaProviderConfig is a BaseModel |
providers.ollama.model | str | mistral:latest | none |
providers.ollama.timeout_seconds | int | 120 | none |
providers.ollama.temperature | float | 0.7 | none |
providers.ollama.top_p | float | 1.0 | none |
providers.strategy.llm_provider | enum | openrouter | PROVIDERS__STRATEGY__LLM_PROVIDER |
providers.strategy.agentic_timeline_provider | enum | openrouter | PROVIDERS__STRATEGY__AGENTIC_TIMELINE_PROVIDER |
providers.strategy.tts_provider | enum | piper | PROVIDERS__STRATEGY__TTS_PROVIDER |
providers.strategy.sfx_provider | enum | local | PROVIDERS__STRATEGY__SFX_PROVIDER |
providers.strategy.music_provider | enum | local | PROVIDERS__STRATEGY__MUSIC_PROVIDER |
providers.strategy.image_provider | enum | mock | PROVIDERS__STRATEGY__IMAGE_PROVIDER |
(src/storymatrix/config/models.py:ProvidersSettings)
🔀 Precedence and loading
load_config() resolves one typed settings object in this order (src/storymatrix/config/config.py, src/storymatrix/cli/main.py):
- Pydantic defaults and process environment/.env values are loaded.
- YAML values are merged over that base.
- Explicit typed CLI overrides are applied last, so CLI mode and tuning flags win over YAML collisions.
- The resolved object is passed to consumers without a second raw-environment overlay.
Focused precedence evidence closes B15 for the exercised defaults, environment, YAML, and CLI cases. The shipped YAML still documents ordinary application defaults; full generation remains separately scoped.
load_config() no longer writes provider-strategy diagnostics directly to stdout, closing B16 at its named boundary. The loader’s logging and provider selection remain governed by the resolved configuration.
🧪 Worked resolution: app.dev_local_only
| Layer | Value | Evidence |
|---|---|---|
| Pydantic default | false | AppSettings.dev_local_only |
| Environment/.env | profile-provided value | StoryMatrixConfig settings |
| YAML merge | shipped application value | storymatrix_config.yaml |
Explicit CLI --offline | true | typed CLI override |
| Final config value | true when the flag is supplied | focused precedence evidence |
The explicit CLI override is not disguised as ordinary environment state and wins over a colliding YAML value (src/storymatrix/cli/main.py, src/storymatrix/config/config.py).
⏱️ Worked resolution: services.llm.open_router.timeout
The compatibility setting demonstrates the one post-YAML exception (src/storymatrix/config/models.py:OpenRouterServiceConfig).
| Layer | Value | Evidence |
|---|---|---|
| Pydantic default | 30 | OpenRouterServiceConfig.timeout |
| External environment before YAML | 45 when SERVICES__LLM__OPEN_ROUTER__TIMEOUT=45 | Pydantic environment loading |
| YAML merge | 60 when YAML contains services: {llm: {open_router: {timeout: 60}}} | deep_update() |
| Explicit post-YAML environment override | 45 | _apply_env_overrides() reapplies the externally supplied variable |
The final value is 45 for that environment/YAML combination. There is no CLI flag for SERVICES__LLM__OPEN_ROUTER__TIMEOUT; only an externally supplied process environment variable receives this post-YAML treatment (src/storymatrix/config/config.py:_apply_env_overrides).
🧩 Compatibility surface
services.llm.open_router is a minimal, test-facing compatibility shim. The canonical provider configuration lives under providers.openrouter; the shim exposes only timeout under services.llm.open_router (src/storymatrix/config/models.py:OpenRouterServiceConfig).
See 🔑 Environment Variables for the complete environment catalogue and 🔌 Integrations for provider selection.