# Prompt-quality preflight

Seven mechanical anti-pattern checks driven by the Seedance 2.0 Handbook. They
run inside `director-preflight` against every storyboard scene's text. They are
**warnings by default** and promote to blocking **errors** when
`DIRECTOR_STRICT_PROMPT_QUALITY=1` is set in the environment.

The module (`src/video/prompt-quality.ts`) is pure: no disk I/O, no global
state beyond reading the severity env var at runtime. The checks surface
through the existing `director-preflight` JSON output — there is no new CLI
command and no new smoke.

## Issue codes

| Code | What it catches | Threshold |
|---|---|---|
| `prompt-quality-adjective-soup` | A single clause piling up comma-separated modifiers | `> ADJECTIVE_SOUP_THRESHOLD` (default 4) |
| `prompt-quality-multiple-actions` | Serial third-person-present actions in one clause | `> 1` |
| `prompt-quality-multiple-camera-moves` | More than one camera-move family per prompt | `> 1` movement family |
| `prompt-quality-style-word-overload` | Stacked "cinematic/epic/atmospheric" style words | `> STYLE_WORDS_THRESHOLD` (default 3) |
| `prompt-quality-literary-emotion` | Inner-state language (`feels`, `profound sadness`) instead of visible behavior | any match |
| `prompt-quality-overlong` | Prompt word count above the single-shot budget | `> OVERLONG_WORDS_THRESHOLD` (default 120) |
| `negative-direction` | A negated motion/tempo clause (`no`/`don't`/`avoid`/`without`/`never` + `slow-motion`/`blur`/`motion`/`movement`/`zoom`/`camera shake`) — these models ignore negated direction | any match outside the sanctioned-negation allowlist (on-screen text, specular kill, anti-plastic) |

Vocabularies live in `CAMERA_MOVE_VOCABULARY`, `SHOT_TYPE_VOCABULARY`, and
`STYLE_VOCABULARY` in `src/video/prompt-quality.ts` and are deliberately short
and conservative.

Camera movement is checked separately from shot size. Movement families include
`push-in`, `pull-out`, `tracking`, `orbit`, `static` / `locked-off`, `crane reveal`,
`pan`, `tilt`, `zoom`, `handheld`, and `steadicam`. Shot-size terms such as
`wide shot`, `medium shot`, `close-up`, and `establishing shot` do not count as
movement conflicts, so `wide shot, slow push-in` is valid while `push-in and orbit`
still raises `prompt-quality-multiple-camera-moves`.

For Seedance image-to-video work, keep imported still-image guidance separate
from motion prompting. The still image should carry composition, identity,
wardrobe, props, lighting, and framing. The Seedance prompt should preserve the
source image and add one visible action plus one camera move for a short clip.

## Dialogue duration fit

`director-preflight` also runs a dialogue timing check from `dialogue-fit.ts`.
It is separate from the seven prompt-quality anti-pattern checks because it uses
a duration budget rather than prompt wording alone.

Default behavior:

1. scenes without `durationSeconds` use a 15-second target
2. spoken dialogue is estimated at about 2.5 words per second
3. `DIALOGUE_DURATION_OVERFLOW` is emitted when the estimated spoken duration
   exceeds the scene target
4. the issue is a warning by default
5. `DIRECTOR_STRICT_DIALOGUE_FIT=1` promotes it to a blocking error

This check catches scripts that look fine as text but cannot be delivered
naturally inside the clip duration.

## Sample output

A scene with adjective soup and a second camera move produces
`director-preflight` JSON like:

```json
{
  "pass": true,
  "warnings": [
    {
      "severity": "warn",
      "code": "prompt-quality-adjective-soup",
      "scope": "scene:0",
      "message": "Scene 1: clause has 6 adjectives (threshold: 4): ...",
      "suggestion": "Tighten scene wording before approval; see docs/PROMPT_QUALITY.md."
    }
  ],
  "errors": []
}
```

