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": { ... }
}
| Field | Type | Required | Description |
|---|---|---|---|
show |
string | yes | Show name (informational; slug derived from project.json) |
season |
int | no | Season number; used in preamble {season} placeholder and ID3 metadata |
episode |
int | yes* | Episode number; used in preamble {episode} placeholder |
tag_override |
string | no | Raw non-episodic tag (e.g. V01C03, D01). When set, overrides season/episode for tag derivation |
title |
string | no | Episode title; used in preamble {title} placeholder and ID3 metadata |
season_title |
string | no | Season arc title; used in preamble {season_title} placeholder |
artist |
string | no | Artist credit written to MP3/WAV ID3 tags. Default: "XIL Pipeline" |
preamble |
object | no | Broadcast intro block (see below) |
postamble |
object | no | Broadcast outro block (see below) |
cast |
object | yes | Map 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 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"
}
| Field | Type | Required | Description |
|---|---|---|---|
full_name |
string | yes | Character display name (used in ID3 tags and voice sample filenames) |
voice_id |
string | yes | ElevenLabs voice ID. Set to "TBD" when not yet assigned — pipeline will skip TTS for this speaker |
pan |
float −1.0 to 1.0 | yes | Stereo position. 0.0 = centre, −1.0 = full left, 1.0 = full right |
filter |
bool or string | yes | Voice treatment. false/null = none. See the Voice Filters table |
role |
string | yes | Character description passed as previous_text context to ElevenLabs |
stability |
float 0–1 | no | Voice stability. 0.0 = expressive/variable, 1.0 = monotone/consistent. null uses the voice's ElevenLabs default |
similarity_boost |
float 0–1 | no | Adherence to the original cloned voice. 1.0 = strict match. null uses the voice default |
style |
float 0–1 | no | Style exaggeration relative to the original speaker. 0.0 = neutral. null uses the voice default |
use_speaker_boost |
bool | no | Boosts similarity to the original speaker at the cost of slightly higher latency. null uses the voice default |
language_code |
string | no | ISO 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:
| Value | Sounds like | Use for |
|---|---|---|
false / null |
untreated | everyone in the room |
true / "phone" |
narrow and gritty — steep 300 Hz–3.4 kHz skirts, earpiece presence, hard AGC and real GSM codec artifacts | a mobile caller, heard down the line |
"vintage" |
dark, mono, mid-forward, no saturation | aged tape or a record player; period flashbacks |
"film" |
warm and reflective, rolled-off top, recessed presence, audible grain | dialogue heard from a film print (1990s indie register) |
"speakerphone" |
narrow, crunchy, hard-levelled, with a room slap | a 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.
libgsmis 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 defaultffmpegformula does not, and neither the default formula nor thehomebrew-ffmpegtap offers it as an option — a macOS render needs ffmpeg built from source to match.A render that engages
phonewithout 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. Checkffmpeg -encoders | grep libgsmif you are unsure.
libgsmis not in every ffmpeg build. Debian's has it; the stock macOS and Windows builds generally do not. Where it is missing,phonestill 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 withffmpeg -hide_banner -encoders | grep libgsm, or in Python: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
| Value | Position | Typical use |
|---|---|---|
0.0 |
Centre | Host/narrator, solo speaker |
−0.15 to −0.3 |
Left | Female character A |
0.15 to 0.3 |
Right | Male character B |
−0.5 / 0.5 |
Hard L/R | Distinct telephone or off-screen voice |
Avoid values beyond ±0.4 for dialogue — extreme panning fatigues listeners on headphones.
Voice Settings Guide
| Parameter | Lower | Higher | Suggested starting point |
|---|---|---|---|
stability |
More emotional variation | Predictable, consistent | 0.5 |
similarity_boost |
Looser, more creative | Strict clone adherence | 0.75 |
style |
Neutral delivery | Exaggerated expressiveness | 0.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.
| Field | Type | Required | Description |
|---|---|---|---|
speaker |
string | yes | Cast key of the reader (must exist in cast) |
speed |
float 0.7–1.2 | no | TTS 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:
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
| Field | Type | Default | Notes |
|---|---|---|---|
show |
string | — | Required |
season |
int | null |
Optional; used in tag and placeholders |
episode |
int | — | Required unless tag_override is set |
tag_override |
string | null |
Non-episodic tag; overrides season/episode |
title |
string | null |
Episode title for metadata and placeholders |
season_title |
string | null |
Arc title for {season_title} placeholder |
artist |
string | "XIL Pipeline" |
ID3 Artist tag on all audio outputs |
preamble |
object | null |
Broadcast intro |
postamble |
object | null |
Broadcast outro |
cast |
object | — | Required |
Cast Member
| Field | Type | Default | Notes |
|---|---|---|---|
full_name |
string | — | Required |
voice_id |
string | — | Required; "TBD" defers generation |
pan |
float −1–1 | — | Required |
filter |
bool | — | Required |
role |
string | — | Required; used as TTS context |
stability |
float 0–1 | voice default | Optional |
similarity_boost |
float 0–1 | voice default | Optional |
style |
float 0–1 | voice default | Optional |
use_speaker_boost |
bool | voice default | Optional |
language_code |
string | auto | Optional; ISO 639-1 |
Preamble / Postamble
| Field | Type | Default | Notes |
|---|---|---|---|
speaker |
string | — | Required; must be a key in cast |
speed |
float 0.7–1.2 | voice default | Optional 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 MUSICkeys, not in the cast config xil discover-voices— browse and search available ElevenLabs voicesxil voice-sample --episode TAG— generate short TTS samples to audition voice assignmentsxil produce --dry-run --episode TAG— preview TTS character cost before committing