返回技能总览
● 视频生成 · 介绍

网页工具介绍片

把一个「在线网页小工具」(计算器、清单、测评器、生成器等单页 HTML)做成 60 秒竖版介绍短视频(1080x1920 / 30fps)。当用户给出一个自己做的工具页面链接或截图,要求做介绍片、演示视频、宣传视频、功能演示短视频时触发。核心能力:headless 高保真抓取页面各交互状态 → 写 60 秒旁白 → edge-tts 配音 → PIL 逐帧渲染(截图平移 + 逐行点亮字幕 + 章节进度条)→ ffmpeg 出片;同系列多个工具可复用版式、只换主色。

何时用

先按输入形态选技能 —— 这是最容易选错的一步。

你手上有什么走哪个原因
网页链接 / 页面截图(没有录屏)本技能headless 抓多状态截图 → 截图平移渲染
操作录屏(现成 .mp4/.mov),要「真实用户分享感」hsf-chatcut-demo靠 ffmpeg 切视频片段、跟光标与点击同步
操作录屏,要「讲解型」(旁白+字幕+章节)hsf-video-generator同上,为讲解语气定制
纯文案 / 图文卡片类 skill不出视频

判据一句话:有现成视频文件要剪,就走 hsf 系;素材是「链接或静态图」,才走本技能。 别硬套:hsf 系第一步就要一个录屏路径(抽帧聚类切段),喂链接跑不起来。

渲染底座同源(截图/切片 → PIL 图层 → ffmpeg 直管出片),但视觉语言各成一套(本技能是 「深色画布 + 屏幕块 + 逐行点亮字幕」,chatcut 是「黑科技底 + 1:1 画中画 + 紫蓝霓虹描边」), 不要混用版式。

---

环境

# 用 WorkBuddy 托管的 Python(路径随用户名/系统而变,用 ~ 代替用户名):
#   Windows: %USERPROFILE%\.workbuddy\binaries\python\envs\default\Scripts\python.exe
#   其它:    ~/.workbuddy/binaries/python/envs/default/bin/python
# 下面这行会自动算出本机正确路径,直接复制用即可:
PY=$(python -c "import os,sys;print(os.path.expanduser('~/.workbuddy/binaries/python/envs/default/Scripts/python.exe' if sys.platform=='win32' else '~/.workbuddy/binaries/python/envs/default/bin/python'))")

依赖见 requirements.txt,首次使用先装:$PY -m pip install -r requirements.txt (edge-tts pillow numpy imageio-ffmpeg mutagen)。 浏览器:脚本自动探测 Edge;找不到时回退 Chrome/Chromium。可用环境变量 WB_EDGE 强制指定浏览器路径;macOS / Linux 上若没 Edge 需自行安装或设 WB_EDGE。

这个 bash 环境缺 coreutils(没有 ls/head/mkdir/dirname),所有文件操作走 Python。 中文路径不要经 shell 传参(-c 里写中文字符串做比对会因编码丢失匹配失败),要改文件用编辑器工具。

---

推荐工程化入口(v1.1)

新片优先初始化一个独立工程目录,不要直接在 skill 包里产出素材:

$PY <skill-dir>/scripts/init_project.py --output <video-dir> \
  --tool-name "工具名" --url "https://example.com/tool" \
  --page-type unknown --accent "#ff7a3d"

初始化会生成:plan.json、BRIEF.md、ref/、shots/、audio/、out/、evidence/,并复制可编辑脚本。 随后在 <video-dir>/scripts/ 里改 mkshots.py / tts.py / render.py。

先跑环境检查,只修报告出来的缺项,不要盲装:

$PY scripts/check_environment.py --project . --output evidence/environment.json

plan.json 是轻量总表:记录工具名、页面类型、画幅、主色、合规要求、镜头、旁白和字幕断句。 v1.2 起:tts.py 会优先读取 plan.voice / plan.lines;render.py 会优先读取 plan.accent / panelBg / cover / end / shots / subs。复杂镜头仍可直接改 render.py。 正式交付前运行:

$PY scripts/check_delivery.py --project . --video out/video.mp4 --output evidence/delivery-check.json

它会检查 plan / timeline / MP4 的结构、分辨率、帧率、音轨、时长窗口和字幕断句连续匹配;但不替代人工看片、听音和合规审查。

---

六步流程

1. 抓页面、摸清结构

urllib.request 抓 HTML → 存 ref/page.html

要摸出三件事:

用户给的截图分辨率通常不够(浏览器窗口 1900px 宽,但内容卡片只占 400 多逻辑像素,放大必糊)。 只要截图里能看出域名,就去抓线上原页重新截。

先判断页面形态,它决定截图策略:

形态特征截图策略镜头
向导型.step + show(n),一次只显示一步每步单独截,7~9 张一镜一图,oy 微移
长表单型所有 panel 一次铺完,滑块实时算只截 1~2 张完整长图一图多镜,靠 oy 位移取窗口
卡片总览型.hero + 一排 .card 的目录页,纯静态无 JS只截 1 张长图一屏多卡,逐卡套 hl + 压暗其余
单页切换型整页只有一个 switchMode(m) 之类的开关,切按钮态 + 换一段说明文案每个状态截 1 张(2~3 张)状态间硬切(等同"点了一下"),配 hl

卡片总览型(如「工具包总览页」,6 个模块各一张卡)是最特殊的一种:

天然覆盖"它解决什么问题、谁该用"。顺序与页面一致时镜头只往下走,不会来回跳

单页切换型(如「卖点提炼」,① 验证已有卖点 / ② 生成新卖点方案):

比同一个源里做交叉淡化更像真实交互

别去维护两套坐标 —— 在本地副本注入一句 #copy-build{min-height:99px} 把 两块拉成等高,两态几何就对齐了,全片共用一套 oy(见第 2 步「几何拉平」)

长表单型的 oy 合法范围 = [PH - N, 0](N = 截图高)。先在截图上扫出各 panel 的 y 区间, 再按「每镜覆盖约 PH 高度、相邻镜推进 150~400px」排镜头 —— 这个节奏既不呆滞也不会大跳。

2. 造状态变体 + headless 截图

scripts/mkshots.py 是模板:生成本地副本(不动线上)→ 往 </body> 前注入状态脚本 → 逐个截图 → 按背景色差自动裁掉底部空白。

CASES = [("intro", "..."), ("s1_loc", "..."), ...]   # 每个镜头状态的注入 JS
EDGE ... --headless=new --force-device-scale-factor=2 --window-size=520,2600 \
        --virtual-time-budget=5000 --screenshot=xxx.png  file:///本地副本.html

改状态必须派发事件 —— 宿主页面的监听器绑在元素上(addEventListener('input'/'change')), 直接改 el.value 不会触发重算。统一走这三个小工具,在注入脚本里预置:

function _ck(id){ var el=document.getElementById(id); el.checked=true; el.dispatchEvent(new Event('change')); }
function _sv(id,v){ var el=document.getElementById(id); el.value=v; el.dispatchEvent(new Event('input')); }
function _pill(sel,i){ document.querySelectorAll(sel)[i].click(); }

有些页面支持 URL 参数预填(读 location.search),也能用;但注入 JS 更可控 (能同时禁用动画、精确勾某个 checkbox、并排构造「正常态 / 最坏态」两张图)。

要出哪些状态:总览/首页 → 每个关键步骤的「已填/已选」态 → 最终结果页,一般 7~9 张; 长表单型则只需「默认态 + 最坏态」两张。

排镜头前必须拿到每个区块的精确 y 坐标。两种办法,首选第二种:

# 办法一:按背景色差扫(长表单型够用)
a = np.asarray(im.convert("RGB")).astype(int)
isbg = (np.abs(a - np.array([244, 244, 246])).sum(axis=2) < 20).mean(axis=1) > 0.97
# isbg 为 False 的连续段 = 各 panel 的 (y0, y1),留高度 > 40 的
# 办法二(可靠,坐标一目了然):洋红 outline 标记 + 检测
# 注入:.hero,.card{outline:4px solid #ff00ff !important;outline-offset:0 !important}
# 截图后检测:mag = (R>180) & (B>180) & (G<110);每行 mag 占比 > 0.5 即为一条边界线
# 聚类相邻行 -> 每 2 条 = 一个元素的 (top, bottom)

为什么办法二更好:卡片之间只有 gap 十几像素,跟卡片内部"文字断行"造成的空白 差不多宽,色差法要么把 6 张卡粘成一块、要么切得参差。洋红描边直接给边界,一对一个准。 代价是多截一张带标记的图(不影响交付,探测完就丢)。

# 办法三(最省事,精确到 1px,首选):scripts/probe_rect.py
#   py probe_rect.py 本地副本.html 520 3400 h1 .modes .frame-box iframe .deep
# 原理:注入脚本把 getBoundingClientRect() 写进 DOM,再 --dump-dom 读回来。
# 不截图、不做图像处理,直接打印「逻辑 y -> 物理 y(×2)」对照表。

办法三的两个坑: 1. 必须带上截图时用的那套 flag 再测(尤其 --force-device-scale-factor)。 实测 --window-size=520 时 innerWidth 只有 499(21px 滚动条), 截图却是 520 逻辑宽 —— 布局视口和画布宽度不是一回事,坐标一律 ×2 得物理值。 2. docH 会大于实际内容底(iframe 的 min-height 之外还有零碎),别拿它当裁底依据, 裁底仍用背景色差法。

几何拉平:单页切换型 / 多状态页里,若某个状态的内容少一行,它下面所有元素会整体上移, 逼你为每个状态维护一套坐标。别这么干 —— 在本地副本里给短的那块注入一句 min-height:(取长的那块的高度),两态几何就完全对齐,全片共用一套 oy。

裁顶要留够余量:裁掉页面导航栏时,不要裁到紧贴第一个卡片的上缘 —— 留 30~40px 页背景,否则卡片的圆角上缘和阴影会被面板顶边切平,看着像"页面从中间开始"。 先探测出第一个元素的 top,再把裁线定在它上方 30px 左右。

2b. 嵌入第三方内容(问卷 iframe)怎么处理

页面主体是 <iframe src="https://wj.qq.com/...">(腾讯问卷 / 问卷星 / 金数据等)时:

加载转圈。iframe 里的表单是二级文档,虚拟时间下它的 JS 起不来。别在这上面耗时间。

--window-size=600,2800。宽度别用 520 —— 这类表单常有 ~750px 的最小宽度, 视口窄于 ~600 时头部标题会溢出裁切,问题文案也会被切半。

底部「XX 提供技术支持」+ 隐私声明/举报链接。红色提示条必须裁掉,放进片子里很吓人; 底部平台署名属于第三方品牌,一律不入画。

取 min 而不是"按宽铺满",否则底部的「提交」按钮会被框的下沿切掉一半。 差额(此时表单比框窄几十像素)留在两侧 —— 表单底就是白色,看不出来。 裁切范围用 mkshots.py 里的常量写死(如 FORM_CROP = (90, 3480)),别每次重算。

3. 写旁白稿(先定字数,再动笔)

时长靠字数控制,不要靠调语速。

句子结构:封面钩子 → 引子 → 每步一句(动词开头)→ 结果 → 收尾金句。 默认一镜一句。遇到 "一句话点名一长串条目" 的句子就拆成两镜(见第 5 步「同句拆镜」), 否则那句会有 7 秒以上画面几乎不动。

文案强化四招 —— 首稿"讲清楚了"不等于"说得动",交付前按这四刀再过一遍:

