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
125 changes: 54 additions & 71 deletions docs/teaser/shot-script.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
# Teaser shot script

The teaser on the landing page: one continuous board session, recorded long and cut
down. The shipped cut runs 42 seconds. The same master serves the home page, the Store
trailer, and announcements. It is served from the site itself, not embedded — decision 19
in [../decisions.md](../decisions.md).
The teaser on the landing page is film 2 of [tools/films](../../tools/films/README.md),
"One board". The shipped cut runs 50 seconds: one continuous camera move across a single
board on which seven screen captures sit as windows, ending on the whole board and the end
card. It names no date and no deadline, so it does not go out of date when the campaign
ends. The same master serves the home page, the Store trailer, the social renditions,
and three 12-second portrait shorts. The site serves it itself, not through an embed —
decision 19 in [../decisions.md](../decisions.md).

Everything the recording needs is in this folder:
It replaced a 42-second cut of one continuous board session, recorded and edited by hand.
The staging files for that recording are still in this folder:

| File | Used in |
| --- | --- |
| `shot-script.md` | This script |
| `demo-board.wimport` | Beat 3 — the drop that fills the board |
| `paste-me.dax` | Beat 2 — copy its contents to the clipboard before recording |
| `demo-board.wimport` | The drop that filled the board in the 42-second cut |
| `paste-me.dax` | The measure pasted in the 42-second cut |
| `assets/star-schema.png`, `assets/sales-trend.png` | Referenced by the recipe |
| `make-assets.ps1` | Regenerates the two PNGs if they change |
| `overlays/ctrl-v.png`, `overlays/f6.png` | Keystroke callouts for the edit (beats 2's paste and F6) |
| `overlays/ctrl-v.png`, `overlays/f6.png` | Keystroke callouts for a hand edit |
| `make-overlays.ps1` | Regenerates the overlays if they change |

## Recording setup
Expand All @@ -28,85 +32,64 @@ Everything the recording needs is in this folder:
window region only). Default white board, default toolbar position.
- Pen: the Cintiq Pro profile already validated in README § "Wacom Cintiq Pro validation".

## Staging, before every take
The captures of the shipped cut were recorded at 30 fps, `ink-diagram.mp4` at
3840 × 2160 and the other six at 1920 × 1080, so the master is 1920 × 1080 at 30 fps.
The film never shows a capture larger than it was recorded. A re-recording at
2560 × 1440 can be placed on the board at the same size in board units without
changing anything else.

1. Open **Help → Preferences → Snippet format order** and move **DAX above Plain text**,
so the paste in beat 2 lands already highlighted. (Restore afterwards if you prefer
plain-first day to day.)
2. Copy the contents of `paste-me.dax` to the clipboard **as text** (open it in a text
editor and Ctrl+A, Ctrl+C — do not copy the file in Explorer, because a copied file is
pasted as a file rather than as text).
3. Open a File Explorer window showing this folder, positioned on a second monitor or
off-canvas, ready to drag `demo-board.wimport`.
4. Start the LiveView target for beat 5: **Power BI Desktop** with a report page visible
(any demo model — it only needs to look alive). Second monitor, or behind the app.
5. New empty board, Pen tool, default zoom.
## The shot list

## The beats
Each capture is a window on the board with its caption written below it. The camera is
the only thing that moves: it rests on a window with a slow push-in, moves to the next
with one ease-in-out curve and a slight pull back mid-move, and never cuts. The captures
play faster than recorded where the beat is shorter than the capture; the times and rates
are in `tools/films/src/film2/cuts.ts`.

Timings are the *edited* target. Record each beat at natural speed; the cuts and
speed-ups happen in the edit. Total real time per take: 3–5 minutes.

