Skip to content

Models

src.xil_pipeline.models

Pydantic data models for the podcast production pipeline.

Defines validated, typed structures for script parsing output, cast configuration, and production dialogue entries. These models replace untyped dictionaries with field-level validation and type annotations that render as rich API documentation via mkdocstrings.

DEFAULT_SLUG module-attribute

DEFAULT_SLUG = 'sample'

TYPE_DEFAULTS module-attribute

TYPE_DEFAULTS: dict[str, dict] = {'podcast': {'gap_ms': 600, 'stability': None}, 'audiobook': {'gap_ms': 400, 'stability': 0.75}, 'drama': {'gap_ms': 800, 'stability': None}, 'special': {'gap_ms': 600, 'stability': None}}

ProjectConfig

Bases: BaseModel

Typed view of project.json.

All fields are optional with sensible defaults so that a minimal {"show": "My Show"} project.json validates without change.

Attributes:

  • show (str) –

    Human-readable show title.

  • type (Literal['podcast', 'audiobook', 'drama', 'special']) –

    Content type — "podcast" (default), "audiobook", "drama", or "special". Drives section maps, gap defaults, and xil-init sample templates.

  • season (int | None) –

    Season number, or None.

  • season_title (str | None) –

    Season arc title (e.g. "The Holiday Shift").

  • tag_format (str | None) –

    Custom episode tag format string (e.g. "V{volume:02d}C{chapter:02d}" for audiobooks). None uses the standard S01E01 / E01 derivation.

Source code in src/xil_pipeline/models.py
class ProjectConfig(BaseModel):
    """Typed view of ``project.json``.

    All fields are optional with sensible defaults so that a minimal
    ``{"show": "My Show"}`` project.json validates without change.

    Attributes:
        show: Human-readable show title.
        type: Content type — ``"podcast"`` (default), ``"audiobook"``,
            ``"drama"``, or ``"special"``.  Drives section maps, gap defaults,
            and ``xil-init`` sample templates.
        season: Season number, or ``None``.
        season_title: Season arc title (e.g. ``"The Holiday Shift"``).
        tag_format: Custom episode tag format string (e.g.
            ``"V{volume:02d}C{chapter:02d}"`` for audiobooks).  ``None``
            uses the standard ``S01E01`` / ``E01`` derivation.
    """

    show: str = Field(default="Sample Show", description="Show title")
    type: Literal["podcast", "audiobook", "drama", "special"] = Field(
        default="podcast", description="Content type"
    )
    season: int | None = Field(default=None, description="Season number")
    season_title: str | None = Field(default=None, description="Season arc title")
    tag_format: str | None = Field(
        default=None,
        description="Custom tag format (e.g. 'V{volume:02d}C{chapter:02d}')",
    )

show class-attribute instance-attribute

show: str = Field(default='Sample Show', description='Show title')

type class-attribute instance-attribute

type: Literal['podcast', 'audiobook', 'drama', 'special'] = Field(default='podcast', description='Content type')

season class-attribute instance-attribute

season: int | None = Field(default=None, description='Season number')

season_title class-attribute instance-attribute

season_title: str | None = Field(default=None, description='Season arc title')

tag_format class-attribute instance-attribute

tag_format: str | None = Field(default=None, description="Custom tag format (e.g. 'V{volume:02d}C{chapter:02d}')")

ScriptEntry

Bases: BaseModel

A single parsed entry from a production script.

Each entry represents one line or block from the markdown script, classified into one of four types: dialogue, direction, section_header, or scene_header.

Attributes:

  • seq (int) –

    Sequence number, 1-based and unique within a script.

  • type (Literal['dialogue', 'direction', 'section_header', 'scene_header']) –

    Entry classification determining how the line is processed.

  • section (str | None) –

    Current section slug (e.g., "cold-open", "act1").

  • scene (str | None) –

    Current scene slug (e.g., "scene-1") or None.

  • speaker (str | None) –

    Normalized speaker key for dialogue entries (e.g., "adam").

  • direction (str | None) –

    Parenthetical acting direction for dialogue lines.

  • text (str) –

    The spoken text, header text, or stage direction content.

  • direction_type (Literal['SFX', 'MUSIC', 'AMBIENCE', 'BEAT', 'VINTAGE FILTER'] | None) –

    Subtype for direction entries indicating sound category.

Source code in src/xil_pipeline/models.py
class ScriptEntry(BaseModel):
    """A single parsed entry from a production script.

    Each entry represents one line or block from the markdown script,
    classified into one of four types: dialogue, direction,
    section_header, or scene_header.

    Attributes:
        seq: Sequence number, 1-based and unique within a script.
        type: Entry classification determining how the line is processed.
        section: Current section slug (e.g., ``"cold-open"``, ``"act1"``).
        scene: Current scene slug (e.g., ``"scene-1"``) or ``None``.
        speaker: Normalized speaker key for dialogue entries (e.g., ``"adam"``).
        direction: Parenthetical acting direction for dialogue lines.
        text: The spoken text, header text, or stage direction content.
        direction_type: Subtype for direction entries indicating sound category.
    """

    seq: int = Field(..., description="Sequence number")
    type: Literal["dialogue", "direction", "section_header", "scene_header"] = Field(
        ..., description="Entry classification"
    )
    section: str | None = Field(default=None, description="Current section slug")
    scene: str | None = Field(default=None, description="Current scene slug")
    speaker: str | None = Field(default=None, description="Normalized speaker key")
    direction: str | None = Field(default=None, description="Acting direction")
    text: str = Field(..., description="Entry content text")
    direction_type: Literal["SFX", "MUSIC", "AMBIENCE", "BEAT", "VINTAGE FILTER"] | None = Field(
        default=None, description="Sound category for direction entries"
    )
    sfx_source: str | None = Field(
        default=None,
        description="Scriptwriter SFX source hint (e.g. 'SFX/filename.mp3'), "
                    "stripped from '| filename' annotation in the script",
    )