位置弱写法强化后动作
钩子想开店,先别急着找铺子。一家店亏不亏,签字之前就有答案。「温和提醒」→ 代价 + 悬念,同时给全片一个统一坐标("签字之前"),后面每句都能挂上去
引子…拆成六个工具,都能直接上手。…把签字前该想明白的事,一件件做扎实。删零信息量的褒义词("直接上手""很强大"),改说"对谁、做什么动作"
功能句用八张实景图测眼力,看会不会挑靠右截流。用八张实景图测眼力。靠右截流、汇聚点,一眼看出坑。断成「工具 → 判断动作」两拍,第二拍落在观众自己能复现的动作/结论上(看出坑 / 边测边补 / 一次出 / 就别签)
收尾六个工具串成一条链。六个工具串成一条链。想清楚再签。补一个行动指令,与钩子闭环("签字之前"↔"想清楚再签")

另三条铁律:

例:① 验证已有卖点 / ② 生成新卖点方案 两个按钮,首稿把 ② 写成「让工具从零长出一套 能站住的卖点」,观众会理解成不用填表、系统自动生成;而页面 .lead / .note 原文写的是 「选一种,填完报告发你邮箱」—— 两条路都是「填同一份问卷 → 报告自动发邮箱」, 差别只在"你怎么答"。凡「都要 / 自动 / 免费 / 几分钟内」这类共性条件, 一律对着 .lead / .note / .mode-copy 抄,不要从按钮名反推路径。 修法:把共性条件单独起一句(本例「两种用法都要填这份问卷,填完提交,报告自动发到你邮箱。」), 且这一句的镜头要同时框住「机制提示块 + 表单头部」,让画面替口播作证。

SUBS 的每行必须是新句子的连续子串且前后有序,否则 sub_block() 里 text.find() 落空会 静默错位(不报错、字幕亮错行)。改完先跑断句校验,再配音:

# 断句自检:改完 LINES/SUBS 先跑这个,全 OK 再 synth
for ln in tl["lines"]:
    rows = R.SUBS.get(ln["idx"]) or []
    cur = 0
    for r in rows:
        p = r.replace("{", "").replace("}", "")
        i = ln["text"].find(p, cur)
        assert i >= 0, ("MISS", ln["idx"], p)
        cur = i + len(p)

强化文案不改版式:SHOTS 的 hl/oy 坐标与页面绑定,页面没变就不用重截、不用重探坐标 (本例只重跑了 tts.py + render.py)。重截后先比 main.png 尺寸,一致即沿用原坐标。

4. 配音 + 时间轴

scripts/tts.py:优先读取 plan.json 的 voice 和 lines;缺失时用脚本内置示例。 逐句 edge-tts 合成 → ffmpeg 拼成 out/voice.m4a,写项目根目录 timeline.json (每句含 idx/text/file/start/end/dur/words,顶层含 total)。

与 hsf-chatcut-demo/scripts/gen_audio.py 的关系:同源,但故意不合并。 两边都是「edge_tts.Communicate(...).stream() 收 audio + WordBoundary」,都带 NoAudioReceived 重试 —— 这层调用方式要对齐,改一边就同步另一边。 但输出契约本质不同:本技能要全局时间轴(句间插静音 + start/end/total), chatcut 要分段独立 mp3 + 多音字 say 映射。抽共享模块会让两个技能互相 import 外部文件、失去自包含,收益只有 10 行样板,不划算 —— 所以各自留一份,只对齐调用层。 已知差异(有意保留):本技能 VOICE=zh-CN-YunyangNeural 男声 RATE=+4%; chatcut 默认 zh-CN-XiaoyiNeural 女声 RATE=-10%。音色是业务选择,不是重复。

别指望 words(词级时间戳) —— 实测 edge-tts 对中文常返回空列表(本项目 11 句全是 words=0)。 字幕点亮、高亮框触发一律按字符位置比例折算时间:t = start + dur * (字符下标 / 全句字数)。 精度够用(误差 <0.2s),且不依赖网络返回。

5. 逐帧渲染

