Nodes/ComfyUI-OldTimeRadio/ LPL Script Writer (v2.0)
ComfyUI Node

LPL Script Writer (v2.0)

The LLM that writes the whole radio drama

By jbrick2070·Created 5 months ago·Updated about 22 hours ago· 7
LPL Script Writer (v2.0)
    • script_text
    • script_json
    • news_used
    • estimated_minutes
    • technical_model
    episode_title
    num_characters2
    creative_writing_modelQwen/Qwen3.5-4B
    technical_modelQwen/Qwen3.5-4B
    custom_premise
    include_act_breakstrue
    act_count3
    creativitybalanced
    perfect_run_spacesaverfalse
    min_p0.05
    repetition_penalty1.03
    max_new_tokens_cap200
    lemmy_cameoroll (~11% chance)
    use_exchangefalse
    enable_production_stage3_validatorsfalse
    news_briefs_requiredtrue
    openrouter_slot_a_model(enable OpenRouter)
    openrouter_slot_b_model(enable OpenRouter)
    comfy_slot_a_model(enable Comfy Credits)
    comfy_slot_b_model(enable Comfy Credits)
    story_scaffoldauto
    source_bankscifi_news_pro
    visual_stylesci_fi_radio
    google_api_slot_a_model(select Google API model)
    google_api_slot_b_model(select Google API model)
    source_ref
    llm_devicedefault
    llm_attn_implsdpa
    llm_quant_policybnb_nf4
    llm_vram_ceiling_gb14.5
    gguf_n_ctx4096
    gguf_quantQ8_0
    gate_in
    replay_from
    story_characters
    story_plot
    story_setting
    story_author

    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. Default scifi_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), and roll (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) through maximum chaos (0.95 - deliberately capped below 1.0 because higher caused format collapse). min_p (0.05) and repetition_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_style and story_scaffold control 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.

    CategoryOldTimeRadio

    Inputs (38)

    NameTypeDefaultDescription
    episode_titleSTRINGOptional 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_charactersINT21–10REQUESTED 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_modeloptCOMBOQwen/Qwen3.5-4BLLM 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_modeloptCOMBOQwen/Qwen3.5-4BLLM 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_premiseoptSTRINGFor 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_breaksoptBOOLEANtrueWhen 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_countoptCOMBO3Number 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
    creativityoptCOMBObalancedCreativity 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_spacesaveroptBOOLEANfalseDEPRECATED 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_poptFLOAT0.050–0.5min_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_penaltyoptFLOAT1.031–1.2Repetition 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_capoptINT20040–400Per-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_cameooptCOMBOroll (~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_exchangeoptBOOLEANfalseBuild 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_validatorsoptBOOLEANfalseOFF (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_requiredoptBOOLEANtrueON (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_modeloptCOMBO(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_modeloptCOMBO(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_modeloptCOMBO(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_modeloptCOMBO(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_scaffoldoptCOMBOautoHow 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_bankoptCOMBOscifi_news_proStory-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_styleoptCOMBOsci_fi_radioVISUAL 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_modeloptCOMBO(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_modeloptCOMBO(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_refoptSTRINGOptional 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_deviceoptCOMBOdefaultLLM 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_imploptCOMBOsdpaAttention implementation for the transformers lane (the FA2 auto-probe is gone). sdpa = the proven baseline.
    llm_quant_policyoptCOMBObnb_nf4Quantization for the transformers lane. bnb lanes are OFF on ROCm/MPS/CPU tiers (missing bitsandbytes fails loud).
    llm_vram_ceiling_gboptFLOAT14.50–96Pre-download VRAM-fit ceiling (GB). 0 DISABLES the gate (cpu tier only).
    gguf_n_ctxoptINT4096512–32768GGUF lane context window. NO silent downgrade: a window that does not fit free VRAM fails loud.
    gguf_quantoptCOMBOQ8_0GGUF artifact quant (filename + expected size come from the artifact table). Only PINNED quants are offered -- an unpinned artifact is refused at load.
    gate_inoptSTRINGOrdering/validation signal (wire OTR_WorkflowValidator.validation_report).
    replay_fromoptSTRINGCANONICAL 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_charactersoptSTRINGMY 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_plotoptSTRINGMY 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_settingoptSTRINGMY 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_authoroptSTRINGMY 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)

    NameTypeDescription
    script_textSTRING
    script_jsonSTRING
    news_usedSTRING
    estimated_minutesINT
    technical_modelSTRING