seq class-attribute instance-attribute

seq: int = Field(..., description='Sequence number')

type class-attribute instance-attribute

type: Literal['dialogue', 'direction', 'section_header', 'scene_header'] = Field(..., description='Entry classification')

section class-attribute instance-attribute

section: str | None = Field(default=None, description='Current section slug')

scene class-attribute instance-attribute

scene: str | None = Field(default=None, description='Current scene slug')

speaker class-attribute instance-attribute

speaker: str | None = Field(default=None, description='Normalized speaker key')

direction class-attribute instance-attribute

direction: str | None = Field(default=None, description='Acting direction')

text class-attribute instance-attribute

text: str = Field(..., description='Entry content text')

direction_type class-attribute instance-attribute

direction_type: Literal['SFX', 'MUSIC', 'AMBIENCE', 'BEAT', 'VINTAGE FILTER'] | None = Field(default=None, description='Sound category for direction entries')

sfx_source class-attribute instance-attribute

sfx_source: str | None = Field(default=None, description="Scriptwriter SFX source hint (e.g. 'SFX/filename.mp3'), stripped from '| filename' annotation in the script")

ScriptStats

Bases: BaseModel

Aggregate statistics for a parsed production script.

Attributes:

Source code in src/xil_pipeline/models.py
class ScriptStats(BaseModel):
    """Aggregate statistics for a parsed production script.

    Attributes:
        total_entries: Total number of parsed entries.
        dialogue_lines: Count of dialogue-type entries.
        direction_lines: Count of direction-type entries.
        characters_for_tts: Total character count across all dialogue text.
        speakers: Sorted list of unique speaker keys found in the script.
        sections: Sorted list of unique section slugs found in the script.
    """

    total_entries: int = Field(..., ge=0, description="Total parsed entries")
    dialogue_lines: int = Field(..., ge=0, description="Dialogue entry count")
    direction_lines: int = Field(..., ge=0, description="Direction entry count")
    characters_for_tts: int = Field(..., ge=0, description="TTS character budget")
    speakers: list[str] = Field(..., description="Unique speaker keys")
    sections: list[str] = Field(..., description="Unique section slugs")

total_entries class-attribute instance-attribute

total_entries: int = Field(..., ge=0, description='Total parsed entries')

dialogue_lines class-attribute instance-attribute

dialogue_lines: int = Field(..., ge=0, description='Dialogue entry count')

direction_lines class-attribute instance-attribute

direction_lines: int = Field(..., ge=0, description='Direction entry count')

characters_for_tts class-attribute instance-attribute

characters_for_tts: int = Field(..., ge=0, description='TTS character budget')

speakers class-attribute instance-attribute

speakers: list[str] = Field(..., description='Unique speaker keys')

sections class-attribute instance-attribute

sections: list[str] = Field(..., description='Unique section slugs')

ParsedScript

Bases: BaseModel

Complete output of the script parsing stage.

Produced by parse_script() in XILP001, consumed by load_production() in XILP002.

Attributes:

  • show (str) –

    Show title (e.g., "nightowls").

  • season (int | None) –

    Season number, or None if not declared in the script header.

  • episode (int) –

    Episode number.

  • title (str) –

    Episode title.

  • season_title (str | None) –

    Season arc title extracted from Arc: "…" in the script header (e.g. "The Holiday Shift"). None when the header contains no arc declaration.

  • source_file (str) –

    Basename of the source markdown file.

  • entries (list[ScriptEntry]) –

    Ordered list of parsed script entries.

  • stats (ScriptStats) –

    Aggregate statistics for the parsed script.

Source code in src/xil_pipeline/models.py
class ParsedScript(BaseModel):
    """Complete output of the script parsing stage.

    Produced by ``parse_script()`` in XILP001, consumed by
    ``load_production()`` in XILP002.

    Attributes:
        show: Show title (e.g., ``"nightowls"``).
        season: Season number, or ``None`` if not declared in the script header.
        episode: Episode number.
        title: Episode title.
        season_title: Season arc title extracted from ``Arc: "…"`` in the script
            header (e.g. ``"The Holiday Shift"``).  ``None`` when the header
            contains no arc declaration.
        source_file: Basename of the source markdown file.
        entries: Ordered list of parsed script entries.
        stats: Aggregate statistics for the parsed script.
    """

    show: str = Field(..., description="Show title")
    season: int | None = Field(default=None, description="Season number")
    episode: int = Field(..., description="Episode number")
    title: str = Field(..., description="Episode title")
    season_title: str | None = Field(
        default=None,
        description="Season arc title from 'Arc: \"\"' in the script header",
    )
    source_file: str = Field(..., description="Source markdown filename")
    entries: list[ScriptEntry] = Field(..., description="Parsed script entries")
    stats: ScriptStats = Field(..., description="Aggregate statistics")

    @property
    def tag(self) -> str:
        """Compact season/episode tag, e.g. ``S01E01`` or ``E01``."""
        return episode_tag(self.season, self.episode)

show class-attribute instance-attribute

show: str = Field(..., description='Show title')

season class-attribute instance-attribute

season: int | None = Field(default=None, description='Season number')

episode class-attribute instance-attribute

episode: int = Field(..., description='Episode number')

title class-attribute instance-attribute

title: str = Field(..., description='Episode title')

season_title class-attribute instance-attribute

season_title: str | None = Field(default=None, description='Season arc title from \'Arc: "…"\' in the script header')

source_file class-attribute instance-attribute

source_file: str = Field(..., description='Source markdown filename')

entries class-attribute instance-attribute

entries: list[ScriptEntry] = Field(..., description='Parsed script entries')

stats class-attribute instance-attribute

stats: ScriptStats = Field(..., description='Aggregate statistics')

tag property

tag: str

Compact season/episode tag, e.g. S01E01 or E01.

CastMember

Bases: BaseModel

Configuration for a single cast member's voice and audio settings.

Maps a character to their ElevenLabs voice and stereo positioning.