Under `DIRECTOR_STRICT_PROMPT_QUALITY=1` the same issues appear on `errors`,
`pass` flips to `false`, and `execute`/`produce` is blocked (director-mode
preflight failures gate provider submission).

## Promoting to blocking

```bash
DIRECTOR_STRICT_PROMPT_QUALITY=1 vclaw video director-preflight --project <slug>
```

Team workflows that want prompt-quality to be a release gate can export the
variable in CI so every run treats handbook violations the same as content
hazards. Teams still iterating can leave it unset and treat the warnings as
review cues.

## Integration points

- Emitted by `checkPromptQuality` in `src/video/director-preflight.ts`.
- Surfaces in the existing `vclaw video director-preflight` JSON payload.
- Flows into `director-preflight`'s `result.warnings` / `result.errors`
  buckets, so all downstream consumers (tests, CI checks, storyboard.md
  approval review) see them automatically.

## Multi-shot cinematic prompt validation

`runMultiShotChecks(prompt, preset)` enforces the structural rules for
multi-shot cinematic prompts. It lives in the same module as
`runPromptQualityChecks` (`src/video/prompt-quality.ts`) and is invoked
via:

```bash
vclaw video multi-shot --validate
```

Unlike the seven anti-pattern checks above, all issues from
`runMultiShotChecks` are **always `error` severity** — they are
structural requirements of the multi-shot format, not stylistic
guidelines, so they cannot be downgraded to warnings by omitting
`DIRECTOR_STRICT_PROMPT_QUALITY`.

### Presets

Six presets ship today, each declaring its own duration, shot-count
window, per-shot duration bounds, and character budget. The default
remains `cinematic-15s`; the others target a specific provider's clip
duration:

| Preset | total | shot range | shot count | maxChars |
|---|---|---|---|---|
| `cinematic-15s` *(default)* | 15 s | 2–5 s | 3–7 | 1500 |
| `seedance-10s` | 10 s | 2–5 s | 2–5 | 1500 |
| `veo-8s` | 8 s | 2–4 s | 2–4 | 1500 |
| `runway-10s` | 10 s | 2–5 s | 2–5 | 1000 |
| `music-video-15s` | 15 s | 1–4 s | 4–12 | 1500 |
| `social-hook-8s` | 8 s | 1–3 s | 3–8 | 1000 |

### Issue codes

| Code | What it catches |
|---|---|
| `multi-shot-timecode-parse` | A timecode in the prompt cannot be parsed |
| `multi-shot-timecode-start` | First timecode is not `00:00` |
| `multi-shot-timecode-gap` | Consecutive timecodes overlap or leave a gap |
| `multi-shot-timecode-total` | Timecodes do not sum to the preset's `totalSeconds` |
| `multi-shot-shot-duration` | A shot's duration falls outside the preset's `[minShotSeconds, maxShotSeconds]` |
| `multi-shot-shot-count-out-of-range` | Parsed shot count is outside the preset's `[minShots, maxShots]` window. Message branches on under vs over |
| `multi-shot-overlong` | Total prompt character count exceeds the preset's `maxChars` |
| `multi-shot-repeated-parameter` | A consecutive pair of shots shares the same shot size, lens, angle, or camera movement |
| `multi-shot-missing-metadata` | The required `Location / Style / Audio` metadata block is absent |

Matching for consecutive-shot-repeat checks is hyphen/space-insensitive,
so `push in` and `push-in` are treated as the same value.

### Canonical vocabularies

The shot-parameter vocabularies used for consecutive-repeat detection are
defined in `src/video/prompt-quality.ts` and are now canonical for the
entire module:

- `SHOT_SIZE_VOCABULARY` — `wide`, `medium`, `medium close-up`, `close-up`, `macro`
- `LENS_VOCABULARY` — `24mm`, `35mm`, `50mm`, `85mm`
- `ANGLE_VOCABULARY` — `low angle`, `high angle`, `eye-level`, `over-the-shoulder`, `Dutch angle`
- `CAMERA_MOVE_VOCABULARY` — movement families shared with `runPromptQualityChecks`

