Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions video/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
out/
.tmp-narration/
.piper-voices/
*.log
.DS_Store
191 changes: 191 additions & 0 deletions video/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# SimpleModule Showcase Videos

Remotion videos that showcase the SimpleModule framework.

- **`SimpleModule`** — ~39 s master reel covering compile-time module
discovery, `IEndpoint` auto-registration, `MapCrud<>`, Inertia, and the
event bus. Cross-fade + slide transitions connect the scenes.
- **`Module-<Name>`** — 14 s per-module spotlights, one for each core
module (Users, Permissions, Settings, Admin, Dashboard, AuditLogs,
FileStorage, Email, BackgroundJobs, FeatureFlags). Each one shows the
module's tagline, three features, and a `SimpleModule.<Name>` callout.

All videos ship with a Scott Buckley background track and Piper-generated
narration (see **Audio** below).

## Prerequisites

- Node.js 20+
- Linux/macOS/Windows with the system libs Chromium Headless Shell needs
(on Ubuntu/Debian these are: `libnss3 libgbm1 libasound2t64
libatk-bridge2.0-0t64 libatk1.0-0t64 libdrm2 libxkbcommon0 libxcomposite1
libxdamage1 libxfixes3 libxrandr2`). The `@remotion/renderer` package
bundles its own ffmpeg, so the system `ffmpeg` binary is not required.

## Install

```bash
cd video
npm install
```

## Audio

Every video ships with two audio layers:

- **Background music** at `public/background.mp3` — a 42 s clip of
"I Walk With Ghosts" by **Scott Buckley** (CC-BY 4.0), trimmed from
0:30. Attribution required if you redistribute the video —
<https://www.scottbuckley.com.au>.
- **Narration** — generated by **Piper** (a local neural TTS) using the
`en_US-ryan-high` voice. `public/narration/s1.mp3 … s9.mp3` drives the
main showcase; `public/narration/modules/<Name>.mp3` drives each module
spotlight.

### Regenerating narration

Narration lines live next to the data they describe:

- Main reel copy — in `scripts/generate-narration.mjs` (top of the file).
- Module copy — in `src/data/moduleShowcases.ts` (the `narration` field).

After editing either, run:

```bash
npm run narrate
```

The script speaks each line with Piper, re-encodes to MP3, and writes a
`manifest.json` with durations so you can sanity-check that lines still
fit in their scenes.

### Getting a Piper voice

`npm run narrate` expects `.piper-voices/en_US-ryan-high.onnx` (+ `.json`).
Download both from
<https://huggingface.co/rhasspy/piper-voices/tree/main/en/en_US/ryan/high>:

```bash
mkdir -p .piper-voices
cd .piper-voices
curl -LO "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high/en_US-ryan-high.onnx"
curl -LO "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high/en_US-ryan-high.onnx.json"
```

The voices folder is `.gitignore`d. Swap to any Piper voice by updating
the path in `scripts/generate-narration.mjs`. Recommended alternatives:
`en_US-lessac-medium` (clear female), `en_GB-alan-medium` (British male),
`en_US-libritts_r-medium` (neutral narrator).

### Swapping the background track

Drop your own ≥40 s MP3 at `public/background.mp3`. The music is ducked
to 0.20–0.22 so narration stays intelligible; the fade-in/out envelope
lives in `src/Video.tsx` and `src/compositions/ModuleShowcase.tsx`.

As a pure-synthesis fallback, `scripts/generate-audio.mjs` writes an
ambient Cmaj9 pad with no licensing concerns.

### Silent renders

```bash
REMOTION_DISABLE_AUDIO=1 npm run render # main only
REMOTION_DISABLE_AUDIO=1 npm run render:all # every video
```

### Loudness normalization

Remotion just mixes audio sources verbatim — it doesn't compress or limit
— so a hot TTS track can push the mix over -5 LUFS (ear-splitting). Every
render pipeline that produces a deliverable ends with a loudnorm pass:

```bash
npm run render:all # renders, then normalizes every mp4 to -14 LUFS
node scripts/normalize-loudness.mjs # normalize existing files in-place
```

Target: -14 LUFS integrated, -1.5 dBTP ceiling (matches Spotify/YouTube).

## Develop

```bash
npm run start
# opens the Remotion studio at http://localhost:3000
# pick the "SimpleModule" composition
```

## Render

First render downloads the Chrome Headless Shell (~170 MB) to
`~/.cache/remotion/`. On hosts running as root you must disable the
Chromium sandbox — the snippets below already set the flag:

```bash
# pre-warm the Chrome download (recommended)
npx remotion browser ensure

# render the main showcase
REMOTION_CHROME_FLAGS="--no-sandbox --disable-setuid-sandbox" npm run render
# output: out/simplemodule.mp4

# render the main showcase + every per-module spotlight
REMOTION_CHROME_FLAGS="--no-sandbox --disable-setuid-sandbox" npm run render:all
# outputs: out/simplemodule.mp4 + out/modules/<Name>.mp4
```

If your host sits behind an SSL-intercepting proxy (e.g. some corporate
networks), Google Fonts may fail to load. The `remotion.config.ts` in
this project already sets `setChromiumIgnoreCertificateErrors(true)` so
the render still succeeds — fonts simply fall back to the system stack.

## Structure

```
video/
├── public/ # logos, background.mp3, narration/**
├── scripts/ # generate-audio.mjs, generate-narration.mjs, render-all.mjs
└── src/
├── Root.tsx # 1 main + 10 per-module <Composition>s
├── Video.tsx # main: TransitionSeries + audio layers
├── compositions/
│ └── ModuleShowcase.tsx # reusable 14-second per-module template
├── scenes/ # storyboard scenes for the main reel
├── components/ # Logo, CodeBlock, SplitCodePair, ModuleChip, ...
└── data/
├── modules.ts # core-module chip list
├── techStack.ts # badges + stats
├── codeSnippets.ts # code shown in main reel
└── moduleShowcases.ts # per-module data driving Module-<Name> comps
```

## Storyboards

### Main reel (`SimpleModule`)

| # | Scene | Duration |
|---|---|---|
| 1 | Opening logo + tagline | 3.2s |
| 2 | Problem → Solution | 4.7s |
| 3 | `[Module]` attribute + Roslyn generator | 5.3s |
| 4 | `IEndpoint` auto-discovery | 5.0s |
| 5 | `MapCrud<>` helper | 6.2s |
| 6 | Inertia.Render + IEventBus | 5.3s |
| 7 | 10 core module grid | 5.0s |
| 8 | Tech stack + stats | 5.8s |
| 9 | Call to action | 3.3s |

Scenes are stitched with `@remotion/transitions` — alternating cross-fade
and slide (18 frames each), plus a bottom-slide into the module grid.

### Per-module spotlight (`Module-<Name>`)

Same 14 s template for every core module, driven by
`src/data/moduleShowcases.ts`:

| Phase | Frames | What's on screen |
|---|---|---|
| Intro | 0–110 | Accent halo, gradient module name, tagline |
| Features | 120–300 | Three bullets slide in (60 frames apart) |
| Outro | 320–420 | Logo + `SimpleModule.<Name>` callout |

Each module gets its own accent colour and narration line.
Loading
Loading