Attributes:

  • full_name (str) –

    Character's display name (e.g., "Adam Santos").

  • voice_id (str) –

    ElevenLabs voice identifier, or "TBD" if unassigned.

  • pan (float) –

    Stereo pan position from -1.0 (full left) to 1.0 (full right).

  • filter (str | bool | None) –

    Audio filter chain. False/None = none; True/"phone" = phone filter; "vintage" = vintage filter; "vintage,phone" = both filters applied in listed order.

  • role (str) –

    Character role description (e.g., "Host/Narrator").

Source code in src/xil_pipeline/models.py
class CastMember(BaseModel):
    """Configuration for a single cast member's voice and audio settings.

    Maps a character to their ElevenLabs voice and stereo positioning.

    Attributes:
        full_name: Character's display name (e.g., ``"Adam Santos"``).
        voice_id: ElevenLabs voice identifier, or ``"TBD"`` if unassigned.
        pan: Stereo pan position from -1.0 (full left) to 1.0 (full right).
        filter: Audio filter chain. ``False``/``None`` = none; ``True``/``"phone"``
            = phone filter; ``"vintage"`` = vintage filter; ``"vintage,phone"``
            = both filters applied in listed order.
        role: Character role description (e.g., ``"Host/Narrator"``).
    """

    full_name: str = Field(..., description="Character display name")
    voice_id: str = Field(..., description="ElevenLabs voice ID, 'TBD' if unassigned, or '' for non-ElevenLabs backends")
    pan: float = Field(..., ge=-1.0, le=1.0, description="Stereo pan position")
    filter: str | bool | None = Field(..., description="Audio filter chain (false/phone/vintage/vintage,phone)")
    role: str = Field(..., description="Character role description")
    stability: float | None = Field(
        default=None, ge=0.0, le=1.0,
        description="Voice stability (0=expressive, 1=monotone); None uses voice default",
    )
    similarity_boost: float | None = Field(
        default=None, ge=0.0, le=1.0,
        description="Adherence to original voice (0=loose, 1=strict); None uses voice default",
    )
    style: float | None = Field(
        default=None, ge=0.0, le=1.0,
        description="Style exaggeration of the original speaker; None uses voice default",
    )
    use_speaker_boost: bool | None = Field(
        default=None,
        description="Boost similarity to original speaker (higher latency); None uses voice default",
    )
    language_code: str | None = Field(
        default=None,
        description="ISO 639-1 language code for text normalisation (e.g. 'en', 'de'); None = auto",
    )
    speed: float | None = Field(
        default=None, ge=0.7, le=1.5,
        description="TTS speaking rate (0.7=slow … 1.0=default … 1.5=fast); None uses voice default",
    )

full_name class-attribute instance-attribute

full_name: str = Field(..., description='Character display name')

voice_id class-attribute instance-attribute

voice_id: str = Field(..., description="ElevenLabs voice ID, 'TBD' if unassigned, or '' for non-ElevenLabs backends")

pan class-attribute instance-attribute

pan: float = Field(..., ge=-1.0, le=1.0, description='Stereo pan position')

filter class-attribute instance-attribute

filter: str | bool | None = Field(..., description='Audio filter chain (false/phone/vintage/vintage,phone)')

role class-attribute instance-attribute

role: str = Field(..., description='Character role description')

stability class-attribute instance-attribute

stability: float | None = Field(default=None, ge=0.0, le=1.0, description='Voice stability (0=expressive, 1=monotone); None uses voice default')

similarity_boost class-attribute instance-attribute

similarity_boost: float | None = Field(default=None, ge=0.0, le=1.0, description='Adherence to original voice (0=loose, 1=strict); None uses voice default')

style class-attribute instance-attribute

style: float | None = Field(default=None, ge=0.0, le=1.0, description='Style exaggeration of the original speaker; None uses voice default')

use_speaker_boost class-attribute instance-attribute

use_speaker_boost: bool | None = Field(default=None, description='Boost similarity to original speaker (higher latency); None uses voice default')

language_code class-attribute instance-attribute

language_code: str | None = Field(default=None, description="ISO 639-1 language code for text normalisation (e.g. 'en', 'de'); None = auto")

speed class-attribute instance-attribute

speed: float | None = Field(default=None, ge=0.7, le=1.5, description='TTS speaking rate (0.7=slow … 1.0=default … 1.5=fast); None uses voice default')

PreambleSegment

Bases: BaseModel

One text slice of a multi-part preamble or postamble.

Attributes:

  • text (str) –

    Spoken text (may use {season_title}, {episode}, {title} placeholders).

  • shared_key (str | None) –

    Retained for backward compatibility with existing cast JSONs. No longer used at generation time — all segments are joined and sent as a single TTS call to produce seamless prosody across the whole block.

Source code in src/xil_pipeline/models.py
class PreambleSegment(BaseModel):
    """One text slice of a multi-part preamble or postamble.

    Attributes:
        text: Spoken text (may use {season_title}, {episode}, {title} placeholders).
        shared_key: Retained for backward compatibility with existing cast JSONs.
            No longer used at generation time — all segments are joined and sent
            as a single TTS call to produce seamless prosody across the whole block.
    """

    text: str = Field(..., description="Segment text (may use {season_title}, {episode}, {title})")
    shared_key: str | None = Field(
        default=None,
        description="Legacy cache key; retained for backward compatibility, not used for generation",
    )

text class-attribute instance-attribute

text: str = Field(..., description='Segment text (may use {season_title}, {episode}, {title})')

shared_key class-attribute instance-attribute

shared_key: str | None = Field(default=None, description='Legacy cache key; retained for backward compatibility, not used for generation')

Preamble

Bases: BaseModel

Broadcast introduction prepended to every episode.

