文字稿 → 讲解视频
输入一份 .docx(正文段落里用下划线标出要重点呈现的句子片段)+ 一张白底卡通人物图,
输出一条讲解视频:人物在侧,正文在下,下划线要点按说话顺序依次弹出。
时长不设限:成片长度完全由讲稿字数决定(配音时长 + 段间停顿 + 首尾留白),
1 分钟和十几分钟走的是同一条流程,不需要改任何参数。
何时用
- 用户给一份口播/文字稿,要求"做成视频""生成讲解视频""把这份讲稿做成视频"。
- 用户明确要求"文字稿中下划线部分依次出现、要醒目"。
- 普法、合规提示、政策速读、课程要点这类以文字稿为纲的短视频/中长视频。
不适用:需要真人出镜、需要复杂运镜/多镜头、需要音乐与音效设计的片子。
前置检查(缺一不可)
ffmpeg -hide_banner -filters | grep -w -E "overlay|fade|zoompan|drawbox"
关键坑:Homebrew 版 ffmpeg 默认不带 libass / libfreetype / fontconfig ——
ass、subtitles、drawtext 三个滤镜都不存在。
所以不要试图用 ASS 字幕或 drawtext;本 skill 一律用 PIL 把文字渲染成透明 PNG 图层,
再用 overlay + fade + enable='between(t,...)' 合成。这条路反而更好控(字体/描边/高亮块全可控)。
依赖:python-docx、pillow、numpy,以及 isolated venv 里的 edge-tts:
<venv>/bin/pip install edge-tts
执行流程
0. 读稿 + 拿下划线
用 docx 读段落,逐 run 取 r.underline 为真的片段,记成相对段落文本的字符区间 [start, end)。
tencent-local-office-edit 的 doc_resolve_document_structure(mode="full", include_highlights=true)
也能给出 highlight_spans(含 underline: true),可用来交叉核对——但它的 text_preview 上限 200 字,
拿全文仍需 python-docx。两边的区间应完全一致,不一致就停下来查。
1. 出卡通人物图(图像生成工具)
提示词要点(照抄可复用):
> 扁平矢量卡通插画,一位亲切自然、面带温和微笑的中国女性律师,约30多岁。
> 身穿中国律师出庭服装:黑色律师长袍(长款、对襟),颈部系红色领巾,内搭白色衬衫。
> 深色齐肩短发或盘发。正面半身像(头顶到大腿上方),姿态自然放松,不做作。
> 线条简洁、色彩明快,企业品牌吉祥物插画风格。纯白色背景,无任何文字、无水印。
⚠️ 反面清单(必须写进负向约束或事后修图):
- 不要让模型画"胸前徽章"。模型几乎总会画成国徽(五星+天安门+齿轮),
而中国律师袍上的是律师徽章,不是国徽——用在普法物料里是事实性错误。
出图后逐帧核对该位置;不对就用 ImageGen 做局部修图:
> 局部修图:仅移除人物左胸前的那枚红色圆形金色徽章,用纯黑色律师袍自然填补该区域
>(保留原有布料褶皱与阴影的连续性)。人物的面部、发型、表情、姿势、红色领巾、白衬衫、
> 袖口、双手及背景全部保持完全不变。比例、构图、画风均不得改变。
- 别要透明背景——生图模型多半给不出真 alpha,直接要纯白背景再自己抠。
2. 抠图(四步,缺一不可)
为什么必须从边缘泛洪、而不是全局 colorkey:白底 + 白衬衫领口/白袖口,
全局去白会把衬衫一并打穿。边缘泛洪只吃掉与画布边缘连通的白色区域,
衬衫被黑袍包围,自然保住。
但只做泛洪不够,深色背景上会露白纹。完整三步:
- 边缘泛洪:
ImageDraw.floodfill,从四边 + 边中点共 8 个种子点,thresh=60,
填成(255,0,255)键色 → alpha 置 0。 - 掏发丝白缝:泛洪够不到封闭的白色区域——原图白底会从发丝缝隙里透出来,
留在深蓝底上就是一条条突兀的白纹。
判据两条,缺一不可:又薄、又被深色包围。
- "薄"=该连通域腐蚀 3px 后消失;
- "被深色包围"=该连通域外扩 3px 的环带平均亮度 < 120。
实测(局部修图*.png):142 个薄片里只有 19 个 / 585px 满足两条 → 就是那些发丝白缝;
其余 123 个只有 209px、基本是白衬衫上的单像素点,掏了会把领子掏穿露出背景。
⚠️ 只按"薄"判会掏穿白衬衫领口——第一版就是这么翻车的,必须加环带判据。 - 削描边 + 去白污染:alpha 先
MinFilter(5)(腐蚀 2px,削掉原图自带的浅灰描边),
再GaussianBlur(0.8)找回抗锯齿;然后对半透明像素做去白解混
F = (C - (1-a)·255) / a,否则边缘发灰发白。 - 丢弃孤立碎片(
cutout_min_area):细发丝被削薄后,会在轮廓之外留下一小片
半透明像素——在深色底上就是一条"黑影子",看着像头发里漏出来的脏东西。
它与主体已不连通,按连通域面积一刀切掉即可:
面积 <max(cutout_min_area, 最大连通域的 0.2%)的连通域丢弃(默认 300px)。
验收:alpha > 16的连通域应当只有 1 个。
实测那条黑影只有 80px,而主体 382,624px,切得干干净净。
最后 crop(getbbox())。
⚠️ PIL 的 floodfill 在 L 模式 + 整数填充值下会静默不生效(返回 0 像素、不报错)。
组件标记的兜底分支必须用 RGB 图 + 元组填充值,参考主泛洪的写法。
曾因此让整步修复形同虚设却毫无报错——这类"静默无效"必须用数值验证
(掏完后 纯白(≥245)且不透明的像素数 应当归零,不为零就是没生效)。
有 scipy 时走 ndimage.label 快路径(0.65s),无则退回 PIL(约 40s)。
完成后一定渲染一张深色底预览图肉眼看一遍:既看有没有漏抠的白纹,也看有没有把白衬衫掏穿。
3. 配音 + 时间轴
edge-tts + zh-CN-XiaoxiaoNeural(Warm 女声,清晰),rate="+12%"、pitch="+2Hz"。
⚠️ edge-tts 7.x 只返回 SentenceBoundary,没有 WordBoundary。
--write-subtitles 出来的 VTT 也是逐句而非逐字。所以:
- 句级边界 = 精确值(直接用);
- 句内按"字权"插值:汉字 1.0,
,1.5、、1.3、;1.8、。2.0,引号括号 0.2。
句内按累计权重分配时长 → 得到每个字符的[start, end]。
这样算出的下划线片段起点误差在 ±0.3s 内,肉眼无感。
每段 TTS 一定做首尾静音裁剪(silencedetect 定位 → -ss/-to 精确切),
并把 char_times 整体减去头部裁掉的时间,否则后面全段错位。
段间插入真实长度的静音(不是固定值),用 concat demuxer 拼成 final.wav。
4. 版面(1920×1080 / 25fps)
┌──────────────────────────────────────────────┬──────────────┐
│ 白面板 x70..1320, y92..992 │ │
│ ▍标题(页眉) y150, 40pt Medium │ 卡通人物 │
│ ──────────────────────────── y236 │ 右对齐贴边 │
│ [下划线要点卡] 红底白字 y250 起,逐条往下叠 │ 底边与画面齐平 │
│ │ │
│ 正文段落 44pt / 行高68 y650 │ │
│ └ 下划线部分=红字加粗+浅红底纹 │ │
└──────────────────────────────────────────────┴──────────────┘
▂▂▂▂▂▂▂ 进度条 y1064 h8(红) ← 全宽
- 字体:PingFang SC(PingFang.ttc
index=3Regular /index=7Medium)。
备选 Hiragino Sans GB(index=0/index=2)。 - 面板留白右边界到人物左沿留 ~50px 空隙;人物宽度 553px 时高度约 1002px,底边与画面底对齐。
- 要点卡水平+垂直都居中(
key_align=center/key_valign=center):
水平按整组块居中(多张卡共用左沿,块整体居中,比逐张居中更齐);
垂直居中于「页眉分隔线 ~ 正文顶」这段空白区的中心。
⚠️ 卡片 PNG 四周各留了 6px 透明边,定位时要-6补偿,否则会偏 6px。 - 多张卡逐条出现时,整组要随时间平滑重新居中:卡组高度每加一张就变一次,
用overlay的 y 表达式做 0.3s 线性过渡(if(lt(t,tn),v_n-1, if(lt(t,tn+D), 插值, v_n))),
组内各卡共用同一个 y 表达式、只差一个常量偏移。
否则「只出现第一张」的那两三秒里,卡会孤零零挂在顶部、下方一大片空白——正是用户会挑的毛病。 - 下划线要点卡用深红底 + 白字 + 左侧深红竖条 —— 这是全片最"醒目"的元素,务必让它做视觉主角。
- 正文段落里下划线部分同时染红加粗并加浅红底纹,与要点卡互相印证。
- 同段出现多个下划线要点(如"三个不得")时:一条一张卡,按说话顺序自上而下逐条弹出、全部留在屏上,
读完再整组淡出。这是最有说服力的版式,不要做成一次只显示一张。 - 封面标题分行用
cover_lines手工指定,不要按字数中分——否则会把词拦腰折断
(把标题断成"……案件辩护律 / 师的……")。
5. 时间轴与事件
- 封面(大标题 + 副标题)→ 页眉:念完标题后交叉淡入淡出,标题随后常驻页眉。
- 每条事件记
(png, x, y, t_in, t_out, fade_in, fade_out),overlay加
enable='between(t,t_in,t_out)',并对该路输入做fade=...:alpha=1实现淡入淡出。 - 人物用
zoompan做极缓呼吸感:
z='1.028+0.020*sin(2*PI*in_time/8)':x='iw/2-(iw/zoom/2)':y='ih-(ih/zoom)':d=1
——底边锚定,头部才会轻微起伏;d=1时用in_time而不是on(on会恒为 0)。 - 进度条用两个
drawbox:底轨 +w='min(1920,1920*t/TOTAL)'的红色填充。 - 收尾
fade=t=out:...:color=white淡到白(浅色版式淡到白比淡到黑自然)。
6. 合成前先把图层裁到内容区
getbbox() 裁掉透明边再 offset 回原坐标,overlay 输入体积降一个量级,编码快 3~4 倍。
实测 68s 片子裁图后 -preset fast -crf 19 18 秒渲完。
第 7 阶段:口型同步(可选)
默认关闭。开启后人物嘴随配音「闭口 ↔ 微开」开合,其余画面逐像素不变。
何时开:用户说"让人物嘴动起来""对口型""嘴一直不动""说话要有停顿感"。
何时不开:纯图文版式、无人物的片子(没意义,白花一次烘焙)。
架构:补丁贴进「人物图层」,不是贴成片
character_cut.png ──缩放/fade──> character_scaled.png ──┬─ 闭口(原样)
(Base.base) └─ 微开(贴口型补丁)
↓ 按配音时间表逐帧选一张
character_layer.mov(qtrle,带 alpha)
↓
zoompan(与原静态图同一套参数)→ overlay
这样做换来三件确定的好处:
- 零位移补偿——补丁和人物一起被
zoompan缩放,不存在"补丁漂离嘴部"。
(贴成片的做法必须拟合呼吸位移模型,实测嘴部 y 漂移可达 27px。) - 零二次重编码——人物层参与同一次合成,画面没有"先出片再拆帧贴"的画质损失。
- 口型切换在 Python 侧完成——不依赖 ffmpeg 的 enable 表达式(见坑 4)。
三步流程
# ① 先不带 lipsync 跑一次,拿到 work/character_scaled.png
# ② 标定嘴部(每个角色只需一次,之后可长期复用)
$VENV scripts/calibrate_mouth.py <work>/character_scaled.png --crop 200,200,140,120 --scale 5
# 目视读出三个量:唇心 x、上唇顶 y、嘴角间距 → 写进 config 的 lip_anchor
# ③ 用图像模型生成「微开」口型图,再生成补丁
$VENV scripts/lipsync_prepare.py --open-image <微开.png> --ai-lip <唇心x,上唇顶y> \
--ai-lip-w <AI图有效唇宽> --ref-image <work>/character_scaled.png \
--ref-anchor <唇心x,上唇顶y> --ref-lip-w <嘴角间距> --out <work>/patch_open.png
# 先跑 lipsync_preview.py 看静态贴合,确认无接缝再进下一步
# ④ 填好 config 的 lipsync 段,正常跑 build.py —— 口型自动接上
为什么坐标要用网格目视读、不用自动检测:动漫脸的腮红、描边线、红领巾全是暖色,
任何"肤色/唇色阈值"检测都会误检(实测反复踩)。目视读一次 100% 可靠,且一角色只读一次。
生成「微开」口型图的 prompt(照抄可复用)
输入:face_big.png(从人物图裁脸部区域放大到 600×760 左右)。参数 input_fidelity=high。
> mouth very slightly / mildly open, a small soft horizontal oval opening, inside is
> dark warm maroon-brown (almost black), a thin row of white upper teeth along the top edge,
> dark empty space below. Absolutely NO tongue.
必须同时带上这几句(否则整张重绘或改坏脸):
> Keep the EXACT SAME character: same face shape, same eyes, same eyebrows, same nose,
> same hairstyle, same clothing, same flat cel-shaded style, same colors, same lighting,
> same background. Change NOTHING except the mouth. The mouth must stay at exactly the same
> position, same width and same size as the original. Do not redraw or move anything else.
> No text, no watermark.
⚠️ 只生成这一张「微开」,不要生成"中开/大张"——实测在成片里显夸张(像喊话),已弃用。
⚠️ 每次生成用不同的 output_dir:并行调用会撞同名文件互相覆盖(实测丢过一张)。
🔒 已定稿参数(陆律师 2026-09-25 确认"就保持这个节奏",勿擅改)
| 参数 | 值 | 作用 |
|---|---|---|
th_q |
0.55 | 开口阈值 = 有声帧 q15 + th_q × span |
release |
0.75 | 能量回落到阈值×该系数即闭嘴 |
min_open / min_closed |
2 / 2 帧 | 开、闭各最短保持 80ms(防闪、防抖) |
定稿实测:开口段均长 261ms、切换 3.2 次/秒、开口帧 42%。
th_q 是"停顿感"的总开关:中文朗读音节率 4~5 次/秒 → 切换 3 次/秒最自然。
判据看开口段均长:目标 200~350ms;>500ms 就是"嘴一直张着"(0.20 时实测 930ms,被打回);
<2 次/秒显得嘴懒,>4 次/秒变"金鱼嘴"。改动一次只动 ±0.05。
必踩的坑(都真踩过)
- 整幅画布图层被二次偏移。正文段落是画在整张 1920×1080 画布上的,
事件里的偏移必须写(0, 0)。若照抄"卡片类图层的左上角偏移"写成(pad_x, cap_top),
整段文字会被推到画面外 —— 表现是字幕整段消失、面板空着,而要点卡(小画布)正常,
很容易误判成字体或 alpha 问题。 - 封面/页眉忘了注册成事件。只
render_cover()而不events.append(...),
画面就是一片空白面板。凡是渲染出来的图层,必须有一条对应事件。 -
画底纹的循环忘了推进 x。正文的"红字加浅红底纹"是两层:
第一层铺底纹矩形、第二层画字。第一层若忘了x += 字宽,每个下划线字都会把底纹
叠在该行行首,于是每一行开头都莫名多出一块粉红(宽度取最宽的那个下划线字)。
典型误判是"颜色规则没规律"——其实规律很明确:只有含下划线字符的行,行首才长粉块。
对照截图可自证:没有下划线字符的那一行,行首是正常的。
修法:两层循环都要x = pad_x起、逐字x += 该字实际字宽(下划线字用 Medium 字宽,非下划线用 Regular 字宽)。 -
用
overlay的enable表达式按时间切换口型 → ffmpeg 直接报错。
开口区间有 110 段,拼成between(t,a,b)+between(t,c,d)+...约 3.2k 字符,
ffmpeg 报Error when evaluating the expression ... Cannot allocate memory,整条合成失败。
凡是"按时间表在两条流之间切换"的需求,都不要用 enable 表达式——
改为在 Python 侧逐帧选帧、烤成带 alpha 的人物层视频(qtrle),ffmpeg 侧只剩一路输入。
附带好处:口型与人物同被zoompan缩放,零位移补偿。 -
静默吞掉外部命令的报错。
build.py的sh()原先用check=True+capture_output=True,
ffmpeg 失败时只抛一句exit status,真正的报错看不到,排查得重跑整条流水线(含 TTS)。
已改为失败时打印 stderr 末尾 14 行。凡是包装外部命令的地方都要这样做。
自检(必做,各一条命令)
# 1. 音画同步:静音区应精确框住每个段落边界(与 gt0/gt1 对照,误差 < 0.15s)
ffmpeg -hide_banner -i work/final.wav -af silencedetect=noise=-45dB:d=0.20 -f null - 2>&1 | grep silence_
# 2. 抽关键帧肉眼看:封面 / 页眉+正文 / 每张要点卡出现时 / 结尾淡出
ffmpeg -y -ss 2.2 -i out.mp4 -frames:v 1 f1.png # 依次取 8 帧拼 contact sheet
# 3. 规格
ffprobe -v error -show_entries format=duration -show_entries stream=codec_name,width,height -of default=nw=1 out.mp4
自检不过就不交付。逐帧目检只做一次,别反复通读。
用法
VENV=<python 虚拟环境>/bin/python
$VENV <skill>/scripts/build.py config.json
config.json 字段说明见 references/config.schema.md,可直接改
references/example.config.json。
最小可用配置:docx / out / work / character / gaps / keys。
两套预设
| 预设 | 画幅 | 适用 | 参考配置 |
|---|---|---|---|
| 横版(默认) | 1920×1080 16:9 | 网页 / 大屏 / 课件 / 电脑观看 | references/example.config.json |
| 竖版 | 1080×1920 9:16 | 抖音 / 快手等竖屏短视频、竖屏微视频征集 | references/preset.vertical-9x16.json |
竖版预设同时满足常见的"竖屏微视频"征集规范:
- 深底(
bg_style=dark,深蓝渐变+光晕+底部压暗)+ 不画白色面板(panel_style=none)。
深底不是审美选择而是必需:规范要求标题黄色#FFCD43、字幕白色,这两种颜色放在浅底上几乎看不清。 - 标题条
title_bar:黄色、视频上方居中、最多两行;字体单独用title_bar.font指定
(规范常点名特定字体,本机没有就用最接近的替代,并在交付说明里写明替代关系)。
启用title_bar后不再画封面和页眉,标题从 0 秒起常驻。 - 同期声字幕
sync_sub:仅限一行、画面下方居中、白色;按显示宽度把整段话切成一行一句,
用char_times定时,min_dwell保证每条停留不低于设定值(默认 0.5s,对应"切换空隙不小于 0.5 秒")。
switch_gap是切换时的空白间隙,默认0= 交叉淡化;若规范要求必须留白再调大。 - 正文整段关闭(
caption_enabled=false):"字幕仅限一行"与"整段显示"天生冲突,
下划线要点交给要点卡承担"依次出现的醒目文字",职责分开才不打架。 - 人物居中下置(
ch_align=center+ch_bottom),并用ch_fade_bottom让下沿淡出 ——
半身像在画面中间"平切悬空"很难看,淡出融进背景才自然。 - 要点卡区用
layout.key_zone=[top,bottom]显式给,layout.content_x0/content_x1定左右内容区
(竖版没有面板,不能再用panel推算)。 - 收尾淡出用
fade_to=black(深底)。进度条底轨用colors.track_dark。 - 竖版人物宽约 520/1080,重心在画面中下部;别为了显得"大"而把人物顶高,
会与要点卡区撞车(要点卡最多三张时约占 340px 高)。
长稿与画幅
- 时长不固定:成片长度 = 配音总时长 + 段间停顿 + 首尾留白,由稿件字数决定,
1 分钟和十几分钟是同一条流程,不需要任何特殊参数。 - 长稿唯一需要留意的是 overlay 图层数 = 段落数 ×(1 + 要点卡数),几十段仍可正常工作;
若单段特别长,正文会自动逐 2pt 缩字号(下限cap_min)保证不溢出面板。 - 画幅由
w/h控制,但layout里的坐标是为 1920×1080 横版手工调的。
换 9:16 竖版(1080×1920)不能只改w/h,必须另配一整套layout:
panel矩形、ch_w/ch_x_right(人物改为下置)、cap_top/cap_size、
要点卡与封面的纵向坐标都要重算。横竖版是两套版式,不要混用坐标。
代跑口径(速度)
一次出片默认 ≤2 分钟。68s 成片实测:配音约 40 秒,图层渲染 + 编码约 20 秒。
禁止三种自造开销:为"刷新"重出与已有成片等价的整条片子、通读 build.py 源码、逐帧读图复核。
脚本打印的定量结果就是确认依据(总时长 / 事件数 / 静默检测与各段 gt0·gt1 的对齐情况);
逐帧目检留给人,Agent 只对脚本报警处读图。改完版式后抽 4–8 帧拼一张 contact sheet 给人过目即可,不要反复通看。
成本与耗时
- 图像生成:每个白底人物图约 5–10 credits(含一次徽章修图就是两张)。
开第 7 阶段口型再加 1 张微开口型图(每个角色一次,可长期复用)。 - 配音:edge-tts 免费。
- 全程本地编码,68s 成片约 1 分钟出片(开 1697 帧的口型烘焙约 +20 秒,中间文件自动清理)。
不需要用视频生成模型——本形态是版式动画,
视频模型既贵(5s 约 50–100 credits)又无法与中文口型对齐。
> 法律科技实务工具 · 维护者陆凌燕律师(北京德恒·无锡)· 关注公众号「鹿鸣于野 UMU」获取更多内容