[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CADEDY083TX8R4PCF2EMD6":3,"skill-same:01M6CADEDY083TX8R4PCF2EMD6":93},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":22,"publishedAt":50,"linkOnly":51,"markdownContent":52,"markdownBody":53,"frontMatter":54,"refName":67,"commitSha":68,"skillDirectory":69,"forksCount":70,"repositoryHtmlUrl":71,"repositoryHomepageUrl":6,"ownerHtmlUrl":72,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":73,"install":74,"resources":87},"01M6CADEDY083TX8R4PCF2EMD6","mp4-caption-burn",null,"Capture screen activity to a captioned MP4 video — the full pipeline from\nscreen-recording to a publishable, captioned video. Triggers when the user\nasks for \"captioned MP4 proof\", \"MP4 with subtitles\", \"screen recording\nwith captions\", \"video walkthrough\", \"show me a video\", or any request that\nnames MP4 + captions\u002Fsubtitles\u002Fburned-in-text as the deliverable. Class-level\nskill: covers screen capture (macOS screencapture with window-scoped capture\nvia CGWindowID), frame selection, ffmpeg drawtext caption burn (with its\nparser pitfalls), h264 encoding, and the verification loop (vision_analyze\non extracted frames).\n\nAnti-trigger: plain-text transcripts (use a markdown\u002Fcode-block skill).\nAnti-trigger: GIF animations (use the gif-search or p5js skills — different\npipeline). Anti-trigger: asciinema recordings (use asciinema + agg, not\nMP4). Anti-trigger: video editing from existing footage (use manim-video\nor ffmpeg directly without going through this skill's capture pipeline).","jleechanorg\u002Fjleechan-skills","jleechanorg","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F221404966?v=4","01M6C1VJ99XG950X1DWPEBCBGX","01M6C1VJDK5YVDNT3362RT5WSB",6,"MIT","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002Fhermes\u002Fskills\u002Fmedia\u002Fmp4-caption-burn\u002FSKILL.md",[17],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000016","图像与多媒体","SCENARIO","使用场景",[23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],"caption burn","burn","mp4 caption burn","mp4","when","search","plain-text","recording","selection","show","cmux","only","use","fix","editing","video editing","verification loop","loop","ide","show me","walkthrough","Captions","Video","sha-256","screen recording","pipeline","subtitles","2026-11-10 10:12:30",false,"---\nname: mp4-caption-burn\nversion: 1.1.0\ndescription: |\n  Capture screen activity to a captioned MP4 video — the full pipeline from\n  screen-recording to a publishable, captioned video. Triggers when the user\n  asks for \"captioned MP4 proof\", \"MP4 with subtitles\", \"screen recording\n  with captions\", \"video walkthrough\", \"show me a video\", or any request that\n  names MP4 + captions\u002Fsubtitles\u002Fburned-in-text as the deliverable. Class-level\n  skill: covers screen capture (macOS screencapture with window-scoped capture\n  via CGWindowID), frame selection, ffmpeg drawtext caption burn (with its\n  parser pitfalls), h264 encoding, and the verification loop (vision_analyze\n  on extracted frames).\n\n  Anti-trigger: plain-text transcripts (use a markdown\u002Fcode-block skill).\n  Anti-trigger: GIF animations (use the gif-search or p5js skills — different\n  pipeline). Anti-trigger: asciinema recordings (use asciinema + agg, not\n  MP4). Anti-trigger: video editing from existing footage (use manim-video\n  or ffmpeg directly without going through this skill's capture pipeline).\nmetadata:\n  hermes:\n    tags: [video, ffmpeg, screen-recording, mp4, drawtext, captions, proof]\n    verified: \"2026-07-16 v1.1.0 -- two reference runs: v1.0.0 initial: 935 frames @ 2fps full-screen, 1920x1240, 58s, 3.6 MiB, SHA-256 4825b98d0aff1e659b43f6db5c3ac6d7b936363072f8854e34252de9e281ed57 -- user called out that the underlying footage was the cmux IDE, not the Terminal where the work ran. v1.1.0 fix: 145 frames @ 2fps window-scoped (screencapture -l 443), 1280x770, 75s, 930 KiB, SHA-256 e0338b4108c9b8051a3e8b011eea49321e076fd20d7d30c85de03877eb21909a -- Terminal-only footage matching captions.\"\n---\n\n# MP4 caption-burn pipeline\n\nEnd-to-end recipe: macOS screen recording → frame selection → ffmpeg drawtext caption burn → h264 MP4 → verification via vision_analyze on extracted frames.\n\n## When to use\n\nWhen the user wants a **publishable video** with text captions burned in (not separate SRT files, not YouTube auto-captions, not open-captioned VTT). Common phrasings:\n\n- \"give me a captioned MP4 \u002F video walkthrough \u002F screen capture with labels\"\n- \"show me \u002Fes-level proof\" (often paired with `\u002Fes`-style proof requirements)\n- \"burn subtitles into the video\"\n- \"produce a demo video\"\n- \"make a captioned demo\" \u002F \"step-by-step video\"\n\nThe end deliverable is an MP4 file that **renders inline in Slack and GitHub PRs without external captions** — viewers see the text in the video itself.\n\n## When NOT to use\n\n- The user wants raw screen recording without captions (use macOS QuickTime or `screencapture -V` directly).\n- The user wants GIFs (different pipeline; use p5js or terminal-record).\n- The user has a video they want to add captions to later (use ffmpeg drawtext directly with `subtitles=...` filter, not this full pipeline).\n- The \"video\" is a single still image with text overlay (use vision_analyze + image_generate, not MP4).\n- The user wants auto-generated captions from speech (use Whisper → SRT → ffmpeg, not drawtext).\n\n## The pipeline — 6 stages\n\n### Stage 1 — Record screen at 2fps via macOS `screencapture`\n\n**Use the macOS built-in `screencapture` CLI in a tight loop, NOT QuickTime.** QuickTime's CLI requires UI prompts; `screencapture -x -t png -C` runs silently.\n\n**Pitfall 1.0 — full-screen captures include whatever background is behind the foreground app, and users WILL notice.** When your work is happening in one Terminal pane inside a larger IDE\u002Fagent UI, `-x -C` (full screen) captures the dominant window (your IDE) and only a sliver of the Terminal. The user will watch a captioned MP4 whose underlying footage is unrelated screens. Symptom: \"That demo is wrong, it's not related to what i asked for\" (Cloud Build E2E re-capture, 2026-07-16). The fix is **window-scoped capture**, not full-screen:\n\n```bash\n# 1. Discover the target window's CGWindowID via Swift.\n#    System Events \"id of window\" fails (-1728); use Quartz directly.\n#    Helper script in this skill: scripts\u002Flist-window-ids.swift\n#      swift scripts\u002Flist-window-ids.swift                 # default: Terminal\n#      swift scripts\u002Flist-window-ids.swift \"Google Chrome\" # any owner name\n#    Inline if you prefer (or if the helper is unavailable):\n#\nswift - \u003C\u003C'EOF'\nimport Cocoa; import CoreGraphics\nlet info = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []\nfor i in info {\n  if (i[kCGWindowOwnerName as String] as? String) == \"Terminal\" {\n    print(i[kCGWindowNumber as String] as? Int ?? 0)\n  }\n}\nEOF\n# Output: 443\n\n# 2. Capture ONLY that window, no other apps visible:\nscreencapture -x -l 443 -t png \"$PROOF_DIR\u002Fframe_$(printf '%05d' $n).png\"\n```\n\nWhy this matters: the captioned MP4's value comes from the *underlying footage matching the captions*. If captions say \"HAND-OFF to cloud.superpowers.build\" and the screen shows IDE panes, the video is misleading regardless of how nice the captions look. Re-record before posting — never post a video you haven't vision-verified against the actual work it claims to document.\n\n**Pitfall 1.1 — `$(date)` doesn't expand inside double-quoted heredoc commands.** If you build the loop with `frame_$(date +%s%N).png`, the literal `$()` is captured by bash because of single-quote context. Use `printf '%05d'` or a counter variable.\n\n```bash\nPROOF_DIR=\"$HOME\u002Fcb-demo\u002Fproof\"\nmkdir -p \"$PROOF_DIR\"\nn=0\n# Window-scoped (clean capture of single app, no background bleed):\n#   screencapture -x -l \u003CCGWindowID> -t png \"\u003Cout>\"\n# Full-screen (legacy, bleeds in background apps):\n#   screencapture -x -t png -C \"\u003Cout>\"\nwhile true; do\n  screencapture -x -l 443 -t png \"$PROOF_DIR\u002Fframe_$(printf '%05d' $n).png\"\n  n=$((n+1))\n  sleep 0.5   # 2fps\ndone\n```\n\n**Pitfall 1.2 — the screencap will pollute git status** if the project dir is a git repo. Add the proof dir to `.gitignore` BEFORE starting, or use `~\u002F...` outside the repo. Verified: 935 screencap frames at 2fps = 7.8 min of screen at 1.8 GiB total before downsampling.\n\n**Pitfall 1.3 — `screencapture` requires Screen Recording permission** on macOS Mojave+. First run will fail silently. Open System Settings → Privacy & Security → Screen Recording, grant your terminal. Re-run the loop.\n\n**Pitfall 1.4 — `screencapture -l \u003CCGWindowID>` captures the wrong (or empty) window when the window ID changes.** CGWindowIDs are volatile across app reactivation, focus changes, and `cmd-w` close\u002Freopen. Re-fetch the ID via the Swift snippet above immediately before the demo run. If the capture starts producing blank frames (size ~1 MB but only the window chrome, no content), the window ID is stale — re-run the discovery script. Verify a single frame with `vision_analyze` before committing to a 90s capture.\n\n### Stage 2 — Downsample frames\n\n2fps is fine for proof but burns disk fast. For a ~2 min capture, keep 1 in every 4 frames (≈30s of video). Use symlinks (not copies) to save disk:\n\n```bash\nSRC=\"$HOME\u002Fcb-demo\u002Fproof\"\nDST=\"$HOME\u002Fcb-demo\u002Fproof\u002Fvideo_src\"\nrm -rf \"$DST\"; mkdir -p \"$DST\"\ni=0\nfor src in $(ls \"$SRC\"\u002Fframe_*.png | sort); do\n  if [ $((i % 4)) -eq 0 ]; then\n    ln -sf \"$src\" \"$DST\u002Ff$(printf '%05d' $i).png\"\n  fi\n  i=$((i+1))\ndone\n```\n\n### Stage 3 — Build the caption timeline\n\nDecide which caption appears at which time. Keep each caption's text to **alphanumeric + spaces + hyphens only**. See Pitfall 3.1.\n\n```python\ncaptions = [\n    (0.0, 5.0,  \"STEP 1 \u002F 6 - DOWNLOAD\"),\n    (5.0, 11.0, \"STEP 2 \u002F 6 - STATIC REVIEW\"),\n    ...\n]\n```\n\nFor 2fps video, frame N corresponds to time N\u002F2. For longer videos (>1 min), downsample to 1fps and rewrite the timestamps.\n\n### Stage 4 — ffmpeg drawtext, per-segment + concat\n\n**Pitfall 4.1 — ffmpeg's drawtext filter parser treats ANY text matching an option name as an option key.** Symptom: `Error applying option 'state' to filter 'drawtext': Option not found` when your caption text contains the literal word `state`. Same trap for `enable`, `time`, `rate`, `text`, `font`, `color`, etc. — any string that matches a known drawtext option name. Fix: **avoid all drawtext option names in your caption text**. Replace `state=done` with `DONE`, `enable` with `on`, `text` with `caption`, etc.\n\n**Pitfall 4.2 — `:` inside `text='...'` is parsed as an option separator.** `text='RESULT: DONE'` triggers the same option-parser failure because the parser sees `RESULT` as a key and ` DONE` as the value. Fix: replace `:` with `-` or ` - `. Or build the filter via `-filter_complex_script \u003Cfile>` (see Stage 5) where the parser is more lenient.\n\n**Pitfall 4.3 — the giant `-vf` chain with all drawtext instances in one ffmpeg invocation often fails for non-obvious reasons** (the literal-text collisions above are the common case, but there are other parser quirks). The reliable pattern is **one drawtext per ffmpeg invocation, then concat**:\n\n```bash\nfont_path=\"\u002FSystem\u002FLibrary\u002FFonts\u002FHelvetica.ttc\"\nfont_size=42\nseg_dir=\"$HOME\u002Fcb-demo\u002Fproof\u002Fsegs\"\nmkdir -p \"$seg_dir\"\n\nfor i in \"${!captions[@]}\"; do\n  IFS=' ' read -r start end text \u003C\u003C\u003C \"${captions[$i]}\"\n  s_idx=$((start * 2))    # 2fps\n  e_idx=$((end * 2 - 1))\n  sub_dir=\"$seg_dir\u002Fseg$i\"\n  mkdir -p \"$sub_dir\"\n  for j in $(seq \"$s_idx\" \"$e_idx\"); do\n    [ -f \"$DST\u002Ff$(printf '%05d' $j).png\" ] && \\\n      ln -sf \"$DST\u002Ff$(printf '%05d' $j).png\" \"$sub_dir\u002Ff$(printf '%05d' $j).png\"\n  done\n  vf=\"drawtext=fontfile=${font_path}:text='${text}':fontsize=${font_size}:fontcolor=white:box=1:boxcolor=black@0.6:boxborderw=18:x=(w-text_w)\u002F2:y=h-th-50\"\n  ffmpeg -y -framerate 2 -i \"$sub_dir\u002Ff%05d.png\" \\\n    -vf \"$vf\" -c:v libx264 -pix_fmt yuv420p \\\n    -preset veryfast -crf 26 -r 2 \"$seg_dir\u002Fseg$i.mp4\"\ndone\n```\n\n### Stage 5 — Concat + downscale\n\n```bash\nconcat_list=\"$seg_dir\u002Fconcat.txt\"\nfor i in \"${!captions[@]}\"; do\n  echo \"file 'seg$i.mp4'\" >> \"$concat_list\"\ndone\nffmpeg -y -f concat -safe 0 -i \"$concat_list\" \\\n  -c copy -movflags +faststart \"$HOME\u002Fcb-demo\u002Fproof\u002Fcb-demo.mp4\"\n\n# Downscale for posting\nffmpeg -y -i \"$HOME\u002Fcb-demo\u002Fproof\u002Fcb-demo.mp4\" \\\n  -vf \"scale=1920:1240\" -c:v libx264 -preset veryfast -crf 28 \\\n  -pix_fmt yuv420p -movflags +faststart \"$HOME\u002Fcb-demo\u002Fproof\u002Fcb-demo-small.mp4\"\n```\n\n**Pitfall 5.1 — Retina displays produce 3456×2234 frames at full resolution.** That's 7.7 MPx per frame. Without `-vf scale=1920:1240`, the MP4 is 30+ MiB and unwieldy for Slack threads \u002F PR comments. Downscale before posting.\n\n**Pitfall 5.2 — `-c copy` on the concat demuxer requires all segments to have identical codec, resolution, framerate, pix_fmt.** If one segment fails to encode (Stage 4 fallback path leaving the caption off), the concat will fail with \"Non-monotonous DTS\" or similar. Verify each segment MP4 plays independently before concat.\n\n### Stage 6 — Verify with vision_analyze\n\nExtract 3-5 frames at known caption-time boundaries and run vision_analyze to confirm the captions actually appear in the rendered MP4:\n\n```bash\nout_dir=\"$HOME\u002Fcb-demo\u002Fproof\u002Fverify_frames\"\nmkdir -p \"$out_dir\"\nfor t in 3 15 30 42 55; do\n  ffmpeg -y -ss \"$t\" -i \"$HOME\u002Fcb-demo\u002Fproof\u002Fcb-demo-small.mp4\" \\\n    -frames:v 1 -q:v 2 \"$out_dir\u002Fframe_t$(printf '%02d' $t).jpg\"\ndone\n```\n\nThen call `vision_analyze(image_url=\u003Ceach_path>, question=\"What text caption is visible at the bottom? Is it the exact caption I expected?\")`.\n\n**This is mandatory.** ffmpeg returns exit 0 even when drawtext silently no-ops (e.g. the filter was parsed but text was empty due to the option-name collision). The only ground truth is the rendered pixels.\n\n## Pitfalls summary\n\n| Pitfall | Symptom | Fix |\n|---|---|---|\n| Vision-verify passes but user complains \"wrong demo\" | Full-screen screencapture captured the dominant background window (IDE \u002F cmux panes), not the Terminal where the work ran. Captions say HAND-OFF but screen shows unrelated panes | Re-capture with `screencapture -l \u003CCGWindowID>` per Pitfall 1.0; re-vision-verify. Never post without confirming the footage matches the captions' claims |\n| `screencapture -l` produces blank\u002Fempty frames | Stale `CGWindowID` — IDs change across focus events and `cmd-w` close\u002Freopen | Re-run the Swift discovery script right before capture; verify one frame via `vision_analyze` before committing to a long capture |\n| `$(date)` not expanding in screencap loop | All frames named `frame_$(date +%s%N).png` literally | Use `printf '%05d' $n` with a counter variable |\n| Screencap frames polluting git status | Untracked `.png` files in the project dir | Add `proof\u002F` to `.gitignore` BEFORE starting; or run from `~\u002F...` outside any repo |\n| `screencapture` permission denied | No frames captured, exit 0 with empty output | Grant Screen Recording in System Settings → Privacy & Security |\n| drawtext `Error applying option 'state'` | The literal word `state` (or any drawtext option name) appears in your caption text | Replace with a synonym: `state` → `DONE`, `enable` → `on`, `time` → `minute`, etc. |\n| drawtext `Error parsing filterchain` | Caption text contains `:` | Replace `:` with `-` or ` - ` |\n| Concat \"Non-monotonous DTS\" | One segment failed to encode | Run each `seg$i.mp4` independently; the failed one will be much shorter than expected. Re-encode or remove it. |\n| MP4 is 30+ MiB | No downscale applied | Add `-vf \"scale=1920:1240\"` to the final encode |\n| vision_analyze shows wrong caption | The drawtext filter ran but on the wrong segment | Verify the `s_idx` \u002F `e_idx` math: `start * 2` and `end * 2 - 1` for 2fps video |\n\n## Posting the MP4\n\nOnce you have a small MP4, follow the `evidence-attach-to-slack` skill (v1.10+) for the post path. On the jleechanai.slack.com workspace both bot and xoxp tokens lack `files:write` (verified 2026-07-16), so the canonical path is **gist clone-and-replace → gist raw URL → embed in chat.postMessage with `unfurl_media: true`**. The gist raw URL serves `content-type: application\u002Foctet-stream` for MP4 — Slack still renders inline because the URL unfurler detects `.mp4` from the extension.\n\n## End-to-end reference run (verified 2026-07-16)\n\nCloud Build E2E proof:\n- 935 screencap frames @ 2fps captured during install + enroll + hand-off + status polling + land\n- Downsampled to 233 frames (1 in 4) → 116s raw video\n- 8 caption segments (DOWNLOAD → STATIC REVIEW → TEST REPO → INSTALL → RUN → LIBRARY-DRIVEN → RESULT DONE → PROOF)\n- Output: 1920×1240, h264, 58s, 3.6 MiB\n- SHA-256: `4825b98d0aff1e659b43f6db5c3ac6d7b936363072f8854e34252de9e281ed57`\n- Gist: https:\u002F\u002Fgist.github.com\u002Fjleechan2015\u002F77dd5406ec125ccb2a916c3a98787a4a\n- Slack thread: C09GRLXF9GR \u002F ts=1784230762.399999\n- vision_analyze confirmed all 8 caption segments burned in correctly\n\nTotal elapsed time: ~5 min for capture + ~3 min for encode + ~1 min for upload + post.","# MP4 caption-burn pipeline\n\nEnd-to-end recipe: macOS screen recording → frame selection → ffmpeg drawtext caption burn → h264 MP4 → verification via vision_analyze on extracted frames.\n\n## When to use\n\nWhen the user wants a **publishable video** with text captions burned in (not separate SRT files, not YouTube auto-captions, not open-captioned VTT). Common phrasings:\n\n- \"give me a captioned MP4 \u002F video walkthrough \u002F screen capture with labels\"\n- \"show me \u002Fes-level proof\" (often paired with `\u002Fes`-style proof requirements)\n- \"burn subtitles into the video\"\n- \"produce a demo video\"\n- \"make a captioned demo\" \u002F \"step-by-step video\"\n\nThe end deliverable is an MP4 file that **renders inline in Slack and GitHub PRs without external captions** — viewers see the text in the video itself.\n\n## When NOT to use\n\n- The user wants raw screen recording without captions (use macOS QuickTime or `screencapture -V` directly).\n- The user wants GIFs (different pipeline; use p5js or terminal-record).\n- The user has a video they want to add captions to later (use ffmpeg drawtext directly with `subtitles=...` filter, not this full pipeline).\n- The \"video\" is a single still image with text overlay (use vision_analyze + image_generate, not MP4).\n- The user wants auto-generated captions from speech (use Whisper → SRT → ffmpeg, not drawtext).\n\n## The pipeline — 6 stages\n\n### Stage 1 — Record screen at 2fps via macOS `screencapture`\n\n**Use the macOS built-in `screencapture` CLI in a tight loop, NOT QuickTime.** QuickTime's CLI requires UI prompts; `screencapture -x -t png -C` runs silently.\n\n**Pitfall 1.0 — full-screen captures include whatever background is behind the foreground app, and users WILL notice.** When your work is happening in one Terminal pane inside a larger IDE\u002Fagent UI, `-x -C` (full screen) captures the dominant window (your IDE) and only a sliver of the Terminal. The user will watch a captioned MP4 whose underlying footage is unrelated screens. Symptom: \"That demo is wrong, it's not related to what i asked for\" (Cloud Build E2E re-capture, 2026-07-16). The fix is **window-scoped capture**, not full-screen:\n\n```bash\n# 1. Discover the target window's CGWindowID via Swift.\n#    System Events \"id of window\" fails (-1728); use Quartz directly.\n#    Helper script in this skill: scripts\u002Flist-window-ids.swift\n#      swift scripts\u002Flist-window-ids.swift                 # default: Terminal\n#      swift scripts\u002Flist-window-ids.swift \"Google Chrome\" # any owner name\n#    Inline if you prefer (or if the helper is unavailable):\n#\nswift - \u003C\u003C'EOF'\nimport Cocoa; import CoreGraphics\nlet info = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []\nfor i in info {\n  if (i[kCGWindowOwnerName as String] as? String) == \"Terminal\" {\n    print(i[kCGWindowNumber as String] as? Int ?? 0)\n  }\n}\nEOF\n# Output: 443\n\n# 2. Capture ONLY that window, no other apps visible:\nscreencapture -x -l 443 -t png \"$PROOF_DIR\u002Fframe_$(printf '%05d' $n).png\"\n```\n\nWhy this matters: the captioned MP4's value comes from the *underlying footage matching the captions*. If captions say \"HAND-OFF to cloud.superpowers.build\" and the screen shows IDE panes, the video is misleading regardless of how nice the captions look. Re-record before posting — never post a video you haven't vision-verified against the actual work it claims to document.\n\n**Pitfall 1.1 — `$(date)` doesn't expand inside double-quoted heredoc commands.** If you build the loop with `frame_$(date +%s%N).png`, the literal `$()` is captured by bash because of single-quote context. Use `printf '%05d'` or a counter variable.\n\n```bash\nPROOF_DIR=\"$HOME\u002Fcb-demo\u002Fproof\"\nmkdir -p \"$PROOF_DIR\"\nn=0\n# Window-scoped (clean capture of single app, no background bleed):\n#   screencapture -x -l \u003CCGWindowID> -t png \"\u003Cout>\"\n# Full-screen (legacy, bleeds in background apps):\n#   screencapture -x -t png -C \"\u003Cout>\"\nwhile true; do\n  screencapture -x -l 443 -t png \"$PROOF_DIR\u002Fframe_$(printf '%05d' $n).png\"\n  n=$((n+1))\n  sleep 0.5   # 2fps\ndone\n```\n\n**Pitfall 1.2 — the screencap will pollute git status** if the project dir is a git repo. Add the proof dir to `.gitignore` BEFORE starting, or use `~\u002F...` outside the repo. Verified: 935 screencap frames at 2fps = 7.8 min of screen at 1.8 GiB total before downsampling.\n\n**Pitfall 1.3 — `screencapture` requires Screen Recording permission** on macOS Mojave+. First run will fail silently. Open System Settings → Privacy & Security → Screen Recording, grant your terminal. Re-run the loop.\n\n**Pitfall 1.4 — `screencapture -l \u003CCGWindowID>` captures the wrong (or empty) window when the window ID changes.** CGWindowIDs are volatile across app reactivation, focus changes, and `cmd-w` close\u002Freopen. Re-fetch the ID via the Swift snippet above immediately before the demo run. If the capture starts producing blank frames (size ~1 MB but only the window chrome, no content), the window ID is stale — re-run the discovery script. Verify a single frame with `vision_analyze` before committing to a 90s capture.\n\n### Stage 2 — Downsample frames\n\n2fps is fine for proof but burns disk fast. For a ~2 min capture, keep 1 in every 4 frames (≈30s of video). Use symlinks (not copies) to save disk:\n\n```bash\nSRC=\"$HOME\u002Fcb-demo\u002Fproof\"\nDST=\"$HOME\u002Fcb-demo\u002Fproof\u002Fvideo_src\"\nrm -rf \"$DST\"; mkdir -p \"$DST\"\ni=0\nfor src in $(ls \"$SRC\"\u002Fframe_*.png | sort); do\n  if [ $((i % 4)) -eq 0 ]; then\n    ln -sf \"$src\" \"$DST\u002Ff$(printf '%05d' $i).png\"\n  fi\n  i=$((i+1))\ndone\n```\n\n### Stage 3 — Build the caption timeline\n\nDecide which caption appears at which time. Keep each caption's text to **alphanumeric + spaces + hyphens only**. See Pitfall 3.1.\n\n```python\ncaptions = [\n    (0.0, 5.0,  \"STEP 1 \u002F 6 - DOWNLOAD\"),\n    (5.0, 11.0, \"STEP 2 \u002F 6 - STATIC REVIEW\"),\n    ...\n]\n```\n\nFor 2fps video, frame N corresponds to time N\u002F2. For longer videos (>1 min), downsample to 1fps and rewrite the timestamps.\n\n### Stage 4 — ffmpeg drawtext, per-segment + concat\n\n**Pitfall 4.1 — ffmpeg's drawtext filter parser treats ANY text matching an option name as an option key.** Symptom: `Error applying option 'state' to filter 'drawtext': Option not found` when your caption text contains the literal word `state`. Same trap for `enable`, `time`, `rate`, `text`, `font`, `color`, etc. — any string that matches a known drawtext option name. Fix: **avoid all drawtext option names in your caption text**. Replace `state=done` with `DONE`, `enable` with `on`, `text` with `caption`, etc.\n\n**Pitfall 4.2 — `:` inside `text='...'` is parsed as an option separator.** `text='RESULT: DONE'` triggers the same option-parser failure because the parser sees `RESULT` as a key and ` DONE` as the value. Fix: replace `:` with `-` or ` - `. Or build the filter via `-filter_complex_script \u003Cfile>` (see Stage 5) where the parser is more lenient.\n\n**Pitfall 4.3 — the giant `-vf` chain with all drawtext instances in one ffmpeg invocation often fails for non-obvious reasons** (the literal-text collisions above are the common case, but there are other parser quirks). The reliable pattern is **one drawtext per ffmpeg invocation, then concat**:\n\n```bash\nfont_path=\"\u002FSystem\u002FLibrary\u002FFonts\u002FHelvetica.ttc\"\nfont_size=42\nseg_dir=\"$HOME\u002Fcb-demo\u002Fproof\u002Fsegs\"\nmkdir -p \"$seg_dir\"\n\nfor i in \"${!captions[@]}\"; do\n  IFS=' ' read -r start end text \u003C\u003C\u003C \"${captions[$i]}\"\n  s_idx=$((start * 2))    # 2fps\n  e_idx=$((end * 2 - 1))\n  sub_dir=\"$seg_dir\u002Fseg$i\"\n  mkdir -p \"$sub_dir\"\n  for j in $(seq \"$s_idx\" \"$e_idx\"); do\n    [ -f \"$DST\u002Ff$(printf '%05d' $j).png\" ] && \\\n      ln -sf \"$DST\u002Ff$(printf '%05d' $j).png\" \"$sub_dir\u002Ff$(printf '%05d' $j).png\"\n  done\n  vf=\"drawtext=fontfile=${font_path}:text='${text}':fontsize=${font_size}:fontcolor=white:box=1:boxcolor=black@0.6:boxborderw=18:x=(w-text_w)\u002F2:y=h-th-50\"\n  ffmpeg -y -framerate 2 -i \"$sub_dir\u002Ff%05d.png\" \\\n    -vf \"$vf\" -c:v libx264 -pix_fmt yuv420p \\\n    -preset veryfast -crf 26 -r 2 \"$seg_dir\u002Fseg$i.mp4\"\ndone\n```\n\n### Stage 5 — Concat + downscale\n\n```bash\nconcat_list=\"$seg_dir\u002Fconcat.txt\"\nfor i in \"${!captions[@]}\"; do\n  echo \"file 'seg$i.mp4'\" >> \"$concat_list\"\ndone\nffmpeg -y -f concat -safe 0 -i \"$concat_list\" \\\n  -c copy -movflags +faststart \"$HOME\u002Fcb-demo\u002Fproof\u002Fcb-demo.mp4\"\n\n# Downscale for posting\nffmpeg -y -i \"$HOME\u002Fcb-demo\u002Fproof\u002Fcb-demo.mp4\" \\\n  -vf \"scale=1920:1240\" -c:v libx264 -preset veryfast -crf 28 \\\n  -pix_fmt yuv420p -movflags +faststart \"$HOME\u002Fcb-demo\u002Fproof\u002Fcb-demo-small.mp4\"\n```\n\n**Pitfall 5.1 — Retina displays produce 3456×2234 frames at full resolution.** That's 7.7 MPx per frame. Without `-vf scale=1920:1240`, the MP4 is 30+ MiB and unwieldy for Slack threads \u002F PR comments. Downscale before posting.\n\n**Pitfall 5.2 — `-c copy` on the concat demuxer requires all segments to have identical codec, resolution, framerate, pix_fmt.** If one segment fails to encode (Stage 4 fallback path leaving the caption off), the concat will fail with \"Non-monotonous DTS\" or similar. Verify each segment MP4 plays independently before concat.\n\n### Stage 6 — Verify with vision_analyze\n\nExtract 3-5 frames at known caption-time boundaries and run vision_analyze to confirm the captions actually appear in the rendered MP4:\n\n```bash\nout_dir=\"$HOME\u002Fcb-demo\u002Fproof\u002Fverify_frames\"\nmkdir -p \"$out_dir\"\nfor t in 3 15 30 42 55; do\n  ffmpeg -y -ss \"$t\" -i \"$HOME\u002Fcb-demo\u002Fproof\u002Fcb-demo-small.mp4\" \\\n    -frames:v 1 -q:v 2 \"$out_dir\u002Fframe_t$(printf '%02d' $t).jpg\"\ndone\n```\n\nThen call `vision_analyze(image_url=\u003Ceach_path>, question=\"What text caption is visible at the bottom? Is it the exact caption I expected?\")`.\n\n**This is mandatory.** ffmpeg returns exit 0 even when drawtext silently no-ops (e.g. the filter was parsed but text was empty due to the option-name collision). The only ground truth is the rendered pixels.\n\n## Pitfalls summary\n\n| Pitfall | Symptom | Fix |\n|---|---|---|\n| Vision-verify passes but user complains \"wrong demo\" | Full-screen screencapture captured the dominant background window (IDE \u002F cmux panes), not the Terminal where the work ran. Captions say HAND-OFF but screen shows unrelated panes | Re-capture with `screencapture -l \u003CCGWindowID>` per Pitfall 1.0; re-vision-verify. Never post without confirming the footage matches the captions' claims |\n| `screencapture -l` produces blank\u002Fempty frames | Stale `CGWindowID` — IDs change across focus events and `cmd-w` close\u002Freopen | Re-run the Swift discovery script right before capture; verify one frame via `vision_analyze` before committing to a long capture |\n| `$(date)` not expanding in screencap loop | All frames named `frame_$(date +%s%N).png` literally | Use `printf '%05d' $n` with a counter variable |\n| Screencap frames polluting git status | Untracked `.png` files in the project dir | Add `proof\u002F` to `.gitignore` BEFORE starting; or run from `~\u002F...` outside any repo |\n| `screencapture` permission denied | No frames captured, exit 0 with empty output | Grant Screen Recording in System Settings → Privacy & Security |\n| drawtext `Error applying option 'state'` | The literal word `state` (or any drawtext option name) appears in your caption text | Replace with a synonym: `state` → `DONE`, `enable` → `on`, `time` → `minute`, etc. |\n| drawtext `Error parsing filterchain` | Caption text contains `:` | Replace `:` with `-` or ` - ` |\n| Concat \"Non-monotonous DTS\" | One segment failed to encode | Run each `seg$i.mp4` independently; the failed one will be much shorter than expected. Re-encode or remove it. |\n| MP4 is 30+ MiB | No downscale applied | Add `-vf \"scale=1920:1240\"` to the final encode |\n| vision_analyze shows wrong caption | The drawtext filter ran but on the wrong segment | Verify the `s_idx` \u002F `e_idx` math: `start * 2` and `end * 2 - 1` for 2fps video |\n\n## Posting the MP4\n\nOnce you have a small MP4, follow the `evidence-attach-to-slack` skill (v1.10+) for the post path. On the jleechanai.slack.com workspace both bot and xoxp tokens lack `files:write` (verified 2026-07-16), so the canonical path is **gist clone-and-replace → gist raw URL → embed in chat.postMessage with `unfurl_media: true`**. The gist raw URL serves `content-type: application\u002Foctet-stream` for MP4 — Slack still renders inline because the URL unfurler detects `.mp4` from the extension.\n\n## End-to-end reference run (verified 2026-07-16)\n\nCloud Build E2E proof:\n- 935 screencap frames @ 2fps captured during install + enroll + hand-off + status polling + land\n- Downsampled to 233 frames (1 in 4) → 116s raw video\n- 8 caption segments (DOWNLOAD → STATIC REVIEW → TEST REPO → INSTALL → RUN → LIBRARY-DRIVEN → RESULT DONE → PROOF)\n- Output: 1920×1240, h264, 58s, 3.6 MiB\n- SHA-256: `4825b98d0aff1e659b43f6db5c3ac6d7b936363072f8854e34252de9e281ed57`\n- Gist: https:\u002F\u002Fgist.github.com\u002Fjleechan2015\u002F77dd5406ec125ccb2a916c3a98787a4a\n- Slack thread: C09GRLXF9GR \u002F ts=1784230762.399999\n- vision_analyze confirmed all 8 caption segments burned in correctly\n\nTotal elapsed time: ~5 min for capture + ~3 min for encode + ~1 min for upload + post.",{"name":5,"version":55,"metadata":56,"description":66},"1.1.0",{"hermes":57},{"tags":58,"verified":65},[59,60,61,26,62,63,64],"video","ffmpeg","screen-recording","drawtext","captions","proof","2026-07-16 v1.1.0 -- two reference runs: v1.0.0 initial: 935 frames @ 2fps full-screen, 1920x1240, 58s, 3.6 MiB, SHA-256 4825b98d0aff1e659b43f6db5c3ac6d7b936363072f8854e34252de9e281ed57 -- user called out that the underlying footage was the cmux IDE, not the Terminal where the work ran. v1.1.0 fix: 145 frames @ 2fps window-scoped (screencapture -l 443), 1280x770, 75s, 930 KiB, SHA-256 e0338b4108c9b8051a3e8b011eea49321e076fd20d7d30c85de03877eb21909a -- Terminal-only footage matching captions.","Capture screen activity to a captioned MP4 video — the full pipeline from\nscreen-recording to a publishable, captioned video. Triggers when the user\nasks for \"captioned MP4 proof\", \"MP4 with subtitles\", \"screen recording\nwith captions\", \"video walkthrough\", \"show me a video\", or any request that\nnames MP4 + captions\u002Fsubtitles\u002Fburned-in-text as the deliverable. Class-level\nskill: covers screen capture (macOS screencapture with window-scoped capture\nvia CGWindowID), frame selection, ffmpeg drawtext caption burn (with its\nparser pitfalls), h264 encoding, and the verification loop (vision_analyze\non extracted frames).\n\nAnti-trigger: plain-text transcripts (use a markdown\u002Fcode-block skill).\nAnti-trigger: GIF animations (use the gif-search or p5js skills — different\npipeline). Anti-trigger: asciinema recordings (use asciinema + agg, not\nMP4). Anti-trigger: video editing from existing footage (use manim-video\nor ffmpeg directly without going through this skill's capture pipeline).\n","main","05df7380981a2c4af868ce0fa57962ae0eea62a9","hermes\u002Fskills\u002Fmedia\u002Fmp4-caption-burn",0,"https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills","https:\u002F\u002Fgithub.com\u002Fjleechanorg","2026-11-11 22:19:36",{"source":8,"skillName":5,"commands":75,"prompt":85,"zipAvailable":86},[76,79,82],{"tool":77,"command":78},"npx","npx skills add jleechanorg\u002Fjleechan-skills --skill mp4-caption-burn",{"tool":80,"command":81},"bunx","bunx skills add jleechanorg\u002Fjleechan-skills --skill mp4-caption-burn",{"tool":83,"command":84},"pnpm","pnpm dlx skills add jleechanorg\u002Fjleechan-skills --skill mp4-caption-burn","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002Fhermes\u002Fskills\u002Fmedia\u002Fmp4-caption-burn\u002FSKILL.md",true,[88],{"path":89,"resourceType":89,"resourceTypeDesc":90,"sizeBytes":91,"sourceHtmlUrl":92},"LICENSE","许可证",1070,"https:\u002F\u002Fgithub.com\u002Fboisenoise\u002Fskills-collections\u002Fblob\u002Fmain\u002Fskills\u002Fantigravity-write-swift\u002FLICENSE",[94,110,132,146,160,176],{"skillNo":95,"name":96,"nameZh":6,"description":97,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":98,"categories":99,"keywords":100,"publishedAt":50},"01M716Y5MTJ6GYJZ2ATXCJK8G4","instinct","Use when the user invokes \u002Finstinct or \u002Fin, or asks to \"message Instinct\", \"ask Instinct\", \"tell Instinct\", \"check Instinct\", or read replies from the configured peer assistant. iMessage first, WhatsApp Web as fallback.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Finstinct\u002FSKILL.md",[],[96,101,27,102,103,35,104,105,106,107,108,109],"user","replies","check","identity","whatsapp","gmail","read","message","ask",{"skillNo":111,"name":112,"nameZh":6,"description":113,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":114,"categories":115,"keywords":121,"publishedAt":50},"01M716Y4HF7FVVMJSA2CCVN5VF","hr","Compatibility alias for history-resume; produce the same fresh-state, provenance-aware resume briefing.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fhr\u002FSKILL.md",[116],{"categoryNo":117,"categoryName":118,"taxonomyType":119,"taxonomyTypeDesc":120},"01JBSEEDCAT000000000000038","人力资源","PROFESSION","适用职业",[112,122,123,124,125,126,127,128,129,130,131],"resume","compatibility","provenance","for","history","produce","alias","briefing","state","fresh",{"skillNo":133,"name":134,"nameZh":6,"description":135,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":136,"categories":137,"keywords":139,"publishedAt":50},"01M716Y1FFNFKFJYYKF8HQA6T8","history-resume","Resume briefing for the current repository branch or worktree using fresh state and admitted history sources.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fhistory-resume\u002FSKILL.md",[138],{"categoryNo":117,"categoryName":118,"taxonomyType":119,"taxonomyTypeDesc":120},[126,122,140,141,101,142,129,143,125,144,145,130,131],"history resume","branch","analysis","repository","worktree","sources",{"skillNo":147,"name":148,"nameZh":6,"description":149,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":150,"categories":151,"keywords":152,"publishedAt":50},"01M716XWXJ8EPHK4RH9T14TR85","grok","Alias for the canonical grokbot browser conversation workflow; \u002Fgrok talks to the Grok bot the same way \u002Fdot talks to Dot.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgrok\u002FSKILL.md",[],[148,153,154,155,125,156,128,157,158,159],"dot","bot","workflow","canonical","conversation","browser","grokbot",{"skillNo":161,"name":162,"nameZh":6,"description":163,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":164,"categories":165,"keywords":166,"publishedAt":50},"01M70N9Q07DTE0EW5KFXMMMSJ0","codexa-bench","Compare `codexa` and normal `codex` on a fair split of comparable work by default; use `--strict` for duplicate runs of the exact same task.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fcodexa-bench\u002FSKILL.md",[],[167,168,169,35,170,125,171,172,173,174,175],"codexa bench","work","split","default","duplicate","task","compare","normal","codex",{"skillNo":177,"name":178,"nameZh":6,"description":179,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":180,"licenseFileUrl":181,"sourceHtmlUrl":182,"categories":183,"keywords":193,"publishedAt":50},"01M6C24S2X60SP8BBFGAEKPEGW","frontend-design","Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML\u002FCSS layouts, or when styling\u002Fbeautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Ffrontend-design\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Ffrontend-design\u002FSKILL.md",[184,187,190],{"categoryNo":185,"categoryName":186,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":188,"categoryName":189,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":191,"categoryName":192,"taxonomyType":119,"taxonomyTypeDesc":120},"01JBSEEDCAT000000000000032","设计师",[194,195,196,35,197,198,199,101,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,27,218],"frontend","design","frontend design","generic","ui design","react","html","components","code","artifacts","ui","aesthetics","Production","web components","landing","applications","quality","web","css","create","examples","with","react components","web ui","pages"]