Attributes:

  • text (str | None) –

    Single-string intro text (legacy). Mutually exclusive with segments.

  • segments (list[PreambleSegment] | None) –

    Ordered list of cacheable text segments. Stock segments carry a shared_key so they are generated once and reused; the variable episode-identifier segment has shared_key=None.

  • speaker (str) –

    Cast key for the reader (e.g. "tina").

  • speed (float | None) –

    TTS speaking rate passed to ElevenLabs VoiceSettings (0.7–1.2, default 1.0). Values below 1.0 slow the reader down.

Source code in src/xil_pipeline/models.py
class Preamble(BaseModel):
    """Broadcast introduction prepended to every episode.

    Attributes:
        text: Single-string intro text (legacy).  Mutually exclusive with ``segments``.
        segments: Ordered list of cacheable text segments.  Stock segments carry a
            ``shared_key`` so they are generated once and reused; the variable
            episode-identifier segment has ``shared_key=None``.
        speaker: Cast key for the reader (e.g. "tina").
        speed: TTS speaking rate passed to ElevenLabs VoiceSettings (0.7–1.2,
            default 1.0). Values below 1.0 slow the reader down.
    """

    text: str | None = Field(
        default=None,
        description="Intro text (may use {season_title}, {episode}, {title}); legacy single-string form",
    )
    segments: list[PreambleSegment] | None = Field(
        default=None,
        description="Ordered cacheable segments; preferred over 'text' for new episodes",
    )
    speaker: str = Field(..., description="Cast member key for TTS generation")
    speed: float | None = Field(
        default=None, ge=0.7, le=1.2,
        description="TTS speaking rate (0.7–1.2); None uses the voice default"
    )

text class-attribute instance-attribute

text: str | None = Field(default=None, description='Intro text (may use {season_title}, {episode}, {title}); legacy single-string form')

segments class-attribute instance-attribute

segments: list[PreambleSegment] | None = Field(default=None, description="Ordered cacheable segments; preferred over 'text' for new episodes")

speaker class-attribute instance-attribute

speaker: str = Field(..., description='Cast member key for TTS generation')

speed class-attribute instance-attribute

speed: float | None = Field(default=None, ge=0.7, le=1.2, description='TTS speaking rate (0.7–1.2); None uses the voice default')

CastConfiguration

Bases: BaseModel

Complete cast configuration for a production episode.

Loaded from the cast config JSON and used by load_production() to map speaker keys to voice and audio settings.

Attributes:

  • show (str) –

    Show title (e.g., "nightowls").

  • season (int | None) –

    Season number, or None if not set in the cast file.

  • episode (int | None) –

    Episode number.

  • title (str | None) –

    Episode title (optional, not used during production).

  • season_title (str | None) –

    Season subtitle/arc title (e.g., "The Letters").

  • preamble (Preamble | None) –

    Broadcast intro configuration, or None if not configured.

  • cast (dict[str, CastMember]) –

    Mapping of speaker keys to their voice configurations.

Source code in src/xil_pipeline/models.py
class CastConfiguration(BaseModel):
    """Complete cast configuration for a production episode.

    Loaded from the cast config JSON and used by ``load_production()``
    to map speaker keys to voice and audio settings.

    Attributes:
        show: Show title (e.g., ``"nightowls"``).
        season: Season number, or ``None`` if not set in the cast file.
        episode: Episode number.
        title: Episode title (optional, not used during production).
        season_title: Season subtitle/arc title (e.g., ``"The Letters"``).
        preamble: Broadcast intro configuration, or ``None`` if not configured.
        cast: Mapping of speaker keys to their voice configurations.
    """

    show: str = Field(..., description="Show title")
    season: int | None = Field(default=None, description="Season number")
    episode: int | None = Field(default=None, description="Episode number")
    tag_override: str | None = Field(
        default=None,
        description="Raw tag for non-episodic content (e.g. V01C03, D01) — overrides season/episode derivation",
    )
    title: str | None = Field(default=None, description="Episode title")
    season_title: str | None = Field(default=None, description="Season subtitle/arc title")
    artist: str = Field(
        default="XIL Pipeline",
        description="Artist/creator credit for audio metadata",
    )
    preamble: Preamble | None = Field(default=None, description="Broadcast intro config")
    postamble: Preamble | None = Field(default=None, description="Broadcast outro config")
    cast: dict[str, CastMember] = Field(..., description="Speaker-to-config mapping")

    @property
    def tag(self) -> str:
        """Compact tag: raw override (e.g. ``V01C03``) or derived ``S01E01`` / ``E01``."""
        if self.tag_override:
            return self.tag_override
        if self.episode is None:
            raise ValueError("CastConfiguration requires either tag_override or episode")
        return episode_tag(self.season, self.episode)

show class-attribute instance-attribute

show: str = Field(..., description='Show title')

season class-attribute instance-attribute

season: int | None = Field(default=None, description='Season number')

episode class-attribute instance-attribute

episode: int | None = Field(default=None, description='Episode number')

tag_override class-attribute instance-attribute

tag_override: str | None = Field(default=None, description='Raw tag for non-episodic content (e.g. V01C03, D01) — overrides season/episode derivation')

title class-attribute instance-attribute

title: str | None = Field(default=None, description='Episode title')

season_title class-attribute instance-attribute

season_title: str | None = Field(default=None, description='Season subtitle/arc title')

artist class-attribute instance-attribute

artist: str = Field(default='XIL Pipeline', description='Artist/creator credit for audio metadata')

preamble class-attribute instance-attribute

preamble: Preamble | None = Field(default=None, description='Broadcast intro config')

postamble class-attribute instance-attribute

postamble: Preamble | None = Field(default=None, description='Broadcast outro config')

cast class-attribute instance-attribute

cast: dict[str, CastMember] = Field(..., description='Speaker-to-config mapping')

tag property

tag: str

Compact tag: raw override (e.g. V01C03) or derived S01E01 / E01.

VoiceConfig

Bases: BaseModel

Simplified voice configuration used during voice generation.

Built from CastMember by load_production(), carrying only the fields needed for TTS generation and audio assembly.

