# Create Apple Notes Video Ad

> Produce a 9:16 social-native ad recreating the iPhone Apple Notes typing experience — the note begins with 1–2 visible lines, then progressively types additional paragraphs character-by-character with per-keystroke iPhone keyboard SFX. One continuous Playwright recording, music bed underneath, brand end card with CTA. Assembled to a master MP4 plus a 9:16 social export.

- Skill: `criptogus/create-apple-notes-video-ad` (Agent Skill, multi-file: 18 files)
- Install (CLI): `npx skillmds@latest add criptogus/create-apple-notes-video-ad`
- Raw SKILL.md: https://api.skillmd.com/api/skills/criptogus/create-apple-notes-video-ad/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: criptogus (https://skillmd.com/u/criptogus)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/criptogus/create-apple-notes-video-ad

---


# create-apple-notes-video-ad

Use this skill when an ad concept calls for an authentic Apple Notes typing reveal: a note appears with a pre-existing first line, then the user "types" the rest in front of you (more lines, more thoughts, a CTA) — punctuated by iPhone-keyboard ticks. Finishes on a brand end card.

## Purpose

Produce a 9:16 social-native ad that recreates the iPhone Apple Notes typing experience: the note opens with 1–2 pre-existing lines, then progressively types additional paragraphs character-by-character with per-keystroke iPhone keyboard SFX, a music bed underneath, and a brand end card with CTA. The whole typing portion is captured as **one continuous Playwright recording** (no per-paragraph reloads, no scene-by-scene concatenation) so there are no micro-flickers — the only cut is a 300ms crossfade into the static end card.

The driver in `scripts/record-master.template.js` renders the Apple Notes screen once via the `create-apple-notes-mockup` atom (`generateHtml()`), tags the typed paragraphs, then walks a deterministic seeded timing table on `requestAnimationFrame`, appending one character at a time to a growing `span.typed` while the yellow cursor follows and the note auto-scrolls to stay above the keyboard. The same table produces a keystroke-aligned SFX cue list (`master-typing.sfx.json`), and `scripts/stitch.template.sh` mixes the music bed + per-keystroke SFX (`kb-tick`/`kb-space`/`kb-return`), crossfades typing → end card, and exports a master MP4 (1180×2100) plus a clean 9:16 (1080×1920) social cut.

Reach for this when the punchline is the **note itself** — a written-out promo code, a confession, a checklist, a calm "memo to self". For a back-and-forth chat reveal, use `create-imessage-video-ad` instead.

## Reference build

Sample run lives at `tests/output/sample-run/` (generated by `bash tests/run-all.sh`). It uses the canonical script "Hello / This is a screen recording of me typing on apple notes app." with three typed paragraphs landing on `I'm not sure how th|` mid-word, ending on a "FREEDEMO" end card. 12s total.

## When to use

- Reveals where the punchline is the **note itself** — a written-out promo code, a confession, a checklist, a thought spiral, a "memo to self".
- Brands whose voice is calm/considered/personal (vs the rapid-fire iMessage angle).
- Sustainable-pace ads — typing typically averages **~80–140ms per character**, which gives copy time to land.

If your concept is a back-and-forth chat reveal, use `create-imessage-video-ad` instead.

## Composed Atoms

- `create-apple-notes-mockup` — renders the light-mode Apple Notes HTML (status bar, title, body paragraphs, yellow cursor, autocorrect underline, smart quotes, floating toolbar). `record-master.template.js` calls its `generateHtml()` once with `show_keyboard: true`, then the molecule drives it character-by-character via Playwright.
- `render-ios-keyboard` — supplies the iOS QWERTY keyboard chrome (suggestion bar + alpha/format rows) the Apple Notes screen shows during typing. It is static-only; the molecule owns the layout — the recorder anchors the keyboard at the shortened `VIEW_H = 2100` canvas (`bottom: 0`) and the auto-scroll math targets the keyboard's top edge (`.kbd-format`/`.kbd`) to keep the active cursor visible.
- `stitch-videos-ffmpeg` — final concat + crossfade + audio mux. `stitch.template.sh` is adapted from this atom: it xfades typing → end card (300ms), layers the per-keystroke SFX cues from `master-typing.sfx.json`, mixes the music bed, and exports the master plus the 9:16 social cut.
- (optional) ElevenLabs / Suno music generation OR a music asset already in the repo (the `music_bed_path` in `note.json`).

