动态促销视频
仅凭业务描述,构建一个具有广播级外观的垂直促销视频。视频被编创为一个由暂停主时钟驱动的单个HTML页面,然后逐帧渲染并编码。无需编辑器、素材库或时间线应用——并且每一帧都可以通过时间戳复现。
输入
$brief— 产品的定义、目标用户、独特之处。必需。从对话、仓库的README中获取,或直接询问用户。$reference— 可选。一个视频URL或本地文件,其外观风格应被匹配。$brand— 可选。名称、标语、网址、颜色、Logo。如果缺失,则使用明确标记的占位符(切勿静默地发明一个品牌并将其作为最终结果呈现)。$duration— 可选。默认30-35秒无声版,45-60秒配音版(语音决定时长——参见references/audio.md)。对于Shorts/Reels/TikTok,请控制在60秒以内。$audio— 默认:配音,无音乐。 编写画外音,并围绕它构建时间线,除非用户要求无声版。音乐是唯一一项,除非明确要求,否则保持关闭。
目标
promo.mp4 — 1080×1920,30 fps,H.264,首次观看即可播放且正确——外加用于生成它的scene.html、用于复现它的render.py、用于配音版本的voice.srt字幕文件,以及包含节拍表和用于重新品牌化的常量的README。成功不仅仅意味着“文件存在”:而是你已经查看过的帧,它们清晰可读,没有重叠的文本,没有元素在其提示时间之前可见,也没有无内容的时间段。
步骤
1. 检查工具链
which ffmpeg ffprobe yt-dlp python3
python3 -m venv .venv && .venv/bin/pip install -q playwright && .venv/bin/playwright install chromium
ffmpeg和用于Playwright的Chromium是硬性要求。yt-dlp仅在分析参考视频时需要。推荐使用虚拟环境——用户的系统Python可能受外部管理。
成功标准:ffmpeg -version可工作,并且.venv/bin/python -c "from playwright.sync_api import sync_playwright; sync_playwright().start().chromium.executable_path" 能打印出一个路径。
2. 确定视觉目标
两条路径——根据$reference是否存在来选择。
2a. 提供了参考 → 进行分析
阅读references/teardown.md并遵循它。简而言之:下载视频,探测其属性,提取1 fps的联系表,实际查看联系表图像,并在编写任何代码之前记录下分辨率、时长、调色板、字体处理方式、节拍结构和运动词汇。
切勿描述你未逐帧查看过的参考视频。页面标题或描述对了解其动态设计毫无帮助。
2b. 没有参考 → 首先推导主题自身的视觉语言
不要立即打开default-style.md。花两分钟时间思考关于实际主题的四个问题:
- 物体是什么? 书本、罐子、发票、面孔、仪表盘。这些应该是动画的对象。
- 它们存在于何种光线中? 阅读灯、展厅、月光、屏幕光、透过窗户的日光。
- 它们如何物理移动? 有质量的物体会沉降。纸张会翻转和皱褶。液体会扩散。数据会瞬间变化。
- 什么是主线? 贯穿始终、承载论点的元素——倒计时、穿越夜晚的时钟、不断增长的收据、逐渐填充的路线。理想情况下,它就是核心主张,而非装饰。
如果这四个答案能给你一个连贯的外观,那就构建它。一个关于“夜间修复”的护肤品促销片应使用夜晚调色板、衬线字体、缓慢的沉降动画和运行时间为晚上10点到早上7点的刻度盘——而不是发光的光环。
当主题确实没有提供任何具体灵感时(大多数B2B软件、抽象服务),回退到默认风格。然后阅读references/default-style.md并按原样使用:它是一个完整的、经过验证的规范(深海军蓝+蓝色光晕、几何无衬线字体、模糊淡入的动态文字、发光的地平线、光环结尾)。用一句话告诉用户你正在使用哪种风格,并且他们可以通过指定参考视频来更换风格——然后继续进行。不要停下来等待审批。
构建前的诚实检查。 如果你的节拍时间与模板的节拍时间相差在约0.5秒以内,并且标志性镜头(如回响的字词堆栈、光环绽放、地平线加板块卡片、作为唯一文字动画的模糊淡入)都存在,那么你只是重新设计了模板的外观,而非针对主题进行设计。当你故意选择使用回退风格时,这没问题;但如果你没有有意选择,这就是一个缺陷。
成功标准:一个简短的书面规范——画布大小、fps、时长、调色板十六进制颜色、字体以及你将使用的动画动作——你在构建时可以参考它。如果你推导出了主题原生的视觉语言,它还应命名出主线以及你故意不使用的动画。
3. 编写节拍表
首先阅读references/audio.md并编写语音轨道——它默认是开启的。语音掌控时间线:生成台词,测量它们,并根据测量结果推导节拍时间,而不是将语音硬塞进整数时间点。保留每句台词的测量时长——它们在交付时会成为voice.srt。仅当用户要求无声版时才跳过此步骤。
阅读references/beats.md了解结构和文案规则,然后将用户实际的差异化优势映射到节拍上。中间三分之一是推销产品的地方:它必须包含这个产品更优的具体原因,而不仅仅是形容词。假设无声播放——大多数Shorts/Reels观众从未听到语音——因此语音中的每一个主张也必须在屏幕上作为文本呈现,而不仅仅是作为旁白。
在编写代码之前,先将节拍表写成表格形式(时间范围 → 节拍)。在场景构建完成后,再修复节奏问题要昂贵得多。
产物:包含明确秒数范围且总和等于$duration的节拍表。
成功标准:每个节拍都追溯到产品的真实特性,并且这些时间范围是连续的,没有超过0.3秒的间隙。
4. 从模板构建骨架
下面的$SKILL是本技能自身的目录——即包含此SKILL.md的目录。根据你的技能列表解析它,而不是猜测路径;它通常以只读方式挂载,这没问题,因为所有内容都会被复制出来。
mkdir -p video && cp "$SKILL/templates/render.py" video/
cp "$SKILL/templates/scene-starter.html" video/scene.html
bash "$SKILL/templates/fetch-fonts.sh" video/fonts # Poppins子集,约40 KB
scene-starter.html已经包含了动画框架(seek、win、io、pop、swap、draw、种子随机数生成器、氛围)以及七个示例节拍,涵盖了所有值得复制的模式。不要重写框架——它已经编码了规则部分列出的bug。替换示例节拍;保持它们之上的代码不变。
成功标准:python render.py --contact 从未修改过的启动器中生成出contact.png。
5. 构建场景
每个节拍一个IIFE,每个IIFE创建一个由win(layer, tIn, tOut)控制门限的layer()。在内部,每个动画元素都恰有一次io() / pop() / swap()调用。
将UI模型(仪表盘、手机、笔记本电脑、卡片、芯片)构建为纯DOM + CSS,并让它们动画进入——它们能带来纯文字无法实现的“真实产品”感觉。图表使用div和内联SVG;不使用图表库、图像或外部请求。
硬切是一种合理的技巧:两个win()层共享一个精确的时间边界,并以steps(1,end)进入,会产生故意的单帧切换——适用于色调突变,例如故事世界 → 纯品牌色板。其他所有动画都应使用缓动;而这个硬切因为它是唯一的,所以效果突出。
真实的彩色Logo在深色场景和纯色品牌背景上往往都难以辨认——将Logo放置在白色小板子上,而不是重新着色。
规则:参见规则部分。它们不是风格偏好——每一条都是此技能已经遇到过的特定失败。
成功标准:python render.py --contact 渲染无错误。
6. 预览循环——查看每一帧
python render.py --contact # 24帧概览,约20秒
python render.py --at 6.2 12.0 19.5 # 特定时刻,全分辨率
使用你的图像阅读工具——无论框架如何调用它——读取生成的PNG并检查它们。 然后修复并重复。在此处迭代直到联系表清晰——完整渲染速度慢10倍,因此永远不要将其用作反馈循环。
两种视图回答不同问题。 联系表用于检查结构:节奏、重叠、空白帧、节拍顺序。它每帧为270×480,并且会系统性地低估深色或低对比度作品——在联系表中看起来模糊且破碎的片段在全尺寸下往往是清晰的。仅从--at帧判断对比度、调色板和易读性,并且每当调色板是深色时,每个节拍至少提取一张,此外每个象形图或插图也至少提取一张——可辨识性无法在缩略图尺寸下判断(参见规则12)。不要为了提高亮度而修复由联系表夸大的问题。
每次检查需要寻找什么:
- 过渡期间同时有两个文本可读
- 任何在其提示时间之前可见的元素(经典的
fill: 'both'bug——参见规则) - 被误认为是可辨认形状而非氛围的背景元素
- 堆叠的半透明色块——重叠的Alpha层会复合出可见的相交弧线,看起来像圆盘;使用一个带有在其自身盒子内溶解的边缘的元素,尺寸大于帧
- 空白帧——任何屏幕上没有任何内容的时刻
- 文本或UI模型挤在帧边缘,或者对于手机屏幕来说太小
人工检查点:非必需,但如果用户在场,请在完整渲染之前向他们展示联系表——此时获取关于节奏和文案的反馈成本最低。
成功标准:你已经查看过联系表,其中每一帧都清晰可读,并且不存在上述任何缺陷。
7. 渲染并验证
首先对编码器进行冒烟测试。 渲染一个约2秒的片段——通过你的render.py副本中的范围标志(如果有的话),或者临时缩短时长常量——然后用ffprobe检查结果。编码器和混流问题在此处几秒钟内就会暴露,而不是在多分钟的完整渲染之后。
python render.py # -> promo.mp4,每帧约1张截图
# 对于配音版本:仅在编码器进程*退出*后进行混流
ffmpeg -y -i promo.mp4 -i vo/voice.wav -c:v copy -c:a aac -b:a 192k promo_av.mp4
ffprobe -v error -show_entries format=duration \
-show_entries stream=codec_type,codec_name,width,height,nb_frames \
-of default=noprint_wrappers=1 promo_av.mp4
ffmpeg -y -v error -i promo_av.mp4 -vf "fps=1/1.2,scale=180:320,tile=10x3" -frames:v 1 verify.png
在后台运行完整渲染。 大约每帧一张截图——一个50秒的视频大约1500帧,并且很容易超过600秒的命令超时时间。轮询渲染进程是否退出,而不是mp4文件是否出现:文件在完成之前很久就已存在于磁盘上,过早混流会导致moov atom not found错误。
对于配音版本,根据步骤3中测量的每句台词时长编写voice.srt——这些数字已经存在;这只是一个格式化步骤,不是新的工作。
阅读verify.png——验证编码后的输出,而非浏览器,这才是重点。然后删除临时联系表和帧目录。
成功标准:ffprobe报告预期的时长、预期的分辨率(除非用户更改了宽高比,否则为1080×1920),并且nb_frames == fps × duration;对于配音版本,存在音频流,时长与语音轨道匹配,并且存在voice.srt;verify.png从头到尾与节拍表匹配。一个缺少结尾的短视频通常意味着-shortest遇到了过时的语音轨道——参见references/audio.md。
8. 交付
在视频旁边编写一个README,内容包含:节拍表、重品牌化常量、渲染命令以及两个框架注意事项。然后向用户报告:
- 文件位置及其确切规格(大小、时长、fps,以及是配音版还是无声版——此外还有使用的TTS语音,以便可以重新生成)
- 配音版本的
voice.srt路径,并说明Shorts/Reels/TikTok接受将其与视频一起上传 - 节拍表
- 明确指出的每一个占位符和虚构的数字——品牌名称、网址以及模型中显示的任何指标。明确说明,如果要求索赔具有可辩护性,则必须在发布前替换虚构的数字。
- 没有可分享的链接——这是一个本地文件——除非用户要求托管
提供打开视频的选项:open promo.mp4。
规则:未经明确批准,切勿提交或发布。切勿将视频上传到任何地方作为生产的副作用。
成功标准:用户知道文件路径、哪些是占位符,以及如何更改它们。
规则
硬性约束。每一条都是此流水线已经产生过的失败。
-
默认配音,绝无音乐。 编写画外音,并让它掌控时间线,除非用户要求无声版。音乐和声音设计保持关闭,除非明确要求——你不能提供授权音轨,并且永远不要从流媒体站点拉取音频。无论何种情况,都要假设无声播放:画外音中的每一个主张都作为屏幕文本镜像呈现,并且配音版本附带一个
voice.srt。当你确实交付无声版时,报告时请注明“无声”,以便静音播放不被视为bug。 -
每个元素一个动画,覆盖进入 → 保持 → 退出。 两个独立的WAAPI动画且带有
fill: 'both'会产生冲突:退出片段从t = 0开始应用其第一个关键帧,导致整个视频中该元素一直可见。io()辅助函数的存在正是为了防止这种情况。症状:场景中的所有内容都在场景开始时同时出现,而不是动画进入。 -
连续的文本必须完全退出后才能进入下一个。 在交叉淡入淡出过程中屏幕上同时出现两行可读文本,这会被视为渲染错误,而不是过渡。安排第N行文本的退出在第N+1行文本的进入开始之前完成。
-
确定性是不可妥协的。 为随机数生成器设置种子。永远不要调用
Date.now()、Math.random()。