Attributes:

  • id (str) –

    ElevenLabs voice identifier.

  • pan (float) –

    Stereo pan position from -1.0 (full left) to 1.0 (full right).

  • filter (str | bool | None) –

    Audio filter chain (see CastMember.filter).

Source code in src/xil_pipeline/models.py
class VoiceConfig(BaseModel):
    """Simplified voice configuration used during voice generation.

    Built from ``CastMember`` by ``load_production()``, carrying only
    the fields needed for TTS generation and audio assembly.

    Attributes:
        id: ElevenLabs voice identifier.
        pan: Stereo pan position from -1.0 (full left) to 1.0 (full right).
        filter: Audio filter chain (see ``CastMember.filter``).
    """

    id: str = Field(..., description="ElevenLabs voice ID")
    pan: float = Field(..., ge=-1.0, le=1.0, description="Stereo pan position")
    filter: str | bool | None = Field(..., description="Audio filter chain (false/phone/vintage/vintage,phone)")

id class-attribute instance-attribute

id: str = Field(..., description='ElevenLabs voice ID')

pan class-attribute instance-attribute

pan: float = Field(..., ge=-1.0, le=1.0, description='Stereo pan position')

filter class-attribute instance-attribute

filter: str | bool | None = Field(..., description='Audio filter chain (false/phone/vintage/vintage,phone)')

DialogueEntry

Bases: BaseModel

A single dialogue line prepared for voice generation.

Produced by load_production() from parsed script entries, enriched with the stem filename for audio output.

Attributes:

  • speaker (str) –

    Normalized speaker key (e.g., "adam").

  • text (str) –

    Spoken dialogue text to synthesize.

  • stem_name (str) –

    Output filename stem (e.g., "003_cold-open_adam").

  • seq (int) –

    Sequence number from the parsed script.

  • direction (str | None) –

    Acting direction for the line, if any.

Source code in src/xil_pipeline/models.py
class DialogueEntry(BaseModel):
    """A single dialogue line prepared for voice generation.

    Produced by ``load_production()`` from parsed script entries,
    enriched with the stem filename for audio output.

    Attributes:
        speaker: Normalized speaker key (e.g., ``"adam"``).
        text: Spoken dialogue text to synthesize.
        stem_name: Output filename stem (e.g., ``"003_cold-open_adam"``).
        seq: Sequence number from the parsed script.
        direction: Acting direction for the line, if any.
    """

    speaker: str = Field(..., description="Speaker key")
    text: str = Field(..., description="Dialogue text for TTS")
    stem_name: str = Field(..., description="Output audio stem name")
    seq: int = Field(..., description="Sequence number")
    section: str | None = Field(default=None, description="Script section slug (e.g. 'preamble', 'act1')")
    direction: str | None = Field(default=None, description="Acting direction")

speaker class-attribute instance-attribute

speaker: str = Field(..., description='Speaker key')

text class-attribute instance-attribute

text: str = Field(..., description='Dialogue text for TTS')

stem_name class-attribute instance-attribute

stem_name: str = Field(..., description='Output audio stem name')

seq class-attribute instance-attribute

seq: int = Field(..., description='Sequence number')

section class-attribute instance-attribute

section: str | None = Field(default=None, description="Script section slug (e.g. 'preamble', 'act1')")

direction class-attribute instance-attribute

direction: str | None = Field(default=None, description='Acting direction')

SfxEntry

Bases: BaseModel

A single sound effect mapping from script direction to API parameters.

Maps a direction entry's text (e.g., "SFX: PHONE BUZZING") to the ElevenLabs Sound Effects API parameters needed to generate it, or marks it as silence (for BEAT entries).

Note: per-effect volume is always volume_percentage, not the prefixed form (ambience_volume_percentage etc.) — those belong in defaults only.

Attributes:

  • prompt (str | None) –

    Natural-language description for the ElevenLabs SFX API. None for silence entries.

  • type (Literal['sfx', 'silence']) –

    Whether this is an API-generated sound effect or local silence.

  • duration_seconds (float) –

    Length of the generated audio (0.5–30.0s).

  • prompt_influence (float | None) –

    How closely the output follows the prompt (0.0–1.0). None to use the config-level default.

  • loop (bool) –

    Whether the effect should be loopable (useful for ambience).