## Inputs

1. **`brief`** (required) — what the note says, where it pauses, what the brand sting at the end is.
2. **`output_folder`** — usually `<brand>/ads/video-NN-apple-notes-<slug>/`.
3. **`pre_typed_body`** — list of paragraphs already on screen at t=0 (1–2 lines is the typical sweet spot).
4. **`typed_body`** — list of paragraphs that get typed during the video. Each has `text`, `type_seconds`, `pre_pause_seconds`, optional `autocorrect_underline[]`.
5. **`end_card`** — `{wordmark, code, tagline, background_color, ink_color}`.

The master `note.json` lives at `<ad>/notes/note.json`. Schema is in `examples/full-note.example.json`.

## Critical knowledge — read before producing your first ad

These are mistakes that have already been made and fixed. Do not re-introduce them.

### 0. Use the real brand wordmark on the end card, never styled text

Drop the official SVG at `assets/brand-logo.svg` and `scripts/render-end-card.template.js` injects it via the `<!--{{BRAND_LOGO_SVG}}-->` placeholder. CSS-styled lookalikes read as amateur even when the typeface is close — same lesson the imessage molecule has under its own #0.

### 1. Use real iPhone-keyboard click SFX, not generic typing sounds

**The "Apple Notes feel" is 60% the SFX — but ONLY with real iPhone clicks.** Generic
mechanical-keyboard clack ruins it, and so do the bundled placeholders: the files in
`assets/sfx/{kb-tick,kb-space,kb-return}.mp3` are short noise-burst stand-ins for LOCAL PREVIEW
ONLY. **A final delivery ships either real iPhone clicks (source them per
`assets/sfx/README.md`) or silent (`--no-sfx`) — never the stand-ins, and never sounds you
synthesized yourself** (fabricated ffmpeg noise bursts shipped once, 2026-06-11 Alitu, and read
as static). When the real clicks can't be sourced (offline, LFS stubs), silent is the correct
delivery, stated to the user.

After swapping, **do NOT re-run the recorder** — only re-run `bash edits/stitch.sh`. The cue list (`clips/master-typing.sfx.json`) only references files by name; the new MP3s are picked up automatically.

### 2. Record the entire note as ONE continuous Playwright session

Same architecture as `create-imessage-video-ad` — no per-paragraph reloads, no scene-by-scene concatenation. The driver walks a deterministic per-character timing table and appends one character at a time to a growing `.note-paragraph > span.typed`. The yellow cursor follows.

### 3. Pop-pending must remove from layout, not just hide

If a typed paragraph occupies its full height at t=0 (zero text inside a fixed-height `<p>`), the note doesn't appear to "grow" as the user types. The driver leaves each typed paragraph at `min-height: 1.38em` so it always occupies one line — that mirrors how an empty paragraph behaves in real Apple Notes when the user presses Return.

### 4. Per-character timing must be deterministic (seeded RNG)

The driver's `buildTimingTable()` uses an LCG seeded at `0xa5b1c2d3` so the same `note.json` always produces identical typing rhythm — which means the SFX cue list lines up exactly with the visual keystrokes. If you swap the seed (or use `Math.random()` directly), the audio will drift away from the visual after a few re-runs.

### 5. End card stays static — no Ken-Burns, no zoom-pan

Same rule as the imessage molecule. Render the end-card PNG once, ffmpeg it to a fixed-frame MP4 via the bundled `render-end-card.template.js`. Crossfade typing → end card with a **300ms** `xfade`.

### 6. ffmpeg `amix` divides by N inputs by default

If you mix the music + the silent base + ~80 keystroke SFX, every input gets divided by 82 unless you pass `normalize=0`. Audio sounds mysteriously quiet. Always:

