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 model | Fields and types |
|---|---|
Story | id: uuid.UUID; title: str; summary: str; prompt: str; language: str; `style: str |
Character | id: uuid.UUID; name: str; description: str; traits: list[str]; `voice_profile: VoiceProfile |
Scene | id: uuid.UUID; title: str; sequence_number: int; scene_type: SceneType; setting: SceneSetting; mood: str; description: str; segments: Segments; `ambient_sound: str |
SceneSetting | location: str; time_of_day: str; `weather: str |
Segment | id: str, generated by UUID4 by default; extra fields are forbidden. |
NarrationSegment | text: 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 |
SoundEffectSegment | segment_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. |
MusicSegment | segment_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. |
Segments | root: list[AnySegment]; a Pydantic root model that accepts a root list or unwraps a root mapping. |
Voice | id: str; name: str; provider: str; provider_id: str; is_available: bool; supported_languages: list[str]; metadata: dict. |
VoiceProfile | `voice_id: str |
CharacterVoiceMapping | character_id: str; character_name: str; voice_id: str; provider: str. |
VoiceCharacteristics | `accent: str |
VoiceFilter | Dataclass fields `provider: VoiceProvider |
MediaAsset | id: uuid.UUID; asset_type: MediaAssetType; created_at: datetime; `updated_at: datetime |
AudioAsset | MediaAsset fields plus `duration_seconds: float |
ImageProperties | width: int = 1024; height: int = 1024; `aspect_ratio: str |
Image | id: uuid.UUID; created_at: datetime; `filename: str |
Artifact | id: UUID; `story_id: UUID |
SearchResult | asset: AudioAsset; score: float. |
Track | track_id: str; track_type: Literal["narration", "dialogue", "sfx", "music"]; segments: list[AnySegment]. |
AgenticTimeline | title: str; tracks: list[Track]; estimated_duration: float. |
🏷️ Enum members
Domain enums
| Enum | Exact members |
|---|---|
SegmentType | NARRATION = narration, DIALOGUE = dialogue, SFX = sfx, MUSIC = music, IMAGE = image |
TTSProvider | PLAYAI = playai, ELEVENLABS = elevenlabs, PIPER = piper, COQUI = coqui, SYSTEM = system, MOCK = mock |
SFXProvider | LOCAL = local, FREESOUND = freesound, ELEVENLABS = elevenlabs, MOCK = mock |
MediaAssetType | MUSIC = music, SFX = sfx, IMAGE = image, VOICE = voice, MOCK = mock |
SceneType | INTRO = intro, CHAPTER = chapter, OUTRO = outro, TRANSITION = transition |
ArtifactType | STORY_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
| Enum | Exact members |
|---|---|
AudioType | NARRATION = narration, DIALOGUE = dialogue, MUSIC = music, SFX = sfx, TRANSITION = transition, UNKNOWN = unknown |
ElevenLabsModelStrategy | V3_EXPRESSIVE = eleven_v3, V2_MULTILINGUAL = eleven_multilingual_v2, FLASH_REALTIME = eleven_flash_v2_5, TURBO_BALANCED = eleven_turbo_v2_5 |
LLMProvider | OPENROUTER = openrouter |
StoryDataProvider | OPENROUTER = openrouter, OLLAMA = ollama, MOCK = mock |
AgenticTimelineProvider | OPENROUTER = openrouter, OPENAI = openai, ANTHROPIC = anthropic, MOCK = mock |
ImageProvider | DALLE = dalle, STABLE_DIFFUSION = stable_diffusion, MIDJOURNEY = midjourney, PILLOW = pillow, MOCK = mock |
MusicProvider | MUSICGEN = musicgen, LOCAL = local, MOCK = mock |
🔌 Abstract service ports
application/interfaces/services.py declares 18 abstract service classes. Their abstract method signatures are:
| Port | Methods |
|---|---|
LLMService | `async generate_text(prompt: str, max_tokens: int = 1000, temperature: float = 0.7, stop_sequences: list[str] |
TimelineService | async generate_timeline(story: Story) -> AgenticTimeline. |
StoryDataService | async 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 |
AudioProcessingService | async 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 |
ImageProcessingService | async 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] |
AudioSearchService | index_assets(assets: list[AudioAsset]) -> None; search(query: SemanticSearchQuery) -> list[SearchResult]. |
AgenticTimelineService | `async create_timeline_from_story(story: Story, context: dict[str, Any] |
AudioDirectionService | async direct_audio_timeline(timeline: dict[str, Any]) -> dict[str, Any]. |
VoiceCastingService | async 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.
| Port | Methods |
|---|---|
Repository[T] | async save(entity: T) -> T; `async get(entity_id: UUID) -> T |
StoryRepository | async get_by_title(title: str) -> list[Story]; async find_by_prompt(prompt_fragment: str) -> list[Story]. |
CharacterRepository | async 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 |
MediaAssetRepository | async find_by_type(asset_type: MediaAssetType) -> list[AudioAsset]; async search_by_description_or_tags(query: str) -> list[AudioAsset]. |
CharacterVoiceMappingRepository | async save_mapping(mapping: CharacterVoiceMapping) -> None; `async get_mapping_by_character_id(character_id: str) -> CharacterVoiceMapping |
UnitOfWork | async __aenter__() -> UnitOfWork; async __aexit__(_exc_type, _exc_val, _exc_tb) -> None; async commit() -> None; async rollback() -> None. |
🧾 SQLite schema
| Database | Table | Columns |
|---|---|---|
data/storymatrix.db | stories | id: String(36) primary key; title: String; summary: Text; prompt: Text; language: String(10); style: String; genre: String; metadata: JSON. |
data/storymatrix.db | characters | id: String(36) primary key; name: String; description: Text; traits: JSON; voice_profile: JSON; metadata: JSON; story_id: String(36) foreign key. |
data/storymatrix.db | scenes | id: 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.db | segments | id: 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_mappings | character_id: String primary key; character_name: String; voice_id: String; provider: String. |
data/storymatrix.db | media_assets | id: 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.
| Identity | Namespace | Exact name passed to uuid5 |
|---|---|---|
| Character | STORYMATRIX_NAMESPACE | storymatrix_character_{normalized_name}, where normalized_name = character_name.lower().strip() |
| Segment | STORYMATRIX_NAMESPACE | `storymatrix_segment |
| Asset | ASSET_NAMESPACE | The exact file_path_str argument |
| Generic | Caller-selected namespace, default STORYMATRIX_NAMESPACE | The 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
| Exception | Raising condition |
|---|---|
StoryMatrixException | Base for all application-specific exceptions. |
StoryMatrixError | Base for application-specific errors. |
ApplicationError | General application-level failure. |
ConfigurationError | Configuration is invalid or missing. |
EntityNotFoundError | A repository lookup cannot find the requested entity ID and type. |
DomainError | Domain logic rejects an operation and supplies optional detail fields. |
RepositoryError | A repository operation fails and supplies an operation name or detail. |
ValidationError | Input data fails validation. |
StoryGenerationError | Story generation encounters an error. |
LLMGenerationError | Base class for LLM interaction failures. |
LLMTimeoutError | An LLM request exceeds its timeout; subclasses LLMGenerationError. |
LLMInvalidRequestError | An LLM provider rejects a request as invalid; subclasses LLMGenerationError. |
LLMRateLimitError | An LLM provider reports a rate limit; subclasses LLMGenerationError. |
LLMServiceUnavailableError | An LLM provider is unavailable; subclasses LLMGenerationError. |
ExternalServiceError | An external service call fails; the exception records its service name and optional details. |
🔌 Integrations maps these ports to infrastructure adapters.