# `scenes/<id>.yaml`

> The scenes/<id>.yaml reference — backgrounds, avatars, narration, duration, and editing.

# `scenes/<id>.yaml`

One file per scene. **The filename is the scene id** — `scenes/demo.yaml` is the scene `demo`. Do
**not** put an `id:` field inside; the loader injects it, and a stray `id:` is a legible error.

A scene is a **background** (what fills the frame) + an optional **avatar** overlay + **narration**.

```yaml
# yaml-language-server: $schema=../../../schemas/scene.schema.json
background: { kind: web, source: dashboard_demo }   # slide | image | cli | web | video | avatar
avatar: { placement: pip }                          # off (default) | pip | fullscreen
narration: Here is the live dashboard.
duration: null                                      # null for cli/web/video/avatar; a number for slide/image
chapter: Dashboard
```

## `background`

What fills the frame. The `kind` selects the visual; most kinds reference a `source` id from
[`sources.yaml`](./sources):

| `kind` | Source of visual |
|--------|------------------|
| `slide` | An HTML template (no source needed) |
| `image` | An `image` source |
| `cli` | A `cli` (terminal) source — **executes** the tape (records a real terminal via VHS) |
| `web` | A `web` (browser) source |
| `video` | A `video` clip source |
| `avatar` | A generated talking-head |

> Pasting a CLI transcript to *show* — not run — a terminal session? Use the `terminal` **slide
> template** (below), not this `cli` background: the `cli` kind above executes the tape for real
> (VHS screen-records it); `slide`/`terminal` only renders pasted text, nothing runs.

### Video backgrounds

A `video` background uses a pre-recorded clip. It can carry a `frame` mat — the clip inset centered
over a solid color or an image source:

```yaml
background: { kind: video, source: demo_clip, frame: { color: "#0b0f19", scale: 0.8 } }
# or frame: { image: backdrop }  where `backdrop` is an image source
```

A bare fullscreen `video` scene (no effects, frame, or avatar) renders via an **ffmpeg-only
passthrough** — it skips the headless-Chrome compositor entirely, so it's faster.

### Slide templates

A `slide` background names a `template` + its `data`:

| `template` | `data` |
|------------|--------|
| `title` | `{ title, subtitle? }` — a centered statement card. |
| `bullets` | `{ title?, items: [...] }` — an enumerated list. |
| `diagram-3col` | `{ title?, cols: [...] }` (≥2) — boxes connected left-to-right. |
| `gradient` | `{ from, to, accent, angle?, accentOpacity?, motion?, headline?, subtitle? }` — a colored/gradient background with a slow two-layer parallax drift. No image, renders instantly, never fails. |
| `terminal` | `{ transcript, prompt? }` — a pasted CLI transcript rendered as a styled command/output window. A line prefixed by `prompt` (default `$`) renders as a command; any other line renders as output. **Nothing is executed** — this is a static look built from pasted text, not the `cli`/VHS background below. |

```yaml
background: { kind: slide, template: gradient, data: { from: "#0A0B10", to: "#12141C", accent: "#818CF8", angle: 135 } }
```

```yaml
background: { kind: slide, template: terminal, data: { transcript: "$ npm install\nadded 42 packages" } }
```

## `avatar`

An optional talking-head overlay:

```yaml
avatar: { placement: pip }     # off (default) | pip | fullscreen
```

## `narration`

The spoken script for the scene. Decodx generates the voice — the words are the editing surface.
Multi-line reads best as a block scalar:

```yaml
narration: |
  Welcome to the dashboard.
  Everything you see here updates live.
```

## `duration` — authored vs. auto

| Scene kind | `duration` |
|------------|------------|
| `slide`, `image` | An **authored** number (seconds) — you set how long it holds. |
| `cli`, `web`, `video`, `avatar` | `null` — the length is locked automatically from the recording or clip. |

## Retiming a video: `speed`

A `video` background can be retimed:

- a **number** — `speed: 1.5` is 1.5× faster (`0.8` slower); the scene duration becomes `clip / speed`.
- **`speed: auto`** — let the narration set the pace: the clip is stretched or compressed to last
  exactly as long as the narration (+ padding), clamped to `[0.9, 2.5]×`.
- project-wide default: `meta.defaults.video_speed` (a per-scene `speed` still wins).

If narration covers less than 70% of a fixed-speed clip, `decodx narrate` prints an **underrun**
advisory with a words-to-add hint and the `speed: auto` pointer.

## Editing without pre-cutting: `trim` / `cuts` / `crop`

Author edits on the background instead of pre-cutting the file — so one recording can back many
trimmed scenes:

| Field | Effect |
|-------|--------|
| `trim: [start, end]` | Source seconds — **drives the scene duration**. |
| `cuts: [[in, out], …]` | Remove dead-time spans; kept segments are concatenated. |
| `crop: [x, y, w, h]` | A fractional (0–1) window of the frame. |

All three are applied in one cached ffmpeg pre-pass.

## `chapter`

An optional chapter label — collected into `output/chapters.txt` (paste-ready YouTube chapters) and
`chapters.json`.

---

That's the full authoring surface. To generate a whole scene set from a recording instead of writing
these by hand, see `decodx ingest` in the [Quickstart](../getting-started/quickstart).