```
amix=inputs=N:duration=first:dropout_transition=0:normalize=0
```

Then take the mix through `volume=2.2,alimiter=limit=0.95`. Already in `scripts/stitch.template.sh`.

### 7. Music bed: ambient/lofi, 60Hz highpass, ~-11dB

Keep the bed unobtrusive: `volume=0.28` plus `highpass=f=60` to clear room for the typing ticks. Fade out 1.5s before the end so the brand sting lands in (relative) silence. Same envelope as the imessage molecule.

### 8. Pacing: typing is not uniform

Real human typing has rhythm — fast strings, slow strings, pauses on punctuation. The seeded LCG hands a 35% jitter to each character (`step = avg * (0.65..1.35)`). Most ads benefit from a **~0.5–0.8s pause before each new paragraph** — set via `typed_body[].pre_pause_seconds`. That breathing room is what makes the typing feel intentional and not robotic.

### 9. Smart punctuation is applied by the atom, not the driver

The atom's `smartQuotes()` converts straight `'` `"` `...` to curly `'` `"` `…` before HTML escaping. The driver inherits this for free — typing `I'm` yields `I'm` in the output. If you skip the atom and write raw HTML, you'll lose this.

### 10. Autocorrect underlines fire per-paragraph, on the return event

The driver fires autocorrect underlines on the **return event for each paragraph** — right when that paragraph finishes typing, before the cursor jumps to the next paragraph. This matches iOS behavior: the spelling layer runs at end-of-word/end-of-line, not mid-character. The end-of-run forEach still handles the last paragraph (which has no return event).

Per-paragraph words go in `typed_body[].autocorrect_underline` (case-sensitive list). The driver looks them up in `window.__underlineMap` keyed by paragraph pid and calls `applyUnderline()` from the return handler.

**Anti-pattern:** moving underline application to a single end-of-run loop. The operator sees the typed word complete without any underline for the full remaining seconds of the typing animation — reads as a bug. The per-event firing was a deliberate fix during the Bioma run; do not refactor back.

### 11. 9:16 delivery requires a 9:16 source canvas — never crop

The iPhone's real screen is 19.5:9 (1180×2556). Social-native delivery is 9:16 (1080×1920). Every Y-crop value forces a destructive choice: lose the status bar, or lose the bottom of the keyboard, or split the loss across both. The fix is to record at 9:16 natively: `VIEW_H = 2100` in `record-master.template.js` plus a styleOverride that injects `html, body, .screen { height: ${VIEW_H}px !important; }` to override the atom's hardcoded 2556. The keyboard's `position: absolute; bottom: 0` then anchors to the shorter canvas, moving the entire keyboard up ~460px naturally. The `--export-9x16` step becomes a clean linear scale to 1080×1920 — no crop, no Y-offset to guess at.

The atom's static screenshots (consumed by `apple-notes-skill-build.html`) still render at the real iPhone 1180×2556 — only the molecule's recording uses the shortened canvas.

**When changing `VIEW_H`, all three files must move together** (the constant drifts otherwise and the xfade between typing video and end card fails silently):

1. `record-master.template.js` — `const VIEW_H = 2100`
2. `render-end-card.template.js` — `const VIEW_H = 2100`
3. `end-card.template.html` — `html, body { width: 1180px; height: 2100px; }`

## Workflow

### State 0 — Lock the script

Write the full text of every paragraph (pre-typed + typed), the per-paragraph timing, and the CTA copy. Most "the ad is fine but feels off" feedback comes from the script, not the production — get it right here.

A starter timing model:
- `pre_typed_body` paragraph: ~3.0–4.5s on screen before typing starts
- Each typed paragraph: 1.5–2.2s typing time + 0.6–1.0s pre-pause
- Post-hold: 1.0s after the last character lands before crossfade

### State 1 — Build `notes/note.json`

Match the schema in `examples/full-note.example.json`. The atom's note JSON schema is in `create-apple-notes-mockup/SKILL.md` — every field there is honored here, plus the molecule-specific `pre_typed_body / typed_body / post_hold_seconds / end_card / music_bed_path` fields.

### State 2 — Wire up the ad folder

Standard 5-folder layout. Inside the ad folder:

```
<ad>/
  notes/note.json
  assets/brand-logo.svg          # optional but recommended
  audio/sfx/kb-tick.mp3          # copied from this skill's assets/sfx/
  audio/sfx/kb-space.mp3
  audio/sfx/kb-return.mp3
  audio/music-bed.mp3            # optional
  clips/record-master.js         # adapted from scripts/record-master.template.js
  clips/render-end-card.js       # adapted from scripts/render-end-card.template.js
  clips/end-card.template.html   # adapted from scripts/end-card.template.html
  edits/stitch.sh                # adapted from scripts/stitch.template.sh
