推文 → 小红书图片笔记
把已成稿的长文,拆成小红书那种 3:4 竖版多图笔记:1 张封面 + 若干正文页 + 1 张结尾页, 每页 1080×1440,自动排版、自动导出 PNG。
分工是死的:AI 负责读懂文章并提炼分页,脚本负责排版出图。 脚本不理解内容, 所以每一页放什么、标题怎么写,是你要干的活。
工作流
1. 拿到原文
用户可能给:本地文件路径(.md/.txt/.html)、粘贴的正文、或一个 URL。
如果是微信公众号链接,走这两步(WebFetch 只能拿文字、拿不到图):
# ① 用 wechat-article-scraper 技能抓全文 + 全部图片
printf '%s\n' "<url>" > urls.txt
python3 ~/.workbuddy/skills/wechat-article-scraper/scripts/download_wechat_articles.py \
urls.txt --output-dir ./raw --min-image-size 500
--min-image-size 一定要调到 500 左右而不是默认的 6000 —— 我们要把所有图都拿下来,
自己按尺寸判定装饰,而不是让脚本用一个粗糙的字节数阈值替我们决定。
# ② 量尺寸 + 算哈希,用数据筛装饰,别靠肉眼猜
# 宽高比、重复的 md5、白底大留白,是三个决定性的信号
微信推文的装饰图识别规则(编辑器模板给装饰图用的也是 <img>,标签层面区分不了):
| 信号 | 判定 | 说明 |
|---|---|---|
| 同一张图(md5 相同)在文中出现 ≥2 次 | 装饰 | 分割条、章节隔断 —— 这是最强的信号,一票否决 |
| 高度 < 200px 或 宽高比 > 4 | 装饰 | 分割线、装饰横幅 |
| 宽高比 < 0.4 且白底面积大、字节数小 | 装饰 | 竖版收尾点缀(如羽毛笔、印章) |
| 出现在正文结束后、参考文献/落款之前 | 装饰 | 版式收尾,不是内容 |
| 单边 500px 宽但有完整画面叙事 | 配图 | 编辑器的半宽插图,是内容 |
data-w=1280 且字节数 > 500KB |
配图 | 全宽实拍/大插画 |
装饰性文本也要剔掉:小节序号(#1/#2/裸数字 1、5)、模板自带的
"点击上方蓝字关注"、空 <section>。这些在 get_text() 里会混进正文,分页时会变成
莫名其妙的独立短行 —— 正文里出现孤立的数字或 #N,基本就是这个。
⚠️ 引用标号(
[1]/[2][3]/[4]这类方括号数字)绝不能删。 用户 2026-09-11 明确定下规则:引用标号要作为正文文本原样保留,留在它原本所引用的 那半句末尾(例:「…让营养吸收更顺畅;[1]兼顾强健骨骼:…」)。 它们与文末「参考文献」一一对应,删掉参考文献就成了没有锚点的孤儿。区分口诀:
#N/ 独立成行的裸数字 = 编辑器序号装饰 → 可删;[N]= 引用标号 → 必留。两者长得像,性质完全相反,别混。 (如果删了标号后,卖点要点读起来更通顺,那是假象 —— 通顺的代价是失去出处。)
判定完把结论连依据一起告诉用户(哪张判为装饰、为什么),让他能推翻你的判断。
2. 提炼成 spec(核心步骤,别偷懒)
通读全文,然后决定分页。分页原则:
- 封面:一句钩子标题,15 字以内,要有反差或数字。不是文章原标题直接搬 —— 原标题通常太"文学",小红书要的是"点进去的冲动"。
- 正文页:一页只讲一件事。3~4 个要点最佳,超过 5 个就该拆页。 要点是短句(≤25 字),不是完整段落。
- 金句页:文章里最扎人的一句话,单独一页放。没有就不放。
- 数据页:有数字/成果时用,2~4 组最佳。
- 结尾页:一句总结。互动引导与话题标签不上图,走单独的文字稿(见第 5 步)。
页数控制在 5~9 页。少于 5 页内容撑不住,多于 9 页没人划到底。
3. 写 spec JSON
{
"style": "ink",
"accent": "#C1440E",
"kicker": "顶部小标签 · 品牌或栏目",
"source": "页脚来源",
// footnote: 页脚右可选文本,缺省不渲染。建议留空,或放日期/栏目名
"cover": {
"title": "封面钩子标题",
"subtitle": "一句补充说明",
"img": "cover.jpg"
},
"cards": [
{ "kind": "stat", "title": "先摆结果",
"items": [{ "num": "69", "label": "年品牌历史" }] },
{ "kind": "points", "index": "01", "title": "这一页的小标题",
"points": ["要点一", "要点二", "要点三"],
"note": "底部补充一句(可选)" },
{ "kind": "quote", "text": "金句", "by": "出处(可选)" }
],
"ending": {
"title": "收尾一句",
"img": "art01_img04.png"
}
}
话题标签与引导文案(如"评论区聊聊""更多内容见公众号")不要写进 spec —— 它们不再渲染到图上。这些统一放进另外单独交付的文字稿里(见第 5 步)。
页眉页脚的 4 段文本来自四个不同地方,交付时要能说清哪个是提取、哪个是撰写的:
| 位置 | 字段 / 代码 | 来源 |
|---|---|---|
页眉左 品牌 · 由头 |
kicker |
撰写。惯例「品牌 · 栏目/由头」,由头从标题与发布日期推 |
页眉右 09 / 09 |
head_foot() |
脚本自动生成,不用填 |
页脚左 品牌名 |
source |
从原文提取(公众号 var nickname / nick_name) |
| 页脚右(默认空) | footnote |
可选,自己填。缺省不渲染 |
页脚右没有默认文案 —— 这个位置早先硬编码过一个占位符,结果每张图都挂着一句 与文章无关的废话,发出去就是废信息。现在改成 spec 的
footnote字段:填了就渲染, 不填就整格不出现。别再把任何写死的默认值加回脚本。内容给什么,取决于你想让读者看到什么。常见的几种:日期(
2026.09.10)、 栏目/系列名(养生日常)、一句品牌主张。不建议放账号 ID —— 页眉、页脚左 通常已经有品牌名了,再叠一次是重复。
五种卡片类型:points(要点列表,最常用)、quote(金句)、stat(数据)、
image(图片)、product(场景+产品叠图)。
字段缺省即不渲染,不用凑。图上不要放话题标签和引导文案 —— 那些单独出一份文字稿。
图片卡(有配图时用)
{ "kind": "image",
"img": "art01_img01.png", // 只写文件名,配合 --img-base 用
"title": "图片下方的小标题",
"points": ["要点一", "要点二"], // 可选,字号会自动比 points 页小
"caption": "图片说明 / 产品名", // 可选
"layout": "top" } // top=图上文下(默认);imgonly=整页只有图
场景卡(把产品图叠到插画上)
产品图缩小后叠在插画的角上,省一页版面、又不遮主体。前提:产品图必须是
RGBA 透明底("漂浮罐体");用 PIL 检查四角 alpha==0 即可确认,
不要只看 RGB —— 透明区的 RGB 通常是黑的,很容易误判成"黑底图"。
{ "kind": "product",
"base": "art01_img07.png", // 底图(插画),必须是不透明图
"product": "art01_img05.png", // 产品图(透明底)
"title": "场景标题",
"caption": "产品名",
"base_fit": "cover", // cover(默认) | contain —— 竖版底图用 contain
"prod_anchor": "left", // right(默认) | left
"prod_w": 34, // 产品图宽度,占画布宽 %(建议 30~38)
"prod_x": 3 } // 距锚点边的内缩 %
两条硬规则:
prod_w不要小于 30%。画布 1080px,30% ≈ 324px;再小罐体上的产品名和 卖点数字就看不清了,叠了等于没叠。"完全不遮挡"和"看得清"是冲突的,取后者。- 产品图的底边要与底图下缘齐平(
bottom:0,CSS 已内置)。产品图自身的罐底 在原图里往往就是被裁掉的,底边对齐正好把切口藏进画面边界,看起来是"从画面底部 立出来"而不是"被切断"。
别把"边缘密度最低"当成唯一依据选叠图侧。那种纯量化分析不认识语义 —— 实测有一次它推荐了右侧,但右侧恰好是插画里唯一给"关节"做论证的膝关节特写圆圈。 必须亲眼看一遍候选底图,确认要叠的角上没有关键画面元素。
封面图与结尾图
cover.img 会以通栏形式放在封面底部(替代原先的话题标签位置);
ending.img 会作为"漂浮插画"贴在结尾页底部。两张都可选,不放也不影响。
配图目录用 --img-base 传,脚本会把用到的图复制到 out/images/,HTML 用相对路径引用,
所以整个 out/ 目录可以整个搬走不会掉图。
图片卡文字被裁掉的坑:.textbox 必须是 flex: 0 0 auto。如果给它 flex-shrink:1,
它会自己压掉高度把内容藏起来,外层 .stage 就检测不到溢出,字号自适应永远不触发 ——
表现就是最后的文字被切掉一半。这个坑踩过一次,别改回去。
4. 跑脚本出图
python3 ~/.workbuddy/skills/WeChat-to-xhs-cards/scripts/render.py \
--spec /path/to/cards.json --out /path/to/out --style ink --scale 2
--style:ink(暖白编辑风,默认) /glass(液态玻璃) /bold(深色撞色)--accent:覆盖主色,如#C1440E--scale:2 = 2160×2880(默认,够清晰);1 = 1080×1440--html-only:只出 HTML 不出图,调样式时用
产物:01-cover.png … NN-ending.png + preview.html(本地打开就能一屏看完全部)。
5. 交付
两样东西一起给:
- 图片:用 present_files 把 PNG 丢给用户,附一句你分了几页、为什么这样分
- 文字稿:另外写一个
.md,四个区块 —— 标题(2 个备选 + 原文标题)/ 简短正文 / 话题标签 / 参考文献。正文区块里不要混 markdown 语法,用户要整段复制到小红书发布框。 正文从原文重组、不新增事实,控制在 200~300 字。
排版是自动的,不用你操心
- 字号自适应:每页的文字块会自己缩到不溢出(脚本内置的 fit 逻辑)
- 内容不满一屏时纵向居中,不会顶在上半页
- 想改视觉(配色/字号/圆角)→ 只改
assets/card.css,不用碰脚本
字体与商用授权(重要,别改回去)
出图只用内置的开源字体,不碰任何商业字体。
- 中文正文 = 思源黑体(Noto Sans SC);金句页的装饰引号 = 思源宋体(Noto Serif SC)
- 两者都是 SIL OFL 1.1:免费商用,且协议明确允许"与任何软件捆绑再分发", 所以可以随本技能一起进公开仓库
- 字体放在
assets/fonts/(子集化 woff2,6 个字重合计约 6 MB),脚本每次渲染 复制到out/fonts/,整个 out 目录可以独立搬走;断网也能跑 - 字体栈里不要用
-apple-system/PingFang SC/Microsoft YaHei打头 —— 那会让 macOS 命中苹方(苹果商业字体)用于商用物料,且换到 Windows 会变成别的字形, 结果不可控。内置字体必须排首位,系统字体只做子集外生僻字的兜底 - 子集覆盖 7556 字(GB2312 全字 + ASCII + 常用全角标点),日常中文 99.9%+; 生僻字会回退到系统字体
- 体积、重建命令、字符集生成脚本:见
assets/fonts/README.md
为什么没用阿里普惠体 / MiSans / HarmonyOS Sans:它们同样免费商用、字形也很好, 但都是厂商自有协议且明确禁止再分发字体文件(普惠体协议:"未经授权,任何人不得 上传、发布、转载阿里巴巴字体文件")——装在本地自己用没问题,随开源仓库分发不行。 选字体时"免费商用"只是第一层,"允许再分发"才是决定能不能打包的那一层。
坑(踩过,别重踩)
- Chrome 152+ 截图后进程不退出:macOS 上 headless 截图写完 PNG 就卡住
(CVDisplayLink 报错循环)。脚本已经处理成"轮询产物文件 + 稳定后杀进程组",
不要去改成
subprocess.run等它自己结束,会永久挂住。 - 每次截图必须用全新的临时 profile:复用
user-data-dir会被上一次的残留 进程锁住,第二张开始卡死。 - 必须带
--no-sandbox:外层沙箱和 Chrome 自带 seatbelt 会互相打架, 不加就一张图都出不来(脚本已内置)。 mkdir(exist_ok=True)在部分环境下仍会抛 EEXIST:脚本里所有建目录都改成了if not d.exists(): d.mkdir(...)。不这么写的话第二次渲染必挂,而且报错信息 很有迷惑性(明明写了 exist_ok)。.textbox/.cover-img/.end-float必须flex: 0 0 auto:给它们flex-shrink:1会让它们自己压掉高度把内容藏起来,外层.stage就检测不到溢出, 字号自适应永远不触发 —— 表现是文字被切掉一半,图消失。这是本技能最容易误伤的地方。- 叠图卡的拼贴区必须设
min-height:底图和产品图都是绝对定位、不计入scrollHeight,如果拼贴区允许被压到 0,长标题会把整块图挤没,而溢出检测抓不到。 base_fit:"contain"时拼贴区要width:fit-content:否则产品图的百分比锚点 是相对"含左右留白的拼贴区"算的,罐体会溢出插画边界浮在页面空白上。- 找不到 Chrome:脚本按顺序找 Chrome / Edge / Brave / Chromium,都没有就报错。
此时用
--html-only先出 HTML,让用户自己在浏览器里截图。 - 别把 spec 写太大:单页要点超过 5 条、标题超过 18 字,出图会挤。宁可多分一页。
- 从 spec 里删掉某张图后,记得手动删
out/images/里的旧副本:脚本只做 "复制本次用到的图",不会清理历史留下的文件。如果那张图恰好是不能用的 (版权/授权问题),残留副本会跟着交付目录一起被发出去。 - 字体加载失败是静默的:
card.css里@font-face用的是../fonts/xxx.woff2—— 因为脚本把 CSS 内联进out/html/*.html、字体复制到out/fonts/,路径是相对 输出目录的html/算的。改字体文件名时 CSS 那侧要跟着改;写错了不会有任何报错, 页面会安静地退回系统字体(macOS 上就是苹方),出图看着"正常"但字形已经变了。 验证办法:同一段文字分别用"Noto Sans SC Local"和sans-serif渲染两版, 比对图片哈希 —— 相同就说明内置字体没生效。