Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cast Config Reference — cast_<slug>_<TAG>.json

The cast config file assigns ElevenLabs voices to characters, configures stereo positioning and audio effects, and optionally defines broadcast preamble and postamble blocks read by a host. One file covers an entire episode; it is read by xil produce, xil daw, xil mix, and xil studio.

Filename pattern: cast_<slug>_<TAG>.json (e.g. cast_the413_S03E02.json, cast_nightowls_V01C03.json)


Top-level Structure

{
  "show":         "The 4 1 3",
  "season":       3,
  "episode":      2,
  "title":        "The Weight of the Letters",
  "season_title": "The Architect",
  "artist":       "XIL Pipeline",
  "preamble":     { ... },
  "postamble":    { ... },
  "cast":         { ... }
}
FieldTypeRequiredDescription
showstringyesShow name (informational; slug derived from project.json)
seasonintnoSeason number; used in preamble {season} placeholder and ID3 metadata
episodeintyes*Episode number; used in preamble {episode} placeholder
tag_overridestringnoRaw non-episodic tag (e.g. V01C03, D01). When set, overrides season/episode for tag derivation
titlestringnoEpisode title; used in preamble {title} placeholder and ID3 metadata
season_titlestringnoSeason arc title; used in preamble {season_title} placeholder
artiststringnoArtist credit written to MP3/WAV ID3 tags. Default: "XIL Pipeline"
preambleobjectnoBroadcast intro block (see below)
postambleobjectnoBroadcast outro block (see below)
castobjectyesMap of speaker key → voice config (see below)

* episode is required unless tag_override is set.


cast Block

Each key must match a speaker key used in the production script (all lowercase, no spaces — e.g. adam, mr_patterson). Keys are matched case-sensitively against the speaker field of parsed dialogue entries.

"cast": {
  "adam": { ... },
  "maya": { ... }
}

Cast Member Fields

"adam": {
  "full_name":        "Adam Santos",
  "voice_id":         "6wrQFQPrvgNyJY74SxwG",
  "pan":              0.0,
  "filter":           false,
  "role":             "Host/Narrator. American male, early 20s. Warm, intimate...",
  "stability":        0.5,
  "similarity_boost": 0.75,
  "style":            0.0,
  "use_speaker_boost": true,
  "language_code":    "en"
}
FieldTypeRequiredDescription
full_namestringyesCharacter display name (used in ID3 tags and voice sample filenames)
voice_idstringyesElevenLabs voice ID. Set to "TBD" when not yet assigned — pipeline will skip TTS for this speaker
panfloat −1.0 to 1.0yesStereo position. 0.0 = centre, −1.0 = full left, 1.0 = full right
filterbool or stringyesVoice treatment. false/null = none. See the Voice Filters table
rolestringyesCharacter description passed as previous_text context to ElevenLabs
stabilityfloat 0–1noVoice stability. 0.0 = expressive/variable, 1.0 = monotone/consistent. null uses the voice’s ElevenLabs default
similarity_boostfloat 0–1noAdherence to the original cloned voice. 1.0 = strict match. null uses the voice default
stylefloat 0–1noStyle exaggeration relative to the original speaker. 0.0 = neutral. null uses the voice default
use_speaker_boostboolnoBoosts similarity to the original speaker at the cost of slightly higher latency. null uses the voice default
language_codestringnoISO 639-1 code for ElevenLabs text normalisation (e.g. "en", "de", "uk" for British English). null = auto-detect

Voice Filters

The filter field names a voice treatment applied to that speaker’s dialogue stems during assembly and DAW export. Pick by how it should sound:

ValueSounds likeUse for
false / nulluntreatedeveryone in the room
true / "phone"narrow and gritty — steep 300 Hz–3.4 kHz skirts, earpiece presence, hard AGC and real GSM codec artifactsa mobile caller, heard down the line
"vintage"dark, mono, mid-forward, no saturationaged tape or a record player; period flashbacks
"film"warm and reflective, rolled-off top, recessed presence, audible graindialogue heard from a film print (1990s indie register)
"speakerphone"narrow, crunchy, hard-levelled, with a room slapa phone on a table, on speaker

For a character who is on the phone in one scene and in the room in another, prefer the script span markers over a second cast entry — the span decides line by line. Use [PHONE FILTER: ENGAGES <speaker>] for an ordinary call and [SPEAKERPHONE ENGAGES] for a phone on speaker in the room. Use the cast field when a voice is only ever heard through a phone.

Comma-separated values chain left to right, so "vintage,phone" and "phone,vintage" give different results. Unknown names are ignored with a warning in the log.

"vintage" and "film" are easy to confuse: both are “old and warm”. vintage is darker and collapses to mono; film keeps stereo, keeps low-end body, and adds compression and grain. When in doubt, film is the one that still sounds like a modern recording of something old.