```

### State 3 — Record + render + stitch

```bash
# 1) Record the typing as one continuous video.
NODE_PATH=<repo>/create-apple-notes-mockup/node_modules \
  node clips/record-master.js

# 2) Render the end card.
NODE_PATH=<repo>/create-apple-notes-mockup/node_modules \
  node clips/render-end-card.js

# 3) Stitch.
bash edits/stitch.sh                  # produces edits/master-final.mp4 at 1180×2556
bash edits/stitch.sh --export-9x16    # ALSO produces meta-upload/master-9x16-1080.mp4
```

### State 4 — Review

Open the master in a player and watch start to finish. Re-time the typing by editing `notes/note.json` (the only file you'll touch for iterations) and re-running step 1 + step 3 — step 2 only needs to re-run if `end_card` changes.

### State 5 — Export variants

`--export-9x16` scales the native 1180×2556 down to 1080×2340 and center-crops to 1080×1920, retaining the keyboard and the body. Adjust the vertical crop with `EXPORT_Y_CROP=<px>` if your spec needs different framing (default `260`, which keeps keyboard + body and trims status bar).

## Output

```
<ad>/
  notes/note.json
  clips/
    master-typing.mp4            # 1180×2556, no audio, ~5–15s typically
    master-typing.sfx.json       # deterministic cue list
    end-card.png
    end-card.mp4
  edits/
    master-final.mp4             # 1180×2556 typing + xfade + end card, h.264 + AAC
  meta-upload/
    master-9x16-1080.mp4         # 1080×1920 social export
```

## Quality Checks

Before declaring the ad shippable:

- [ ] No micro-flicker between paragraphs — there should be NO scene-by-scene boundaries inside the typing portion. Only one cut: typing → end card (crossfade).
- [ ] Every character in `typed_body` shows up on screen exactly once.
- [ ] Autocorrect underlines appear on the right words after typing finishes, case-sensitive.
- [ ] Smart curly quotes (`'`, `"`, `…`) render correctly — never `'`, `"`, `...`.
- [ ] Cursor is yellow `#FFCC00`, sits at the end of the actively-typing paragraph, jumps when a new paragraph begins.
- [ ] No Dynamic Island at the top.
- [ ] Audio peaks at 0dB (`ffmpeg -af volumedetect`); SFX audible but not dominating any music bed.
- [ ] End card is static, crossfade is 300ms.

## Failure Modes

