# Radio Drama Producer

> >

- **Type:** Skill
- **Install:** `agentstack add skill-aeon-7-aeon-radio-drama-aeon-radio-drama`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [AEON-7](https://agentstack.voostack.com/s/aeon-7)
- **Installs:** 0
- **Category:** [Content & Media](https://agentstack.voostack.com/c/content-and-media)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [AEON-7](https://github.com/AEON-7)
- **Source:** https://github.com/AEON-7/aeon-radio-drama

## Install

```sh
agentstack add skill-aeon-7-aeon-radio-drama-aeon-radio-drama
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Radio Drama Producer — Full Pipeline Skill

Produce complete radio dramas, audiobooks, and audio fiction from a creative premise. This skill orchestrates the full pipeline: script writing, voice casting, audio generation (dialogue + music + SFX), and final mixed output.

## The Four-Phase Pipeline

**Phase 1: Script** — Write a structured radio drama script from the user's creative input
**Phase 2: Voice Casting** — Design unique voices for every character using the Three-Lock system
**Phase 3: Audio Generation** — Render all dialogue, music, and SFX via ComfyUI workflows
**Phase 4: Assembly & Mix** — Stitch and master everything into a finished audio file

## Phase 1: Script Writing

Accept the user's creative input (premise, genre, characters, tone, target length) and write a complete radio drama script in structured JSON format.

The script JSON is the backbone of the entire pipeline — every downstream phase parses it. See `references/script-format.md` for the full schema.

### Script Structure

```json
{
  "title": "The Last Lighthouse",
  "genre": "mystery/thriller",
  "target_duration_minutes": 15,
  "characters": ["KEEPER", "INSPECTOR", "NARRATOR"],
  "acts": [
    {
      "act_number": 1,
      "title": "The Arrival",
      "scenes": [
        {
          "scene_id": "act1_scene1",
          "location": "Lighthouse exterior, stormy night",
          "elements": [
            {"type": "sfx_cue", "description": "Heavy rain and crashing waves against rocks", "duration_s": 5},
            {"type": "music_cue", "mood": "tense_ambient", "description": "Low drone with distant foghorn motif", "duration_s": 30, "fade_in_s": 3},
            {"type": "narration", "text": "The lighthouse had been dark for three days when Inspector Mara arrived.", "emotion": "neutral"},
            {"type": "sfx_cue", "description": "Car door slam, footsteps on wet gravel", "duration_s": 3},
            {"type": "dialogue", "character": "INSPECTOR", "text": "Hello? Anyone here?", "emotion": "cautious"},
            {"type": "pause", "duration_s": 2},
            {"type": "dialogue", "character": "KEEPER", "text": "You shouldn't have come.", "emotion": "fearful"}
          ]
        }
      ]
    }
  ]
}
```

### Element Types

| Type | Purpose | Key Fields |
|------|---------|------------|
| `narration` | Narrator voice-over | text, emotion |
| `dialogue` | Character speech | character, text, emotion, (optional) delivery_note |
| `music_cue` | Background score | mood, description, duration_s, fade_in_s, fade_out_s |
| `sfx_cue` | Sound effect | description, duration_s |
| `pause` | Silence/beat | duration_s |
| `direction` | Production note (not rendered) | note |

### Pacing Guidelines

- Open scenes with SFX/ambience to establish location (3-5s)
- Music cues should overlap dialogue, not compete — use low-energy backgrounds during speech
- Insert 1-2s pauses after emotional beats to let moments land
- Scene transitions: 2-3s crossfade with transitional SFX or music shift
- Target ~120-150 words per minute for dialogue pacing

## Phase 2: Voice Casting

Design voices for every character using the Three-Lock system from the tts-voice-designer skill.

For each character, define:
1. **voice_instruct** — natural language vocal description
2. **seed** — fixed integer for consistency
3. **emotion_overrides** — per-emotion instruct additions

### Casting Manifest

```json
{
  "characters": {
    "NARRATOR": {
      "voice_instruct": "Calm, measured feminine voice with BBC-documentary clarity, unhurried pacing",
      "seed": 200,
      "emotion_overrides": {
        "tense": ", pace tightening slightly, an undercurrent of urgency",
        "reflective": ", softer and more contemplative, almost speaking to herself"
      }
    },
    "INSPECTOR": {
      "voice_instruct": "Sharp, precise feminine voice with authority, clipped professional cadence",
      "seed": 310,
      "emotion_overrides": {
        "cautious": ", speaking more quietly, carefully measuring each word",
        "commanding": ", projecting with full authority"
      }
    },
    "KEEPER": {
      "voice_instruct": "Weathered elderly man, hoarse from years of sea air, speaking in halting fragments",
      "seed": 445,
      "emotion_overrides": {
        "fearful": ", voice dropping to a whisper, words tumbling over each other",
        "defiant": ", finding unexpected steel, each word planted firmly"
      }
    }
  }
}
```

Design for **contrast** — listeners distinguish characters by voice alone. Vary pitch, pace, texture, accent, and energy across the cast. See `references/voice-casting.md` for detailed guidance.

## Phase 3: Audio Generation

> ## ⛔ STOP — DO NOT BUILD COMFYUI WORKFLOWS BY HAND
>
> Every workflow shown below is **already built and tested** inside `scene_production_tool/radio_drama.py`. The JSON examples in this section are **reference material** — they document what the orchestrator constructs internally, not a runtime contract you should reimplement.
>
> **The correct way to generate audio:**
>
> ```bash
> # Full pipeline (dialogue + music + SFX + mix), one command:
> python ${COMFYUI_ROOT}\scene_production_tool\radio_drama.py  --stage all
>
> # Or per-stage (idempotent — already-rendered assets are skipped on re-run):
> python radio_drama.py  --stage tts
> python radio_drama.py  --stage music
> python radio_drama.py  --stage sfx
> python radio_drama.py  --stage mix
> ```
>
> The orchestrator handles: workflow construction, APG chain wiring (xl_base/xl_sft music), schema validation, ComfyUI submission, retry-on-transient-error, output collection, canonicalization to 48 kHz/stereo/pcm_s24le, and dynamics-preserving mastering.
>
> **Symptoms you're heading the wrong direction:**
> - "I'm checking the model loader for ACE Step…"
> - "Let me find the proper VAE node…"
> - "I need to figure out the negative conditioning structure…"
> - HTTP 400 on `/prompt` submission with cryptic node validation errors
> - "I don't have a compatible workflow template"
>
> All of these mean **stop and use `radio_drama.py`**. The workflow JSON in this section exists so you can understand what's happening, not so you can rebuild it. See `radio-drama-production` for the SSH runbook.
>
> **When to use `music_maker.py` instead:** if dialogue was generated outside the radio-drama project structure (or doesn't exist yet) and you only need music cues, call `music_tool/music_maker.py` directly — see "Standalone music generation" subsection at the end of this Phase.

The three systems below handle different tracks. The JSON shown is **what the orchestrator builds**, not what you should write by hand.

### Track 1: Dialogue & Narration (Qwen3-TTS)

For each `narration` and `dialogue` element in the script, the orchestrator constructs a workflow like this:

```json
{
  "1": {
    "class_type": "FB_Qwen3TTSVoiceDesign",
    "inputs": {
      "text": "",
      "voice_instruct": "",
      "seed": ""
    }
  },
  "2": {
    "class_type": "SaveAudio",
    "inputs": {
      "audio": ["1", 0],
      "filename_prefix": "__"
    }
  }
}
```

For scenes with rapid dialogue, use DialogueInference for natural conversational flow:

```json
{
  "1": {
    "class_type": "FB_Qwen3TTSRoleBank",
    "inputs": {
      "roles": []
    }
  },
  "2": {
    "class_type": "FB_Qwen3TTSDialogueInference",
    "inputs": {
      "script": "[character1] Line... [character2] Line...",
      "role_bank": ["1", 0],
      "pause_between_roles_ms": 400,
      "pause_between_sentences_ms": 200
    }
  },
  "3": {
    "class_type": "SaveAudio",
    "inputs": { "audio": ["2", 0], "filename_prefix": "_dialogue" }
  }
}
```

### Track 2: Music & Score (ACE Step 1.5 XL)

> **Reminder: do not assemble ACE Step workflows by hand.** `radio_drama.py --stage music` is the entry point. It imports `music_tool/music_maker.py` and dispatches to the right template (APG vs simple) based on the variant in your script. The JSON below is **reference for understanding what gets built**, not something to copy-paste into ComfyUI.

#### Default: xl_base with APG chain (12-node graph)

The current default is `xl_base` (50 steps, CFG 7.0, full fp32 base model, with the APG chain required for clean output from base models). This is the same chain `music_maker.py` uses for standalone tracks — both routes share `music_tool/templates/ace_step_music_apg_api.json`.

```json
{
  "1":  {"class_type": "UNETLoader",           "inputs": {"unet_name": "acestep_v1.5_xl_base.safetensors", "weight_dtype": "default"}},
  "2":  {"class_type": "CLIPLoader",           "inputs": {"clip_name": "umt5_base.safetensors", "type": "ace"}},
  "3":  {"class_type": "VAELoader",            "inputs": {"vae_name": "ace_step_audio_vae.safetensors"}},
  "4":  {"class_type": "TextEncodeAceStepAudio1.5", "inputs": {
            "tags": "", "lyrics": "[Instrumental]",
            "duration": "", "seed": "",
            "clip": ["2", 0]}},
  "5":  {"class_type": "EmptyAceStepLatentAudio", "inputs": {"seconds": "", "batch_size": 1}},
  "6":  {"class_type": "ModelSamplingAuraFlow", "inputs": {"shift": 3.0, "model": ["1", 0]}},
  "7":  {"class_type": "APG",                   "inputs": {"eta": 0.7, "norm_threshold": 2.5, "momentum": -0.75, "model": ["6", 0]}},
  "8":  {"class_type": "CFGGuider",             "inputs": {"cfg": 7.0, "model": ["7", 0], "positive": ["4", 0], "negative": ["4", 1]}},
  "9":  {"class_type": "KSamplerSelect",        "inputs": {"sampler_name": "gradient_estimation"}},
  "10": {"class_type": "BasicScheduler",        "inputs": {"scheduler": "simple", "steps": 50, "denoise": 1.0, "model": ["6", 0]}},
  "11": {"class_type": "RandomNoise",           "inputs": {"noise_seed": ""}},
  "12": {"class_type": "SamplerCustomAdvanced", "inputs": {
            "noise": ["11", 0], "guider": ["8", 0], "sampler": ["9", 0],
            "sigmas": ["10", 0], "latent_image": ["5", 0]}},
  "13": {"class_type": "VAEDecodeAudio",        "inputs": {"samples": ["12", 0], "vae": ["3", 0]}},
  "14": {"class_type": "SaveAudio",             "inputs": {"audio": ["13", 0], "filename_prefix": "_music"}}
}
```

The APG node + `gradient_estimation` sampler + `linear_quadratic`/`simple` scheduler are **required** for `xl_base` and `xl_sft` to render without artifacts. Without APG, those base models produce hollow/distorted audio. Distilled variants (`xl_turbo`, `base_turbo`, `xl_base_sft`) use a simpler 6-node KSampler graph instead. See `music_tool/templates/ace_step_music_simple_api.json` for that one.

#### Variants — choose per cue or per script

| Variant | Graph | Steps | CFG | Time @ 90 s | Notes |
|---|---|---|---|---|---|
| **`xl_base`** (default) | APG | 50 | 7.0 | ~21 s | Highest fidelity. Recommended unless iterating fast. |
| `xl_sft` | APG | 45 | 6.0 | ~18 s | Near-base quality, bf16 (~10 GB), ~20% faster. |
| `xl_base_sft` | simple KSampler | 35 | 3.0 | ~21 s | Merged base+SFT, no APG needed. |
| `xl_turbo` | simple KSampler | 10 | 1.0 | ~12 s | Distilled. Use for **preview iterations** while writing. |
| `base_turbo` | simple KSampler | 8 | 1.0 | ~8 s | Smallest (~4.8 GB), lowest quality. |

Set per-cue or script-wide:
```json
"music_cues": {
  "open":    {"tags": "...", "duration": 45, "variant": "xl_base"},
  "preview": {"tags": "...", "duration": 30, "variant": "xl_turbo"}
},
"ace_variant": "xl_base"   // script-wide default if cue doesn't specify
```

#### Music mastering (automatic, under-dialogue tuned)

Every music cue produced by `radio_drama.py --stage music` is automatically run through `scene_production_tool/music_mastering.py` between ACE output and canonicalization. The default is dialed for under-dialogue placement:

- **Preset:** `default` (EQ + light saturation, no compression)
- **Target LUFS:** `−18` (quieter than standalone music — the mix's sidechain ducking + final loudnorm bring it up)
- **Ceiling:** `−1 dBTP`

**Per-cue overrides in the script:**

```json
"music_cues": {
  "open":     {"tags": "...", "duration": 30, "master": "orchestral", "master_target_lufs": -20},
  "tension":  {"tags": "...", "duration": 30, "master": "default"},
  "hero":     {"tags": "...", "duration": 60, "master": "orchestral", "master_target_lufs": -14},
  "notes":    {"tags": "...", "duration": 20, "master": "off"}
}
```

`"master"` choices: `"default" | "edm" | "trap" | "chill" | "orchestral" | "jazz" | "auto" | "off"`. `"auto"` detects the preset from the cue's tags.

**Script-wide defaults:**

```json
{
  "music_master": "default",       // global preset for cues that don't set "master"
  "music_master_lufs": -18.0       // global LUFS target
}
```

**Prompting rules for under-dialogue beds:** avoid heavy dynamics (no `[drop]`, no `loud-quiet-loud`, no `sudden silence`) — they fight sidechain ducking. Prefer "sustained", "slow evolving", "consistent energy", "drone", "warm bed". Save the dynamics vocabulary for standalone music via `music_maker.py`.

**Hero moments:** when a cue takes center stage (opening titles, act breaks, silent-scene montage), override with `"master": "orchestral"` + `"master_target_lufs": -16` or `-14`, and raise its mix `volume` to `1.0`. Those moments deserve the full LRA of a proper master.

### Track 3: Sound Effects (MMAudio primary, SAO fallback)

> **Reminder: do not assemble SFX workflows by hand.** `radio_drama.py --stage sfx` runs `generate_sfx()` which automatically routes each event to the right engine based on duration. You don't pick the engine — the orchestrator does. JSON below is reference for what it builds.

SFX routing is **duration-aware**. After A/B testing on transients, foley, mechanical, ambient, and biological sounds, MMAudio beat both Stable Audio Open and ACE Step on quality — it's now the default. Engine per event:

| Requested duration | Engine | Why |
|---|---|---|
| 1–10 s | **MMAudio Large 44k v2** | Best quality for discrete SFX; 10 s coherence window |
| 10–47 s | **Stable Audio Open 1.0** | MMAudio drifts past 10 s; SAO handles up to 47 s cleanly |
| 47+ s | **ACE Step 1.5** | Only engine with unlimited duration (music-model; use for ambient only) |

This routing is already wired in `radio_drama.py`'s `generate_sfx()` priority chain — you don't set the engine per event; the tool picks based on duration.

#### Reference: MMAudio workflow (≤10 s) — 4-node graph the orchestrator builds:

```json
{
  "1": {"class_type": "MMAudioModelLoader", "inputs": {
    "mmaudio_model": "mmaudio_large_44k_v2_fp16.safetensors", "base_precision": "fp16"
  }},
  "2": {"class_type": "MMAudioFeatureUtilsLoader", "inputs": {
    "vae_model":         "mmaudio_vae_44k_fp16.safetensors",
    "synchformer_model": "mmaudio_synchformer_fp16.safetensors",
    "clip_model":        "apple_DFN5B-CLIP-ViT-H-14-384_fp16.safetensors",
    "mode": "44k", "precision": "fp16"
  }},
  "3": {"class_type": "MMAudioSampler", "inputs": {
    "mmaudio_model": ["1", 0], "feature_utils": ["2", 0],
    "duration": "", "steps": 50, "cfg": 5.5, "seed": "",
    "prompt": "",
    "negative_prompt": "music, speech, vocals, singing",
    "mask_away_clip": true, "force_offload": true
  }},
  "4": {"class_type": "SaveAudio", "inputs": {"audio": ["3", 0], "filename_prefix": "_sfx_"}}
}
```

#### Reference: Stable Audio Open workflow (10–47 s, fallback) — orchestrator builds, uses ComfyUI native nodes:

```json
{
  "1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "stable-audio-open-1.0.safetensors"}},
  "2": {"class_type": "CLIPLoader", "inputs": {"clip_name": "stable-audio-open-t5.safetensors", "type": "stable_audio"}},
  "3": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["2", 0]}},
  "4": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["2", 0]}},
  "5": {"class_type": "EmptyLatentAudio", "inputs": {"seconds": "", "batch_size": 1}},
  "6": {"class_type": "ConditioningStableAudio", "inputs": {
    "positive": ["3", 0], "negative": ["4", 0], "seconds_start": 0.0, "seconds_total": ""
  }},
  "7": {"clas

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [AEON-7](https://github.com/AEON-7)
- **Source:** [AEON-7/aeon-radio-drama](https://github.com/AEON-7/aeon-radio-drama)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-aeon-7-aeon-radio-drama-aeon-radio-drama
- Seller: https://agentstack.voostack.com/s/aeon-7
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