| # | Edited | Action, exactly | Must be visible |
| Time | Camera | Capture | Caption on the board |
| --- | --- | --- | --- |
| 1 | 0–5 s | On the empty board, handwrite **"Contoso — sales review"** and underline it. Vary pressure; this beat stays real-time in the edit, it is the "feel" shot. | Wet ink following the pen with no lag; pressure width variation; the hover dot before contact. |
| 2 | 5–11 s | Click empty canvas below the title, **Ctrl+V**. The measure appears as a DAX container, highlighted, crammed on long lines. Press **F6**. It reformats in place. | Syntax colors on paste; the visible before/after of F6. |
| 3 | 11–16 s | Drag **`demo-board.wimport`** from Explorer onto the right half of the board. Five containers materialize: two images, DAX, SQL, notes. | The one-gesture board build. Pause a beat so the layout registers. |
| 4 | 16–21 s | With the pen, circle the December column on the **Sales by month** chart and draw a short arrow — strokes must stay entirely on that one image. Then left-drag the chart container to a clearer spot. | The ink travels with the container. This is the "aha" beat; move it far enough to be unmistakable. |
| 5 | 21–26 s | **View → LiveView**, pick the Power BI Desktop window in the system picker, place it. Ink one arrow onto the live feed, then **View → Freeze**. | A real application, live inside the board, annotated. The picker dialog gets jump-cut to ~1 s in the edit. |
| 6 | 26–30 s | Click empty canvas, then **double-click empty canvas**: the camera fits the whole board. Hold two seconds. | The zoom-to-fit reveal: title ink, formatted DAX, the dropped board, annotated chart, frozen LiveView — one composition. |

Notes that keep beats honest:

- Beat 4's strokes link only if they touch **exactly one** container — keep the circle and
arrow fully on the chart image, not grazing a neighbor.
- The tab strip appears on camera in beat 5 (View tab) — that covers the tab-strip
requirement without its own beat.
- Optional touch: one short two-finger pan somewhere between beats 3 and 4 shows touch
navigation. Finger drawing is shown in a Store screenshot instead, because 30 s is too
short to include it.
- Optional end card: before the take, handwrite **whiteboard.sqlbi.com** small near the
board's edge; beat 6's fit reveals it without an overlay.

## Takes

Rehearse the full sequence twice without recording — the handwriting beats are the only
part that cannot be fixed in post. Then record **three to five full takes**, letting
mistakes run (restart the beat on the same board rather than the take; the edit keeps the
best pass of each beat). Name recordings `take-01.mkv`, `take-02.mkv`, …

## Edit

- Jump cuts only. Every cut lands on the same white board with more content — no
transitions needed.
- Beat 1 stays 1× always. Dialogs (capture picker) and any dead pen travel get cut or
sped to 4×. Keep at least one second of stillness after beats 3 and 6.
- Keystroke callouts: overlay `overlays/ctrl-v.png` on the paste and `overlays/f6.png` on
the format in beat 2 — transparent PNGs, drawn large; scale to taste (a keycap about
1/10 of the frame height reads well), bottom-right corner, quick fade in and out.
- No voiceover. Music optional, added at upload; the clip must work silent.
- Export master: **2560 × 1440, 60 fps, H.264 high bitrate** (or ProRes/DNxHR if the
editor offers it), audio track only if music was added.
| 0–6 s | Rests on D1 | `ink-diagram.mp4`: the pen draws three boxes and two connectors | SQLBI Whiteboard |
| 6–13 s | Pans right to D2 | `liveview-powerbi.mp4`: View, LiveView, the Power BI window lands, Freeze, highlighter marks | Capture a running application onto the board and draw on it. |
| 13–20 s | Pans down to D3 | `dax-f6.mp4`: a one-line measure is pasted, F6 formats it | Paste DAX or SQL. F6 formats it. |
| 20–27 s | Pans left to D4, past D7 | `shapes-connectors.mp4`: a rectangle, a diamond, a connector; the rectangle moves and the arrow follows | Shapes, sticky notes, and arrows that follow. |
| 27–34 s | Pans up to D5 | `frames-export.mp4`: Export to PowerPoint and the deck open in PowerPoint | Frames become slides. Export the deck. |
| 34–41 s | Pans down and left to D6 | `open-pptx-zip.mp4`: a .pptx dropped on the board becomes its slides | Open a PowerPoint deck or a Microsoft Whiteboard export. |
| 41–47 s | Pulls back until the whole board is in frame | `full-board.mp4` at the centre of the board: a double-click fits the board in its window as the camera pulls back | One board. A file on your PC. |
| 47–50 s | The board gives way to the end card | | App icon, SQLBI Whiteboard, Free and open source, whiteboard.sqlbi.com |