phone and speakerphone are independent — speakerphone is not a stronger phone, it is a separate chain with saturation, hard AGC and a short delay. phone is a voice against an ear; speakerphone is a small loudspeaker in a room.

phone, film and speakerphone are rendered through ffmpeg. If ffmpeg is unavailable the stem passes through untreated and a warning is logged; set XIL_STRICT_FX=1 to make that a hard error instead.

phone additionally round-trips the audio through the GSM 06.10 codec at 8 kHz — the actual 2G mobile codec — which is where its grit and its hard 4 kHz ceiling come from. If that encoder is missing but ffmpeg is present, the treatment keeps its filtering and loses only the codec character.

libgsm is not in every ffmpeg build. It has to be compiled in (--enable-libgsm); ffmpeg has a native GSM decoder but no native encoder, and installing the library afterwards does not help a pre-built binary. Debian and Ubuntu ship it. Homebrew’s default ffmpeg formula does not, and neither the default formula nor the homebrew-ffmpeg tap offers it as an option — a macOS render needs ffmpeg built from source to match.

A render that engages phone without the encoder warns before writing its first stem. The audio is usable, but it will not match a render from a machine that has the encoder, so masters of the same episode can differ by which machine made them. Check ffmpeg -encoders | grep libgsm if you are unsure.

libgsm is not in every ffmpeg build. Debian’s has it; the stock macOS and Windows builds generally do not. Where it is missing, phone still band-limits properly (~20 dB down at 6 kHz instead of ~47 dB) but sounds smoother and less like a mobile. Render an episode on the same machine you auditioned it on, or two masters of the same episode will not match. Check with ffmpeg -hide_banner -encoders | grep libgsm, or in Python:

from xil_pipeline.audio_fx import encoder_available
encoder_available("libgsm")

This treatment was rebuilt in a later release and sounds materially different. It used to be a pydub one-liner whose single-pole filters left 80 Hz only ~11 dB down and 8 kHz only ~9 dB down — a slightly muffled voice rather than a phone. It is now ~37 dB and ~47 dB respectively. The old version also added a flat +5 dB, making the caller the loudest thing in the scene; the new one lands ~1.4 dB under an untreated stem. Episodes rendered with the old filter need re-rendering to pick this up.

Note that a speaker whose filter names a treatment and who speaks inside a matching script span (see FILM AUDIO in the direction types reference) receives that treatment twice. This has always been the behaviour for VINTAGE FILTER; it is more audible for film, which compounds compression and grain.

phone is the one exception. A phone span treatment is skipped when the speaker’s own filter already applied it. Applying it twice band-limits twice and runs the voice through GSM twice, compounding the codec artifacts into mush rather than doubling an effect. The exception is deliberately narrow: only phone is deduped, and only against an identical cast filter. A vintage speaker inside a PHONE FILTER span still gets both.

Pan Guide

ValuePositionTypical use
0.0CentreHost/narrator, solo speaker
−0.15 to −0.3LeftFemale character A
0.15 to 0.3RightMale character B
−0.5 / 0.5Hard L/RDistinct telephone or off-screen voice

Avoid values beyond ±0.4 for dialogue — extreme panning fatigues listeners on headphones.

Voice Settings Guide

ParameterLowerHigherSuggested starting point
stabilityMore emotional variationPredictable, consistent0.5
similarity_boostLooser, more creativeStrict clone adherence0.75
styleNeutral deliveryExaggerated expressiveness0.0

Omitting any of these fields (or setting to null) inherits the ElevenLabs voice default. Use xil voice-sample --episode TAG to audition voice changes before committing them.


preamble and postamble Blocks

Both blocks are optional and share an identical schema. They control TTS speaking rate only — the spoken text lives in the production script as PREAMBLE and POSTAMBLE section blocks, not in the cast config.

"preamble":  { "speaker": "tina", "speed": 0.85 },
"postamble": { "speaker": "tina", "speed": 0.85 }
FieldTypeRequiredDescription
speakerstringyesCast key of the reader (must exist in cast)
speedfloat 0.7–1.2noTTS speaking rate override for this section. 1.0 = normal speed; 0.85 = slightly slowed for broadcast clarity

xil produce applies the speed override when generating stems for entries whose section field is "preamble" or "postamble". All other voice settings (voice ID, stability, pan) come from the speaker’s cast entry as normal.

The spoken text, music cues ([INTRO MUSIC], [OUTRO MUSIC]), and section placement are written in the production script:

===

PREAMBLE

TINA  Welcome to the show. Season 1, Episode 1, Pilot.

[INTRO MUSIC]

===

COLD OPEN
…

===

POSTAMBLE