| Symptom | Cause | Fix |
|---|---|---|
| `cannot locate create-apple-notes-mockup/generate.js` | Ad folder is not under a tree that contains `skills/` within 8 levels up | Edit `resolveAtomGenerate()` in `record-master.js` to hardcode the path |
| Typing animation runs but no characters appear | The driver couldn't find `p.note-paragraph[data-typed-pid]` — usually means `tagTypedParagraphs()` failed because the atom's HTML changed | Update the regex in `tagTypedParagraphs()` to match the atom's current paragraph markup |
| SFX cues fire but no sound comes out of the final mp4 | LFS pointer files instead of audio at `assets/sfx/*` | `git lfs pull --include="skills/molecules/create-apple-notes-video-ad/assets/sfx/*"`. The goose-video CLI cache clones WITHOUT LFS, so stubs are guaranteed there — re-source per `assets/sfx/README.md` or ship `--no-sfx` |
| Keyboard ticks sound like static / noise bursts | Shipped the bundled placeholder stand-ins, or synthesized substitute clicks | Placeholders are preview-only. Real iPhone clicks (`assets/sfx/README.md`) or `--no-sfx` silent — NEVER fabricate stand-in audio (live incident 2026-06-11, Alitu) |
| Final video is silent | `amix` divided by N inputs | Confirm `normalize=0` is in the `amix=` filter |
| 9:16 export crops out the body | `EXPORT_Y_CROP` too aggressive | Lower the value (e.g. `EXPORT_Y_CROP=180 bash edits/stitch.sh --export-9x16`) |
| Cursor sits in the wrong paragraph at start | First typed paragraph's `pre_pause_seconds` is 0 | Set `pre_pause_seconds >= 0.4` so the cursor blink is legible before typing kicks in |
| Music drowns out the ticks | Music volume too high | Lower `volume=0.28` to `0.22` in the stitch script's music filter |
| Typing feels robotic / metronome-like | Seed jitter not applied | Confirm `buildTimingTable()` is using the seeded RNG, not `Math.random()` |
| End card's wordmark renders as styled text instead of brand SVG | `assets/brand-logo.svg` is missing | Drop the official SVG there; `render-end-card.js` injects it automatically |

## Decision Rules

- Use this molecule when the reveal **is the note itself** typed in front of the viewer (a written-out promo code, a confession, a checklist, a memo-to-self). For a back-and-forth chat reveal, use `create-imessage-video-ad`. For a single static Apple Notes screenshot (no typing animation), call the `create-apple-notes-mockup` atom directly.
- Pick a calm/considered/personal brand voice for this format; the typing pace (~80–140ms/char) is deliberately slower than the rapid-fire iMessage angle, so copy has time to land.
- Keep the typing portion as **one continuous Playwright recording** — never record per-paragraph and concat. The only cut in the whole ad is the 300ms `xfade` from typing → end card.
- Per-character timing **must** come from the seeded LCG in `buildTimingTable()` (seed `0xa5b1c2d3`), never `Math.random()` — otherwise the SFX cue list drifts away from the visible keystrokes across re-runs.
- Use real iPhone-keyboard click SFX (`assets/sfx/kb-{tick,space,return}.mp3`), never generic mechanical-keyboard sounds — the "Apple Notes feel" is ~60% the SFX. After swapping SFX, re-run **only** `bash edits/stitch.sh`; the cue list references files by name, so don't re-run the recorder.
- Record at a 9:16-native canvas (`VIEW_H = 2100`), never crop a 19.5:9 iPhone screen — and move all three files (`record-master.template.js`, `render-end-card.template.js`, `end-card.template.html`) together whenever `VIEW_H` changes, or the typing↔end-card xfade fails silently.
- Use the real brand wordmark SVG on the end card (`assets/brand-logo.svg`, injected via `<!--{{BRAND_LOGO_SVG}}-->`), never CSS-styled lookalike text.
- Keep the end card **static** (no Ken-Burns/zoom-pan); let the music bed (ambient/lofi, `highpass=f=60`, `volume=0.28`, fade out 1.5s before the end) leave room for the keyboard ticks, and keep `normalize=0` on the `amix` so the mix isn't divided by N inputs.
- Fire autocorrect underlines per-paragraph on each return event (the last paragraph is handled by the end-of-run pass) — do not refactor them into a single end-of-run loop.
- Give each new typed paragraph ~0.5–0.8s of `pre_pause_seconds` so the typing reads as intentional, not robotic; set the first typed paragraph's `pre_pause_seconds >= 0.4` so the cursor blink is legible before typing starts.
- Iterate on `notes/note.json` only — re-run record (step 1) + stitch (step 3); re-run the end-card render (step 2) only when `end_card` changes.

## Reference example

`examples/full-note.example.json` is the canonical 4-paragraph "screen recording" demo used by the test rig. Copy it as your starting `notes/note.json` and edit text + timings to fit your brief.

