vid-product-launch
从产品描述和发布背景中,生成一个叙事性的 MP4 产品发布视频。 流程:HTML/CSS 动画 → 无头 Chromium(Playwright,逐帧抓取)→ FFmpeg(H.264 MP4)。 无需 React。无需 AI 视频 API。无需 Python。运行成本仅为 Playwright + FFmpeg。
关键规则(每次生成前必读)
-
标语不可省略。不要跳过它。 它是产品对用户的核心承诺,只需 4-6 个词。如果用户未提供,请从描述中推导出一个——一句精炼、主动语态的总结。绝不要写 "[在此处填写标语]" 或留空。
-
揭秘时刻必须让人觉得是水到渠成。 铺垫和构建部分的存在是为了制造悬念。如果产品名称在视频的前 20% 出现,叙事就崩塌了。在揭秘部分之前,绝不要透露产品名称。
-
产品名称 字号最小:16:9 宽高比下不小于 120px,9:16 宽高比下不小于 80px。 产品名称在揭秘时刻必须是最大的元素。如果它不占主导地位,揭秘就失败了。
-
一个佐证数据,而不是一个列表。 如果用户提供了多个数据,请选择最强的那一个。列出 5 个数字会破坏冲击力。一个被突出的巨大数字才能创造它。
-
使用
window.renderFrame(t)——不要使用 CSS@keyframes进行场景过渡。 在 Chromium 中,CSS 的currentTime定位对于向后定位会被静默忽略。renderFrame函数直接从毫秒数计算元素样式。Playwright 每帧调用一次。 -
不要在任意元素上使用
animation-delay。 使用renderFrame就不需要它。如果你写了animation-delay,停下来——你正在使用错误的架构。 -
window.__videoReady = true只能放在document.fonts.ready.then(...)内部。 绝不要同步设置。 -
暴露
window.__stopPreview()。rAF预览循环会与 Playwright 的evaluate/screenshot调用产生争用。捕获脚本会在帧循环之前调用__stopPreview()。 -
在场景边界检查中使用
t < startMs(而不是t <= startMs)。 在第 0 帧时t <= 0会使场景 1 变黑。 -
Body = 精确的像素尺寸。 宽高为整数。不使用
%、vw/vh、rem。 -
在生成任意 HTML 之前,请先阅读
references/scene-library.md和references/style-presets.md。 -
绝不要在聊天中直接输出 HTML。 请保存到文件中。只显示摘要。
-
胶片颗粒的画布必须是 240×135,而不是 W×H。 在 canvas 元素上设置
width="240" height="135",然后通过style="width:[W]px;height:[H]px"进行拉伸。在 1920×1080 分辨率下使用全分辨率颗粒会慢 64 倍——每帧产生 8MB 的 ImageData——这将使 1800 帧的导出花费数小时。 -
铺垫部分必须是暗色调(#000 或接近黑色),无论何种风格。 铺垫部分使用白色背景看起来像演示幻灯片,而不是产品发布视频。从暗到亮的叙事弧线(暗色调铺垫 → 暗色调构建 → 产品揭秘)正是发布视频营造戏剧效果的方式。即使是
minimal和energetic预设,也应在铺垫问题场景和张力构建场景中使用background: #000。 -
每个铺垫词都必须是独立的节奏点,字号为 200px 以上。 将多个词逐词放在一行不够有冲击力。每个问题词(如 "Research." / "Write." / "Outreach." / "Repeat.")在居中显示时,应获得 1500-1800ms 的屏幕时间,字号为
font-size: 200px; font-weight: 900,一次只显示一个。请参考 scene-library.md 中的tease-words模式。 -
构建部分必须展示能叙述问题的内容——不仅仅是粒子效果。 使用
terminal-card模式:3 张卡片依次出现,并带有打字机动画,展示正在进行的体力劳动。卡片带有border: 1px solid rgba(255,255,255,0.07)、蓝色圆点强调、等宽文本。仅靠粒子效果持续 20 秒是空白的屏幕时间。 -
在暗色调场景上使用点阵 CSS 背景。 在构建、揭秘、佐证和 CTA 场景中,添加
background-image: radial-gradient(circle, rgba(255,255,255,0.035) 1px, transparent 1px); background-size: 60px 60px;。它几乎不消耗计算资源,但能增加深度感。
第一步:信息收集
必需:
product_name—— 正在发布的产品或功能名称product_description—— 2-3 句话:它做什么,为谁设计,核心优势
可选参数及默认值:
| 参数 | 默认值 | 描述 |
|---|---|---|
| tagline | 自动 | 4-6 个词 —— 产品的核心承诺 |
| problem_statement | 自动推断 | 用于铺垫部分的 1 句话(产品要解决的痛点) |
| proof_stat | 自动推断 | 单一指标(例如 "500+ 团队"、"10 倍更快"、"节省 $2M") |
| cta | 自动 | URL 或行动短语(例如 "opendirectory.dev"、"加入候补名单") |
| launch_date | — | ISO 日期字符串(启用倒计时卡片场景) |
| tone | cinematic | cinematic / energetic / minimal / emotional |
| duration | 60 | 30 / 60 / 90(秒) |
| aspect_ratio | 16:9 | 16:9 (1920×1080) / 9:16 (1080×1920) |
| letterbox | false | 电影宽银幕 2.35:1 黑色边框 —— 仅限 16:9 |
| music | — | 音频文件路径 (mp3/m4a/wav) |
| fps | 30 | 每秒帧数 (24, 30, 或 60) |
如果 product_name 或 product_description 缺失,请准确询问:
"要制作发布视频,我需要两样东西:
- 产品名称 —— 这个产品叫什么?
- 产品描述 —— 2-3 句话:它做什么,为谁设计,核心优势是什么?
可选:标语 (4-6 个词),佐证数据(一个数字),CTA(URL 或行动短语),风格(cinematic / energetic / minimal / emotional),时长 (30 / 60 / 90s)。"
如果两者都存在 → 立即进入第二步。
第二步:内部架构(绝不向用户展示)
1. 推导缺失的参数:
- tagline(如未提供):根据
product_description编写一个。4-6 个词。主动语态。无填充词("The future of…"、"Introducing…")。推导出最尖锐的承诺。 - problem_statement(如未提供):从
product_description推断。用 1 句话陈述痛点。绝不提及产品名称。编写的目的是让观众点头认同。 - proof_stat(如未提供):从
product_description推断。如果没有可用数据,请使用一个可信的规模指标("受 500+ 团队信赖"、"从数天到数分钟")。 - cta(如未提供):如果可以从
product_name推断出产品域名,则使用该域名,否则使用行动短语 "了解详情"。
2. 根据 duration 计算各部分时间:
| 部分 | 30s | 60s | 90s | 起始公式 |
|---|---|---|---|---|
| 铺垫 | 0-5s | 0-10s | 0-15s | 0ms |
| 构建 | 5-12s | 10-30s | 15-40s | 铺垫结束时间 |
| 揭秘 | 12-20s | 30-45s | 40-60s | 构建结束时间 |
| 佐证 | 20-25s | 45-55s | 60-75s | 揭秘结束时间 |
| CTA | 25-30s | 55-60s | 75-90s | 佐证结束时间 |
将每个边界转换为毫秒。赋值给常量:
TEASE_START_MS, TEASE_END_MS
BUILD_START_MS, BUILD_END_MS
REVEAL_START_MS, REVEAL_END_MS
PROOF_START_MS, PROOF_END_MS
CTA_START_MS, CTA_END_MS
3. 为每个部分选择场景:
- 铺垫: 始终以
blackout-opener开始 (1500ms)。然后是tease-words(每个问题关键词作为自己的 1600-1800ms 节奏点,字号 200px 以上)。如果提供了launch_date,可选择在铺垫末尾添加countdown-card。 - 构建:
terminal-card序列(3 张卡片,展示正在输入的手动任务)+ 可选的结束强调行。时长 ≥ 30s 会展示全部 3 张卡片。时长 = 30s 使用 2 张卡片。绝不在超过 5 秒的时间内仅使用纯粒子效果——屏幕上必须展示能够叙述问题的内容。 - 揭秘:
reveal-hero—— 始终是该部分的第一项。如果时长 ≥ 60s,可以选择后跟tagline-card。 - 佐证: 如果有数字数据可用,则使用
proof-stat。否则使用feature-bullet。 - CTA:
cta-card—— 始终、始终放在最后。
典型的场景数量:30s = 4 个场景,60s = 6-7 个场景,90s = 7-8 个场景。
4. 计算每个场景的时间:
在每个部分内,将该部分的时间平均分配给该部分中的各个场景。
例外:blackout-opener 始终正好占据铺垫部分的前 1500ms。
5. 确定像素尺寸:
16:9→ W=1920, H=10809:16→ W=1080, H=1920
6. 宽银幕计算(如果启用,仅限 16:9):
- 2.35:1 内容高度 = W / 2.35 = 1920 / 2.35 ≈ 817px
- 条状高度 = (H - 817) / 2 = (1080 - 817) / 2 ≈ 132px
- 顶部条:
top: 0; height: 132px - 底部条:
bottom: 0; height: 132px
7. 电影效果标志(按预设):
cinematic:胶片颗粒=开启(画布 240×135,不透明度 0.025),暗角=开启,光漏=开启(暖金色),点阵背景=在暗场景上开启energetic:胶片颗粒=关闭,暗角=关闭,点阵背景=开启,揭秘时白色闪光minimal:胶片颗粒=开启(画布 240×135,不透明度 0.022),暗角=开启(柔和,0.5 径向),点阵背景=开启,无光漏——强调色 = #4B9FFF(电光蓝)emotional:胶片颗粒=开启(画布 240×135,不透明度 0.018,混合暖色调),暗角=开启(柔和),光漏=开启(铜色暖调),点阵背景=关闭
所有预设:无论何种预设,铺垫和构建场景始终使用暗色背景 (#000 或接近黑色)。
8. 将 window.__sfxTimeline 嵌入 HTML 中(始终执行——即使没有提供音乐):
导出脚本会读取此数组,使用 FFmpeg 的 aevalsrc/anoisesrc 原生合成每种 SFX 类型,并使用 adelay 将事件放置在精确的毫秒偏移处。无需外部音频文件。
SFX 类型参考:
| 类型 | 声音 | 时长 | 备注 |
|---|---|---|---|
word-hit | 低音重击 (50Hz) + 瞬态咔哒声 (2.2kHz) + 噪声爆发 | 180ms | 每个铺垫词一个 —— 通过 amix 混合 3 层 |
type-sequence | 频率 15Hz 的机械键盘咔哒声(sin^30 脉冲包络) | 1.9s | 每张卡片一个 —— 持续整个 TYPE_DUR 时间 |
whoosh | 双频段噪声扫频 (1.1kHz 主体 + 4-8kHz 空气感) | 700ms | 在 BUILD_CLOSE 过渡处 |
tension-riser | 低频隆隆声 (250Hz) + 随 t/2.8 斜坡增长的低音 | 2.9s | 在 BUILD_CLOSE + 700ms 处开始 —— 在 REVEAL_START 处达到峰值 |
reveal-boom | 低音 (45Hz) + 主体 (90Hz) + 闪光 (4.5-11kHz) + 85ms 回声 | 900ms | 正好在 REVEAL_START 处 —— 重头戏 |
counter-tick | 谐波咔嗒声 (880Hz + 440Hz + 1760Hz + 2640Hz) | 80ms | 3 拍 —— 渐弱 0.32 → 0.26 → 0.20 |
cta-chime | A 大调和弦 (440 + 554 + 659 + 880Hz) + 类似回声的钟声闪光 | 1.2s | 正好在 CTA_START 处 |
SFX 时间映射 —— 根据部分常量计算 ms 值,绝不硬编码:
window.__sfxTimeline = [
// ── 铺垫:每个词一个 word-hit,最后一个更响 ───────────────────────
{ ms: WORD_BEATS[0].start, sfx: 'word-hit', vol: 0.55 },
{ ms: WORD_BEATS[1].start, sfx: 'word-hit', vol: 0.55 },
{ ms: WORD_BEATS[2].start, sfx: 'word-hit', vol: 0.55 },
{ ms: WORD_BEATS[N].start, sfx: 'word-hit', vol: 0.65 }, // 最后一个词更响
// ── 构建:每张卡片一个 type-sequence (1.9s, 匹配 TYPE_DUR 1800ms) ────────
{ ms: CARDS[0].start + 180, sfx: 'type-sequence', vol: 0.28 },
{ ms: CARDS[1].start + 180, sfx: 'type-sequence', vol: 0.28 },
{ ms: CARDS[2].start + 180, sfx: 'type-sequence', vol: 0.28 },
// ── 过渡:whoosh 然后 2.9s 的 riser,在 REVEAL_START 处达到峰值 ───────────
{ ms: BUILD_CLOSE, sfx: 'whoosh', vol: 0.50 },
{ ms: BUILD_CLOSE + 700, sfx: 'tension-riser', vol: 0.35 },
// ── 揭秘:电影感重击 ─────────────────────────────────────────────
{ ms: REVEAL_START, sfx: 'reveal-boom', vol: 0.88 },
// ── 佐证:谐波滴答声,渐弱 ───────────────────────────────────
{ ms: PROOF_START, sfx: 'counter-tick', vol: 0.32 },
{ ms: PROOF_START + 2200, sfx: 'counter-tick', vol: 0.26 },
{ ms: PROOF_START + 4400, sfx: 'counter-tick', vol: 0.20 },
// ── CTA:A 大调和弦着陆 ────────────────────────────────────────────
{ ms: CTA_START, sfx: 'cta-chime', vol: 0.62 },
];
将此代码块紧放在预览循环之前,放在 window.renderFrame 之后。
第三步:HTML 生成
在编写任何代码之前,请先阅读 references/scene-library.md 和 references/style-presets.md。
使用这些文件中精确的 CSS 类名、HTML 结构和 renderFrame 模式。
必需的 HTML 骨架:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
[样式预设中的字体 CDN 链接]
<style>
:root {
[样式预设中的所有 CSS token]
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: [W]px; height: [H]px;
overflow: hidden;
background: var(--bg);
font-family: var(--font-body);
position: relative;
}
.scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px;
opacity: 0;
will-change: opacity, transform;
}
.scene-inner {
width: 100%;
max-width: 1200px;
}
[粘贴 scene-library.md 中每种场景类型的 CSS]
</style>
</head>
<body>
<!-- 电影叠加元素 —— 渲染在所有场景之上 -->
[如果胶片颗粒:开启]
<canvas id="grain-overlay"
width="[W]" height="[H]"
style="position:fixed;inset:0;pointer-events:none;opacity:0.025;mix-blend-mode:overlay;z-index:50"></canvas>
[结束 if]
[如果暗角:开启]
<div id="vignette-overlay"
style="position:fixed;inset:0;background:radial-gradient(ellipse at center,transparent 35%,rgba(0,0,0,0.65) 100%);pointer-events:none;z-index:51"></div>
[结束 if]
[如果光漏:开启]
<div id="light-leak"
style="position:fixed;inset:0;background:linear-gradient(135deg,rgba(255,220,140,0.5) 0%,rgba(255,255,255,0.3) 45%,transparent 70%);opacity:0;pointer-events:none;z-index:52"></div>
[结束 if]
[如果宽银幕:开启]
<div id="lbox-top" style="position:fixed;top:0;left:0;width:[W]px;height:132px;background:#000;z-index:100"></div>
<div id="lbox-bot" style="position:fixed;bottom:0;left:0;width:[W]px;height:132px;background:#000;z-index:100"></div>
[结束 if]
<!-- 场景 -->
[scene-library.md 模板中的场景 HTML,每个选中的场景一个]
<script>
window.__videoReady = false;
window.TOTAL_DURATION_MS = [duration * 1000];
// ── 部分时间常量 ──────────────────────────────────────────────────
const TEASE_START_MS = [N];
const TEASE_END_MS = [N];
const BUILD_START_MS = [N];
const BUILD_END_MS = [N];
const REVEAL_START_MS = [N];
…
[截断以进行翻译]