render-chatgpt-chat
The free renderer for the chatgpt-chat video ad format — the "I just asked
ChatGPT…" creative, where someone asks ChatGPT a question and the streamed
assistant answer is the punchline (the brand surfacing as the natural response).
Deterministic Playwright + ffmpeg assembly; no generative video of the UI, so the
bubble text and streamed answer stay pixel-crisp.
This is the ChatGPT sibling of render-imessage-chat. Reach for this one when
ChatGPT is the more credible host for the answer; reach for iMessage when the
punchline is a peer's reaction in a DM. The template recipe (DB) supplies the
per-brand thread + timeline + end_card config and gates the paid music call
(music bed → create-music-elevenlabs) to its own capability.
What it renders
One continuous take — never scene-by-scene (every reload flickers):
- User types in the composer with the iOS keyboard up (
composer-type).
- Send-tap is ONE beat — the user bubble pops, the keyboard slides down, and
the header right-cluster swaps (
personPlus/dottedCircle → edit/more) all on
the same t. Never sequence them across frames.
- One gray loading dot holds ~500ms (never three — three reads as iMessage
typing, wrong app), silently (no SFX on the dot).
- The assistant answer streams in word-by-word (
stream-words, ~7 wps) with a
soft opacity ramp; the conversation auto-scrolls to keep it in view.
- Crossfade to a designed end card (wordmark + ⭐ proof row + trust trio + CTA
pill) and mux a ducked music bed → master MP4.
The chat records at the ChatGPT-native ~9:19.5 (default 750×1624) to match a real
iPhone screen recording. Never stretch the chat to a different aspect ratio — the
end card is scaled-to-fit + padded to the chat's dimensions in stitch, so the chat
is never touched.
Run
cd scripts && npm install # once — installs Playwright
npx playwright install chromium # once
node record-chat.js --config config.json --out-dir <work> # → master-chat.mp4 + .sfx.json
node render-end-card.js --config config.json --out-dir <work> # → scene-end-endcard.mp4
bash stitch.sh --chat <work>/master-chat.mp4 --end <work>/scene-end-endcard.mp4 \
--sfx <work>/master-chat.sfx.json --out <work>/master-final.mp4 \
--pad-color "#ffffff" [--music <work>/music-bed.mp3] [--also-1x1]
record-chat.js — reads config.json (thread + timeline + geometry),
renders the bundled create-chatgpt-mockup HTML once with every message
pending, walks the timeline on requestAnimationFrame inside the page, records
it as one continuous MP4, and emits the deterministic SFX cue list.
render-end-card.js — fills end-card.template.html from config.end_card
(wordmark/logo_svg, stars, proof, trust trio, CTA, colors) → still MP4. This is
the SAME generic end card as render-imessage-chat (copied verbatim).
stitch.sh — normalizes the end card to the chat's dimensions, crossfades
chat → end card, layers the subliminal ChatGPT SFX, optionally ducks a music bed
under it, and optionally derives a 1:1 crop. All FREE ffmpeg. Pass
--pad-color = end_card.bg (default #ffffff, ChatGPT light mode) so the pad
under the end card is seamless.
The chat body: bundled create-chatgpt-mockup
The ChatGPT chat HTML comes from create-chatgpt-mockup (its generate.js +
templates/ produce the light-mode ChatGPT iOS HTML — status bar, header, message
rows, streaming word-spans, composer, and the inline iOS keyboard). Those files are
bundled into scripts/mockup/ so this capability renders the chat body
standalone — no sibling fetch of create-chatgpt-mockup is required. record-chat.js
does require('./mockup/generate.js').
The keyboard is inlined by the mockup (renderKeyboard) — no separate keyboard atom.
Timeline events (consumed by record-chat.js)
| Kind |
Meaning |
composer-type |
{ text, dur_sec } — type into the composer. SFX = one key-tap per word. |
composer-clear |
Wipe the composer instantly (fire at send-tap). |
keyboard-show / keyboard-hide |
Slide the iOS keyboard up / down. |
send-tap |
Pulse the send button. SFX = send-tap. |
pop |
{ target: <msg-id> } — reveal a message row. |
header-swap |
{ value: "alt" } — swap the header right-cluster. |
loading-dot-show / loading-dot-hide |
{ target: <dot-id> } — the single gray dot. |
send-state |
{ value: "streaming"|"active" } — composer send-button state. |
stream-words |
{ target, dur_sec, wps } — reveal the assistant answer word-by-word. SFX = stream-tick every 12 words + response-done at the end. |
scroll-to |
{ target, dur_ms } — smooth-scroll a row into view. |
See scripts/config.example.json for the canonical thread + timeline (the "one
beat" send-tap and the streamed list answer are both wired there).
Contract
- FREE assembly: Playwright record + ffmpeg composite/mux + the bundled SFX. No
AI-rendered text — the bubbles, the streamed answer, and the end-card copy are all
real HTML/PIL, never invented by a model.
- The recipe (DB) supplies the per-brand config: the
thread (light-mode ChatGPT,
assistant message set stream: true), the timeline, the end_card (prefer a
real logo_svg wordmark), and an optional music bed.
- SFX are subliminal by design (ChatGPT has no native chime): key-tap -28dB,
send-tap -20dB, stream-tick -32dB, response-done -22dB, and never a cue on the
loading dot. Set
"sfx": false in the config to ship the chat silent.
Gaps / routing notes
- Music bed is an input, not generated here — the recipe gates it to
create-music-elevenlabs (paid, proxy-routed, billed to the Ads agent) and passes
the file into stitch.sh --music.
- Bundled SFX are synthesized stand-ins. The original four wavs
(
key-tap/send-tap/stream-tick/response-done) were lost from Git LFS (the
objects 404 on the server), so assets/sfx/*.wav are freshly synthesized
subliminal clicks/ticks. They work as-is; swap in real wavs (same filenames) for
tuned SFX.
- Portability: everything runs from the fetched
/tmp/gooseworks-scripts/render-chatgpt-chat/scripts/… — the chatgpt-mockup
generator + templates are bundled under scripts/mockup/, and the generic end
card is bundled under scripts/. No /Users/… or repo-relative paths, and no
required sibling fetch.
- Requires ffmpeg/ffprobe on PATH and Playwright Chromium (
npx playwright install chromium).
Self-QC (per project rule — always /watch the master)
- Keyboard is up the whole time the user types, and slides down only on the
send-tap beat (never visible while the answer streams).
- Send-tap is one beat: user bubble + keyboard-down + header-swap on the same frame.
- Exactly one gray loading dot for ~500ms (not three), and no SFX on the dot.
- The answer streams word-by-word, left-to-right / top-to-bottom, not all-at-once.
- No OpenAI spiral logo above any assistant title (the spiral is empty-state only).
- No micro-flicker / scene cuts; the end-card pad color matches
end_card.bg.
1---2name: render-chatgpt-chat3description: Assemble a ChatGPT chat-reveal video ad from a thread + timeline JSON — one continuous Playwright recording of a ChatGPT mobile chat (user types with the iOS keyboard up → taps send → keyboard slides down + header cluster swaps in one beat → one gray loading dot → the assistant answer streams in word-by-word) crossfaded into a designed end card, with subliminal ChatGPT SFX and an optional ducked music bed. FREE assembly (Playwright + ffmpeg); the recipe supplies the per-brand thread + timeline + end-card config and gates the paid music call to its own capability. The ChatGPT sibling of render-imessage-chat. Use for the chatgpt-chat format.4---5
6# render-chatgpt-chat
7
8The free renderer for the **chatgpt-chat** video ad format — the "I just asked
9ChatGPT…" creative, where someone asks ChatGPT a question and the *streamed
10assistant answer is the punchline* (the brand surfacing as the natural response).
11Deterministic Playwright + ffmpeg assembly; no generative video of the UI, so the
12bubble text and streamed answer stay pixel-crisp.
13
14This is the **ChatGPT sibling of `render-imessage-chat`**. Reach for this one when
15ChatGPT is the more credible host for the answer; reach for iMessage when the
16punchline is a peer's reaction in a DM. The template recipe (DB) supplies the
17per-brand `thread` + `timeline` + `end_card` config and gates the paid music call
18(music bed → `create-music-elevenlabs`) to its own capability.
19
20## What it renders
21
22One continuous take — never scene-by-scene (every reload flickers):
23
241. **User types** in the composer with the iOS keyboard up (`composer-type`).
252. **Send-tap is ONE beat** — the user bubble pops, the keyboard slides down, and
26 the header right-cluster swaps (`personPlus/dottedCircle` → `edit/more`) all on
27 the same `t`. Never sequence them across frames.
283. **One gray loading dot** holds ~500ms (never three — three reads as iMessage
29 typing, wrong app), silently (no SFX on the dot).
304. **The assistant answer streams in word-by-word** (`stream-words`, ~7 wps) with a
31 soft opacity ramp; the conversation auto-scrolls to keep it in view.
325. **Crossfade to a designed end card** (wordmark + ⭐ proof row + trust trio + CTA
33 pill) and **mux a ducked music bed** → master MP4.
34
35The chat records at the ChatGPT-native **~9:19.5 (default 750×1624)** to match a real
36iPhone screen recording. **Never stretch the chat to a different aspect ratio** — the
37end card is scaled-to-fit + padded to the chat's dimensions in stitch, so the chat
38is never touched.
39
40## Run
41
42```bash
43cd scripts && npm install # once — installs Playwright
44npx playwright install chromium # once
45node record-chat.js --config config.json --out-dir <work> # → master-chat.mp4 + .sfx.json
46node render-end-card.js --config config.json --out-dir <work> # → scene-end-endcard.mp4
47bash stitch.sh --chat <work>/master-chat.mp4 --end <work>/scene-end-endcard.mp4 \
48 --sfx <work>/master-chat.sfx.json --out <work>/master-final.mp4 \
49 --pad-color "#ffffff" [--music <work>/music-bed.mp3] [--also-1x1]
50```
51
521. **`record-chat.js`** — reads `config.json` (`thread` + `timeline` + geometry),
53 renders the bundled `create-chatgpt-mockup` HTML once with every message
54 pending, walks the timeline on `requestAnimationFrame` inside the page, records
55 it as one continuous MP4, and emits the deterministic SFX cue list.
562. **`render-end-card.js`** — fills `end-card.template.html` from `config.end_card`
57 (wordmark/`logo_svg`, stars, proof, trust trio, CTA, colors) → still MP4. This is
58 the SAME generic end card as `render-imessage-chat` (copied verbatim).
593. **`stitch.sh`** — normalizes the end card to the chat's dimensions, crossfades
60 chat → end card, layers the subliminal ChatGPT SFX, optionally ducks a music bed
61 under it, and optionally derives a 1:1 crop. All FREE ffmpeg. Pass
62 `--pad-color` = `end_card.bg` (default `#ffffff`, ChatGPT light mode) so the pad
63 under the end card is seamless.
64
65## The chat body: bundled create-chatgpt-mockup
66
67The ChatGPT chat HTML comes from **`create-chatgpt-mockup`** (its `generate.js` +
68`templates/` produce the light-mode ChatGPT iOS HTML — status bar, header, message
69rows, streaming word-spans, composer, and the inline iOS keyboard). Those files are
70**bundled into `scripts/mockup/`** so this capability renders the chat body
71**standalone** — no sibling fetch of `create-chatgpt-mockup` is required. `record-chat.js`
72does `require('./mockup/generate.js')`.
73
74The keyboard is inlined by the mockup (`renderKeyboard`) — no separate keyboard atom.
75
76## Timeline events (consumed by record-chat.js)
77
78| Kind | Meaning |
79|---|---|
80| `composer-type` | `{ text, dur_sec }` — type into the composer. SFX = one key-tap per word. |
81| `composer-clear` | Wipe the composer instantly (fire at send-tap). |
82| `keyboard-show` / `keyboard-hide` | Slide the iOS keyboard up / down. |
83| `send-tap` | Pulse the send button. SFX = send-tap. |
84| `pop` | `{ target: <msg-id> }` — reveal a message row. |
85| `header-swap` | `{ value: "alt" }` — swap the header right-cluster. |
86| `loading-dot-show` / `loading-dot-hide` | `{ target: <dot-id> }` — the single gray dot. |
87| `send-state` | `{ value: "streaming"\|"active" }` — composer send-button state. |
88| `stream-words` | `{ target, dur_sec, wps }` — reveal the assistant answer word-by-word. SFX = stream-tick every 12 words + response-done at the end. |
89| `scroll-to` | `{ target, dur_ms }` — smooth-scroll a row into view. |
90
91See `scripts/config.example.json` for the canonical thread + timeline (the "one
92beat" send-tap and the streamed list answer are both wired there).
93
94## Contract
95
96- FREE assembly: Playwright record + ffmpeg composite/mux + the bundled SFX. No
97 AI-rendered text — the bubbles, the streamed answer, and the end-card copy are all
98 real HTML/PIL, never invented by a model.
99- The recipe (DB) supplies the per-brand config: the `thread` (light-mode ChatGPT,
100 assistant message set `stream: true`), the `timeline`, the `end_card` (prefer a
101 real `logo_svg` wordmark), and an optional music bed.
102- SFX are **subliminal by design** (ChatGPT has no native chime): key-tap -28dB,
103 send-tap -20dB, stream-tick -32dB, response-done -22dB, and **never a cue on the
104 loading dot**. Set `"sfx": false` in the config to ship the chat silent.
105
106## Gaps / routing notes
107
108- **Music bed** is an input, not generated here — the recipe gates it to
109 `create-music-elevenlabs` (paid, proxy-routed, billed to the Ads agent) and passes
110 the file into `stitch.sh --music`.
111- **Bundled SFX are synthesized stand-ins.** The original four wavs
112 (`key-tap`/`send-tap`/`stream-tick`/`response-done`) were lost from Git LFS (the
113 objects 404 on the server), so `assets/sfx/*.wav` are freshly synthesized
114 subliminal clicks/ticks. They work as-is; swap in real wavs (same filenames) for
115 tuned SFX.
116- **Portability:** everything runs from the fetched
117 `/tmp/gooseworks-scripts/render-chatgpt-chat/scripts/…` — the chatgpt-mockup
118 generator + templates are bundled under `scripts/mockup/`, and the generic end
119 card is bundled under `scripts/`. No `/Users/…` or repo-relative paths, and no
120 required sibling fetch.
121- Requires **ffmpeg/ffprobe** on PATH and Playwright Chromium (`npx playwright
122 install chromium`).
123
124## Self-QC (per project rule — always /watch the master)
125
126- Keyboard is up the whole time the user types, and slides down only on the
127 send-tap beat (never visible while the answer streams).
128- Send-tap is one beat: user bubble + keyboard-down + header-swap on the same frame.
129- Exactly one gray loading dot for ~500ms (not three), and no SFX on the dot.
130- The answer streams word-by-word, left-to-right / top-to-bottom, not all-at-once.
131- No OpenAI spiral logo above any assistant title (the spiral is empty-state only).
132- No micro-flicker / scene cuts; the end-card pad color matches `end_card.bg`.