视觉画布设计
canvas-design
基于设计原则创作静态视觉作品,用于海报与品牌图形。
create-chatgpt-video-ad【文档与办公】Produce a 9:16 social-native ad that recreates a ChatGPT mobile chat — user types in the composer with the iOS keyboard visible, taps send, keyboard slides down, header right-cluster swaps (personPlus/dotted → edit/more), one gray loading dot appears, then the assistant response streams in word-by-word with a soft opacity ramp. Continuous Playwright …
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Use this skill when an ad concept calls for an authentic ChatGPT mobile chat: someone asks ChatGPT something, the response streams in, and the content of the response is the punchline (the brand answer, a surprising bullet list, a recommendation). It's the "I just asked ChatGPT…" creative format.
Animate a ChatGPT mobile conversation into a 9:16 social-native video ad where the streamed assistant response is the punchline — the brand surfacing as the natural answer to "I just asked ChatGPT…". The molecule composes the create-chatgpt-mockup renderer (driven via Playwright) with the stitch-videos-ffmpeg assembly step to choreograph the full beat: the user types in the iOS composer with the keyboard up, taps send (bubble pops + keyboard slides down + header right-cluster swaps in one frame), a single gray loading dot holds ~500ms, then the assistant answer streams in word-by-word with a soft opacity ramp. Everything is captured as ONE continuous Playwright recording (no scene cuts / micro-flicker), assembled into a master MP4 plus 9:16 and 1×1 exports. It is the ChatGPT-host sibling of create-imessage-video-ad: reach for this one when ChatGPT is the more credible host for the answer; reach for the create-chatgpt-mockup atom directly when you only need a still screenshot rather than the typing + streaming + auto-scroll choreography animated to video. By default the molecule ships silent — the recorder emits an empty SFX cue list and stitch passes the video straight through; the four bundled SFX wavs and the cue-derivation logic exist for an optional subliminal pass (see Critical knowledge #4).
create-imessage-video-ad molecule but where ChatGPT is the more credible hostIf the brief just needs a still ChatGPT screenshot, reach for the atom create-chatgpt-mockup directly. Use this molecule only when you need the typing + streaming + auto-scroll choreography animated to video.
create-chatgpt-mockup — renders the light-mode ChatGPT iOS HTML (renderHTML(thread)); this molecule loads it once with every message popState: "pending" and drives the reveal via Playwright. (create-chatgpt-mockup)render-ios-keyboard — the iOS QWERTY keyboard fragment (suggestion bar + alpha/shift/backspace/123/space/return rows) that gets mounted at the bottom of the stage and slid up on keyboard-show / down on keyboard-hide. The chatgpt-mockup atom inlines this same .kbd block, so the molecule slides it via data-state rather than importing it separately. (render-ios-keyboard)stitch-videos-ffmpeg — the assembly/ffmpeg step (edits/stitch.sh) that takes the silent master-chat.mp4, layers the deterministic master-chat.sfx.json cue list (when cues exist) over the four bundled SFX wavs, and muxes/exports master-final.mp4. (stitch-videos-ffmpeg)brief (required) — what the ad is selling, the question the user types, the response ChatGPT givesoutput_folder — usually <brand>/ads/video-NN-chatgpt-<slug>/thread — JSON matching the create-chatgpt-mockup schema, with these animation extensions:
messages[].id must be set so the timeline can reference itmessages[].popState defaults to "pending" (hidden); the driver flips it to "now" at the right tstream: true so its body words are wrapped in <span class="word" data-stream="0">header.rightIconsAlt is the active-chat alt cluster (typically ["edit", "more"])keyboard: { layout: "qwerty-lower", suggestions: ["I","The","I'm"], state: "shown" } mounts the keyboardtimeline — array of events; see "Timeline" belowSame rule as the iMessage molecule. Render the full HTML once with all messages pending, walk a TIMELINE on requestAnimationFrame inside the page, and record the whole thing as ONE Playwright session. Do not concat scene clips — every page reload causes a micro-flicker at the cut.
The atom renders at 750×1624 (ratio 0.461) which matches iPhone 14/16 Pro screen recordings. The older 9:16 ratio (750×1334) reads as proportionally squat next to a real iPhone capture — the keyboard dominates and the chat zone feels stubby. When uploading via upload-ad-sample, tag as 9:16 (Meta accepts both; no transcode needed).
Real ChatGPT streams words with a 200ms opacity+blur ramp on each. The atom's wrapWordsForStreaming pre-wraps every visible word in a span; the driver flips data-stream="1" on a schedule. Default cadence: 7 words/sec (~140ms between word reveals). Slow it down for short responses, speed up for very long ones.
The frame that the user bubble lands in is also the frame the header right-icons swap and the keyboard starts sliding down. Three things in one beat. Pulling them apart over multiple frames reads as glitchy.
Real ChatGPT shows a single small dark-gray solid dot below the user bubble for ~500ms before the response starts streaming. Three dots would read as iMessage typing — wrong app.
This is the inverse of the iMessage rule. iMessage SFX are the Apple chime — they're a feature. ChatGPT has no native SFX, so anything we play has to be subliminal. The pre-bundled levels are:
| Cue | Level | Where it sits |
|---|---|---|
key-tap | -28 dB | Once per word (not per char — too busy) during composer typing |
send-tap | -20 dB | On the send-tap event only |
stream-tick | -32 dB | Every ~12 words during streaming, as a "still working" pulse |
response-done | -22 dB | After the final word of the response lands |
No music bed by default. If the brief asks for one, lofi at -18dB.
The dot is a silent state change. Same rule as the iMessage typing-pop — adding a "thinking" sound makes the chat feel fake. Skip it.
When the user taps send: the bubble pops in (220ms), the keyboard starts sliding down (280ms), the composer rides up by -518px (the keyboard's height) over the same 280ms. The slide and the bubble overlap — the bubble lands fully visible just before the keyboard finishes hiding. Don't sequence them — let them run concurrently.
The driver consumes an array of events. Each event has { t: <seconds>, kind, target?, value? }.
| Kind | Meaning | Notes |
|---|---|---|
composer-type | Type chars over a duration | { text, dur_sec }. Caret remains visible. SFX = one key-tap per word boundary. |
composer-clear | Wipe composer text instantly | Used after send-tap |
keyboard-show | Set keyboard data-state="shown" + stage data-keyboard-shown="1" | CSS handles the slide |
keyboard-hide | Set keyboard data-state="hidden" + clear data-keyboard-shown | CSS handles the slide |
send-tap | Pulse send button (scale 0.9 → 1.0 over 120ms) | SFX = send-tap |
pop | Flip a row from data-pending="1" → pop-now class | { target: <msg-id> } |
header-swap | Set header right[data-active] to "primary" or "alt" | { value: "alt" } |
loading-dot-show | Pop the loading-dot row | { target: <dot-id> } |
loading-dot-hide | Hide the loading-dot row | { target: <dot-id> } |
send-state | Switch composer send button class to disabled / active / streaming | { value: "streaming" } |
stream-words | Reveal an assistant message word-by-word | { target: <msg-id>, dur_sec, wps?: 7 }. SFX = stream-tick every 12 words, response-done at the end. |
scroll-to | Smooth-scroll the conversation so a target row's bottom aligns with the visible-area bottom | { target: <msg-id>, dur_ms: 250 } |
Pacing for the canonical example (Q="Hey what's happening in this app?", short list response):
t=0.0 keyboard-show
t=0.5 composer-type text="Hey what's happening in this app?" dur_sec=2.4
t=3.2 send-tap
t=3.20 pop target=msg-user-1
t=3.20 composer-clear
t=3.20 header-swap value="alt"
t=3.20 keyboard-hide
t=3.20 send-state value="streaming"
t=3.55 loading-dot-show target=dot-1
t=4.10 loading-dot-hide target=dot-1
t=4.10 pop target=msg-assistant-1
t=4.15 stream-words target=msg-assistant-1 dur_sec=6.0 wps=7
t=4.50 scroll-to target=msg-assistant-1
t=10.5 send-state value="active"
Total: ~11s. Pad with a 1s hold at the end so the response can breathe before the cut.
thread.json{
"statusBar": { "time": "9:41" },
"header": {
"style": "plain-title",
"title": "ChatGPT",
"rightIcons": ["personPlus", "dottedCircle"],
"rightIconsAlt": ["edit", "more"]
},
"keyboard": { "layout": "qwerty-lower", "suggestions": ["I", "The", "I'm"], "state": "hidden" },
"messages": [
{ "type": "user-text", "id": "msg-user-1", "text": "Hey what's happening in this app?", "popState": "pending" },
{ "type": "loading-dot", "id": "dot-1", "popState": "pending" },
{
"type": "assistant",
"id": "msg-assistant-1",
"stream": true,
"popState": "pending",
"feedback": false,
"text": "Could you tell me what you mean by \"this app\"?\n\nIf you're referring to the ChatGPT app itself, I can explain:\n\n* How conversations work\n* Memory and personalization\n* Tools (web search, calendars, email, file analysis, image generation, etc.)\n* Why certain UI elements appear\n* What data is saved vs. not saved\n* How tasks/reminders work"
}
],
"composer": { "placeholder": "Ask ChatGPT" }
}
NODE_PATH=<repo>/create-chatgpt-mockup/node_modules \
node clips/record-master.js
Writes master-chat.mp4 (no audio) and master-chat.sfx.json (the deterministic SFX cue list).
bash edits/stitch.sh
Consumes master-chat.sfx.json and the four SFX wavs in assets/sfx/. Writes master-final.mp4.
HARD RULE: never export a width×height whose ratio differs from the master's 750:1624
(≈0.4618). scale=1080:1920 (16:9) on this format stretches the UI ~22% horizontally —
this shipped once (2026-06-11, Alitu) and reads as "fat" iPhone chrome immediately. The
9:16-tagged deliverable is either the native master or an aspect-true upscale; only the 1×1
crop changes shape (by cropping, never by scaling).
# 9:16-tagged deliverable — aspect-true upscale (1080×2338 keeps 750:1624 exactly).
# Shipping the native 750×1624 master unscaled is equally fine.
ffmpeg -y -i edits/master-final.mp4 -vf "scale=1080:-2:flags=lanczos" \
-c:v libx264 -pix_fmt yuv420p -c:a copy -movflags +faststart \
meta-upload/master-9x16-1080.mp4
# 1×1 — CROP a square from the master (centered on the chat zone; adjust y to
# frame the response), then upscale. Cropping changes shape legitimately; scaling must not.
ffmpeg -y -i edits/master-final.mp4 -vf "crop=750:750:0:437,scale=1080:1080:flags=lanczos" \
-c:v libx264 -pix_fmt yuv420p -c:a copy -movflags +faststart \
meta-upload/master-1x1-1080.mp4
Per project rule (CLAUDE.md #3), always /watch:watch master-final.mp4 and confirm:
renderAssistant should not emit it)<output_folder>/
thread.json # the script as data
clips/
record-master.js # adapted from this skill's template
master-chat.mp4 # ~12s continuous chat recording (silent)
master-chat.sfx.json # deterministic SFX cue list
edits/
stitch.sh # adapted from this skill's template
master-final.mp4 # 750×1624 (native ~9:19.5), h.264
meta-upload/
master-9x16-1080.mp4 # 1080×2338 aspect-true upscale (or ship the native master)
master-1x1-1080.mp4 # square CROP of the master, then upscale
Run these before declaring the ad shippable. Most map directly to the State 4 /watch:watch pass above.
master-chat.page.html rendered once, timeline walked on requestAnimationFrame).keyboard-hide fires at the same t as send-tap).personPlus/dottedCircle → edit/more) all land on the same frame. Pulling them apart reads as glitchy.stream: true so the atom pre-wraps every word in a .word[data-stream] span).scroll-to after the assistant pop and a mid-stream scroll around the halfway mark.deviceScaleFactor: 2), h.264, and the 9:16 / 1×1 variants are present under meta-upload/. Confirm with ffprobe -v error -show_entries stream=width,height -of default=nw=1 edits/master-final.mp4.ffprobe each file under meta-upload/ and do the division — a 1080×1920 output from this format is ALWAYS a horizontal stretch (live incident 2026-06-11, Alitu).master-chat.sfx.json contains {"cues": []} and master-final.mp4 is a stream-copy of the silent master (no audio track is expected). If you opted into the subliminal SFX pass, every cue must sit at the bundled level (key-tap -28dB, send-tap -20dB, stream-tick -32dB, response-done -22dB), with no cue on the loading dot, and audio should peak at 0dB after stitch's volume=2.5,alimiter pass.| Symptom | Cause | Fix |
|---|---|---|
| Keyboard visible during streaming | Forgot keyboard-hide at send-tap | Add the event at the same t as send-tap |
| Keyboard half-clipped at bottom of stage | Translated the composer up by a hardcoded pixel value that didn't match the keyboard's actual rendered height | Use the apple-notes .kbd pattern: keyboard is position: absolute; bottom: 0; transform: translateY(0|100%); composer rides above it via position: absolute; bottom: 498px when the stage has data-keyboard-shown="1". Don't reinvent keyboard layout — port the working pattern. |
| Header right-cluster doesn't swap | rightIconsAlt missing in thread.json | Add header.rightIconsAlt array |
| Assistant text appears all at once | Forgot stream: true on the assistant message | Set it; the atom wraps words for you |
Edits to working/thread.json don't take effect | Recorder reads thread.json from the project root, not from working/ | Either copy both thread.json and timeline.json to the project root after every edit, or use the resolveSpec fallback shipped in the template (root → working/) |
| SFX too loud | Default levels were overridden | The bundled wavs are pre-normalized; if you must adjust, do it in stitch.sh, not by re-recording |
| Bubble pops in but conversation doesn't scroll | Skipped scroll-to events | Add a scroll-to after every pop and again at the end of stream-words |
| Loading dot still visible after response starts | Forgot loading-dot-hide | Add it at the same t as the assistant pop |
| Micro-flicker between scenes | Recorded scene-by-scene with reloads | Use the single-session pattern in scripts/record-master.template.js — same architecture as the iMessage molecule |
| UI looks horizontally fat / stretched | Export forced 1080×1920 (16:9) onto the 750×1624 (~9:19.5) master | Aspect-true export only: ship native or scale=1080:-2 (→1080×2338); never scale to a different ratio (State 3 hard rule) |
create-chatgpt-mockup atom directly.create-imessage-video-ad when the punchline is the answer to a question and ChatGPT is the credible source of that answer; pick iMessage when the punchline is a peer's reaction/discovery in a DM.popState: "pending" and walk the timeline inside the page; every page reload introduces a cut/flicker.stream: true on the assistant message and pace at ~7 words/sec (stream-words wps), slowing for short responses and speeding up for long ones; ChatGPT does not reveal text char-by-char.pop (user bubble), composer-clear, header-swap value="alt", keyboard-hide, and send-state value="streaming" all at the send-tap t; never sequence them across frames.loading-dot row (never three), and never attach SFX to it.9:16 even though it records at 750×1624 (~9:19.5) — Meta accepts both and needs no transcode. If you upscale, preserve the ratio (scale=1080:-2); never export 1080×1920 from this format — that's a horizontal stretch, not a resize.thread.json and timeline.json in sync at the project root (or rely on the template's resolveSpec root→working/ fallback); the recorder reads them from the project root, so a stale root copy silently wins over a working/ edit.