Source code in src/xil_pipeline/models.py
class SfxEntry(BaseModel):
    """A single sound effect mapping from script direction to API parameters.

    Maps a direction entry's text (e.g., ``"SFX: PHONE BUZZING"``) to the
    ElevenLabs Sound Effects API parameters needed to generate it, or marks
    it as silence (for BEAT entries).

    Note: per-effect volume is always ``volume_percentage``, not the prefixed
    form (``ambience_volume_percentage`` etc.) — those belong in ``defaults`` only.

    Attributes:
        prompt: Natural-language description for the ElevenLabs SFX API.
            ``None`` for silence entries.
        type: Whether this is an API-generated sound effect or local silence.
        duration_seconds: Length of the generated audio (0.5–30.0s).
        prompt_influence: How closely the output follows the prompt (0.0–1.0).
            ``None`` to use the config-level default.
        loop: Whether the effect should be loopable (useful for ambience).
    """

    prompt: str | None = Field(default=None, description="ElevenLabs SFX prompt")
    type: Literal["sfx", "silence"] = Field(
        default="sfx", description="Effect type: API-generated or local silence"
    )
    duration_seconds: float = Field(
        default=5.0, ge=0.0, description="Audio duration in seconds (0.0 for stop markers)"
    )
    prompt_influence: float | None = Field(
        default=None, ge=0.0, le=1.0,
        description="Prompt adherence (0.0–1.0), None for config default",
    )
    loop: bool = Field(default=False, description="Generate loopable audio")
    source: str | None = Field(
        default=None,
        description="Path to a pre-existing audio file (bypasses API generation)",
    )
    volume_percentage: float | None = Field(
        default=None, ge=0.0, le=200.0,
        description="Playback volume as percentage (100=unity); None uses category default",
    )
    ramp_in_seconds: float | None = Field(
        default=None, ge=0.0, le=30.0,
        description="Fade-in duration in seconds; None uses category default",
    )
    ramp_out_seconds: float | None = Field(
        default=None, ge=0.0, le=30.0,
        description="Fade-out duration in seconds; None uses category default",
    )
    play_duration: float | None = Field(
        default=None, ge=0.0, le=100.0,
        description="Percentage of clip duration to play (100=full); None plays full clip",
    )

    @model_validator(mode="before")
    @classmethod
    def _reject_prefixed_volume(cls, data: object) -> object:
        if not isinstance(data, dict):
            return data
        bad = [k for k in data if k in ("ambience_volume_percentage",
                                         "music_volume_percentage",
                                         "sfx_volume_percentage",
                                         "vintage_filter_volume_percentage")]
        if bad:
            raise ValueError(
                f"Unknown field(s) in SfxEntry: {bad}. "
                "Use 'volume_percentage' for per-effect volume; "
                "the prefixed forms (ambience_volume_percentage, etc.) "
                "belong in the 'defaults' block only."
            )
        return data

    @model_validator(mode="after")
    def _check_api_duration_cap(self) -> "SfxEntry":
        """Enforce the 30 s ElevenLabs API cap and zero-duration guard."""
        if self.type == "sfx" and self.source is None:
            if self.duration_seconds == 0.0:
                raise ValueError(
                    "duration_seconds must be > 0 for API-generated effects; "
                    "use type='silence' for stop markers"
                )
            if self.duration_seconds > 30.0:
                raise ValueError(
                    f"duration_seconds must be ≤ 30.0 for API-generated effects "
                    f"(got {self.duration_seconds}); set source= for pre-existing files"
                )
        return self

prompt class-attribute instance-attribute

prompt: str | None = Field(default=None, description='ElevenLabs SFX prompt')

type class-attribute instance-attribute

type: Literal['sfx', 'silence'] = Field(default='sfx', description='Effect type: API-generated or local silence')

duration_seconds class-attribute instance-attribute

duration_seconds: float = Field(default=5.0, ge=0.0, description='Audio duration in seconds (0.0 for stop markers)')

prompt_influence class-attribute instance-attribute

prompt_influence: float | None = Field(default=None, ge=0.0, le=1.0, description='Prompt adherence (0.0–1.0), None for config default')

loop class-attribute instance-attribute

loop: bool = Field(default=False, description='Generate loopable audio')

source class-attribute instance-attribute

source: str | None = Field(default=None, description='Path to a pre-existing audio file (bypasses API generation)')

volume_percentage class-attribute instance-attribute

volume_percentage: float | None = Field(default=None, ge=0.0, le=200.0, description='Playback volume as percentage (100=unity); None uses category default')

ramp_in_seconds class-attribute instance-attribute

ramp_in_seconds: float | None = Field(default=None, ge=0.0, le=30.0, description='Fade-in duration in seconds; None uses category default')

ramp_out_seconds class-attribute instance-attribute

ramp_out_seconds: float | None = Field(default=None, ge=0.0, le=30.0, description='Fade-out duration in seconds; None uses category default')

play_duration class-attribute instance-attribute

play_duration: float | None = Field(default=None, ge=0.0, le=100.0, description='Percentage of clip duration to play (100=full); None plays full clip')

SfxConfiguration

Bases: BaseModel

Sound effects configuration for a production episode.

Analogous to :class:CastConfiguration for voices. Maps parsed direction entry text to ElevenLabs Sound Effects API parameters.

Attributes:

  • show (str) –

    Show title (e.g., "nightowls").

  • season (int | None) –

    Season number, or None if not declared.

  • episode (int | None) –

    Episode number.

  • defaults (dict) –

    Shared default settings (e.g., prompt_influence).

  • effects (dict[str, SfxEntry]) –

    Mapping of direction text to SFX entry configurations.

Source code in src/xil_pipeline/models.py
class SfxConfiguration(BaseModel):
    """Sound effects configuration for a production episode.

    Analogous to :class:`CastConfiguration` for voices. Maps parsed
    direction entry text to ElevenLabs Sound Effects API parameters.

    Attributes:
        show: Show title (e.g., ``"nightowls"``).
        season: Season number, or ``None`` if not declared.
        episode: Episode number.
        defaults: Shared default settings (e.g., ``prompt_influence``).
        effects: Mapping of direction text to SFX entry configurations.
    """

    show: str = Field(..., description="Show title")
    season: int | None = Field(default=None, description="Season number")
    episode: int | None = Field(default=None, description="Episode number")
    tag_override: str | None = Field(
        default=None,
        description="Raw tag for non-episodic content (e.g. V01C03, D01) — overrides season/episode derivation",
    )
    defaults: dict = Field(default_factory=dict, description="Shared SFX defaults")
    effects: dict[str, SfxEntry] = Field(
        ..., description="Direction text to SFX mapping"
    )
    vintage_scenes: list[str] = Field(
        default_factory=list,
        description=(
            "Scene labels whose dialogue receives the vintage audio filter "
            "(e.g. [\"scene-3\", \"scene-4\"]). Empty list = no vintage treatment."
        ),
    )

    @property
    def tag(self) -> str:
        """Compact tag: raw override (e.g. ``V01C03``) or derived ``S01E01`` / ``E01``."""
        if self.tag_override:
            return self.tag_override
        if self.episode is None:
            raise ValueError("SfxConfiguration requires either tag_override or episode")
        return episode_tag(self.season, self.episode)

show class-attribute instance-attribute

show: str = Field(..., description='Show title')

season class-attribute instance-attribute

season: int | None = Field(default=None, description='Season number')

episode class-attribute instance-attribute

episode: int | None = Field(default=None, description='Episode number')

tag_override class-attribute instance-attribute

tag_override: str | None = Field(default=None, description='Raw tag for non-episodic content (e.g. V01C03, D01) — overrides season/episode derivation')