### Smoke

```bash
npm run smoke:multi-shot
```

Builds the project, runs `vclaw video multi-shot --plan`, then validates
a fixture at `references/video/.fixtures/multi-shot-valid.txt` — a
no-network plan → validate round-trip.

## filmmaking-prompts artifact lint (`prompt-lint`)

Distinct from the storyboard-scene anti-pattern checks above,
`lintFilmmakingPrompts` (`src/video/prompt-lint.ts`) is a separate, pure
validator that runs over a **`filmmaking-prompts.json` artifact** (not
storyboard scenes). It is exposed as its own command:

```bash
vclaw video prompt-lint (--project <slug> | --file <path>) [--register prose|numeric] [--cast <Name:descriptor> ...] [--brand <token> ...]
```

Per Seedance packet it checks the 10-block order (text-driven packets only),
word count (warn outside the **280–600 words/packet** window), the required
video blocks `SUBJECT LOCK` / `CAPTURE REALISM` / `CAMERA CAPTURE` (text-driven
only), the single-full-frame grid guard whenever a storyboard-grid reference is
attached (else error — the grid leaks as a moving split-screen), no Kelvin/degree
numeric tokens in a prose-register packet, and optional proper-name / brand scrubs
when `--cast` / `--brand` are passed. It also runs an **anti-slop advisory** over
every packet — empty hype words the model cannot act on (empty evaluators like
`cinematic`/`epic`, borrowed image-model tokens like `8K`/`masterpiece`, stacked
superlatives, feel-suffix vibe words, and quality-insurance negation like
`no blur`) are flagged with the observable production language to use instead.
It is advisory (warning severity, so it never flips `ok`); the toolchain's own
emitted guard blocks are ignored and constraint-slot negation (`no on-screen
text`) is allowlisted, so the linter never flags its own wording. The lexicon is
adapted from the MIT `Emily2040/seedance-2.0` repo. Output is
`{ packets: [{ sceneIndex, issues[] }], ok }`, where `ok` is true **iff** no
`error`-severity issue is raised on any packet.

### Issue codes

| Code | What it catches |
|---|---|
| `seedance-block-order` | 10-block order violated (text-driven packets only) |
| `word-count` | Packet word count outside the 280–600 window (warning) |
| `missing-required-block` | A required video block (`SUBJECT LOCK` / `CAPTURE REALISM` / `CAMERA CAPTURE`) is absent |
| `grid-guard-missing` | A storyboard-grid reference is attached but the single-full-frame guard is missing (error) |
| `numeric-in-prose` | Kelvin/degree numeric tokens in a `prose`-register packet |
| `proper-name-leak` | A proper name survives the cast scrub (only when `--cast` is passed) |
| `brand-leak` | A brand token survives the scrub (only when `--brand` is passed) |
| `slop` | Empty hype language flagged (warning) with its observable-language replacement — one aggregated issue per packet; tool-emitted blocks ignored, constraint-slot negation allowlisted |
| `reference-transfer` | A packet's references span ≥2 bleed-relevant domains (identity/continuity/environment/appearance) without a transfer/ignore clause (warning); references bleed, so state what each controls and what must not transfer |
| `allocation` | One packet over-allocates the fidelity budget — ≥3 of identity-detail / bold-motion / scene-density / readable-text collide in a single shot (warning); name one primary spend and split the rest into separate shots. Conservative strict cues; standing blocks never self-trigger |

## Follow-on roadmap

- Per-project threshold overrides (today: hardcoded constants).
- LLM-backed checks for subtler anti-patterns ("show don't tell", beat
  structure).
- Auto-fix suggestions, analogous to `DIRECTOR_AUTO_FIX_CONTENT=1` for
  content hazards.
- Tie into the Tier-2 prompt-structure schema (item 9 in
  `MASTER_PLAN_ALIGNMENT.md`) so each check points at the matching schema
  section.