The site rendition loops, so its end card fades to paper over the last 500 ms and the
restart cuts to the white board of the first frame. No rendition has a voice. The site
files are silent; the social files and the Store trailer carry the music bed.

The shorts take the LiveView, DAX, and shapes stations: 10 seconds on the station with
the capture at its own speed, and a 2-second end card.

## Publish

The site serves the clip itself (decision 19). From the 4K master, encode the two
`npm run render:film2-site` in `tools/films` renders the master
(`out/master/film2-16x9-master.mp4`) and runs these three commands, which write the two
renditions and the poster straight into `site/`; they deploy with the next site publish.

```bash
ffmpeg -y -i master.mp4 -vf "scale=2560:-2:flags=lanczos" -c:v libsvtav1 -crf 38 -preset 6 -movflags +faststart -an site/teaser-av1.mp4
ffmpeg -y -i master.mp4 -vf "scale=1920:-2:flags=lanczos,fade=t=out:st=49.5:d=0.5:color=0xF7F6F2" -c:v libsvtav1 -crf 46 -preset 6 -movflags +faststart -an site/teaser-av1.mp4
```

```bash
ffmpeg -y -i master.mp4 -vf "scale=1920:-2:flags=lanczos" -c:v libx264 -crf 22 -preset slow -pix_fmt yuv420p -movflags +faststart -an site/teaser-h264.mp4
ffmpeg -y -i master.mp4 -vf "scale=1920:-2:flags=lanczos,fade=t=out:st=49.5:d=0.5:color=0xF7F6F2" -c:v libx264 -crf 31 -preset veryslow -pix_fmt yuv420p -movflags +faststart -an site/teaser-h264.mp4
```

```bash
ffmpeg -y -ss 41.0 -i master.mp4 -frames:v 1 -vf "scale=1920:-2:flags=lanczos" -q:v 3 site/teaser-poster.jpg
ffmpeg -y -ss 46.0 -i master.mp4 -frames:v 1 -vf "scale=1920:-2:flags=lanczos" -q:v 3 site/teaser-poster.jpg
```

The `<video>` element in `site/index.html` lists the AV1 source first and H.264 as the
fallback; if a re-encode changes resolution, frame rate, or codec profile, update its
`codecs=` strings (read them with `ffprobe -show_entries stream=profile,level`). Keep
the master outside the repository, and reuse it for the Store trailer per
The CRFs are higher than those of the 42-second cut (38 and 22) because the camera never
stops, so every frame differs from the one before. At 46 and 31 each file is about
3.5 MB, the size of the 42-second cut, and the text of the captures and the captions stays
readable, at rest and in the pans.

The poster is the whole board at 46 seconds. The `<video>` element in `site/index.html`
lists the AV1 source first and H.264 as the fallback; if a re-encode changes resolution,
frame rate, or codec profile, update its `codecs=` strings (read them with
`ffprobe -show_entries stream=profile,level`). The Store trailer is
`out/film2-store-1920x1080.mp4` from `npm run render:film2-store`, for
`installer/msix/STORE-LISTING.md`.
8 changes: 4 additions & 4 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ <h1>The whiteboard for <span class="accent">teaching</span></h1>
blocked. Decision 19 in docs/decisions.md. -->
<video controls autoplay muted loop playsinline
poster="teaser-poster.jpg"
title="SQLBI Whiteboard, forty seconds">
<source src="teaser-av1.mp4" type='video/mp4; codecs="av01.0.12M.08"'>
<source src="teaser-h264.mp4" type='video/mp4; codecs="avc1.640032"'>
title="SQLBI Whiteboard, fifty seconds">
<source src="teaser-av1.mp4" type='video/mp4; codecs="av01.0.08M.08"'>
<source src="teaser-h264.mp4" type='video/mp4; codecs="avc1.640033"'>
</video>
</div>
<p class="launch-caption">Forty seconds on a board.</p>
<p class="launch-caption">Fifty seconds on one board.</p>
</section>
</div>
</header>
Expand Down
Binary file modified site/teaser-av1.mp4
Binary file not shown.
Binary file modified site/teaser-h264.mp4
Binary file not shown.
Binary file modified site/teaser-poster.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions tools/films/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Film 1, "Export before 16 October", is 30 seconds long. It tells people who use
Microsoft Whiteboard app with a personal account to export their boards before the app
retires on 16 October 2026, and shows SQLBI Whiteboard opening the exported ZIP.