defaults class-attribute instance-attribute

defaults: dict = Field(default_factory=dict, description='Shared SFX defaults')

effects class-attribute instance-attribute

effects: dict[str, SfxEntry] = Field(..., description='Direction text to SFX mapping')

vintage_scenes class-attribute instance-attribute

vintage_scenes: list[str] = Field(default_factory=list, description='Scene labels whose dialogue receives the vintage audio filter (e.g. ["scene-3", "scene-4"]). Empty list = no vintage treatment.')

tag property

tag: str

Compact tag: raw override (e.g. V01C03) or derived S01E01 / E01.

get_workspace_root

get_workspace_root() -> Path

Return the active workspace root.

Resolves in priority order: 1. XIL_PROJECTROOT environment variable (absolute path). 2. Current working directory (existing behaviour).

Source code in src/xil_pipeline/models.py
def get_workspace_root() -> Path:
    """Return the active workspace root.

    Resolves in priority order:
    1. ``XIL_PROJECTROOT`` environment variable (absolute path).
    2. Current working directory (existing behaviour).
    """
    env_val = os.environ.get("XIL_PROJECTROOT")
    if env_val:
        return Path(env_val).expanduser().resolve()
    return Path.cwd()

show_slug

show_slug(show_name: str) -> str

Convert a show title to a filesystem-safe slug.

Lowercases the string and strips all non-alphanumeric characters.

Parameters:

  • show_name (str) –

    Human-readable show title (e.g., "nightowls").

Returns:

  • str

    Compact slug like "nightowls" or "mypodcast".

Source code in src/xil_pipeline/models.py
def show_slug(show_name: str) -> str:
    """Convert a show title to a filesystem-safe slug.

    Lowercases the string and strips all non-alphanumeric characters.

    Args:
        show_name: Human-readable show title (e.g., ``"nightowls"``).

    Returns:
        Compact slug like ``"nightowls"`` or ``"mypodcast"``.
    """
    return re.sub(r"[^a-z0-9]", "", show_name.lower())

derive_paths_legacy

derive_paths_legacy(slug: str, tag: str) -> dict[str, str]

Legacy workspace layout paths (pre-0.1.8) — used by the migration tool.

Parameters:

  • slug (str) –

    Show slug (e.g., "the413").

  • tag (str) –

    Episode tag (e.g., "S01E01").

Returns:

  • dict[str, str]

    Dictionary mapping logical names to legacy absolute file paths.

Source code in src/xil_pipeline/models.py
def derive_paths_legacy(slug: str, tag: str) -> dict[str, str]:
    """Legacy workspace layout paths (pre-0.1.8) — used by the migration tool.

    Args:
        slug: Show slug (e.g., ``"the413"``).
        tag: Episode tag (e.g., ``"S01E01"``).

    Returns:
        Dictionary mapping logical names to legacy absolute file paths.
    """
    root = str(get_workspace_root())
    return {
        "cast": os.path.join(root, f"cast_{slug}_{tag}.json"),
        "sfx": os.path.join(root, f"sfx_{slug}_{tag}.json"),
        "parsed": os.path.join(root, "parsed", f"parsed_{slug}_{tag}.json"),
        "parsed_csv": os.path.join(root, "parsed", f"parsed_{slug}_{tag}.csv"),
        "annotated_csv": os.path.join(root, "parsed", f"parsed_{slug}_{tag}_annotated.csv"),
        "master": os.path.join(root, f"{slug}_{tag}_master.mp3"),
        "cues": os.path.join(root, "cues", f"cues_{slug}_{tag}.md"),
        "cues_manifest": os.path.join(root, "cues", f"cues_manifest_{tag}.json"),
        "orig_parsed": os.path.join(root, "parsed", f"orig_parsed_{slug}_{tag}.json"),
        "revised_script": os.path.join(root, "scripts", f"revised_{slug}_{tag}.md"),
        "stems": os.path.join(root, "stems", slug, tag),
        "daw": os.path.join(root, "daw", tag),
    }

derive_paths

derive_paths(slug: str, tag: str) -> dict[str, str]

Derive all standard pipeline file paths from a show slug and episode tag.

Auto-detects workspace layout: returns legacy paths when the cast config exists at the legacy root location (pre-0.1.8 workspaces), and normalized paths otherwise (new workspaces or post-migration). Run xil migrate-workspace to move an existing workspace to the normalized layout.

Parameters:

  • slug (str) –

    Show slug (e.g., "nightowls").

  • tag (str) –

    Episode tag (e.g., "S01E01").

Returns:

  • dict[str, str]

    Dictionary mapping logical names to relative file paths.

Source code in src/xil_pipeline/models.py
def derive_paths(slug: str, tag: str) -> dict[str, str]:
    """Derive all standard pipeline file paths from a show slug and episode tag.

    Auto-detects workspace layout: returns legacy paths when the cast config
    exists at the legacy root location (pre-0.1.8 workspaces), and normalized
    paths otherwise (new workspaces or post-migration).  Run ``xil migrate-workspace``
    to move an existing workspace to the normalized layout.

    Args:
        slug: Show slug (e.g., ``"nightowls"``).
        tag: Episode tag (e.g., ``"S01E01"``).

    Returns:
        Dictionary mapping logical names to relative file paths.
    """
    new = _derive_paths_new(slug, tag)
    legacy = derive_paths_legacy(slug, tag)
    use_legacy = os.path.exists(legacy["cast"]) and not os.path.exists(new["cast"])
    return legacy if use_legacy else new

load_project_config

load_project_config(project_path: str = 'project.json') -> ProjectConfig

Load and validate project.json, returning a :class:ProjectConfig.

Parameters:

  • project_path (str, default: 'project.json' ) –

    Path to the project config file.

Returns:

  • ProjectConfig

    class:ProjectConfig with all fields populated (defaults where absent).