[OUTRO MUSIC]

TINA  That was the show. Thank you for listening.

===

END OF EPISODE

Preamble/postamble voice stems follow the same {seq:03d}_preamble_{speaker}.mp3 naming convention as all other dialogue stems. Music stems are generated by xil sfx via the standard SFX pipeline using INTRO MUSIC / OUTRO MUSIC entries in the sfx config.


Non-episodic Tags

For content that isn’t organised as S01E01 (audiobooks, shorts, one-offs), use tag_override instead of season/episode:

{
  "show":         "Signals",
  "tag_override": "V01C01",
  "title":        "The Signal",
  "cast": { ... }
}

tag_override is used verbatim for all file path derivations (stems/signals/V01C01/, parsed/parsed_signals_V01C01.json, etc.). It must match ^[A-Za-z0-9_-]+$.


Auto-generated Skeleton

When you run xil parse with --episode TAG and no cast file exists yet, a skeleton is written automatically:

{
  "show": "THE 413", "season": 1, "episode": 1, "title": "...",
  "cast": {
    "adam": { "voice_id": "TBD", "pan": 0.0, "filter": false, "role": "" },
    ...
  }
}

Fill in each voice_id using xil discover-voices to browse available ElevenLabs voices, then audition assignments with xil voice-sample --episode TAG.


Field Reference Summary

Top-level

FieldTypeDefaultNotes
showstring—Required
seasonintnullOptional; used in tag and placeholders
episodeint—Required unless tag_override is set
tag_overridestringnullNon-episodic tag; overrides season/episode
titlestringnullEpisode title for metadata and placeholders
season_titlestringnullArc title for {season_title} placeholder
artiststring"XIL Pipeline"ID3 Artist tag on all audio outputs
preambleobjectnullBroadcast intro
postambleobjectnullBroadcast outro
castobject—Required

Cast Member

FieldTypeDefaultNotes
full_namestring—Required
voice_idstring—Required; "TBD" defers generation
panfloat −1–1—Required
filterbool—Required
rolestring—Required; used as TTS context
stabilityfloat 0–1voice defaultOptional
similarity_boostfloat 0–1voice defaultOptional
stylefloat 0–1voice defaultOptional
use_speaker_boostboolvoice defaultOptional
language_codestringautoOptional; ISO 639-1

Preamble / Postamble

FieldTypeDefaultNotes
speakerstring—Required; must be a key in cast
speedfloat 0.7–1.2voice defaultOptional TTS rate override for this section

Complete Minimal Example

{
  "show": "Night Owls",
  "season": 1,
  "episode": 3,
  "title": "The Long Shift",
  "cast": {
    "jordan": {
      "full_name": "Jordan Blake",
      "voice_id": "abc123ElevenLabsVoiceID",
      "pan": 0.0,
      "filter": false,
      "role": "Host. American, warm, late-night energy."
    },
    "caller": {
      "full_name": "The Caller",
      "voice_id": "xyz456ElevenLabsVoiceID",
      "pan": 0.35,
      "filter": true,
      "role": "Nervous, mid-30s, calls in anonymously."
    }
  }
}

Complete Preamble/Postamble Example

Cast config (cast_nightowls_S01E03.json):

{
  "show": "Night Owls",
  "season": 1,
  "episode": 3,
  "title": "The Long Shift",
  "season_title": "Season of Static",
  "preamble":  { "speaker": "host", "speed": 0.9 },
  "postamble": { "speaker": "host", "speed": 0.9 },
  "cast": {
    "host": {
      "full_name": "Jordan Blake",
      "voice_id": "abc123ElevenLabsVoiceID",
      "pan": 0.0,
      "filter": false,
      "role": "Host. American, warm, late-night energy.",
      "stability": 0.5,
      "similarity_boost": 0.75,
      "style": 0.0,
      "use_speaker_boost": true,
      "language_code": "en"
    }
  }
}

Script (scripts/nightowls/S01E03_the_long_shift.md, beginning and end):

===

PREAMBLE

HOST  You're listening to Night Owls. [pause] Season of Static [pause] Episode 3 [pause] "The Long Shift".

[INTRO MUSIC]

===

COLD OPEN
…

===

POSTAMBLE

[OUTRO MUSIC]

HOST  That was Night Owls, Episode 3, "The Long Shift". [pause] Thank you for listening.

===

END OF EPISODE

See Also

  • SFX Config Reference — intro/outro music lives in the sfx config under INTRO MUSIC / OUTRO MUSIC keys, not in the cast config
  • xil discover-voices — browse and search available ElevenLabs voices
  • xil voice-sample --episode TAG — generate short TTS samples to audition voice assignments
  • xil produce --dry-run --episode TAG — preview TTS character cost before committing