scripts/render.py 是模板。v1.2 起优先读取 plan.json 的主色、封面/结尾、shots 和 subs;复杂镜头仍可直接改脚本。核心参数(改这几处就能复用):

W, H, FPS = 1080, 1920, 30
PANEL   = (22, 104, 1058, 1500)      # 内容面板;上方 104px 留给标签 + 进度条
SRC_W   = 1040                       # 截图物理宽(2x 设备像素 → 逻辑 520)
PANEL_BG= (246,247,249)              # 页面底色,填留白
ACCENT  = (255,122,61)               # ← 换成该工具的主色
SHOTS   = [dict(line=0, kind="cover"), dict(line=1, src="intro", oy=-16, oy2=-112), ...]
SUBS    = {1: ["验车有检查单,", "选址也该有份{写字楼}。"], ...}   # {} 内为高亮

oy 是「截图相对面板左上角的偏移」,负值 = 画面往上走;合法范围 [min(0, PH-N), max(0, PH-N)]

同句拆镜(一个 line 拆成多个镜头,不动配音、不改时间轴):

dict(line=2, at=0.00, src="main", oy=-120, oy2=-160, subs=["第一关硬门槛,", "{五条一票否决}:"]),
dict(line=2, at=0.36, src="main", oy=-150, oy2=-175, subs=["排烟电力上下水、油烟、", "{时段错配、证照}、商圈衰退。"]),

at = 切分点(相对该句时长的比例),subs = 该镜只显示这几行。 build_spans() 会把分镜表折算成 [(t0,t1)];镜头内的进度 e 按本段时长算,不是按整句 —— 所以第二个子镜的开头不会已经"走完位移"。拆镜只动 SHOTS,一句改两镜不用重跑 TTS。

高亮框(给静止画面造落点,用来解决"页面滚到底了、画面没法再动"的镜头):

dict(line=9, oy=-4254, oy2=-4254, hl=(5390, 5610, 50, 955)),        # 固定框:圈住结论区
dict(line=2, at=0.36, ...,                                           # 跟读框:念一条框一条
     seq=[(0.00,(576,695)), (0.32,(688,807)), (0.44,(800,919))], seqx=(48, 958)),

压暗非当前区域(一屏装了多张卡 / 多个区块时必需,draw_focus()):

# 整块铺半透明黑,再把当前卡的圆角矩形"挖空"(挖空区做高斯羽化,避免硬边)
ov  = Image.new("RGBA", (PW, PH), (0,0,0,0)); ImageDraw.Draw(ov).rectangle([...], fill=(8,6,5,120))
hole= Image.new("L", (PW, PH), 255); ImageDraw.Draw(hole).rounded_rectangle([...], radius=26, fill=0)
hole= hole.filter(ImageFilter.GaussianBlur(24))

6. 出片 + 验收

$PY scripts/render.py            # 后台跑,2~4 分钟
$PY scripts/check_delivery.py --project . --video out/video.mp4 --output evidence/delivery-check.json

验收四条:

最后把 out/video.mp4 复制到工作区根目录,命名 <工具名>_60秒介绍片.mp4(品牌后缀按需要自行加;规格 1080x1920 / 30fps / H.264+AAC 不写进文件名)。

---

交付物

常见问题

Q:网页工具介绍片是做什么的?

把一个「在线网页小工具」(计算器、清单、测评器、生成器等单页 HTML)做成 60 秒竖版介绍短视频(1080x1920 / 30fps)。当用户给出一个自己做的工具页面链接或截图,要求做介绍片、演示视频、宣传视频、功能

Q:网页工具介绍片适合什么场景使用?

先按输入形态选技能 —— 这是最容易选错的一步| 你手上有什么 | 走哪个 | 原因 | |||| | 网页链接 / 页面截图(没有录屏) | 本技能 | headless 抓多状态截图 → 截图平移渲染 | | 操作录

Q:使用网页工具介绍片最终能得到什么?

60秒介绍片mp4(品牌后缀按需要自行加;规格 1080x1920 / 30fps / H