Source code in src/xil_pipeline/models.py
def load_project_config(project_path: str = "project.json") -> ProjectConfig:
    """Load and validate ``project.json``, returning a :class:`ProjectConfig`.

    Args:
        project_path: Path to the project config file.

    Returns:
        :class:`ProjectConfig` with all fields populated (defaults where absent).
    """
    data = _read_project(project_path)
    return ProjectConfig(**data)

resolve_project_type

resolve_project_type(project_path: str = 'project.json') -> str

Return the content type from project.json, defaulting to "podcast".

Parameters:

  • project_path (str, default: 'project.json' ) –

    Path to the project config file.

Returns:

  • str

    One of "podcast", "audiobook", "drama", or "special".

Source code in src/xil_pipeline/models.py
def resolve_project_type(project_path: str = "project.json") -> str:
    """Return the content type from ``project.json``, defaulting to ``"podcast"``.

    Args:
        project_path: Path to the project config file.

    Returns:
        One of ``"podcast"``, ``"audiobook"``, ``"drama"``, or ``"special"``.
    """
    data = _read_project(project_path)
    return data.get("type", "podcast")

resolve_slug

resolve_slug(show_arg: str | None = None, project_path: str = 'project.json') -> str

Resolve the show slug from CLI arg, project.json, or the default.

Resolution order: 1. Explicit show_arg (passed through :func:show_slug). 2. project.json "show" field (if the file exists). 3. :data:DEFAULT_SLUG ("sample").

Parameters:

  • show_arg (str | None, default: None ) –

    Value of --show CLI flag, or None.

  • project_path (str, default: 'project.json' ) –

    Path to the project config file.

Returns:

  • str

    Filesystem-safe show slug.

Source code in src/xil_pipeline/models.py
def resolve_slug(show_arg: str | None = None, project_path: str = "project.json") -> str:
    """Resolve the show slug from CLI arg, project.json, or the default.

    Resolution order:
    1. Explicit *show_arg* (passed through :func:`show_slug`).
    2. ``project.json`` ``"show"`` field (if the file exists).
    3. :data:`DEFAULT_SLUG` (``"sample"``).

    Args:
        show_arg: Value of ``--show`` CLI flag, or ``None``.
        project_path: Path to the project config file.

    Returns:
        Filesystem-safe show slug.
    """
    if show_arg:
        return show_slug(show_arg)
    data = _read_project(project_path)
    if "show" in data:
        return show_slug(data["show"])
    return DEFAULT_SLUG

resolve_season_title

resolve_season_title(season_title_arg: str | None = None, project_path: str = 'project.json') -> str | None

Resolve the season/arc title from an explicit value or project.json.

Resolution order: 1. Explicit season_title_arg (e.g. extracted from the script header Arc: token). 2. project.json "season_title" field (if the file exists and the key is present). 3. None — no season title is available.

Parameters:

  • season_title_arg (str | None, default: None ) –

    Season title already known (e.g. from the script header), or None.

  • project_path (str, default: 'project.json' ) –

    Path to the project config file.

Returns:

  • str | None

    Season title string, or None when not available from any source.

Source code in src/xil_pipeline/models.py
def resolve_season_title(
    season_title_arg: str | None = None,
    project_path: str = "project.json",
) -> str | None:
    """Resolve the season/arc title from an explicit value or project.json.

    Resolution order:
    1. Explicit *season_title_arg* (e.g. extracted from the script header ``Arc:`` token).
    2. ``project.json`` ``"season_title"`` field (if the file exists and the key is present).
    3. ``None`` — no season title is available.

    Args:
        season_title_arg: Season title already known (e.g. from the script header), or ``None``.
        project_path: Path to the project config file.

    Returns:
        Season title string, or ``None`` when not available from any source.
    """
    if season_title_arg is not None:
        return season_title_arg
    data = _read_project(project_path)
    return data.get("season_title") or None

resolve_season

resolve_season(season_arg: int | None = None, project_path: str = 'project.json') -> int | None

Resolve the season number from an explicit value or project.json.

Resolution order: 1. Explicit season_arg (e.g. parsed from the script header Season N: token). 2. project.json "season" field (if the file exists and the key is present). 3. None — no season number is available.

Parameters:

  • season_arg (int | None, default: None ) –

    Season number already known (e.g. from the script header), or None.

  • project_path (str, default: 'project.json' ) –

    Path to the project config file.

Returns:

  • int | None

    Season number as an integer, or None when not available from any source.

Source code in src/xil_pipeline/models.py
def resolve_season(
    season_arg: int | None = None,
    project_path: str = "project.json",
) -> int | None:
    """Resolve the season number from an explicit value or project.json.

    Resolution order:
    1. Explicit *season_arg* (e.g. parsed from the script header ``Season N:`` token).
    2. ``project.json`` ``"season"`` field (if the file exists and the key is present).
    3. ``None`` — no season number is available.

    Args:
        season_arg: Season number already known (e.g. from the script header), or ``None``.
        project_path: Path to the project config file.

    Returns:
        Season number as an integer, or ``None`` when not available from any source.
    """
    if season_arg is not None:
        return season_arg
    data = _read_project(project_path)
    val = data.get("season")
    return int(val) if val is not None else None

episode_tag

episode_tag(season: int | None, episode: int) -> str

Format season/episode as a compact tag like S01E01 or E01.

Parameters:

  • season (int | None) –

    Season number, or None if not declared.

  • episode (int) –

    Episode number.

Returns:

  • str

    "S01E01" when season is set, "E01" otherwise.

Source code in src/xil_pipeline/models.py
def episode_tag(season: int | None, episode: int) -> str:
    """Format season/episode as a compact tag like ``S01E01`` or ``E01``.

    Args:
        season: Season number, or ``None`` if not declared.
        episode: Episode number.

    Returns:
        ``"S01E01"`` when season is set, ``"E01"`` otherwise.
    """
    if season is not None:
        return f"S{season:02d}E{episode:02d}"
    return f"E{episode:02d}"