sugarforever/producing-video
Turn a user-provided voiceover audio file + SRT subtitle into a finished, narration-synced MP4 using HyperFrames (HTML-to-video). The audio + SRT are the source of truth — scenes are timed to the SRT cues, content is read from the SRT, and the audio is muxed in automatically. Use when the user hands over an mp3/wav + srt and wants a video, says "把音频做成视频", "做一期视频", "audio + srt to video", "把这期早读做成视频", "render this narration into a video", or provides a recording + subtitles for an explainer / daily / 解读 / 口播. NOT for generating the voiceover (that's the user's job here) and NOT for slide decks (use a slides skill).
npx skills add https://github.com/sugarforever/01coder-agent-skills --skill producing-video
把"用户已经录好的口播音频 + SRT 字幕"做成一条画面跟着声音走的 MP4。用 HyperFrames(HTML 即视频)出片。
铁律:音频和 SRT 是唯一事实源。 画面的内容来自 SRT,画面的时间轴来自 SRT 的 cue 时间戳,音频作为一个 <audio> clip 直接挂进合成里、渲染时自动合流 —— 没有"先出视频再合音频"这一步。
| 谁 | 做什么 |
|---|---|
| 用户 | 写稿 → 录音/合成音频 → 生成 SRT → 把 audio.mp3 + audio.srt 交给你 |
| 本 skill(你) | 选 frame/品牌 → 按 SRT 搭 HyperFrames 合成 → 校对 → 渲染成片 |
用户不指望你生成配音(那是上游)。如果用户还没有音频、问的是"怎么配音",那不是本 skill —— TTS / 声音克隆是另一条线(见下方"超出范围")。
npx hyperframes doctor # 需要 Node ≥ 22 · FFmpeg · Chrome
需要 hyperframes / hyperframes-cli 两个 skill 在场(编写合成 + 跑 CLI)。缺了就让用户 npx hyperframes skills 安装后重来。
确认用户给了两个文件:音频(mp3/wav/m4a)+ SRT。只给音频没给 SRT → 本 skill 需要 SRT 拿时间轴;可让用户补 SRT(很多录音工具/剪辑软件能导出),不要默认去跑 Whisper 转写(用户没给 SRT 往往是有意的,先问)。
画面的风格 = 一个 frame.md / visual-style / 既有系列品牌。三种来源,按情况选:
creative-mode / biennale-yellow / cobalt-grid。取 token:curl -sSL https://www.hyperframes.dev/design/<slug>.md (站点是 JS 渲染,多半取不到正文);open-design 仓库里找 design-templates/*<slug>*/template.json,里面有精确的 palette / typography(hex + 字体名)。hyperframes skill 的 visual-styles.md 里,直接抄 YAML token。字体一律本地 woff2(见 Gotcha "字体")。中文必须配 Noto Sans SC(400/500/700/900 视用量);英文 display 按 frame 选(Archivo Black / Manrope / Oswald…);标签数字常用 JetBrains Mono。从 fontsource CDN 下到项目的 fonts/:
curl -sSL -o fonts/<name>.woff2 "https://cdn.jsdelivr.net/fontsource/fonts/<family>@latest/<subset>-<weight>-normal.woff2"
# 中文:subset 用 chinese-simplified;拉丁:latin
cd <repo>/studio/videos # 仓库约定:成片放这里
npx hyperframes init <YYYYMMDD-slug> --example blank --non-interactive
cd <YYYYMMDD-slug> && mkdir -p fonts audio
cp <user-audio> audio/narration-full.mp3
cp <user-srt> audio/narration.srt
读完整 SRT(scripts/srt-cues.mjs 可打印每条 cue 的开始秒数 + 文本,方便规划)。然后:
> 把场景开始时间放进一个 JS 数组 const B = [...],所有 tween 用 B[i-1] + 局部偏移 定位。日后微调时间轴只改数组,不用逐条改 tween。
#root,不是 clip),整片不动 —— 营造"节目"感。 <audio id="vo" src="audio/narration-full.mp3" data-start="0" data-duration="<总时长>" data-track-index="20" data-volume="1"></audio>
媒体元素不需要 class="clip";给它独立的 data-track-index。
.scene.clip,各自独立 data-track-index(重叠的 wipe 需要不同 track),z-index 递增(后面的盖前面的)。 function wipe(sel, at){ tl.fromTo(sel,{clipPath:"inset(0 100% 0 0)"},{clipPath:"inset(0 0% 0 0)",duration:0.5,ease:"power3.inOut"}, at); }
gsap.from(),定位在对应 cue 时间。短(0.3–0.7s),错峰,变化 ease。npx hyperframes lint # 0 error 才继续(var(--x) 字体告警是误报,可忽略)
npx hyperframes validate # WCAG AA 对比度;改掉过暗的次级灰
npx hyperframes inspect --samples 30 # 版面溢出,带时间戳;场景多就多采样
装饰元素故意出血到画外 → 标 data-layout-ignore。真实溢出 → 改容器/字号/padding。
ffmpeg 抽几帧验证同步:在"你知道这一刻在讲什么"的时间点抽帧,确认画面对得上。 ffmpeg -y -ss <秒> -i renders/x.mp4 -frames:v 1 /tmp/f.png # 然后看图
npx hyperframes render --resolution landscape-4k --quality high --output renders/<slug>-4k.mp4
--resolution landscape-4k 是把同一合成按 2× DPR 真·超采样到 3840×2160(不是放大);4K master 即使观众看 1080p 也更耐平台二压。4K + 长片渲染较久(几分钟到十几分钟),可后台跑。
ffprobe 确认有 video(h264) + audio(aac) 两条轨且时长对得上;抽帧确认每场落在它的 cue 上。成片留在 studio/videos/<slug>/renders/。不要自动提交(除非用户明确要)。
这些是踩过的坑,违反任何一条都会出废片:
.pad 容器套整体 opacity 的 "pushIn" 包装。容器级 opacity 动画在 seek 渲染里可能留在 0,把整场变黑。用每个元素各自的 gsap.from()。overwrite:"auto"。它会把配对的另一条 tween 杀掉(比如"扫入"在、"扫出"没了)。lint 的 overlapping_gsap_tweens 是无害告警,宁可留着。<link> 会被 lint 标记、且 sandbox 渲染里不可靠。中文配 Noto Sans SC;中文字在彩色 accent 色块里要给足竖直 padding/line-height(CJK 字形比 em 框高,padding 太紧 inspect 会报 text_box_overflow,给到 ~0.2em 竖直 padding + line-height ~1.12)。Date.now() / Math.random()(破坏可复现渲染);要随机用种子化 PRNG。10. 每个场景独立 track-index;音频单独高 track-index。装饰出血标 data-layout-ignore。
11. 画质:原生 1080p 在 Retina 上看会发虚(被放大 + H.264 4:2:0 软化彩色字缘);master 用 --resolution landscape-4k --quality high。
场景[i].start = cue[第一条].start (秒)
场景[i].duration = 场景[i+1].start − 场景[i].start + 0.5 (末场到音频末尾)
场景内某元素入场 = 它对应 cue 的 start
音频 clip = data-start=0, data-duration=总时长
root data-duration = 音频末句之后留 ~3s 收尾
JS 里:
const B = [0, 38.63, 53.96, /* ...每场 start... */]; // 从 SRT 量
const at = (i, off) => B[i-1] + off; // i 是 1-based 场号
function wipe(sel, i){ tl.fromTo(sel, {clipPath:"inset(0 100% 0 0)"}, {clipPath:"inset(0 0% 0 0)", duration:0.5, ease:"power3.inOut"}, B[i-1]); }
zh、espeak 要 cmn,且质量差);本机临时方案 macOS say -v Tingting。lint 0 error · validate 全过 · inspect 0 issueffprobe:video + audio 两轨、时长 = 音频时长studio/videos/<slug>/renders/,未自动提交Take sugarforever/producing-video from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.