
Producing Video
- 126 installs
- 130 repo stars
- Updated June 19, 2026
- sugarforever/01coder-agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
producing-video is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- producing-video
- AI & Agent Building
- AI-coding skill
Producing Video by the numbers
- 126 all-time installs (skills.sh)
- +17 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #3,699 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sugarforever/01coder-agent-skills --skill producing-videoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 126 |
|---|---|
| repo stars | ★ 130 |
| Last updated | June 19, 2026 |
| Repository | sugarforever/01coder-agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Producing-Video · 音频 + 字幕 → 成片
把"用户已经录好的口播音频 + SRT 字幕"做成一条画面跟着声音走的 MP4。用 HyperFrames(HTML 即视频)出片。
铁律:音频和 SRT 是唯一事实源。 画面的内容来自 SRT,画面的时间轴来自 SRT 的 cue 时间戳,音频作为一个 <audio> clip 直接挂进合成里、渲染时自动合流 —— 没有"先出视频再合音频"这一步。
分工(很重要)
| 谁 | 做什么 |
|---|---|
| 用户 | 写稿 → 录音/合成音频 → 生成 SRT → 把 audio.mp3 + audio.srt 交给你 |
| 本 skill(你) | 选 frame/品牌 → 按 SRT 搭 HyperFrames 合成 → 校对 → 渲染成片 |
用户不指望你生成配音(那是上游)。如果用户还没有音频、问的是"怎么配音",那不是本 skill —— TTS / 声音克隆是另一条线(见下方"超出范围")。
依赖检查(pre-flight)
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 往往是有意的,先问)。
---
工作流
Step 1 · 选 frame / 品牌
画面的风格 = 一个 frame.md / visual-style / 既有系列品牌。三种来源,按情况选:
1. 延续系列品牌(推荐用于日更/系列)。如果这期属于一个已有系列(如"AI 早读"),去翻该系列的封面/历史成片,沿用同一套 token(颜色、字体、栅格、页眉页脚),让视频和封面一脉相承。 2. HyperFrames frame.md 模板。用户可能直接点名,例如 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 + 字体名)。
3. 8 个内置 visual-style(Swiss Pulse / Velvet Standard / Shadow Cut / Maximalist 等)。在 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;拉丁:latinStep 2 · 起项目
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.srtStep 3 · 解析 SRT → 切场景
读完整 SRT(scripts/srt-cues.mjs 可打印每条 cue 的开始秒数 + 文本,方便规划)。然后:
1. 按内容把 cue 归成"幕/场景"。一条 SRT cue 通常是一句话;把讲同一件事的几条 cue 合成一个场景(scene)。一支 6~7 分钟的日更,大约 12~16 个场景比较舒服(平均 ~30s/场,画面不至于久不动)。 2. 场景开始时间 = 它第一条 cue 的开始时间戳(秒)。 3. 场景时长 = 下一场开始 − 本场开始 + ~0.5s(这 0.5s 重叠让转场 wipe 能盖住上一场,避免穿帮)。最后一场到音频结束。 4. 场景内的逐步出现(sub-reveal)= 对应子 cue 的开始时间。让标题/要点/数字"在被念到的那一刻"出现 —— 这是同步感的关键。 5. 画面文案从 SRT 来,且不能和口播打架。可以精炼成海报式短句,但不能说的是 A、画面写 B。顺手核对事实(数字、专有名词、人名)—— 用户很在意准确。
把场景开始时间放进一个 JS 数组const B = [...],所有 tween 用B[i-1] + 局部偏移定位。日后微调时间轴只改数组,不用逐条改 tween。
Step 4 · 编写合成(index.html)
- 持久 chrome:页眉/页脚/栅格/边框这类每一场都在的元素,放在场景之外(直接挂
#root,不是 clip),整片不动 —— 营造"节目"感。 - 音频一条连续 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递增(后面的盖前面的)。 - 转场只用"incoming 场景自身的 clip-path 揭幕"(见 Gotcha「转场」):
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。 - 复用一套 class 化的 CSS 工具件(kicker / headline / note / stat / chip-xform / cards / numbered-list / accent-mark),15 个场景共享,别每场写一套 id 样式。
- 品牌纪律:严格按 frame 的 token;强调色当"标点"不当"填充"(深色系:强调色只给小而实的标记;浅色系:强调色做色块、文字用墨色压在色块上)。
Step 5 · 校对(每次改完都跑)
npx hyperframes lint # 0 error 才继续(var(--x) 字体告警是误报,可忽略)
npx hyperframes validate # WCAG AA 对比度;改掉过暗的次级灰
npx hyperframes inspect --samples 30 # 版面溢出,带时间戳;场景多就多采样装饰元素故意出血到画外 → 标 data-layout-ignore。真实溢出 → 改容器/字号/padding。
Step 6 · 渲染 + 验收
- 先 standard 跑一版自检(快),用
ffmpeg抽几帧验证同步:在"你知道这一刻在讲什么"的时间点抽帧,确认画面对得上。
ffmpeg -y -ss <秒> -i renders/x.mp4 -frames:v 1 /tmp/f.png # 然后看图- 成片渲染(master):
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/。不要自动提交(除非用户明确要)。
---
Gotchas(血泪,务必遵守)
这些是踩过的坑,违反任何一条都会出废片:
1. 音频即时钟。场景时长从 SRT 量出来,不要凭感觉定时长再硬塞音频。 2. 有 SRT 就别转写。用户给了 SRT = 精确时间轴免费拿到,不需要 Whisper。(没 SRT 想自己转写前先问用户。) 3. 转场只用场景自身 clip-path 揭幕。绝不用单独的全屏色块/幕布/刀闸/砸场板去做转场 —— 在渲染引擎里它会"扫进来盖住下一场后卡住不走",整场变成纯色/黑屏。揭幕揭的是 incoming 场景本体。 4. 不要给 `.pad` 容器套整体 opacity 的 "pushIn" 包装。容器级 opacity 动画在 seek 渲染里可能留在 0,把整场变黑。用每个元素各自的 gsap.from()。 5. 配对 tween 不要加 `overwrite:"auto"`。它会把配对的另一条 tween 杀掉(比如"扫入"在、"扫出"没了)。lint 的 overlapping_gsap_tweens 是无害告警,宁可留着。 6. 字体必须本地 woff2。Google Fonts <link> 会被 lint 标记、且 sandbox 渲染里不可靠。中文配 Noto Sans SC;中文字在彩色 accent 色块里要给足竖直 padding/line-height(CJK 字形比 em 框高,padding 太紧 inspect 会报 text_box_overflow,给到 ~0.2em 竖直 padding + line-height ~1.12)。 7. 深色主题别信亮度探测。1×1 平均亮度对深底+稀疏文字永远偏低,会把正常场景误判成黑屏。靠抽帧看图确认。 8. 浅色主题的对比度误报。validate 在固定几个时间戳采样所有 DOM 文字,包括当时未激活的场景。浅底上"浅色文字"(白字、奶油字)一旦不在自己场景的激活时刻被采到,就报低对比度——这是误报。规避:accent 色块上一律用墨色(深)文字,未激活时墨字压奶油底仍是高对比,零误报。 9. 确定性。禁止 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。
SRT → 场景时间轴(配方)
场景[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]); }超出范围
- 生成配音 / 声音克隆:本 skill 只吃用户给的音频。要让它"听起来像我"用云端 TTS(MiniMax / ElevenLabs,支持中文 + 克隆),克隆只换"出声那一步",下游时间轴/挂载/渲染不变。HyperFrames 自带的 Kokoro TTS 做不了中文(CLI 传
zh、espeak 要cmn,且质量差);本机临时方案 macOSsay -v Tingting。 - 幻灯片 deck:要的是横向翻页 deck 而非视频 → 用 slides 类 skill。
输出清单
- [ ]
lint0 error ·validate全过 ·inspect0 issue - [ ] 抽帧确认每场落在它的 cue 上(尤其数据页/转折页)
- [ ]
ffprobe:video + audio 两轨、时长 = 音频时长 - [ ] master 用 4K high(除非用户另说)
- [ ] 成片在
studio/videos/<slug>/renders/,未自动提交
#!/usr/bin/env node
// srt-cues.mjs — print every SRT cue as "start_seconds | text" for scene planning.
// Usage: node srt-cues.mjs path/to/narration.srt
// node srt-cues.mjs narration.srt --json # machine-readable [{i,start,end,text}]
import { readFileSync } from "node:fs";
const file = process.argv[2];
const asJson = process.argv.includes("--json");
if (!file) {
console.error("usage: node srt-cues.mjs <file.srt> [--json]");
process.exit(1);
}
const toSec = (ts) => {
// 00:01:23,456 -> 83.456
const m = ts.trim().match(/(\d+):(\d+):(\d+)[,.](\d+)/);
if (!m) return null;
return +m[1] * 3600 + +m[2] * 60 + +m[3] + +m[4] / 1000;
};
const blocks = readFileSync(file, "utf8").replace(/\r/g, "").split(/\n\n+/);
const cues = [];
for (const b of blocks) {
const lines = b.split("\n").filter((l) => l.trim() !== "");
if (!lines.length) continue;
const tl = lines.find((l) => l.includes("-->"));
if (!tl) continue;
const [a, z] = tl.split("-->");
const start = toSec(a);
const end = toSec(z);
if (start == null) continue;
const text = lines.slice(lines.indexOf(tl) + 1).join(" ").trim();
cues.push({ i: cues.length + 1, start, end, text });
}
if (asJson) {
console.log(JSON.stringify(cues, null, 2));
} else {
for (const c of cues) {
console.log(`${c.start.toFixed(3).padStart(9)} | ${c.text}`);
}
const last = cues[cues.length - 1];
console.log(`\n${cues.length} cues · audio ends ~${last ? last.end.toFixed(1) : "?"}s`);
}