DIRECTION_TYPES — Reference and Pipeline Impact
Definition
Defined in XILP001_script_parser.py at module level:
DIRECTION_TYPES = ["SFX", "MUSIC", "AMBIENCE", "BEAT", "VINTAGE FILTER", "FILM AUDIO", "SPEAKERPHONE"]
These are the recognized sound-category prefixes for stage directions enclosed in
brackets ([...]) in the production script markdown. The values classify direction
entries and are stored on ScriptEntry as direction_type.
Effect in XILP001
Role 1: Prefix scanner in classify_direction() (lines 84–86)
Called with the bracket-interior text only (e.g., "SFX: DOOR SLAMS", not the full
"[SFX: DOOR SLAMS]"). The first prefix match wins and is returned as the
direction_type value. Order in the list matters if any prefixes were overlapping,
but the current seven are disjoint.
is_stage_direction() fires on any [...] line regardless — it does not consult
DIRECTION_TYPES. Classification happens after detection, not before.
Role 2: direction_type field on direction entries only (line 429)
All other entry types hardcode "direction_type": None:
| Entry type | direction_type |
|---|---|
section_header |
Always None |
scene_header |
Always None |
dialogue |
Always None |
direction |
classify_direction() result — one of the five values, or None |
The BEAT quirk (lines 87–88)
"BEAT" appears both in DIRECTION_TYPES (loop at line 84) and as an explicit
exact-match fallback here. The loop handles [BEAT: some qualifier] variants; the
fallback handles bare [BEAT] and [LONG BEAT]. Without the fallback, bare [BEAT]
would be caught by the loop (since "BEAT".startswith("BEAT") is true), but [LONG BEAT]
would return None. The duplication is intentional coverage for both forms.
Effect in XILP002 — None
Zero references. load_production() filters to entry["type"] == "dialogue" only.
All direction entries (the only entries that can have a non-None direction_type) are
discarded before voice generation begins. direction_type never influences which stems
are generated, what text is sent to ElevenLabs, or any filename.
Effect in XILP003 — None
Zero references. Assembly reads only the speaker suffix from stem filenames and the
config dict (pan/filter). direction_type is not present in stems or in the cast
configuration.
Effect in XILP005 and XILP011 — span markers
"VINTAGE FILTER" and "FILM AUDIO" are span markers: an ENGAGES entry opens a
span, the next DISENGAGES (or end of episode) closes it, and every dialogue stem inside
receives the corresponding voice treatment. The mapping lives in
mix_common.SPAN_DIRECTION_TREATMENTS:
| Direction type | Treatment applied to enclosed dialogue |
|---|---|
VINTAGE FILTER |
vintage — mono, dark, mid-forward |
FILM AUDIO |
film — warm, soft top, gentle compression and grain |
SPEAKERPHONE |
speakerphone — narrow-band, hard AGC, crunch, room slap |
PHONE FILTER |
phone — mobile call: steep 300 Hz–3.4 kHz skirts, earpiece presence, hard AGC and real GSM codec grit. The same treatment as the cast config filter: phone |
Spans of different types may overlap; treatments then stack in
SPAN_DIRECTION_TREATMENTS declaration order. PHONE FILTER is declared last
because a band-limit reads best applied after any tone shaping.
Narrowing a span to one speaker
By default a span treats every dialogue line it encloses. Naming a speaker on the
ENGAGES marker narrows it to that speaker's lines:
This matters for call scenes. A writer naturally wraps the whole conversation in one
marker pair, but only the remote character belongs on the filter — a blanket span would
band-limit the in-room voice too. The speaker key is the cast config key (matched
case-insensitively against the stem filename suffix), the scope applies to every span
type, and the closing marker may repeat it or not. A bare ENGAGES keeps the original
treat-everything behaviour.
Naming a speaker who has no lines inside the span treats nothing — the span is still opened and closed, it just matches no dialogue.
Treated dialogue stays in the dialogue layer. The separate
{TAG}_layer_vintage_filter.wav holds record-player crackle audio for the span, which is a
different mechanism that happens to share the name — it is not where filtered voices go.
No stems are generated for FILM AUDIO, SPEAKERPHONE or PHONE FILTER markers: generate_sfx_config() writes them as
{"type": "silence", "duration_seconds": 0.0}, which load_sfx_entries() skips, so they
cost no API credits and produce no files. VINTAGE FILTER ENGAGES differs — it binds to a
real crackle asset, and only its DISENGAGES marker is a pure boundary.
Marker spelling: the scanner accepts both [FILM AUDIO ENGAGES] and [FILM AUDIO: ENGAGES],
and likewise for SPEAKERPHONE and PHONE FILTER. For VINTAGE FILTER only the colon-free
form is validated, for backward compatibility with scripts already in production; the mixer
itself tolerates either.
The Pydantic Constraint (models.py line 46)
direction_type: Literal[
"SFX", "MUSIC", "AMBIENCE", "BEAT", "VINTAGE FILTER", "FILM AUDIO",
"SPEAKERPHONE", "PHONE FILTER",
] | None = Field(default=None, description="Sound category for direction entries")
DIRECTION_TYPES in the parser and the Literal in ScriptEntry are independently
maintained — they are not derived from each other. A new sound category requires
updates in both places:
- Add to
DIRECTION_TYPESinXILP001_script_parser.py - Add to the
LiteralinScriptEntryinmodels.py
Updating only DIRECTION_TYPES without updating the Literal will cause
parse_script() to raise a ValidationError at parse time when the new category
is returned by classify_direction().
Comparison with SECTION_MAP
SECTION_MAP |
DIRECTION_TYPES |
|
|---|---|---|
| Controls entry detection | Yes — unknown headers are silently dropped | No — [...] brackets always create direction entries |
| Propagates to stem filenames | Yes — slug appears in every stem MP3 name | No |
| Used downstream in XILP002/XILP003 | Indirectly (via stem filenames) | Not at all |
| Silent failure mode | High risk — mislabels all subsequent content | Low risk — unrecognized directions get direction_type: None, entry still created |
| Pydantic mirror required | No | Yes — Literal in ScriptEntry must match |
Adding a New Direction Type
To add "FOLEY" as a recognized category:
- Add to
DIRECTION_TYPESinXILP001_script_parser.py: - Add to the
LiteralinScriptEntryinmodels.py: - Re-run XILP001 to regenerate the parsed JSON with
"direction_type": "FOLEY"on matching entries.
No changes required in XILP002 or XILP003 — direction types do not affect voice generation or audio assembly.