LPL Script Writer (v2.0)
The LLM that writes the whole radio drama
- script_text
- script_json
- news_used
- estimated_minutes
- technical_model
This is where OldTimeRadio's story actually gets written. OTR_LedgerScriptWriter is the LLM-powered writer: it turns a story source into a complete multi-act radio-drama script - outline, cast, dialogue, music beats - as a structured JSON ledger. Everything downstream (voices, music, video) consumes that ledger. It's the biggest node in the pack, and the one with the most knobs, so let's separate the knobs that matter from the ones you can ignore for weeks.
Start with the two that shape the episode: episode_title (leave blank to let the outline supply one) and num_characters (default 2, up to 10, 1 = monologue/diary mode). Note the tooltip's warning: this is a request, not a cap - a story that genuinely needs another voice may use one. The real ceiling is the voice stock, because two characters never share a voice. act_count (default 3) is the only knob that shapes episode length - more acts, more story; length is reported afterwards, never requested up front.
The inputs that matter
source_bank- which story lane runs. Defaultscifi_news_pro(sci-fi drawn from a live science feed). Also:media_archive,public_domain,shakespeare(CC BY-NC - inherits a noncommercial restriction on the source text),original(entropy-seeded fiction), androll (any eligible bank). There's a "custom_source_bank" row that fails loud until you actually author one - that's by design.custom_premise- empty means the RSS fetcher pulls a real science headline as the seed. Non-empty means your text verbatim, skipping RSS entirely. This is the "I want THIS story" override, and it's also how you work offline.creative_writing_model/technical_model- both default to Mistral-Nemo (the C7 byte-identical audio baseline). Two-slot routing: keep the technical slot smaller if you need VRAM headroom. Gemma options and GGUF quants are in the dropdown; OpenRouter/Google/Comfy Credits lanes activate only when you set the env key.creativity- curated presets:safe & tight(temp 0.60) throughmaximum chaos(0.95 - deliberately capped below 1.0 because higher caused format collapse).min_p(0.05) andrepetition_penalty(1.03) are the small-LLM tail-cut and loop-guard defaults.lemmy_cameo- the easter egg. Roll (~11%), always, or never. A grizzled Cockney comms officer may join your cast. It consumes a character slot.visual_styleandstory_scaffoldcontrol the look and the radio-drama shape grammar; both support "roll".
Outputs and gotchas
Outputs: script_text, script_json (the ledger - feed to OTR_LedgerFreezeCascade), news_used, estimated_minutes, and technical_model. Install is pack-level (ComfyUI Manager → ComfyUI-OldTimeRadio, restart). The writer needs a local LLM - the canonical workflow ships Gemma-4-12B, and llm_vram_ceiling_gb (14.5) gates downloads. transformers>=5.10.4 and accelerate>=1.1.0 are the finicky requirements; a "cannot load a model" error usually means one of those was missing on a fresh install. And know the pack's honest limitation: character drift - some episodes hand a line to the wrong character, the detector that would fix it ships disabled because it wasn't reliable. It doesn't break a render; it's just the cost of a 12B writer holding a whole cast in mind.
Inputs (38)
| Name | Type | Default | Description |
|---|---|---|---|
| episode_title | STRING | Optional episode title override. Stamped at ledger.meta.episode_title so SignalLostVideo picks it up directly without title-chain fallback. Leave blank to let the outline supply a title. | |
| num_characters | INT | 21–10 | REQUESTED number of speaking characters (plus ANNOUNCER bookends). 1 = monologue/diary mode. This is a request, not a cap: a story that genuinely needs another voice may use one. The real ceiling is the voice stock, because two characters never share a voice. |
| creative_writing_modelopt | COMBO | Qwen/Qwen3.5-4B | LLM for the creative/narrative passes (outline, cast, dialogue composer, polish, style picker invention). Mistral-Nemo is the C7 byte-identical audio baseline. Suffix tags like [LOCAL HF], [LOCAL GGUF], and [NOT DOWNLOADED] are stripped before HF lookup. To use a remote OpenRouter model, set OPENROUTER_API_KEY and pick OpenRouter A/B (see https://github.com/jbrick2070/ComfyUI-OldTimeRadio/blob/v2.0-alpha/docs/openrouter-setup.md). |
| technical_modelopt | COMBO | Qwen/Qwen3.5-4B | LLM for the technical/structured passes (JSON validators, GBNF grammar output, reviewer verdicts, cast contract checks, format normalization, news interpreter). Profile/platform-owned baseline: your hardware profile pins it, and a direct headless -Set override is sanctioned and wins over the profile when supplied. Pick a smaller model here when you want Slot 1 != Slot 2 routing for VRAM headroom. To use a remote OpenRouter model, set OPENROUTER_API_KEY and pick OpenRouter A/B (see https://github.com/jbrick2070/ComfyUI-OldTimeRadio/blob/v2.0-alpha/docs/openrouter-setup.md). |
| custom_premiseopt | STRING | For My Story, enter your idea here and optionally add characters, plot, setting and who it is by. At least one creative field must contain text. Other banks keep their own source and premise rules. | |
| include_act_breaksopt | BOOLEAN | true | When ON (default), the outline LLM is told to plan music_inter beats between acts so the episode breathes between scenes. When OFF, the outline LLM is told the episode is one continuous flow with no music_inter beats. Outline schema (Beat.speaker_role) supports music_inter either way; this widget just tells the LLM whether to use it. Wired into the outline prompt via the `Target episode shape` line 2026-05-10. |
| act_countopt | COMBO | 3 | Number of acts, 1-6. This is the only knob that shapes episode length, and your pick is always honoured. More acts means a story with more turns in it -- each act gets its own beat skeleton and its own pass. The episode ends up as long as the story needs; length is reported afterwards, never requested up front. 1 -> a single scene 2 -> setup, resolution 3 -> setup, complication, resolution 6 -> the full arc, through crisis and climax |
| creativityopt | COMBO | balanced | Creativity dial — overrides raw temperature + top_p with curated presets: safe & tight -> temp 0.60, top_p 0.90 balanced -> temp 0.85, top_p 0.95 wild & rough -> temp 0.92, top_p 0.98 maximum chaos -> temp 0.95, top_p 0.99 (BUG-014: temp > 1.0 caused format collapse, so 'maximum chaos' caps at 0.95.) |
| perfect_run_spacesaveropt | BOOLEAN | false | DEPRECATED 2026-08-08 -- NO-OP sentinel. Widget preserved to keep positional layout stable (BUG-LOCAL-097). Formerly triggered OTR_RTXUpscale's per-episode intermediate cleanup, which was retired with the RTX-VSR node (queue item 8; nodes/rtx_upscale.py removed). Setting this True has no effect. |
| min_popt | FLOAT | 0.050–0.5 | min_p sampling threshold (HuggingFace transformers). 0.05 (default) cuts the long tail of low-probability tokens that produce the occasional off-key word in an otherwise good line on 7B-14B small local LLMs (Mistral-Nemo, Gemma-2, Qwen2.5). Tier 2 fix #17 (2026-05-11) flipped this from 0.0 — preserving an unvalidated baseline is not preservation. 0.0 = disabled. Aggressive: 0.10. Pairs with the existing creativity top_p — when both are active the tail cut is the union. |
| repetition_penaltyopt | FLOAT | 1.031–1.2 | Repetition penalty for HuggingFace transformers generate. 1.03 (default) is gentle and helps small local LLMs avoid looping on character names / high-frequency tokens in short outputs. Tier 2 fix #17 (2026-05-11) flipped this from 1.0 — preserving an unvalidated baseline is not preservation. 1.0 = disabled. Values above 1.08 commonly damage short generations on the 7B-14B class. |
| max_new_tokens_capopt | INT | 20040–400 | Per-line max_new_tokens ceiling on the composer hot-path. Default 200 preserves current behavior. The Attempt-1 uses this cap directly; attempt-2 retry uses the full cap. It is a per-CALL decode budget, not a length target -- it was scaled from target_words until 2026-08-14, which made it a token ceiling derived from a word request. |
| lemmy_cameoopt | COMBO | roll (~11% chance) | LEMMY easter-egg cameo -- the genial Cockney communications officer who occasionally joins the cast. roll (~11% chance) -- default; LEMMY may appear at random. The roll uses OS entropy and is NOT tied to the seed (BUG-LOCAL-260), so a fixed seed no longer pins him on or off. always include -- force LEMMY into the cast this run. never include -- keep LEMMY out this run. 'always' / 'never' consume one of the num_characters slots, exactly as a natural roll does. |
| use_exchangeopt | BOOLEAN | false | Build 4 grouped-exchange dialogue. OFF (default) keeps the per-beat composer; PD1 byte-identity holds. ON groups 2-3 consecutive voiced beats and renders each as one exchange (compose_exchange) with the Build 3 contracts + Build 2 Tier-A check; one block per slot, one repair-by-group, then legacy fallback. ANNOUNCER/MUSIC + trailing singletons keep their pass. Any failure falls back to legacy per beat -- audio is never blocked. Validate VRAM <= 14.5 GB + zero slot drift on a live N=3 run. |
| enable_production_stage3_validatorsopt | BOOLEAN | false | OFF (default) preserves PD1 byte-identity on the legacy path -- no validators run. ON wires Stage 3 validators (speaker-leak, banned-phrase, length, pronoun, on-beat) into the production compose_line for every character dialogue beat. TELEMETRY ONLY: findings are stamped on meta.lines[].validation_findings and NOTHING is regenerated, rerolled or rejected -- an audit may never fail a story for length, language, style or quality. Costs no extra LLM call at any severity. Flip ON for production smokes; OFF for the byte-identity regression run. |
| news_briefs_requiredopt | BOOLEAN | true | ON (default): typed non-quality source-interpreter failures stay fail-loud. OFF: the legacy branch may degrade to raw news_seed. Rejected/malformed LLM briefs do not reach this switch: they rotate through fresh technical/creative repair passes and end at a validated bank-specific source floor. |
| openrouter_slot_a_modelopt | COMBO | (enable OpenRouter) | OpenRouter model slug bound to the 'openrouter:slot-a' handle (the creative slot). Passive: only used when creative_writing_model is set to 'openrouter:slot-a'. Choices are the curated '~family-latest' aliases (which resolve upstream, so they never go stale) plus your favourites from the cached catalog; run the refresh script or set OTR_OPENROUTER_FULL_CATALOG=1 to browse every cached slug. Shows '(enable OpenRouter)' until OPENROUTER_API_KEY is set. A saved slug is preserved even if absent from a stale cache. See https://github.com/jbrick2070/ComfyUI-OldTimeRadio/blob/v2.0-alpha/docs/openrouter-setup.md. |
| openrouter_slot_b_modelopt | COMBO | (enable OpenRouter) | OpenRouter model slug bound to the 'openrouter:slot-b' handle (the technical slot). Passive: only used when technical_model is set to 'openrouter:slot-b'. Choices are the curated '~family-latest' aliases plus your favourites from the cached catalog; shows '(enable OpenRouter)' until remote is enabled. OTR_OPENROUTER_SLOT_B_REQUIRE_JSON=1 limits the CACHED CATALOG rows to structured-output models; the curated aliases are policy, not catalog discovery, so they are still offered -- check the family before binding one here. See https://github.com/jbrick2070/ComfyUI-OldTimeRadio/blob/v2.0-alpha/docs/openrouter-setup.md. |
| comfy_slot_a_modelopt | COMBO | (enable Comfy Credits) | Comfy Credits model slug bound to the 'comfy:slot-a' handle (the creative slot). Passive: only used when creative_writing_model is set to 'comfy:slot-a'. Choices are the pinned ComfyUI partner-node catalog; shows '(enable Comfy Credits)' until OTR_ENABLE_COMFY_CREDITS=1 and a Comfy account with credits is logged in. Credit-billed. See https://github.com/jbrick2070/ComfyUI-OldTimeRadio/blob/v2.0-alpha/docs/comfy-credits-setup.md. |
| comfy_slot_b_modelopt | COMBO | (enable Comfy Credits) | Comfy Credits model slug bound to the 'comfy:slot-b' handle (the technical slot). Passive: only used when technical_model is set to 'comfy:slot-b'. Choices are the pinned ComfyUI partner-node catalog; shows '(enable Comfy Credits)' until the lane is enabled. Credit-billed. See https://github.com/jbrick2070/ComfyUI-OldTimeRadio/blob/v2.0-alpha/docs/comfy-credits-setup.md. |
| story_scaffoldopt | COMBO | auto | How much the radio-drama SCAFFOLD shapes the story. off = a story drawn straight from the news seed (the base prompt only -- no style catalog, no climax-shape grammar, no grounding gate; the writer's own take). on = the news story shaped by ONE of the ~100 radio-drama styles (varied climax + ending + the premise-grounding body gate). auto (default) = follow the OTR_ENABLE_STYLE_GRAMMAR env / its default (ON). on/off override that env for THIS run. |
| source_bankopt | COMBO | scifi_news_pro | Story-path SOURCE BANK (multi-modal story schema). Selects which registered story pack supplies the pack-routed creative prompts and which lane the episode runs. scifi_news_pro = the local default sci-fi bank, an LLM-first multipass lane using the configured model slots. Each lane is an INDEPENDENT bank (own pack + bank metadata). The only non-runnable row is '+ Add Your Own' (custom_source_bank) -- picking it FAILS LOUD before any story work (no fallback), with its guide_ref naming the real path: author a bundle under user_packs/source_banks/, run 'otr_check bank <path> --activate', restart, and your bank joins this list as its own entry (contract: docs/EXTENDING_OTR.md). A bank's own default_story_model picks its story pack -- there is no separate pack widget. ROLL: pick 'roll (any eligible bank)' to let the run choose for you, uniformly, from every runnable bank whose lane can build the requested shape. This is INDEPENDENT of the visual_style roll -- rolling one does not roll the other. The choice is recorded in the ledger at meta.bank_roll (selected id, seed, and the exact pool it drew from); set OTR_BANK_SEED to replay a past roll. A pinned source_ref cannot be combined with the roll -- a pinned source belongs to one bank. |
| visual_styleopt | COMBO | sci_fi_radio | VISUAL STYLE (multi-modal story schema). Rewrites ONLY the downstream still/video prompt style language (tails); story content is untouched. sci_fi_radio = the production look (default, byte-identical). anime / cartoon / paper_origami / archival_documentary / recur_frac / shakespeare_stage_realism / storybook_engraving / video_art are live immediately. Unknown id fails LOUD before any story work. ROLL: pick 'roll (any style)' to let the run choose the look for you, uniformly, from every registered style (they are all fully live, so there is nothing to exclude). This is a SEPARATE randomizer from the source_bank roll -- either, both, or neither. The choice is recorded at meta.style_roll; set OTR_VISUAL_STYLE_SEED to replay a past roll (that is its own seed -- OTR_STYLE_SEED is the narrative arc-shape seed and is unrelated). |
| google_api_slot_a_modelopt | COMBO | (select Google API model) | Google Gemini API model bound to 'google_api:slot-a' (creative slot). Env-only auth: OTR_GOOGLE_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY. Passive until the main model dropdown selects google_api:slot-a. No local fallback. |
| google_api_slot_b_modelopt | COMBO | (select Google API model) | Google Gemini API model bound to 'google_api:slot-b' (technical slot). Use a structured-output capable text model for JSON passes. Env-only auth, no local fallback. |
| source_refopt | STRING | Optional source reference for source-bank lanes (for example a public-domain URL/id/title). Blank uses the bank's default source selection. This is not a fallback; unsupported nonblank references must fail loud in the consuming bank. | |
| llm_deviceopt | COMBO | default | LLM device. 'default' asks ComfyUI what this machine has and RECORDS what it chose; an explicit device is never second-guessed and still fails loud if it is not there. |
| llm_attn_implopt | COMBO | sdpa | Attention implementation for the transformers lane (the FA2 auto-probe is gone). sdpa = the proven baseline. |
| llm_quant_policyopt | COMBO | bnb_nf4 | Quantization for the transformers lane. bnb lanes are OFF on ROCm/MPS/CPU tiers (missing bitsandbytes fails loud). |
| llm_vram_ceiling_gbopt | FLOAT | 14.50–96 | Pre-download VRAM-fit ceiling (GB). 0 DISABLES the gate (cpu tier only). |
| gguf_n_ctxopt | INT | 4096512–32768 | GGUF lane context window. NO silent downgrade: a window that does not fit free VRAM fails loud. |
| gguf_quantopt | COMBO | Q8_0 | GGUF artifact quant (filename + expected size come from the artifact table). Only PINNED quants are offered -- an unpinned artifact is refused at load. |
| gate_inopt | STRING | Ordering/validation signal (wire OTR_WorkflowValidator.validation_report). | |
| replay_fromopt | STRING | CANONICAL REPLAY (2026-09-02). Empty = normal authorship. A frozen replay bundle directory (scripts/otr_freeze_replay_bundle.py) = re-render THAT episode's ledger through the whole canonical graph as a new episode: no writer, no TTS, no music, no stills minted; same seeds, same audio, only the video phase runs. The A/A null. | |
| story_charactersopt | STRING | MY STORY ONLY. Character names and notes, in ordinary words: 'Ada, the lighthouse keeper. Her brother Tom, who does not believe her.' Names you write here are kept exactly as you type them. If you say someone is a man or a woman, that is honoured; if you do not say, it is a free choice and is never guessed from the name. Someone merely mentioned does not become a speaking part. Leave it empty and the cast is invented for you. | |
| story_plotopt | STRING | MY STORY ONLY. Plot ideas: events, the conflict, a twist, an ending you want. Rough notes are fine. What you ask for here outranks anything the model would rather invent. What you leave out is invented to fit. | |
| story_settingopt | STRING | MY STORY ONLY. Setting: place, era, weather, mood, the world it happens in. This is the story's setting, not its LOOK -- the visual style dropdown chooses the look, and changing it never rewrites your setting. | |
| story_authoropt | STRING | MY STORY ONLY. Who the story is by. Put your own name here, or a pen name, or anyone you want it credited to. The announcer says it out loud and the closing credits print it. Leave it empty and the episode credits 'one of our listeners' -- no name is ever filled in for you. |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| script_text | STRING | — |
| script_json | STRING | — |
| news_used | STRING | — |
| estimated_minutes | INT | — |
| technical_model | STRING | — |