Film 2, "One board", is 50 seconds long. One continuous camera move crosses a single
board on which seven captures sit as windows, each with its caption written below it,
and ends on the whole board and the end card. It is the teaser on the site and the Store
trailer, and three 12-second portrait shorts are cut from it. It names no date, so it
stays true after the campaign.

## License

Remotion is used under its free license, which applies to companies with up to three
Expand Down Expand Up @@ -59,6 +65,40 @@ come from one composition, whose layout in `src/layout.ts` follows the aspect ra

Nothing under `out` is committed.

### Film 2

```powershell
npm run stills:film2 # a still every 5 seconds of each composition, into out\stills
npm run render:film2-all # every rendition, into out, and the site files, into ..\..\site
```

One script renders each rendition: `render:film2-16x9`, `render:film2-1x1`,
`render:film2-4x5`, `render:film2-short-liveview`, `render:film2-short-dax`,
`render:film2-short-shapes`, `render:film2-store`, and `render:film2-site`. Each
composition is rendered once at its own size into `out\master`, not at twice its size,
because six of the captures are 1080p and a larger master would only enlarge them.

| File | Size | Where it goes | Safe area |
| --- | --- | --- | --- |
| `film2-16x9.mp4` | 1920 × 1080 | YouTube, LinkedIn, X | 96 px on every side |
| `film2-1x1.mp4` | 1080 × 1080 | LinkedIn feed, X, Instagram feed | 90 px |
| `film2-4x5.mp4` | 1080 × 1350 | Instagram feed, LinkedIn | 90 px |
| `film2-short-liveview.mp4` | 1080 × 1920 | Reels, TikTok, Shorts: the LiveView station | y 250 to 1550, x 60 to 930 |
| `film2-short-dax.mp4` | 1080 × 1920 | Reels, TikTok, Shorts: the DAX and F6 station | y 250 to 1550, x 60 to 930 |
| `film2-short-shapes.mp4` | 1080 × 1920 | Reels, TikTok, Shorts: the shapes station | y 250 to 1550, x 60 to 930 |
| `film2-store-1920x1080.mp4` | 1920 × 1080 | The Microsoft Store trailer | 96 px |
| `site/teaser-av1.mp4`, `site/teaser-h264.mp4` | 1920 × 1080 | The landing page, silent and looping | |
| `site/teaser-poster.jpg` | 1920 × 1080 | The landing page poster, the whole board at 46 s | |

The files in `out` are H.264 High, yuv420p, 30 fps, with the music bed in AAC at 128 kbps
and the index at the start of the file. The film is exactly 50 seconds long and each short
exactly 12, ending on a 2-second end card. The site files carry no audio, and their end
card fades to paper over the last half second so the loop restarts on the white board;
the commands are in [docs/teaser/shot-script.md](../../docs/teaser/shot-script.md).

`src/film2/board.ts` lays out the board for each shape, and `src/film2/cuts.ts` holds
the camera moves and the moment each capture plays, for the film and for each short.

## Captures

Record with OBS at 2560 × 1440 and 60 fps, with Windows display scaling at 150 % and the
Expand All @@ -78,6 +118,23 @@ with its name and what it will show.
it plays it, and where the numbered callouts sit. The callout positions are estimates, so
check them against the real captures in `npm run studio` and correct them there.

Film 2 uses seven more. They were recorded at 30 fps, `ink-diagram.mp4` at 3840 × 2160 and
the others at 1920 × 1080. Each sits on the board at 1920 × 1080 and the camera never
zooms past that, so no capture is shown larger than it was recorded.

