The StoryMatrix domain models stories as scenes and typed media segments, with ports isolating application behavior from infrastructure.

🕸️ Aggregate graph

graph TD
  Story -->|scenes: Scene[]| Scene
  Story -->|characters: Character[]| Character
  Scene -->|segments| Segment
  Segment --> Narration
  Segment --> Dialogue
  Segment --> SFX
  Segment --> Music
  Image[Image entity]
  Character -.-> Voice
  Voice -.-> CharacterVoiceMapping

Story owns Scene[] and Character[]; each scene owns a discriminated segment collection. The entity vocabulary includes five media roles—Narration, Dialogue, SFX, Music, and Image—while the Pydantic Segments union currently parses the four audio/text segment classes and the separate Image entity models image assets.

🧱 Entities and value-bearing models

Entity or modelFields and types
Storyid: uuid.UUID; title: str; summary: str; prompt: str; language: str; `style: str
Characterid: uuid.UUID; name: str; description: str; traits: list[str]; `voice_profile: VoiceProfile
Sceneid: uuid.UUID; title: str; sequence_number: int; scene_type: SceneType; setting: SceneSetting; mood: str; description: str; segments: Segments; `ambient_sound: str
SceneSettinglocation: str; time_of_day: str; `weather: str
Segmentid: str, generated by UUID4 by default; extra fields are forbidden.
NarrationSegmenttext: str; segment_type = narration; optional character_id: str, tone: str, performance_notes: str, audio_path: str, duration: float, layer: int, start_time_ms: int, end_time_ms: int, volume_db: float, fade_in_ms: int, fade_out_ms: int, word_offsets: dict[str, dict[str, int]], scene_index: int.
DialogueSegment`character_id: str
SoundEffectSegmentsegment_type = sfx; description: str; optional duration_ms: int, sfx_subtype: str, scene_index: int, audio_path: str, duration: float, layer: int, start_time_ms: int, end_time_ms: int, volume_db: float, fade_in_ms: int, fade_out_ms: int.
MusicSegmentsegment_type = music; description: str; optional duration_ms: int, scene_index: int, audio_path: str, duration: float, layer: int, start_time_ms: int, end_time_ms: int, volume_db: float, fade_in_ms: int, fade_out_ms: int.
Segmentsroot: list[AnySegment]; a Pydantic root model that accepts a root list or unwraps a root mapping.
Voiceid: str; name: str; provider: str; provider_id: str; is_available: bool; supported_languages: list[str]; metadata: dict.
VoiceProfile`voice_id: str
CharacterVoiceMappingcharacter_id: str; character_name: str; voice_id: str; provider: str.
VoiceCharacteristics`accent: str
VoiceFilterDataclass fields `provider: VoiceProvider
MediaAssetid: uuid.UUID; asset_type: MediaAssetType; created_at: datetime; `updated_at: datetime
AudioAssetMediaAsset fields plus `duration_seconds: float
ImagePropertieswidth: int = 1024; height: int = 1024; `aspect_ratio: str
Imageid: uuid.UUID; created_at: datetime; `filename: str
Artifactid: UUID; `story_id: UUID
SearchResultasset: AudioAsset; score: float.
Tracktrack_id: str; track_type: Literal["narration", "dialogue", "sfx", "music"]; segments: list[AnySegment].
AgenticTimelinetitle: str; tracks: list[Track]; estimated_duration: float.

🏷️ Enum members

Domain enums

EnumExact members
SegmentTypeNARRATION = narration, DIALOGUE = dialogue, SFX = sfx, MUSIC = music, IMAGE = image
TTSProviderPLAYAI = playai, ELEVENLABS = elevenlabs, PIPER = piper, COQUI = coqui, SYSTEM = system, MOCK = mock
SFXProviderLOCAL = local, FREESOUND = freesound, ELEVENLABS = elevenlabs, MOCK = mock
MediaAssetTypeMUSIC = music, SFX = sfx, IMAGE = image, VOICE = voice, MOCK = mock
SceneTypeINTRO = intro, CHAPTER = chapter, OUTRO = outro, TRANSITION = transition
ArtifactTypeSTORY_OUTLINE = story_outline, CHARACTER_PROFILES = character_profiles, NARRATIVE = narrative, NARRATION = narration, DIALOGUE = dialogue, SFX = sfx, MUSIC = music, IMAGE = image, COVER_IMAGE = cover_image, AUDIO = audio, METADATA = metadata, FINAL_AUDIO = final_audio, TIMELINE_VISUALIZATION = timeline_visualization, LOG_FILE = log_file

Configuration enums used by the domain boundary

EnumExact members
AudioTypeNARRATION = narration, DIALOGUE = dialogue, MUSIC = music, SFX = sfx, TRANSITION = transition, UNKNOWN = unknown
ElevenLabsModelStrategyV3_EXPRESSIVE = eleven_v3, V2_MULTILINGUAL = eleven_multilingual_v2, FLASH_REALTIME = eleven_flash_v2_5, TURBO_BALANCED = eleven_turbo_v2_5
LLMProviderOPENROUTER = openrouter
StoryDataProviderOPENROUTER = openrouter, OLLAMA = ollama, MOCK = mock
AgenticTimelineProviderOPENROUTER = openrouter, OPENAI = openai, ANTHROPIC = anthropic, MOCK = mock
ImageProviderDALLE = dalle, STABLE_DIFFUSION = stable_diffusion, MIDJOURNEY = midjourney, PILLOW = pillow, MOCK = mock
MusicProviderMUSICGEN = musicgen, LOCAL = local, MOCK = mock

🔌 Abstract service ports

application/interfaces/services.py declares 18 abstract service classes. Their abstract method signatures are:

PortMethods
LLMService`async generate_text(prompt: str, max_tokens: int = 1000, temperature: float = 0.7, stop_sequences: list[str]
TimelineServiceasync generate_timeline(story: Story) -> AgenticTimeline.
StoryDataServiceasync generate_story_data(story_plan: dict[str, Any], style_guide: str, language: str, temperature: float) -> dict[str, Any].
StoryPlannerService`async create_story_plan(prompt: str, structure_type: str = three_act, genre: str
TTSService`async synthesize_speech(text: str, voice_id: str, stability: float = 0.5, similarity: float = 0.8, style: float
SFXService`async generate_sound_effect(description: str, duration_seconds: float = 5.0, output_path: str
MusicService`async get_background_music(mood: str, duration_seconds: float = 60.0, output_path: str
AudioProcessingServiceasync combine_audio_segments(segment_paths: list[str], output_path: str, crossfade_ms: int = 0, normalize: bool = True) -> str; async generate_silence(duration_ms: int, output_path: str) -> str; async apply_processing(audio_path: str, output_path: str, settings: AudioProcessingSettings) -> str; async analyze_audio(audio_path: str) -> dict[str, Any].
MontageService`montage_segments(segments: list[dict[str, Any]], output_path: str, ambient_path: str
ImageGenerationService`async generate_image(prompt: str, output_path: str, style: str
ImageProcessingServiceasync overlay_images(base_image_path: str, overlay_elements: list[dict[str, Any]], output_path: str) -> str; `async apply_filters(image_path: str, filters: list[dict[str, Any]], output_path: str
MusicDiscoveryService`discover_music(prompt: str, output_path: str, duration_seconds: int = 30, model: str
LLMQueryEnhancer`async enhance_sfx_query(original_query: str, context: dict[str, Any]
SFXResolverService`async resolve_sfx(query: str, output_path: str, context: dict[str, Any]
AudioSearchServiceindex_assets(assets: list[AudioAsset]) -> None; search(query: SemanticSearchQuery) -> list[SearchResult].
AgenticTimelineService`async create_timeline_from_story(story: Story, context: dict[str, Any]
AudioDirectionServiceasync direct_audio_timeline(timeline: dict[str, Any]) -> dict[str, Any].
VoiceCastingServiceasync cast_characters(characters: list[Character], voice_filter: VoiceFilter) -> list[Character]; `get_voice_for_character(character: Character, voice_filter: VoiceFilter) -> Voice

🗄️ Repository ports

domain/interfaces/repositories.py declares seven abstract repository ports, including the generic Repository base and UnitOfWork.

PortMethods
Repository[T]async save(entity: T) -> T; `async get(entity_id: UUID) -> T
StoryRepositoryasync get_by_title(title: str) -> list[Story]; async find_by_prompt(prompt_fragment: str) -> list[Story].
CharacterRepositoryasync get_by_name(name: str) -> list[Character]; async find_by_traits(traits: list[str], match_all: bool = False) -> list[Character].
VoiceRepository`async get_by_provider_id(provider: str, provider_id: str) -> Voice
MediaAssetRepositoryasync find_by_type(asset_type: MediaAssetType) -> list[AudioAsset]; async search_by_description_or_tags(query: str) -> list[AudioAsset].
CharacterVoiceMappingRepositoryasync save_mapping(mapping: CharacterVoiceMapping) -> None; `async get_mapping_by_character_id(character_id: str) -> CharacterVoiceMapping
UnitOfWorkasync __aenter__() -> UnitOfWork; async __aexit__(_exc_type, _exc_val, _exc_tb) -> None; async commit() -> None; async rollback() -> None.

🧾 SQLite schema

DatabaseTableColumns
data/storymatrix.dbstoriesid: String(36) primary key; title: String; summary: Text; prompt: Text; language: String(10); style: String; genre: String; metadata: JSON.
data/storymatrix.dbcharactersid: String(36) primary key; name: String; description: Text; traits: JSON; voice_profile: JSON; metadata: JSON; story_id: String(36) foreign key.
data/storymatrix.dbscenesid: String(36) primary key; title: String; sequence_number: Integer; scene_type: SceneType; setting: JSON; mood: String; description: Text; ambient_sound: String; background_music: String; story_id: String(36) foreign key.
data/storymatrix.dbsegmentsid: Integer primary key; sequence_number: Integer; segment_type: SegmentType; text: Text; character_id: String(36) foreign key; tone: String; description: Text; duration_ms: Integer; audio_path: String; scene_id: String(36) foreign key. Single-table polymorphism identifies narration, dialogue, SFX, and music.
data/audio_metadata.db (wired repository session)character_voice_mappingscharacter_id: String primary key; character_name: String; voice_id: String; provider: String.
data/storymatrix.dbmedia_assetsid: TEXT primary key; path: TEXT NOT NULL; asset_type: TEXT NOT NULL; source_provider: TEXT; description: TEXT; tags: TEXT; duration_seconds: REAL; created_at: TEXT NOT NULL; updated_at: TEXT NOT NULL; metadata: TEXT. The FileSystemMediaAssetRepository creates this table in the default data/storymatrix.db (repositories/file_media_asset_repository.py).
The container runs Base.metadata.create_all against both SQLite engines, so character_voice_mappings can exist physically in both files even though SQLiteCharacterVoiceMappingRepository is wired to the data/audio_metadata.db session.

🧬 Deterministic UUID5 identity

domain/utils/identity.py uses STORYMATRIX_NAMESPACE = 6ba7b810-9dad-11d1-80b4-00c04fd430c8 for story-domain names and ASSET_NAMESPACE = 1b4e28ba-2fa1-11d2-883f-0016d3cca427 for file assets.

IdentityNamespaceExact name passed to uuid5
CharacterSTORYMATRIX_NAMESPACEstorymatrix_character_{normalized_name}, where normalized_name = character_name.lower().strip()
SegmentSTORYMATRIX_NAMESPACE`storymatrix_segment
AssetASSET_NAMESPACEThe exact file_path_str argument
GenericCaller-selected namespace, default STORYMATRIX_NAMESPACEThe exact name argument

⚠️ Repository async contract blocker

Blocker B6 affects media-asset persistence: FileSystemMediaAssetRepository.save is async, while existing callers invoke it synchronously, so the result is a coroutine and accessing .id raises AttributeError: 'coroutine' object has no attribute 'id'. Track the caller conversion at index > b6.

🚨 Domain exceptions

ExceptionRaising condition
StoryMatrixExceptionBase for all application-specific exceptions.
StoryMatrixErrorBase for application-specific errors.
ApplicationErrorGeneral application-level failure.
ConfigurationErrorConfiguration is invalid or missing.
EntityNotFoundErrorA repository lookup cannot find the requested entity ID and type.
DomainErrorDomain logic rejects an operation and supplies optional detail fields.
RepositoryErrorA repository operation fails and supplies an operation name or detail.
ValidationErrorInput data fails validation.
StoryGenerationErrorStory generation encounters an error.
LLMGenerationErrorBase class for LLM interaction failures.
LLMTimeoutErrorAn LLM request exceeds its timeout; subclasses LLMGenerationError.
LLMInvalidRequestErrorAn LLM provider rejects a request as invalid; subclasses LLMGenerationError.
LLMRateLimitErrorAn LLM provider reports a rate limit; subclasses LLMGenerationError.
LLMServiceUnavailableErrorAn LLM provider is unavailable; subclasses LLMGenerationError.
ExternalServiceErrorAn external service call fails; the exception records its service name and optional details.

🔌 Integrations maps these ports to infrastructure adapters.