[![中文](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-f2e3e3?style=for-the-badge&labelColor=f2e3e3&color=b07070)](README.md)
[![English](https://img.shields.io/badge/English-8b1a1a?style=for-the-badge)](README.en.md)
[![Follow on X](https://img.shields.io/badge/Follow-%40eternityspring-b07070?style=for-the-badge&labelColor=8b1a1a&logo=x&logoColor=f2e3e3)](https://x.com/eternityspring)

# novel-script

Screenwriting for AI short drama: turns novel-outline's per-episode synopses into scenes and dialogue. The premise is baked in: **the script owns the drama, the storyboard owns the filming.** "Is it gripping" and "how to shoot it" iterate at completely different speeds — dialogue gets rewritten constantly, and binding it to shot breakdowns means re-cutting shots on every line change. So this layer has no shot numbers and no frame prompts; those belong to the storyboard skill one layer down.

One line is held firmly though: **dialogue is structured data, not prose.**

- **Beat flow** — every scene is an alternating sequence of action beats and dialogue lines: one event per action beat (narrative prose), every line carrying its speaker and delivery note. Lines feed straight into per-line TTS; action beats are what the picture has to do
- **Per-episode time budget** — dialogue converts at reading speed (4.5 chars/sec by default), action at a fixed per-beat estimate (2.5s). Every episode must land within ±15% of target. **A three-minute episode is three minutes** — overruns are caught here, not in the generation pipeline
- **Hook + cliffhanger** — on paper for every episode, and **the hook is the first beat, not a label**: `hookBeat` claims its concrete image, gated to the episode's first 3 beats (cold open); beats promised by the outline must be claimed by actual scenes
- **Voice-over convention** — `VO` marks inner voice and narration, whose voice goes in the delivery note; the line book groups VO separately

Outputs `script.json`, a Markdown script, and a self-contained `script-report.html`:

![script-report.html](assets/report.webp)

## Ten quality gates, all code

Same stance as the other three skills in this repo: **a checklist the model grades itself on is worthless.**

| Gate | Rule |
| --- | --- |
| **Episode duration** | estimate within ±15% of `targetSeconds` (speed, per-beat seconds and tolerance all tunable via `params`) |
| Line length | ≤ 35 chars — a line you can't say in one breath can't be generated either |
| Speaker legality | speaker must be in the scene's cast, or explicitly `VO` |
| Hook & cliff on paper | `hook` / `cliff` required per episode |
| **Hook lands in the first 3 beats** | `hookBeat` claims where the hook's concrete image appears — the cold open is a gate, not a habit, so the hook and the actual opening can never drift apart |
| **At least one action beat per scene** | a dialogue-only scene is radio drama — nothing for the picture to do |
| Narrative action | no quoted dialogue inside action beats — lines only live in dialogue entries |
| Beats claimed | every beat the outline pins to this episode must be claimed (checked with `--outline`; skipping is **announced**, never silent) |
| Character refs | scene casts must exist in the outline (with `--outline`) |
| Scene refs | scene exists, **lighting state is registered in the art bible**, props exist (with `--art`) |

The selftest **defeats every gate on purpose** to prove each one actually blocks.

## The report

**Reports render with a Chinese UI by default; pass `--lang en` for a fully English report** (or set a top-level `"lang": "en"` in script.json — `--lang` wins when both are given). In English mode the quality-gate labels are translated too (thresholds kept as computed); failing-gate details and all data stay as authored.

A single-page, 1600px-wide review document:

- **KPI band**: episodes / estimated total vs target / line count / dialogue ratio / average scene length — scene changes are a statistic, not a gate; AI scene changes are free
- **Duration gauge**: one bar per episode, dialogue and action stacked, laid over the target-range band; overruns and shortfalls called out in red with exact seconds
- **The script itself**: the main body, **two episodes per row**. Episode header (estimate / hook / cliff / claimed beats) always visible; the scene area clips at 300px with a fade and toggles open per episode; per-line copy buttons on hover
- **Scene table**: every scene × setting × lighting × cast × estimated seconds — computed, never hand-written
- **Line book**: **two blocks per row**, all dialogue grouped by character, each list six rows tall with its own scrollbar, with episode/scene references and a copy-all button; with `--cast`, each character header also carries a **voice-prompt** copy button — lines and voice design on one page, straight into TTS
- **Quality gates** panel + header badge + **Export JSON** (downloads `script.json` verbatim — edit and feed it straight back into `render` / `validate`)
- All graphics inline CSS/SVG, zero external resources, opens offline

## The relay

```
novel-outline    → outline.json (what)
novel-characters → cast.json    (who)
novel-art        → art.json     (where + what's in their hands)
novel-script     → script.json  (the drama: scenes, beats, lines)
```

`seed <outline.json> --eps 1-3` deterministically prefills each episode's skeleton (target seconds, hook, cliff, claimed beats, candidate scenes and cast). `validate --outline --art` cross-checks characters, beats, and scenes/lighting/props — a lighting state the art bible never registered fails right here. `render --outline --art` displays names instead of raw IDs: **IDs in the data, names on screen.**

One layer down comes the storyboard skill: shot numbers, per-shot duration, first-frame prompts, generation batching.

## CLI

```bash
node scripts/novel-script.mjs seed outline.json --eps 1-3
node scripts/novel-script.mjs validate script.json --outline outline.json --art art.json
node scripts/novel-script.mjs checkup script.json
node scripts/novel-script.mjs render script.json --html --outline outline.json --art art.json --cast cast.json > script-report.html
node scripts/novel-script.mjs render script.json --html --lang en --outline outline.json --art art.json > script-report.html
```

The report UI defaults to Chinese; `--lang en` renders it fully in English.

## Limits

- No shots, no shot numbers, no generation prompts, no images — nothing from the storyboard layer
- Duration is an **estimate, not a stopwatch** — that's what the ±15% tolerance is for; tune `params.charsPerSecond` to your voice-over pace
- Report UI ships in Chinese and English (`--lang zh|en`, Chinese by default, or the script.json top-level `lang` field); dialogue follows the drama's language
- Write ≤ 3 episodes per batch — the script is the most-rewritten layer of the whole pipeline

## Selftest

```bash
node scripts/selftest.mjs
```

154 assertions — timing engine, stats, gate-defeating cases, seed, rendering (both UI languages), export. No model calls, runs in about a second.

The bundled example (`examples/渡口-script.json`) is a **complete 6-episode script** — 9 scenes, 123 lines, every episode inside the ±15% band, all gates passing against the outline and art fixtures.

**Only tested on macOS + Node 24.**