| # | File | Length | What it shows |
| --- | --- | --- | --- |
| D1 | `ink-diagram.mp4` | 8.6 s | The pen draws three boxes and two connectors, with pressure visible |
| D2 | `liveview-powerbi.mp4` | 9.6 s | View, LiveView, the Power BI window on the board, Freeze, highlighter marks |
| D3 | `dax-f6.mp4` | 9.2 s | A long one-line measure is pasted; F6 formats it |
| D4 | `shapes-connectors.mp4` | 10.1 s | Insert: a rectangle, a diamond, and a connector; the rectangle moves and the arrow follows |
| D5 | `frames-export.mp4` | 11.0 s | A board of slides, Export to PowerPoint, and the deck open in PowerPoint |
| D6 | `open-pptx-zip.mp4` | 8.5 s | A .pptx dropped on the board becomes its slides |
| D7 | `full-board.mp4` | 6.4 s | A double-click on empty canvas fits the whole board in the window |

The lengths are those of the video tracks. `src/film2/captures.ts` records what happens
when in each capture, read from frame sheets.

## Voice and music

The voice is `audio\film1-vo.mp3`. [audio/film1-vo.txt](audio/film1-vo.txt) holds the
Expand Down Expand Up @@ -106,3 +163,15 @@ between two phrases, gives the wrong count; then set `src/film1/beats.json` by h
The music bed, when there is one, is `audio\film1-bed.mp3`. It plays 22 dB below the voice
and fades out under the end card. Without it the film has no music. The MP3 files are not
committed.

Film 2 has no voice. Its bed is `audio\film2-bed.mp3`, generated 60 seconds long because
the generator fades out over its last 10 seconds, so the first 50 hold their level until
the end card:

```powershell
node scripts\elevenlabs.mjs music --film film2 --seconds 60 --prompt "Calm, minimal instrumental. Soft felt piano and a warm, slowly evolving pad, unhurried and steady, gently optimistic, with an even level throughout. No drums, no percussion, no vocals. Suitable under a quiet product film with no narration. Ends with a gentle fade."
```

The render mixes it with ffmpeg at a level that averages −20 dB: from its start under
the film, fading out over the end card, and from 14 seconds in under each short. Without
it the files in `out` carry a silent track.
12 changes: 11 additions & 1 deletion tools/films/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@
"render:4x5": "node scripts/render.mjs 4x5",
"render:9x16": "node scripts/render.mjs 9x16",
"render:poster": "node scripts/render.mjs poster",
"render:all": "node scripts/render.mjs all"
"render:all": "node scripts/render.mjs all",
"stills:film2": "node scripts/stills.mjs film2",
"render:film2-16x9": "node scripts/render.mjs film2 16x9",
"render:film2-1x1": "node scripts/render.mjs film2 1x1",
"render:film2-4x5": "node scripts/render.mjs film2 4x5",
"render:film2-short-liveview": "node scripts/render.mjs film2 short-liveview",
"render:film2-short-dax": "node scripts/render.mjs film2 short-dax",
"render:film2-short-shapes": "node scripts/render.mjs film2 short-shapes",
"render:film2-store": "node scripts/render.mjs film2 store",
"render:film2-site": "node scripts/render.mjs film2 site",
"render:film2-all": "node scripts/render.mjs film2 all"
},
"dependencies": {
"@remotion/bundler": "4.0.529",
Expand Down
10 changes: 10 additions & 0 deletions tools/films/scripts/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export const ffmpeg = (args) => {
if (r.status !== 0) throw new Error(`ffmpeg failed: ${args.join(' ')}`);
};

// The mean volume in dB of an audio file after an ffmpeg audio filter, read with
// volumedetect.
export const meanVolume = (file, filter) => {
const exe = process.env.FFMPEG ?? 'ffmpeg';
const r = spawnSync(exe, ['-hide_banner', '-i', file, '-af', `${filter},volumedetect`, '-f', 'null', '-'], {encoding: 'utf8'});
const m = /mean_volume: (-?[\d.]+) dB/.exec(r.stderr ?? '');
if (!m) throw new Error(`volumedetect found no level in ${file}`);
return Number(m[1]);
};

export const progressLogger = (label) => {
let last = -1;
return ({progress}) => {
Expand Down
Loading
Loading