wjs-burning-subtitles
**wjs-burning-subtitles** embeds SRT subtitle files into video using ffmpeg with libass support, making subtitles always-visible for platforms like WeChat and TikTok that don't honor embedded subtitle tracks. It also serves as the final encoding stage for localization workflows, combining subtitle burning with optional dub audio mixing and original audio bed in a single ffmpeg pass, eliminating cascading re-encodes. Use when viewers need always-visible captions, when soft-muxing togglable subtitle tracks for compatible players, or when finalizing localized video with dubbed audio and burned-in target-language subtitles.
git clone --depth 1 https://github.com/jianshuo/claude-skills /tmp/wjs-burning-subtitles && cp -r /tmp/wjs-burning-subtitles/wjs-burning-subtitles ~/.claude/skills/wjs-burning-subtitlesSKILL.md
# wjs-burning-subtitles Video + SRT → video with subtitles. Also the final-encode stage for the localization pipeline: takes a video, an optional dub track from `/wjs-dubbing-video`, and an optional SRT to burn, and produces the upload-ready MP4 in **one** ffmpeg pass. No cascade of decodes/re-encodes. ## When to use - User has an SRT and wants it always-visible on the video (burn-in for 微信视频号 / 抖音 / WeChat — players that won't honor embedded subtitle tracks). - User wants a togglable subtitle track (soft-mux) for QuickTime / VLC / IINA / mobile players that support `mov_text`. - Final composite after `/wjs-dubbing-video`: burn target-language subs + mix dub over original-as-bed in one encode. ## When NOT to use - No SRT yet → run `/wjs-transcribing-audio` then `/wjs-translating-subtitles` first. - HTML/CSS captions (kinetic, per-word highlights, custom fonts) on a clip composed in HyperFrames → use `/wjs-overlaying-video` instead. Don't mix libass burn-in with HyperFrames captions on the same output. - The "subtitles" are actually motion graphics (animated callouts, lower-thirds with logos, kinetic typography) → that's `/wjs-overlaying-video`, not this skill. ## The 3 modes of `render.py` `scripts/render.py` auto-detects mode from flags: 1. **Subtitles only** — `--video + --srt` → re-encodes video with burned subs, original audio passes through. 2. **Dub only** — `--video + --dub` → keeps original video stream; replaces or mixes the audio track. 3. **Full localized cut** — `--video + --srt + --dub` → burns subs AND mixes dub. By default keeps original audio at low volume as a "bed" under the dub (set `--bed-volume 0` or `--no-original-audio` to drop it). Burn-in requires an ffmpeg built with libass. The script auto-downloads a static libass-enabled build from evermeet.cx into `/tmp/ff_bin/` on first use if needed. ## Soft-mux (togglable subtitle track) Player apps can show/hide. Works with any `ffmpeg` build — does **not** need libass: ```bash ffmpeg -i input.mp4 -i input.zh-CN.srt \ -map 0:v -map 0:a -map 1:0 \ -c:v copy -c:a copy -c:s mov_text \ -metadata:s:s:0 language=zho -metadata:s:s:0 title="中文" \ output.mp4 ``` This is fast (stream-copy) and reversible. Use it when: - Target platform supports embedded subs (YouTube auto-detects; VLC/QuickTime honors). - User wants viewers to be able to toggle off. - You don't want to re-encode the video. `render.py --video IN.mp4 --srt SUB.srt --soft-mux` runs this path. ## Hardcoded burn-in (always visible, libass) Required for WeChat/抖音/朋友圈 etc. where the player will not honor embedded subtitle tracks. ### Verify libass is available BEFORE promising burn-in ```bash ffmpeg -filters 2>&1 | grep -E "subtitles|^.. ass " ``` If neither `subtitles` nor `ass` shows up, the build lacks libass. Homebrew's default `ffmpeg` formula is often stripped (no `--enable-libass`, no `--enable-libfreetype`, no `drawtext`). Don't waste time fighting the comma-escaping inside `force_style` — it will fail with `No such filter: 'subtitles'` no matter how the shell quotes it. ### Fastest fix on macOS — drop in a static build, no system changes ```bash curl -fsSL -o /tmp/ff.zip https://evermeet.cx/ffmpeg/getrelease/zip unzip -o /tmp/ff.zip -d /tmp/ff_bin >/dev/null FF=/tmp/ff_bin/ffmpeg $FF -version | grep -oE -- "--enable-(libass|libfreetype)" ``` Then use `$FF` instead of `ffmpeg` for the render. The brew binary is fine for everything else (probe, audio extraction, soft-mux). `render.py` does this auto-fallback if its default ffmpeg lacks libass. ### Burn-in render with style overrides 🛑 **Checkpoint — confirm before full-render.** Burn-in re-encodes the entire video (minutes of CPU on a 5-min clip). Before kicking it off: 1. Render only the first 30s with `-t 30` for a fast preview. 2. Extract a frame from the longest-line cue (see Fontsize calibration below) and Read it. 3. Show the user the preview frame + the cue text, ask: "字号/字体/边距 OK 吗?OK 才跑全片。" Wait for explicit confirmation. Skip the checkpoint only if the user has already approved a full render of this exact video at this exact font config in the same conversation. ```bash $FF -i input.mp4 \ -vf "subtitles=input.zh-CN.srt:force_style='Fontname=PingFang SC\,Fontsize=12\,PrimaryColour=&H00FFFFFF\,OutlineColour=&H00000000\,BorderStyle=1\,Outline=2\,Shadow=1\,MarginL=20\,MarginR=20\,MarginV=40'" \ -c:v libx264 -crf 18 -preset medium -pix_fmt yuv420p \ -c:a copy output.mp4 ``` Inside `force_style`, escape every comma as `\,` (the filter graph parser eats the bare comma as a chain separator). All other special chars are fine. ### Fontsize calibration — critical libass scales its internal PlayRes up to the actual video resolution. The number you pass is **not pixels** in the output. As a starting calibration on a 544×960 vertical phone video, `Fontsize=22` rendered each Chinese character at ~55px wide and overflowed the frame, while `Fontsize=12` rendered at ~30–35px wide and fit cleanly with 15-char lines. Rule of thumb: start at `Fontsize=12`, render, then **always** extract a frame and look: ```bash $FF -ss 30 -i output.mp4 -frames:v 1 /tmp/frame.png -y # then Read /tmp/frame.png to verify the longest-line cue fits ``` Pick a timestamp that lands on the cue with the most characters per line — short lines won't expose overflow. Add `MarginL=20 MarginR=20` as a safety inset; never trust default left/right margins. ### Style cheatsheet Keys that matter (libass `force_style`): - `Fontname=PingFang SC` — macOS default CJK; alternates: `Songti SC`, `Heiti SC`, `STHeiti`, `Hiragino Sans GB`. - `Fontsize=12` — start small, scale up only after frame check. - `PrimaryColour=&H00FFFFFF` — white text (BBGGRR + alpha). - `OutlineColour=&H00000000` — black outline. - `BorderStyle=1` — outline only (clean over varied backgrounds). Use `BorderStyle=3` for an opaque box behind text when the background is busy. - `Outline=2` — 2px outline thickness. - `Shadow=1` — subtle drop
Repo-wide drift detector for the wjs-* Claude Code skills in this marketplace. Sweeps every SKILL.md, scores it against the repo's own conventions (V-ing naming, trigger-phrase density, companion files, description shape), and returns a grouped punch list ordered by severity. Read-only — never edits files. Use before pushing a batch of skill changes, or whenever you wonder "are these skills still internally consistent?
|
Use when the user asks to audit what's wrong with a project, "make it right", "看看项目出了什么问题", "为什么用户的需求还没上线", "为什么没提交App Store", "为什么没新build", or wants a holistic state-of-the-project check covering unmerged branches, stalled PRs, failed GitHub Actions, stale builds, plan drift (TODOS.md / ROADMAP), unreleased commits, and log errors. Runs read-only investigation, presents a grouped checklist, fixes only after explicit user confirmation. Aware of the Cathier iOS app workflow (Xcode + fastlane + auto-merge @claude PRs from in-app feedback).
Use when the user complains about spam on his X/Twitter posts — 同城面付 / 寻固炮 / 线下上门 / 免费破处 这类引流号在他推文下刷的 emoji 垃圾回复 — and wants them removed. Covers the last 7 days (X recent-search window). Triggers — "把这些spam删掉", "清理X垃圾回复", "推文下面好多引流号", "clean spam replies", "/wjs-cleaning-spam".
Use when the user wants a 王建硕-style WeChat article (article.md) turned into a narrated short MP4 video — TTS voiceover via 火山引擎 Volcano TTS, HyperFrames CSS/GSAP animation per scene, subtle SFX, abstract watercolor background, full pipeline rendering to 1080×1920 portrait MP4 (30-90s). Triggers — "把这篇文章做成视频", "做一个解说视频", "讲解视频", "/wjs-converting-text-to-video".
Use when migrating a WordPress site to a Hugo static site on GitHub Pages from a WXR export (.xml) plus the wp-content/uploads folder — preserving /archives/<id>/ URLs, localizing images, and deploying via GitHub Actions. Triggers — "把 WordPress 迁成 Hugo", "wordpress 转静态站", "migrate WordPress to Hugo", "WXR to Hugo", "publish WordPress to GitHub Pages", "/wjs-converting-wp-to-hugo".
Use when the user has a video + a target-language SRT and wants the video to actually speak that language — generates a time-aligned TTS voice dub. Routes by voice ID — Volcano (豆包) TTS for Chinese, edge-tts neural for any language. Defaults to one voice (single-speaker); opt-in multi-speaker via visual diarization. Outputs `*_<lang>_dub.mp4` with the dub audio in place of the original. Final mixing (audio bed + burn-in) is handed off to `/wjs-burning-subtitles`. Triggers — "配音", "中文配音", "Chinese dub", "voice over this", "dub the video", "TTS this SRT", "different voice for each speaker".
吃一堑长一智 — 走完 5 步交互式反思(堑 → 自动输出 → 旧权重 → 新参数 → 替代动作),从「情绪复盘」推进到「行为训练」,把第一反应这一层 L3 权重练新。Use when 王建硕 reflects on a personal setback, mistake, or recurring pattern (反思, 复盘, 回顾, 总结教训, 吃一堑, 长一智, "这次又栽了", "怎么又这样", "为什么我总是…", "想开点都做不到", "知道道理但做不到"). For the user as a human, not for Claude's task post-mortems.