# 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

```json
{
  "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.

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

### Cast Member Fields

```json
"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 | yes | `true` applies a phone-speaker bandpass filter (telephone/radio effect) |
| `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 |

#### 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. A `preamble` is prepended to the
episode audio as the broadcast introduction; a `postamble` is appended as the broadcast
sign-off.

When present, `xil produce` generates a single TTS stem for each block (a
concatenation of all segments) and saves it alongside the episode stems:

- Preamble voice → `stems/<slug>/<TAG>/n002_preamble_<speaker>.mp3` (seq −2)
- Preamble music → `stems/<slug>/<TAG>/n001_preamble_sfx.mp3` (seq −1, from `INTRO MUSIC` in sfx config)
- Postamble voice → `stems/<slug>/<TAG>/<max+1>_postamble_<speaker>.mp3`
- Postamble music → `stems/<slug>/<TAG>/<max+2>_postamble_sfx.mp3` (from `OUTRO MUSIC` in sfx config)

### Preamble/Postamble Fields

```json
"preamble": {
  "speaker": "tina",
  "speed":   0.85,
  "segments": [
    {
      "text":       "Welcome to the show...",
      "shared_key": "preamble-myshow-tina-intro"
    },
    {
      "text":       "{season_title}, Episode {episode}, {title}.",
      "shared_key": null
    }
  ]
}
```

| 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. `1.0` = normal speed, `0.85` = slightly slowed for broadcast clarity |
| `segments` | array | yes* | Ordered list of text segments (preferred for new episodes) |
| `text` | string | yes* | Legacy single-string form. Use `segments` for new episodes |

\* Either `segments` or `text` is required; not both.

### Segment Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | yes | Spoken text. May contain `{placeholders}` (see below). Native ElevenLabs v3 audio tags like `[pause]` are supported |
| `shared_key` | string \| null | no | Legacy cache identifier — retained for backward compatibility. No longer used for per-segment caching; all segments are joined into one TTS call |

### Text Placeholders

The following placeholders are resolved at generation time from the cast config fields:

| Placeholder | Resolves to | Field source |
|-------------|-------------|--------------|
| `{show}` | Show name | `cast_config.show` |
| `{season}` | Season number | `cast_config.season` |
| `{episode}` | Episode number | `cast_config.episode` |
| `{title}` | Episode title | `cast_config.title` |
| `{season_title}` | Season arc title | `cast_config.season_title` (or `project.json`); empty string when not set |

Any unknown placeholder (e.g. `{undefined_key}`) raises a `ValueError` naming the bad key,
rather than silently producing an empty string.

#### Native Audio Tags (v3)

ElevenLabs v3 supports inline audio control tags directly in the text:

```
"text": "Thank you for listening. [pause] This has been {show}."
"text": "Episode {episode}. [long_pause] \"{title}\"."
```

Use `[pause]` for a short beat and `[long_pause]` for a longer breath. Do **not** use
SSML `<break time="1s"/>` — the pipeline uses `eleven_v3` unconditionally, which does
not process SSML.

---

## Non-episodic Tags

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

```json
{
  "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:

```json
{
  "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 |
| `segments` | array | — | Required (or `text`) |
| `text` | string | — | Legacy form; use `segments` for new episodes |

---

## Complete Minimal Example

```json
{
  "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

```json
{
  "show": "Night Owls",
  "season": 1,
  "episode": 3,
  "title": "The Long Shift",
  "season_title": "Season of Static",
  "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"
    }
  },
  "preamble": {
    "speaker": "host",
    "speed": 0.9,
    "segments": [
      {
        "text": "You're listening to {show}.",
        "shared_key": "preamble-nightowls-intro"
      },
      {
        "text": "{season_title} [pause] Episode {episode} [pause] \"{title}\".",
        "shared_key": null
      }
    ]
  },
  "postamble": {
    "speaker": "host",
    "speed": 0.9,
    "segments": [
      {
        "text": "That was {show}, Episode {episode}, \"{title}\". [pause] Thank you for listening.",
        "shared_key": null
      }
    ]
  }
}
```

---

## See Also

- [SFX Config Reference](sfx-config-reference.md) — 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
