v0.15: Live Photo 生产规则 — 平台限制、拼图版式、长视频处理、发布提醒、可见文案规则

新增 Live Photo 正式能力:单视频、二宫格、三宫格、四宫格、三连拼图、
长视频低成本诊断。增加 references/live-photo-production.md 生产规范、
scripts/ 工具链(contact sheet / .pvt 打包 / Apple metadata)、
validator R8 visual bounds + R9 title gap、文档约束测试。

由 Codex 完成主体实现。
This commit is contained in:
郭浩 committed 2026-07-01 16:11:34 +08:00
1 parent 032782ff67
commit f492caeb8b
20 files changed
+1982 -22

No files matched your search

+15
View File
@@ -1,12 +1,27 @@
# Local test workspace — demos, screenshots, downloaded assets, debug scripts. # Local test workspace — demos, screenshots, downloaded assets, debug scripts.
# Skill iterates only on SKILL.md / references/ / assets/ / agents/ / validate. # Skill iterates only on SKILL.md / references/ / assets/ / agents/ / validate.
local-tests/ local-tests/
outputs/
# Guard against accidental task artifacts created in the skill root.
/social-card-*/
/livephoto-*/
/wechat-*/
/output/
/*.png
/*.jpg
/*.jpeg
/*.mov
/*.mp4
/*.pvt/
# Node + macOS # Node + macOS
node_modules/ node_modules/
.DS_Store .DS_Store
*.log *.log
npm-debug.log* npm-debug.log*
__pycache__/
*.pyc
# Editor / OS # Editor / OS
.vscode/ .vscode/
+190
View File
@@ -0,0 +1,190 @@
# Guizang Skill 商业授权合作说明
这份说明适用于希望在自己的 AI 产品、Agent 平台、创作工具、插件市场或工作流系统中接入 Guizang Skill 能力的合作方。
Guizang Skill 系列的核心能力包括但不限于:根据正文语义自动规划图文卡片、调用真实图库素材、处理截图与网页素材、生成地图/路线组件、完成多比例社交图片排版、生成高质量 PPT/演示文稿,以及对中文 AI 文本进行自然化改写。
本说明中的价格为首批合作参考价,具体合作范围、产品线、授权期限、适配深度、结算方式和交付内容,以双方最终签署的协议为准。
## 热门项目介绍
目前较受欢迎、适合商业授权或平台合作的项目主要包括以下三个。
### 1. Guizang Social Card Skill
项目地址:[op7418/guizang-social-card-skill](https://github.com/op7418/guizang-social-card-skill)
这个 Skill 面向小红书、小绿书、微信公众号、视频封面、社交媒体图文卡片等场景。它可以根据正文内容自动拆解页面结构,结合真实图片、截图、地图、商品图或网页素材,生成多比例、多页面的社交卡片。
适合接入的产品场景:
- AI 写作工具中的一键配图能力。
- 小红书 / 小绿书 / 公众号内容创作工具。
- Agent 平台中的图文卡片生成 Skill。
- 视频工具中的封面生成能力。
- 企业内容营销、产品说明、教程卡片生成场景。
核心价值:
- 减少用户手工找图、裁图、排版的成本。
- 避免纯色块、emoji 或 AI 插画导致的廉价感。
- 支持真实图库、截图、地图等证据型视觉素材。
- 输出可以直接用于社交平台发布的成品图。
### 2. Guizang PPT Skill
项目地址:[op7418/guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill)
这个 Skill 面向 PPT、演示文稿、汇报材料、课程课件、商业提案和视觉化表达场景。它能够根据用户提供的主题、文案、资料或结构,生成具有明确视觉风格的演示页面。
适合接入的产品场景:
- AI PPT 生成工具。
- 办公套件、在线文档、知识库和协作平台。
- 企业汇报、教育培训、公开演讲、课程内容生成。
- Agent 平台中的 PPT 生成或美化能力。
- 将文章、文档、会议纪要转换为演示稿的工作流。
核心价值:
- 帮用户从空白页直接进入可用的视觉表达。
- 提供比普通模板更强的排版、风格和叙事能力。
- 适合和文档、知识库、会议纪要、商业分析等工具结合。
- 可以作为 AI 办公产品的重要差异化能力。
### 3. Humanizer-zh
项目地址:[op7418/Humanizer-zh](https://github.com/op7418/Humanizer-zh)
这个 Skill 面向中文文本自然化、去除 AI 味、内容润色和表达优化场景。它可以识别并修复常见的 AI 写作痕迹,让中文内容更自然、更像真人书写。
适合接入的产品场景:
- AI 写作工具中的“一键去 AI 味”功能。
- 内容平台、公众号、小红书、知乎、视频脚本等创作工具。
- 简历、邮件、商业文案、社媒文案润色。
- 教育、办公、营销、知识管理类产品。
- Agent 平台中的中文文本优化 Skill。
核心价值:
- 提升 AI 生成文本的可读性和真实感。
- 降低用户后期人工润色成本。
- 适合放在生成链路的最后一步,作为文本质检和风格修正能力。
- 对中文场景更友好,不只是简单改写或同义词替换。
以上三个项目可以单独授权,也可以组合授权。组合授权适合希望一次性补齐“写作、配图、PPT、润色”完整内容生产链路的平台。
## 合作等级
### 1. 深度内置授权
这是最高级别的合作方式,适合希望将 Guizang Skill 的能力深度集成到自有产品中的团队。
合作方可以将相关能力内置到自己的产品流程中,也可以选择不在产品界面中展示 Guizang Skill 的署名。经双方确认后,合作方可以对外将该能力作为自身产品能力的一部分进行介绍。
我会配合合作方完成必要的产品适配、效果优化、提示词与工作流调整,使该能力在合作方产品中达到较好的使用体验。必要时,我也可以配合合作方进行联合宣传或案例发布。
参考费用:
- 创业公司、早期团队或熟人合作:一次性授权费用为 10 万元起。
- 日活 10 万级以上产品:一次性授权费用为 30 万元起。
默认包含:
- 授权一个明确产品或一个明确产品线使用。
- 允许深度内置到合作方产品中。
- 允许不展示 Guizang Skill 署名。
- 提供基础接入建议和一定范围内的效果优化配合。
- 可根据实际情况配合对外宣传。
默认不包含:
- 源代码或知识产权转让。
- 独家授权或品类排他。
- 跨多个产品线、多个品牌或多个客户的转授权。
- 长期驻场开发、持续定制开发或无限期维护。
- 将 Guizang Skill 能力包装后再出售给第三方平台。
如果合作方需要独家授权、区域排他、品类排他、源代码买断、长期深度定制、白标转售或多产品线使用,需要单独报价和签署补充协议。
### 2. 上架与露出合作
这是标准合作方式,适合希望在自己的平台中上架 Guizang Skill,但不需要深度定制或买断能力的合作方。
合作方可以在自己的平台、插件市场、Agent 市场或工作流市场中上架 Guizang Skill,并允许用户通过平台调用相关能力。我会保留 Guizang Skill 的版权和署名,不将其视为合作方自研能力。
参考费用:
- 固定合作费用:3 万元。
这笔费用也可以视为对 Guizang Skill 项目的赞助。作为回报,我会在项目 README、Skill 文档或相关说明中露出合作方品牌,具体露出位置和方式由双方确认。
默认包含:
- 授权合作方在一个平台或一个插件市场中上架 Guizang Skill。
- 保留 Guizang Skill 名称、版权和署名。
- 提供基础上架支持。
- 提供基础版本更新,但不承诺深度适配。
- 在 Guizang Skill 项目文档中露出合作方品牌。
默认不包含:
- 去署名或白标使用。
- 宣称该能力为合作方自研。
- 深度产品定制、私有化重构或长期专项维护。
- 独家授权、排他授权或转授权。
### 3. 收益分成模式
这是最低门槛的合作方式,适合已经具备付费能力的平台,例如用户可以通过积分、Token、额度包、订阅会员或按次付费来使用 Skills 的平台。
合作方可以将 Guizang Skill 上架到平台中,由用户按平台规则付费使用。合作方需要定期向我结算由 Guizang Skill 产生的收益分成。
参考方式:
- 不收取固定上架费用,或仅收取很低的接入费用。
- 合作方按月或按季度结算收益分成。
- 分成比例根据平台计费方式、流量规模、推广资源和技术支持成本另行确认。
默认包含:
- 授权合作方在平台中上架 Guizang Skill。
- 用户可以通过平台的积分、Token、额度或付费系统调用该能力。
- 我不提供额外深度适配,只维护通用版本能力。
默认要求:
- 合作方需要提供清晰的调用数据、收入数据和结算周期。
- 合作方不得隐藏、混淆或挪用 Guizang Skill 产生的实际收益。
- 合作方需要在合理位置保留 Guizang Skill 名称或来源说明。
- 如果后续需要深度适配、去署名、白标或对外包装为自研能力,应升级为深度内置授权。
## 通用授权边界
除非双方另有书面约定,以上任一合作等级均不代表:
- 转让 Guizang Skill 的版权、商标、名称、源代码或其他知识产权。
- 授予合作方独家使用权。
- 允许合作方将 Guizang Skill 再授权、转售或封装给第三方。
- 允许合作方使用 Guizang Skill 的内容、代码、提示词、工作流或生成结果训练竞品模型或复刻类似产品。
- 允许合作方移除版权信息后,将 Guizang Skill 作为完全自有资产再次商业化。
如需上述权利,需要单独协商并签署独立协议。
## 合作流程
1. 合作方说明产品形态、接入场景、预计用户规模和商业模式。
2. 双方确认合作等级:深度内置授权、上架与露出合作,或收益分成模式。
3. 双方确认授权范围,包括产品线、平台、使用方式、是否署名、是否需要适配、是否需要宣传配合。
4. 双方确认费用、付款方式、结算周期和交付内容。
5. 签署协议并完成付款或结算配置。
6. 我提供对应等级的接入、上架或适配支持。
## 备注
以上价格为当前阶段的合作参考价。随着 Guizang Skill 的用户规模、Star 数、案例数量和平台影响力提升,后续商业授权价格可能调整。
如果合作方希望尽快接入,建议优先选择首批合作方案。首批合作方可以获得更低的授权价格、更灵活的合作方式,以及更直接的产品适配沟通。
本说明仅作为商业合作框架,不构成最终法律协议。正式合作前,双方可以根据实际需求补充合同条款。
+68 -1
View File
@@ -1,6 +1,6 @@
# Handoff — guizang-social-card-skill # Handoff — guizang-social-card-skill
最后更新:2026-05-28 · 版本:v0.14(截图美化素材迁移:9 张 WebP 背景从 PPT skill port 过来 + 两个种子加 9 条 `.bg-asset-*`) 最后更新:2026-07-01 · 版本:v0.15(Live Photo 生产规则:3s/5s 平台限制、拼图版式、长视频处理、发布提醒、可见文案与不自造规则)
这份文档只记录**事实**:当前 skill 由哪些文件组成、每个文件管什么、本轮更新改了什么、怎么验证它还能跑。想读"为什么这样做"或"未来要怎么走",去看 `PRODUCT.md`。 这份文档只记录**事实**:当前 skill 由哪些文件组成、每个文件管什么、本轮更新改了什么、怎么验证它还能跑。想读"为什么这样做"或"未来要怎么走",去看 `PRODUCT.md`。
@@ -37,9 +37,15 @@ guizang-social-card-skill/
│ ├── map-component.md Mapbox / OSM / schematic 地图 │ ├── map-component.md Mapbox / OSM / schematic 地图
│ ├── title-shortener.md 公众号 1:1 短标题 │ ├── title-shortener.md 公众号 1:1 短标题
│ ├── category-cookbook.md 小红书 11 个品类的版式路由 │ ├── category-cookbook.md 小红书 11 个品类的版式路由
│ ├── live-photo-production.md Live Photo 生产、拼图、长视频与发布规范
│ └── qa-checklist.md 交付前 checklist │ └── qa-checklist.md 交付前 checklist
├── agents/ ├── agents/
│ └── openai.yaml # 子代理配置(备用) │ └── openai.yaml # 子代理配置(备用)
├── scripts/
│ ├── check-skill-docs.mjs # Skill 文档约束测试
│ ├── make-video-contact-sheet.py # Live Photo 视频抽帧 contact sheet
│ ├── package-live-photo.py # JPG+MOV 打包为 .pvt 的入口
│ └── *.swift # Apple Live Photo metadata helper
├── validate-social-deck.mjs # 6 条规则的渲染后校验器 ├── validate-social-deck.mjs # 6 条规则的渲染后校验器
├── package.json / package-lock.json / node_modules # 仅 playwright ├── package.json / package-lock.json / node_modules # 仅 playwright
├── .gitignore # v0.13 新增 ├── .gitignore # v0.13 新增
@@ -70,6 +76,7 @@ v0.13 起:git 仓库。`local-tests/` 整目录在 `.gitignore` 中,commit
| 小红书品类覆盖 | 11 个品类中 7 个完整支持,4 个需用户提供素材,4 个明确不接 | | 小红书品类覆盖 | 11 个品类中 7 个完整支持,4 个需用户提供素材,4 个明确不接 |
| 种子模板 | 2 份(Editorial / Swiss),单文件即含全部 class 与字体 | | 种子模板 | 2 份(Editorial / Swiss),单文件即含全部 class 与字体 |
| 已交付案例 | 7 个 demo(2 个 image-cover + 5 个 kit) | | 已交付案例 | 7 个 demo(2 个 image-cover + 5 个 kit) |
| Live Photo 支持 | 小红书 `5s` / 微信公众号 `3s`;单视频、二宫格、三宫格、四宫格、三连拼图、长视频低成本诊断 |
--- ---
@@ -77,6 +84,42 @@ v0.13 起:git 仓库。`local-tests/` 整目录在 `.gitignore` 中,commit
仅记录"什么时候新增/变更了哪些可见资产",不记录文风调整。 仅记录"什么时候新增/变更了哪些可见资产",不记录文风调整。
### v0.15 · 2026-07-01(本轮)
主题:**Live Photo 从一次测试能力收口成 Skill 规则**。本轮把两天聊天里关于平台限制、拼图版式、长视频处理、品类判断、发布路径和执行失误的结论写回文档,并补上文档测试。
**新增**
- `references/live-photo-production.md`:Live Photo 生产规范。覆盖 `.pvt` 打包、AirDrop/iPhone 测试、小红书 `5s`、微信公众号 `3s`、信息量判断、三连拼图、material-first 拼图、长视频低成本诊断、contact sheet、平台发布提醒和常见故障。
- `scripts/check-skill-docs.mjs`:文档约束测试。当前卡住根目录产物、Live Photo 拼图能力、M16 图片压字规则、可见文案规则、非模板装饰禁令、平台发布提醒、产品复盘和交接记录。
- `scripts/make-video-contact-sheet.py`:把视频按少量时间点抽成 contact sheet,用于长视频或 Live Photo 视觉检查,避免默认密集抽帧。
- `scripts/package-live-photo.py` 及 Swift helper:把 JPG + MOV 写入 Apple Live Photo metadata 并打包成 `.pvt`,用于 iPhone/AirDrop 测试。
**修改**
- `SKILL.md` 增加 Live Photo 支持范围:短 motion card、三连 Live Photo、material-first 单视频/二宫格/三宫格/四宫格拼图、长视频 intake。明确用户提供视频是正常路径,网页素材只用于 demo / promo / 测试。
- `SKILL.md` 加硬规则:生成产物必须在任务文件夹,默认 `local-tests/<slug>/`;禁止把结果放在 Skill 根目录。
- `SKILL.md` 加可见文案规则:卡面文案必须描述用户真实场景,不能把 `3s`、`5s`、`Live Photo`、`三连拼图`、`信息量`、`长视频处理`、`倍速`、`高光检测` 等内部制作词写成标题或正文。
- `SKILL.md` 加 material-first 规则:单视频加字必须走 M16 / image-overlay 的图片压字逻辑;如果只有一个标题,就用断行、字号、字重、字距、对齐和位置做排版,不发明 kicker、meta、hairline、线条、刻度、徽章、字幕或说明文字。
- `references/category-cookbook.md` 加 Live Photo Scene Library。它不是固定模板清单,而是判断素材在 `3s` / `5s` 内能承载多少信息,以及适合单视频、三连、拼图、修剪还是不做 Live Photo。
- `references/platform-specs.md` 加小红书 / 微信公众号 Live Photo 平台说明:小红书 `5s`,微信文章内 `3s`,发布路径以手机端为准。
- `PRODUCT.md` 加 `Live Photo 复盘(2026-07-01)`,记录这次需求、产品判断、执行问题和验收标准。
**本轮测试素材与输出**
- 用户素材目录:`local-tests/live-photo-best-suite/视频素材/`
- 统一测试目录:`local-tests/live-photo-best-suite/puzzle-layouts/`
- 四个测试视频输出:
- `IMG_XHS_PUZZLE_SINGLE_TEXT_5S_LIVE.MOV`
- `IMG_XHS_PUZZLE_TWO_STACK_5S_LIVE.MOV`
- `IMG_XHS_PUZZLE_THREE_STACK_5S_LIVE.MOV`
- `IMG_XHS_PUZZLE_FOUR_GRID_5S_LIVE.MOV`
- 规格:全部 `1080x1440` / `5s` / `24fps` / `120 frames`。本地测试目录被 `.gitignore` 忽略,不随 Skill 分发。
**线程里暴露的问题,已转成规则**
- 不要把制作要求当成观众可见内容。之前错误例子包括把"三连拼图"、"5 秒看完完整幅度"、"别硬塞"写到画面上。
- 不要自造主题、背景色、线条、刻度字或装饰组件。没有在 `theme-presets.md` / `layout-recipes.md` / `image-overlay.md` 出现,就不能为了"显得设计过"而加。
- 不要为了通过密度警告给 material-first 拼图加字。二宫格、三宫格、四宫格可以无字;单视频也只能加用户场景需要的短标题。
- 长视频不要默认做精准高光检测。只能先做稀疏抽帧和候选段诊断,再让用户选择修剪、倍速、拆成三连或指定时间段。
- 网页下载视频不是用户工作流。那只服务我们自己的测试和宣传案例。
### v0.14 · 2026-05-28(本轮) ### v0.14 · 2026-05-28(本轮)
主题:**截图美化素材从 PPT skill 迁移**。v0.11 当时只 port 了规则文档(`references/screenshot-treatment.md` 154 行)和工具类(`.frame-shot.*` + `.device-browser` / `.device-phone`),但 PPT skill 的真实材质 WebP 背景**整目录没拷过来**,导致 `.bg-paper / bg-grid / bg-dot` 这些舞台底全是纯 CSS 实色,截图密度高的卡片视觉偏薄。本轮把素材补齐。 主题:**截图美化素材从 PPT skill 迁移**。v0.11 当时只 port 了规则文档(`references/screenshot-treatment.md` 154 行)和工具类(`.frame-shot.*` + `.device-browser` / `.device-phone`),但 PPT skill 的真实材质 WebP 背景**整目录没拷过来**,导致 `.bg-paper / bg-grid / bg-dot` 这些舞台底全是纯 CSS 实色,截图密度高的卡片视觉偏薄。本轮把素材补齐。
@@ -352,6 +395,15 @@ v0.13 起:git 仓库。`local-tests/` 整目录在 `.gitignore` 中,commit
## 4. 如何验证 skill 还能跑 ## 4. 如何验证 skill 还能跑
先跑文档层验证,确认 Skill Creator 基础格式和本仓库约束都还成立:
```bash
python3 /Users/guohao/.agents/skills/skill-creator/scripts/quick_validate.py .
npm run test:docs
```
`quick_validate.py` 只检查 Skill Creator 的基础结构:`SKILL.md`、frontmatter、name / description 合法性。`npm run test:docs` 检查本仓库新增的产品规则:根目录不产出、Live Photo 拼图、M16 图片压字、可见文案、发布提醒、产品复盘和交接记录。
最快的验证路径,不要发明新需求。 最快的验证路径,不要发明新需求。
```bash ```bash
@@ -367,6 +419,15 @@ sips -g pixelWidth -g pixelHeight output/xhs-01.png # 应为 1080×1440
更全的 demo:`local-tests/demo-image-02-wukong/index.html` 用 Playwright 渲染,验证 image-overlay 规则下文字与人物未冲突。 更全的 demo:`local-tests/demo-image-02-wukong/index.html` 用 Playwright 渲染,验证 image-overlay 规则下文字与人物未冲突。
Live Photo 视觉回归可以直接看本地测试目录:
```bash
cd /Users/guohao/Documents/code/HyperFrames-test/guizang-social-card-skill/local-tests/live-photo-best-suite/puzzle-layouts
ls output/
```
重点看 `contact-sheet-*.jpg` 和四个 `IMG_XHS_PUZZLE_*_LIVE.MOV`。不要重新开新目录,除非用户明确要新的测试集。
--- ---
## 5. 工作流要点(给接手者的极简版) ## 5. 工作流要点(给接手者的极简版)
@@ -378,6 +439,8 @@ sips -g pixelWidth -g pixelHeight output/xhs-01.png # 应为 1080×1440
文字压图的页(封面 / 大图位 / 全幅图)四步:先 `Read` 图、写 HTML 注释记录主体位置、选 `object-position`、做 360px 缩略图检查。默认先不加遮罩;只有缩略图不可读时才加局部、取自图片色调的 tint。 文字压图的页(封面 / 大图位 / 全幅图)四步:先 `Read` 图、写 HTML 注释记录主体位置、选 `object-position`、做 360px 缩略图检查。默认先不加遮罩;只有缩略图不可读时才加局部、取自图片色调的 tint。
Live Photo 页先当静态社交卡做:先确认第一帧、裁切、字体、可见文案和模板匹配,再渲 MOV / `.pvt`。单视频加字走 M16 / image-overlay;二宫格、三宫格、四宫格默认无字;三连拼图只放能并行理解的三个短片段。
--- ---
## 6. 已知风险与限制(事实层面) ## 6. 已知风险与限制(事实层面)
@@ -386,6 +449,8 @@ sips -g pixelWidth -g pixelHeight output/xhs-01.png # 应为 1080×1440
- **没有跨平台一次出包工具**。一篇文章要同时出小红书 3:4 + 微信 21:9+1:1 + 方封面 1:1,目前要手工开两个任务文件夹。 - **没有跨平台一次出包工具**。一篇文章要同时出小红书 3:4 + 微信 21:9+1:1 + 方封面 1:1,目前要手工开两个任务文件夹。
- **品类能力圈未硬编码**。`category-cookbook.md` 写明了 4 个不接的品类,但 skill 不会主动拒绝;需要执行者在 Step 1 自己读到并主动告知用户。 - **品类能力圈未硬编码**。`category-cookbook.md` 写明了 4 个不接的品类,但 skill 不会主动拒绝;需要执行者在 Step 1 自己读到并主动告知用户。
- **图片来源签名机制是"用户决定"**。`SOURCES.md` 总会写,但是否在图上加标注由用户回答。这不是技术限制是产品决策(见 `PRODUCT.md`)。 - **图片来源签名机制是"用户决定"**。`SOURCES.md` 总会写,但是否在图上加标注由用户回答。这不是技术限制是产品决策(见 `PRODUCT.md`)。
- **Live Photo 高光选择只是低成本诊断**。默认只做稀疏抽帧 / contact sheet / 时间段建议,不承诺精准识别转场、空镜、黑场或最佳高光。
- **Live Photo 发布依赖手机端路径**。交付时必须提醒小红书 `5s`、微信公众号 `3s`,并提醒 `.pvt` AirDrop 到 iPhone 后从对应 App / 手机端编辑路径发布。
--- ---
@@ -395,3 +460,5 @@ sips -g pixelWidth -g pixelHeight output/xhs-01.png # 应为 1080×1440
- 不要把 demo 文件夹删掉来"清理"。它们是回归测试样本。 - 不要把 demo 文件夹删掉来"清理"。它们是回归测试样本。
- 不要在两份种子模板里塞动效系统、键盘翻页、ESC 索引——那是 PPT 的需求,社交卡是静态 PNG。 - 不要在两份种子模板里塞动效系统、键盘翻页、ESC 索引——那是 PPT 的需求,社交卡是静态 PNG。
- 不要把 components.md 的硬上限表"调宽"以接更长的标题。先压文案。 - 不要把 components.md 的硬上限表"调宽"以接更长的标题。先压文案。
- 不要把内部制作词写到卡面上,例如 `三连拼图`、`5 秒看完`、`信息量`、`Live Photo`、`长视频处理`。这些词只能出现在计划、文件名、QA 或交付说明里。
- 不要用自造背景色、线条、刻度、标签、徽章去"补排版"。先找既有模板和 reference;没有规则支持就不要加。
+30
View File
@@ -259,3 +259,33 @@ v0.6-v0.11 都把 validator(`validate-social-deck.mjs`)写成"渲完必须
> 当你出完一张图,先问"如果用户截屏发给朋友,朋友会不会愿意停下来看"。 > 当你出完一张图,先问"如果用户截屏发给朋友,朋友会不会愿意停下来看"。
前者管"不假"。后者管"能打"。两件事都做到,这个 skill 就值得继续存在。 前者管"不假"。后者管"能打"。两件事都做到,这个 skill 就值得继续存在。
---
## 10. Live Photo 复盘(2026-07-01)
Live Photo 不是"把视频塞进卡片"。它是把原来静态图里的一个 image well 换成 motion well,所以仍然要先遵守社交卡的版式、字体、配色、裁切和可见文案规则。
这两天讨论后,Live Photo 的产品定位收敛为四件事:
1. **用户素材优先**。正常用户路径是用户上传自己的视频、录屏、游戏片段或生活素材。网页找视频只用于我们自己做 demo / promo / 回归测试,不应该被写成用户工作流。
2. **信息量判断优先于品类标签**。品类库不是固定模板表,而是判断 `3s` / `5s` 内能展示多少信息:一个动作、一个小过程、一个前后变化、三个并行结果,还是根本不适合 Live Photo。
3. **拼图是正式能力**。当素材质量足够好,可以少字甚至无字,把单视频、上下二宫格、上中下三宫格、四宫格作为 material-first Live Photo 输出。单视频加字时使用 M16 / image-overlay 的图片压字规则;二宫格、三宫格、四宫格默认不加文字。
4. **三连 Live Photo 是高信息密度玩法**。小红书流行的三列长图式 Live Photo,本质是把三个并行短片段放在一个卡片单元里。它适合三种结果、三个视角、三步对比,不适合把一个长流程硬拆成必须顺序阅读的教程。
长视频处理不能承诺"精准自动找高光"。当前合理做法是低成本诊断:稀疏抽帧 / contact sheet / 关键时间点预览,用来发现候选段落,再让用户选择修剪、倍速、拆成三连、或指定时间范围。密集抽帧能更准,但 token 和时间成本过高,不应该作为默认路径。
发布体验只需要在交付时提醒清楚:
- 小红书 Live Photo 默认按 `5s` 控制。
- 微信公众号文章内 Live Photo 按 `3s` 控制。
- 两个平台都应走手机端发布路径。`.pvt` 通过 AirDrop 发到 iPhone 后,从对应 App / 手机端编辑路径发布;电脑端 / 网页端不能被当作可靠 Live Photo 发布入口。
这次线程里暴露的主要问题,不是渲染能力不足,而是执行者没有把旧 Skill 当成硬约束:
- 把内部制作要求写成观众可见文案,例如把"三连拼图"、"5 秒看完"、"别硬塞"写到卡面上。
- 在没有模板依据时自造背景色、主题、橙色线条、刻度字、kicker、meta、hairline。
- 为了显得"有排版"而添加额外组件,而不是用既有模板里的字体、断行、对齐、位置和留白完成排版。
- 把自动密度警告当成必须加装饰的理由,忽略了 material-first 拼图本来就可以少字或无字。
所以 Live Photo 的验收标准要比"能动"更严格:第一帧必须像一张合格的静态社交卡;运动只增强内容证据,不替代版式判断;所有可见文字都必须像用户真的会发给观众看的内容。
+10 -7
View File
@@ -174,14 +174,17 @@ Skill 本身是结构化工作流,Agent 会按 7 步走:
node validate-social-deck.mjs path/to/task-dir node validate-social-deck.mjs path/to/task-dir
``` ```
6 条规则,基于 Playwright 真实渲染测量,不是静态扫描: 9 条规则,基于 Playwright 真实渲染测量,不是静态扫描:
- **R1** Overflow — 任何 section 超出 `.poster` 立刻报错 - **R1** Overflow — 任何 section 超出 `.poster` 立刻报错,并给出超出像素对应的修正阶梯
- **R2** Type Caps — `.h-xl` / `.h-display` 字号 + 字重组合超出种子定义 - **R2** Footer Collision — 内容压到底部 footer / page-number
- **R3** Footer Collision — 内容压到底部 footer / page-number - **R3** Swiss Bold Display — Swiss 大标题违反"越大越细"时报警
- **R4** 4-Band Density — 1440 高画布切 4 横带,每带应有内容或主动留白理由 - **R4** Min Readable Font — 正文、说明、标签字号低于手机可读下限
- **R5** Frame Overflow — `.frame-img` / `.frame-shot` 子元素溢出框 - **R5** 4-Band Density — 1440 高画布切 4 横带,每带应有内容或主动留白理由
- **R6** Swiss Identity — Swiss 模板里 inline `font-weight >= 700` 警告(违反"越大越细") - **R6** H-XL Line Cap — `.h-xl` / `.h-display` / `.h-statement` 行数超出画板预算
- **R7** Figure Margin Drift — 浏览器默认 `<figure>` margin 造成版式漂移
- **R8** Visual Bounds — 测量真实可见内容的上/下边界,报告视觉超出和底部空白
- **R9** Title Gap — 标题与下一块内容之间的距离过小,避免贴在一起
`SKILL.md` 第 7 步明确**默认不自动跑** validator —— 等用户先看完图再问,避免每轮多花数十秒。 `SKILL.md` 第 7 步明确**默认不自动跑** validator —— 等用户先看完图再问,避免每轮多花数十秒。
+40 -5
View File
@@ -1,19 +1,25 @@
--- ---
name: guizang-social-card-skill name: guizang-social-card-skill
description: Generate Guizang-style social card image sets and WeChat official account cover pairs from articles, scripts, screenshots, product notes, subtitles, or photos. Use when the user asks for 小红书图文, Rednote/Xiaohongshu images, social cards, carousel images, 3:4 covers, 微信公众号封面, WeChat 21:9 + 1:1 covers, Swiss Style, or magazine-style social images. description: Generate Guizang-style social card image sets, Live Photo motion cards, material-first Live Photo puzzle layouts, triple Live Photo collages, long-video-to-Live-Photo treatments, and WeChat official account cover pairs from articles, scripts, screenshots, product notes, subtitles, photos, or user-supplied videos. Use when the user asks for 小红书图文, Rednote/Xiaohongshu images, social cards, carousel images, 3:4 covers, Live Photo, 实况照片, 单视频实况拼图, 二宫格实况拼图, 三连实况拼图, 四宫格实况拼图, 微信公众号封面, WeChat 21:9 + 1:1 covers, Swiss Style, or magazine-style social images.
--- ---
# Guizang Social Card Skill # Guizang Social Card Skill
Create polished social card packages for Xiaohongshu/Rednote, WeChat Official Account, article covers, and platform thumbnails. Create polished social card packages for Xiaohongshu/Rednote, WeChat Official Account, article covers, and platform thumbnails.
This skill is self-contained. It borrows visual principles from the Guizang PPT style system, but it must not edit the original PPT skill, its templates, or its references. If the original PPT skill is available, you may read it for reference only. Put all generated work in the current project or in the user-requested output folder. This skill is self-contained. It borrows visual principles from the Guizang PPT style system, but it must not edit the original PPT skill, its templates, or its references. If the original PPT skill is available, you may read it for reference only.
Generated work must live in a task folder, not in the skill root. Default to `local-tests/<slug>/` inside this repository, or use the explicit output folder requested by the user. Do not create root-level task folders such as `social-card-*`, `livephoto-*`, `wechat-*`, `output/`, or loose rendered assets next to `SKILL.md`.
## What To Produce ## What To Produce
Use this skill for: Use this skill for:
- Social card / carousel image sets: cover plus content pages, especially Xiaohongshu/Rednote 3:4. - Social card / carousel image sets: cover plus content pages, especially Xiaohongshu/Rednote 3:4.
- Short Live Photo motion cards when the user asks for Live Photo / 实况照片 and supplies video evidence or screen recordings.
- Material-first Live Photo puzzle layouts when the user has high-quality video assets and wants single-video, two-grid, three-grid, or four-grid motion cards with little or no added text.
- Triple Live Photo collages when the user has three short videos, three results, three viewpoints, or a high-information comparison that should stay inside one image-card unit.
- Long-video intake for Live Photo: diagnose whether to trim, speed up, split into a triple collage, or ask the user for a specific time range before rendering.
- WeChat Official Account cover pairs: one `21:9` main cover plus one `1:1` square cover, composed together in the same HTML for visual checking. - WeChat Official Account cover pairs: one `21:9` main cover plus one `1:1` square cover, composed together in the same HTML for visual checking.
- Screenshot-heavy product posts, article covers, tutorial carousels, outdoor/lifestyle notes, AI/product update explainers. - Screenshot-heavy product posts, article covers, tutorial carousels, outdoor/lifestyle notes, AI/product update explainers.
- Social images that need Guizang-style Swiss or editorial magazine layouts. - Social images that need Guizang-style Swiss or editorial magazine layouts.
@@ -21,7 +27,7 @@ Use this skill for:
Do not use this skill for: Do not use this skill for:
- Full slide decks or horizontal PPT websites. Use the PPT skill for that. - Full slide decks or horizontal PPT websites. Use the PPT skill for that.
- Long-form video generation. Use a video skill for that. - Long-form video generation. Use a video skill for that. This skill only supports short, layout-bound Live Photo cards that replace a still image slot with video.
- Pure image editing with no layout or article extraction requirement. - Pure image editing with no layout or article extraction requirement.
### Rednote Category Capability (capability circle) ### Rednote Category Capability (capability circle)
@@ -70,6 +76,7 @@ Read these files as needed:
- `references/portrait-fill.md` when adapting layouts to 3:4 and avoiding under-filled vertical space. - `references/portrait-fill.md` when adapting layouts to 3:4 and avoiding under-filled vertical space.
- `references/content-planning.md` for cover hooks, page breakdown, and copy compression. - `references/content-planning.md` for cover hooks, page breakdown, and copy compression.
- `references/production-workflow.md` for HTML/CSS rendering and image handling. - `references/production-workflow.md` for HTML/CSS rendering and image handling.
- `references/live-photo-production.md` when the user asks for Live Photo / 实况照片 / 三连实况拼图, supplies video assets for a social card, or wants Xiaohongshu / WeChat motion-card delivery. It covers information budget, single vs triple Live Photo, long-video intake, and platform publishing reminders.
- `references/image-overlay.md` whenever text sits on top of a photo: photo qualification, localized tint fallback, and face / subject avoidance via multimodal subject mapping. - `references/image-overlay.md` whenever text sits on top of a photo: photo qualification, localized tint fallback, and face / subject avoidance via multimodal subject mapping.
- `references/screenshot-treatment.md` when the user supplies an app / web / code / dashboard screenshot — picks `.frame-shot` over `.frame-img`, sets corners/shadow/bg/inset, decides on `.device-browser` or `.device-phone` chrome. - `references/screenshot-treatment.md` when the user supplies an app / web / code / dashboard screenshot — picks `.frame-shot` over `.frame-img`, sets corners/shadow/bg/inset, decides on `.device-browser` or `.device-phone` chrome.
- `references/map-component.md` when the content has spatial relationships (travel route, store locations, walking tour) — real routes default to Mapbox Static or OSM static tiles; schematic SVG is only for conceptual / illustrative maps. Pins are HTML overlays; never use live JS maps. - `references/map-component.md` when the content has spatial relationships (travel route, store locations, walking tour) — real routes default to Mapbox Static or OSM static tiles; schematic SVG is only for conceptual / illustrative maps. Pins are HTML overlays; never use live JS maps.
@@ -87,6 +94,7 @@ Gather only the missing information that changes the output:
- Source text, subtitles, article, or title. - Source text, subtitles, article, or title.
- **Rednote category** — if the user names one of the 11 common types (旅行 / 职场 / 游戏 / 影视 / 美食 / 彩妆 / 穿搭 / 家居 / 健身 / 情感 / 推荐), route via `references/category-cookbook.md` to find the right recipes and to confirm the request is inside the capability circle (see "Rednote Category Capability" above). If a request lands in the outside-scope bucket, surface that to the user **before** designing, do not silently retrofit. - **Rednote category** — if the user names one of the 11 common types (旅行 / 职场 / 游戏 / 影视 / 美食 / 彩妆 / 穿搭 / 家居 / 健身 / 情感 / 推荐), route via `references/category-cookbook.md` to find the right recipes and to confirm the request is inside the capability circle (see "Rednote Category Capability" above). If a request lands in the outside-scope bucket, surface that to the user **before** designing, do not silently retrofit.
- Supplied images/screenshots and where each should appear. **For News / Tutorial / Data / Review content, actively prompt for screenshots or photos** — they are the evidence layer. A poster with no real artifact tends to read as filler. - Supplied images/screenshots and where each should appear. **For News / Tutorial / Data / Review content, actively prompt for screenshots or photos** — they are the evidence layer. A poster with no real artifact tends to read as filler.
- Supplied video assets, if the user asks for Live Photo. Treat user-provided video as the normal path; web-sourced free video is only for demo/promo cases or when the user explicitly asks for sourced material. Confirm the target platform because duration differs: Xiaohongshu supports `5s`; WeChat Official Account Live Photo should stay at `3s` and must be uploaded from iPhone. Before cutting, read `references/live-photo-production.md` and classify the request as single Live Photo, triple collage, or long-video intake. If the source is too long or contains multiple usable moments, do a low-cost diagnosis first, then ask once whether to trim, speed up, split into multiple wells, or use a user-specified time range. If the source is shorter than the target duration, ask whether to provide a longer clip, accept a shorter Live Photo, or explicitly allow a hold/slowdown. If the important focus is ambiguous, suggest possible crop/enlarge options but let the user decide before executing.
- **If the user supplies only text (no images at all), ask once before designing:** - **If the user supplies only text (no images at all), ask once before designing:**
``` ```
@@ -167,6 +175,14 @@ Use `references/layout-recipes.md` to choose page structures. Avoid making every
For 3:4 images, check `references/portrait-fill.md` before coding. A short table or ledger must be expanded into a full portrait composition with a quote column, image evidence, marginalia, larger rows, or a background hero zone. For 3:4 images, check `references/portrait-fill.md` before coding. A short table or ledger must be expanded into a full portrait composition with a quote column, image evidence, marginalia, larger rows, or a background hero zone.
Audience-facing copy must describe the user's actual scene, not the production method. Internal terms such as `3s`, `5s`, `Live Photo`, `triple collage`, `information budget`, `long-video intake`, `speed-up`, `highlight detection`, `one action point`, or `layout template` may guide planning, filenames, QA notes, and delivery summaries, but they must not become the H1, hook, or main body copy unless the user is explicitly making an instructional post about those concepts. Before rendering, read every visible headline once as a real viewer: if it sounds like a task requirement, replace it with scene-specific copy.
Do not add non-template ornaments just to satisfy an automated density warning. Extra rulers, side bars, pseudo time axes, decorative labels, or invented badges must come from an existing layout recipe or a user request. If a density warning appears, fix it by choosing a better recipe, resizing real content, or accepting the advisory warning with a visual rationale; do not put meaningless UI on the card.
For Live Photo cards, plan them as normal social cards first, then decide the motion role: one action point, one small process, a before/after change, three parallel results, or ambience/evidence. The only structural change is that one or more image wells become video wells. Keep the same ratio, crop, safe-area, typography, and style mode rules; apply the still-image crop logic to the video stream. The first frame must pass the same checks as a static image: no excessive crop, key UI/content remains readable, and the card still follows the chosen layout recipe. Read `references/live-photo-production.md` before rendering.
For material-first Live Photo puzzle cards, let the video assets lead. Use little or no copy: one short headline is enough for a single-video cover, and two-grid / three-grid / four-grid versions should usually have no added text. When a single-video cover adds text on top of the footage, use the M16 Image-Led Cover / text-on-image rules: subject map first, safe quiet zone, Editorial serif/Songti title at regular-medium weight, paper-cream text, and no default full-canvas mask. Do not invent extra kicker, meta, hairlines, labels, rulers, badges, subtitles, or explanatory production terms just to make the overlay feel designed. If only one headline is available, make the typography carry the layout: phrase-aware line break, restrained size, tracking, alignment, and placement. Treat source-embedded text as part of the footage; only avoid adding new text unless the user asks.
### 4.5. Copy The Seed Template ### 4.5. Copy The Seed Template
Do not write HTML from scratch. Pick one seed template based on the style mode chosen in Step 3: Do not write HTML from scratch. Pick one seed template based on the style mode chosen in Step 3:
@@ -187,13 +203,23 @@ Replace the single placeholder poster after `<!-- POSTERS_HERE -->` with one `<s
Default implementation pattern: Default implementation pattern:
- Create a task folder in the current workspace, for example `social-card-<slug>/`. - Create a task folder under `local-tests/<slug>/` by default, or inside the user-requested output folder. Never put generated task folders, rendered images, MOV files, `.pvt` packages, or downloaded sources in the skill root next to `SKILL.md`.
- Put source images in `assets/`. - Put source images in `assets/`.
- Start from the seed template copied in Step 4.5, not a blank file. Prefer changing only the `<!-- POSTERS_HERE -->` region page-to-page. If a task needs custom layout CSS, add one clearly named task-scoped block in the copied file and keep semantic defaults reset (`figure { margin:0; }`, no browser-default spacing surprises). - Start from the seed template copied in Step 4.5, not a blank file. Prefer changing only the `<!-- POSTERS_HERE -->` region page-to-page. If a task needs custom layout CSS, add one clearly named task-scoped block in the copied file and keep semantic defaults reset (`figure { margin:0; }`, no browser-default spacing surprises).
- Use Playwright or a browser screenshot tool to export each `.poster` or `.cover` node. - Use Playwright or a browser screenshot tool to export each `.poster` or `.cover` node.
- Save rendered images in `output/`. - Save rendered images in `output/`.
- Verify dimensions and inspect the rendered PNGs. - Verify dimensions and inspect the rendered PNGs.
- Keep `node validate-social-deck.mjs <task-dir>` available for auto-check passes. It checks overflow (R1), footer collision (R2), Swiss bold display (R3), minimum font size (R4), 4-band density (R5), `.h-xl` line caps (R6), and browser-default figure margin drift (R7). Exit code 1 on any FAIL — fix before final delivery when auto-check is requested. WARN is advisory but read it. - Keep `node validate-social-deck.mjs <task-dir>` available for auto-check passes. It checks overflow (R1), footer collision (R2), Swiss bold display (R3), minimum font size (R4), 4-band density (R5), `.h-xl` line caps (R6), browser-default figure margin drift (R7), visual bounds / bottom whitespace (R8), and title-to-content gaps (R9). Exit code 1 on any FAIL — fix before final delivery when auto-check is requested. WARN is advisory but read it.
Live Photo branch:
- Decide the information budget before rendering: `3s` fits one action point; `5s` fits one small process; triple collage fits three parallel clips but not a complex story; long videos require diagnosis before editing.
- For long sources, avoid claiming precise automatic highlight detection. Probe duration/resolution, make a sparse contact sheet, and choose between trim, speed-up, split/triple collage, or asking the user for a time range.
- Extract a first frame from each video well, place it in the final static card layout, and show/inspect that preview before making `.pvt`. This catches crop and hierarchy issues with much lower token and render cost.
- Render the paired MOV at the platform duration: `5s` for Xiaohongshu, `3s` for WeChat Official Account.
- Extract the key JPG from a readable representative frame.
- Package `JPG + MOV` into `.pvt` with `makelive`; AirDrop the `.pvt` as one item for iPhone tests.
- Validate dimensions, duration, frame count, package contents, and motion quality. Prefer a contact-sheet frame strip for visual checks so only the final tiled image needs human/model inspection. Use `references/live-photo-production.md` for exact commands and failure-mode checks.
Do not place visible instructions, keyboard shortcuts, or usage explanations inside the images. Do not place visible instructions, keyboard shortcuts, or usage explanations inside the images.
@@ -283,18 +309,27 @@ Final response (after the user has reviewed or asked for auto-check) should incl
- Rendered images shown inline with absolute paths when useful. - Rendered images shown inline with absolute paths when useful.
- A short note on dimensions and verification (or "not yet validated, awaiting your review"). - A short note on dimensions and verification (or "not yet validated, awaiting your review").
- For any image fetched from the web: source URL + site + the attribution decision the user made. - For any image fetched from the web: source URL + site + the attribution decision the user made.
- For Live Photo: the `.pvt` package path, the debug `JPG + MOV` pair, target platform duration, and validation summary.
- For Live Photo publishing: remind the user of two things: platform limits (`5s` Xiaohongshu, `3s` WeChat Official Account) and publish path (AirDrop the `.pvt` package as one item to iPhone, then publish from the matching mobile app path; desktop/web upload paths generally cannot recognize `.pvt` as a publishable Live Photo).
- Any unresolved risks, such as source images being low resolution. - Any unresolved risks, such as source images being low resolution.
## Non-Negotiables ## Non-Negotiables
- Never edit the original Guizang PPT skill or any upstream skill copied from elsewhere. - Never edit the original Guizang PPT skill or any upstream skill copied from elsewhere.
- Never create generated work in the skill root. All task artifacts must be under `local-tests/<slug>/` by default, or under a user-requested output folder. Root-level generated folders like `social-card-*`, `livephoto-*`, `wechat-*`, and loose output assets are forbidden.
- Do not create random decorative SVG ovals, blobs, rain drops, stickers, or meaningless circles. - Do not create random decorative SVG ovals, blobs, rain drops, stickers, or meaningless circles.
- Do not use nested cards or generic SaaS card layouts as the default. - Do not use nested cards or generic SaaS card layouts as the default.
- Do not let text overflow, touch the edge, or collide with the footer band. Pin `.foot` with `margin-top: auto` inside a flex column, never with `position: absolute` over growing content. - Do not let text overflow, touch the edge, or collide with the footer band. Pin `.foot` with `margin-top: auto` inside a flex column, never with `position: absolute` over growing content.
- When content overflows, measure the amount before editing. Small overflows should get small fixes: `1-40px` means nudge/tighten, `40-90px` means local compaction, `90-160px` means slight title or paragraph compression, and only `160px+` should trigger recipe changes or content removal. After fixing, check R8 bottom whitespace so the page does not swing from overflow to a giant empty lower band.
- Do not let a title touch the next content block. Main display titles should usually keep at least `28px` below; local headings should keep at least `16px`. Use validator R9 before relying on visual inspection.
- Do not let text become too small to read on mobile. - Do not let text become too small to read on mobile.
- Do not write inline `font-size` + `font-weight` on display titles in Swiss. Use the typed classes (`.h-hero` / `.h-statement` / `.h-xl` / `.num-mega`). A 80-120px headline at weight 700-900 is not Swiss; "the larger, the lighter" is a hard rule. - Do not write inline `font-size` + `font-weight` on display titles in Swiss. Use the typed classes (`.h-hero` / `.h-statement` / `.h-xl` / `.num-mega`). A 80-120px headline at weight 700-900 is not Swiss; "the larger, the lighter" is a hard rule.
- Do not deliver Editorial posters with a flat paper background, mono labels on every row, and no atmosphere layer. Run the Editorial Identity Test in `references/style-system.md` — a serif title alone does not make a poster Editorial. - Do not deliver Editorial posters with a flat paper background, mono labels on every row, and no atmosphere layer. Run the Editorial Identity Test in `references/style-system.md` — a serif title alone does not make a poster Editorial.
- Do not fake data, release details, or percentages. - Do not fake data, release details, or percentages.
- Do not crop faces, key UI text, or hardware/product details unless the user explicitly accepts it. - Do not crop faces, key UI text, or hardware/product details unless the user explicitly accepts it.
- Do not turn a video card into a fake still sequence without saying so. If a source is shorter than the target duration, ask for a longer source, use a shorter platform-safe duration, or explicitly document any hold/slowdown.
- Do not skip the first-frame preview for Live Photo. Video is an image well with motion; its first frame must satisfy the static 3:4 layout and crop rules before rendering the MOV.
- Do not let result videos scroll through section boundaries in a shallow crop; this creates the "overlapping video" failure. Inspect contact sheets and choose a stable time window before packaging.
- Do not guess the user's visual priority when enlarging/cropping video for readability. Offer the crop/enlarge tradeoff and apply it after the user confirms, unless the request already makes the focal region explicit.
- Do not reuse a 21:9 cover by blindly cropping it into 1:1. Compose each ratio separately. - Do not reuse a 21:9 cover by blindly cropping it into 1:1. Compose each ratio separately.
- **3:4 卡必须吃满画布**。Content (text + image + data) 必须覆盖 ≥75% 画布高度。任何 >15% 画布高度的纯空白带都需要"留白理由":(a) hero image 自带呼吸、(b) 单句宣言式 hero statement、(c) 段落顶/底 leading & trailing whitespace(前后总和 ≤15%)。**禁止用 `<div style="flex: 1"></div>` 上下夹击把内容塞到中段**——杂志页留白逻辑不适用于社交卡(杂志靠对开页吸收留白,社交卡逐张独立刷,欠填看着像 PPT 漏排)。Recipe-by-recipe 最小密度见 `references/layout-recipes.md` 每条 recipe 的「Minimum density」段。Render 后必须跑 `qa-checklist.md` 的 4 横带密度检查。 - **3:4 卡必须吃满画布**。Content (text + image + data) 必须覆盖 ≥75% 画布高度。任何 >15% 画布高度的纯空白带都需要"留白理由":(a) hero image 自带呼吸、(b) 单句宣言式 hero statement、(c) 段落顶/底 leading & trailing whitespace(前后总和 ≤15%)。**禁止用 `<div style="flex: 1"></div>` 上下夹击把内容塞到中段**——杂志页留白逻辑不适用于社交卡(杂志靠对开页吸收留白,社交卡逐张独立刷,欠填看着像 PPT 漏排)。Recipe-by-recipe 最小密度见 `references/layout-recipes.md` 每条 recipe 的「Minimum density」段。Render 后必须跑 `qa-checklist.md` 的 4 横带密度检查。
+58
View File
@@ -0,0 +1,58 @@
# Agent 工作约定
## 项目说明
这个目录是 `guizang-social-card-skill` 的 Skill 本体,用来给 Claude Code / Codex 等 Agent 提供"归藏风"社交图文生成能力。
它面向的产出包括:
- 小红书 / Rednote 3:4 图文组图。
- 微信公众号 21:9 头图 + 1:1 分享卡封面对。
- 文章封面、产品更新卡片、截图说明图、教程拆页等静态信息流视觉内容。
这个仓库的根目录应该保持为 Skill 可安装、可维护的结构。根目录主要放:
- `SKILL.md`
- `README.md` / `README.en.md`
- `references/`
- `assets/`
- `agents/`
- `validate-social-deck.mjs`
- `package.json` / `package-lock.json`
- 授权、产品说明、交接等项目级文档
不要把一次性的生成案例、渲染输出、临时素材、调试脚本或下载图片直接放在 Skill 根目录。
## 测试产物放置规则
所有测试生成的产物都放到:
```text
local-tests/
```
推荐按一次任务或一个测试案例建独立子目录:
```text
local-tests/<case-name>/
```
每个测试目录可以包含自己的 `index.html`、`render.cjs` / `render.mjs`、`assets/`、`output/`、`SOURCES.md` 等文件。例如:
```text
local-tests/social-card-example/
local-tests/social-card-example/index.html
local-tests/social-card-example/render.cjs
local-tests/social-card-example/assets/
local-tests/social-card-example/output/
```
原则:
- 试跑 HTML 放在 `local-tests/<case-name>/`。
- 渲染出来的 PNG/JPEG/WebP 放在 `local-tests/<case-name>/output/`。
- 下载或临时使用的图片、截图、素材放在 `local-tests/<case-name>/assets/`。
- 一次性调试脚本放在对应测试目录里;只有通用校验或 Skill 本体需要的脚本才放根目录。
- 如果要把某个测试案例升级为正式示例,先明确它属于项目文档、参考资料还是发布资产,再移动到合适位置;不要默认留在根目录。
`local-tests/` 已在 `.gitignore` 中忽略,适合承载这些本地测试和生成产物。保持这个边界,能避免 Skill 本体再次混入大量一次性文件。
+2 -2
View File
@@ -1,8 +1,8 @@
interface: interface:
display_name: "Guizang Social Card Skill" display_name: "Guizang Social Card Skill"
short_description: "Create social card sets and WeChat covers." short_description: "Create social cards, Live Photos, and covers."
brand_color: "#002FA7" brand_color: "#002FA7"
default_prompt: "Use $guizang-social-card-skill to turn this article into a 3:4 social card image set and a paired WeChat 21:9 + 1:1 cover." default_prompt: "Use $guizang-social-card-skill to turn this article or video material into a 3:4 social card set, Live Photo card, and paired WeChat cover."
policy: policy:
allow_implicit_invocation: true allow_implicit_invocation: true
+1
View File
@@ -4,6 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test:docs": "node scripts/check-skill-docs.mjs",
"validate": "node validate-social-deck.mjs" "validate": "node validate-social-deck.mjs"
}, },
"keywords": [], "keywords": [],
+26
View File
@@ -13,6 +13,32 @@ This is a routing document. When the user names a category, find the row here an
For any category, if a poster has a full-bleed image with text on top, also follow `image-overlay.md` (photo qualification, localized tint fallback, and subject avoidance). For any category, if a poster has a full-bleed image with text on top, also follow `image-overlay.md` (photo qualification, localized tint fallback, and subject avoidance).
## Live Photo Scene Library
This is not a fixed template list. When the user supplies video, infer the scenario and information budget from the actual content, then choose single Live Photo, triple collage, long-video trimming, or a non-Live-Photo fallback.
Use `references/live-photo-production.md` for duration limits and rendering details. This section only decides what kind of motion is worth showing.
| Scenario | User problem | Motion value | Best Live Photo form |
| --- | --- | --- | --- |
| 游戏攻略 / gameplay guide | A screenshot cannot explain timing, route, or operation rhythm | show the key move plus success state | single `5s` for one move; triple collage for three strategies or stages |
| 产品更新 / product update | Static screenshots lack "how it works" proof | show trigger → interaction → result | single `3-5s`; triple collage for three features or three generated outputs |
| 教程技巧 / tutorial | Step images are too fragmented | show the one critical action or failure-prone step | single `3-5s`; long videos should be trimmed, not compressed whole |
| AI 模型测试 / model demo | Static results do not prove process or credibility | show prompt/process/result evidence | triple collage for multiple results; single for one clear generation path |
| 旅行攻略 / travel | A pretty place photo has atmosphere but not process | add arrival, route, movement, or现场感 around a static guide card | single `5s`; avoid pure scenery loops with no informational role |
| 生活方式 / packing / living | Product or object photos feel too staged | show real handling, packing, opening, setup, or use | single `5s`; triple collage for checklist-style sequences |
| 电商种草 / product rec | Product photos do not prove texture, scale, or use state | show touch, rotation, fit, before/after, or detail | single `3-5s`; triple collage for detail / scale / result |
| 健身动作 / fitness movement | One pose cannot explain trajectory | show range, rhythm, or correction | single `3-5s`; triple collage for wrong / fix / result |
| 美食手作 / food & craft | Finished photo misses the satisfying change | show pour, stir, cut, assemble, melt, or final reveal | single `3-5s`; avoid full recipes in one Live Photo |
| 家居改造 / home before-after | Before/after static pair feels disconnected | show the transition or the practical action | single for one transformation; triple collage for before / process / after |
Motion role shortcuts:
- **One action point** → single Live Photo, usually `3s` WeChat or `5s` Xiaohongshu.
- **One small process** → single `5s`, possibly with light speed-up.
- **Three parallel results / viewpoints / stages** → triple Live Photo collage.
- **Long sequence or narration-heavy content** → diagnose first; trim, split, or recommend normal video/GIF.
--- ---
## 旅行 · Travel ## 旅行 · Travel
+807
View File
@@ -0,0 +1,807 @@
# Live Photo Production Workflow
This note records the workflow for making Xiaohongshu / Rednote and WeChat Official Account style `3:4` Live Photo assets from supplied videos, screen recordings, or rendered social-card videos.
## Contents
- When To Use Live Photo
- Key Finding
- Recommended Output
- Visual Format
- Live Photo Information Budget
- Triple Live Photo Collage
- Material-First Puzzle Layouts
- Long Video Intake
- Intake Decision Tree
- Video As Image-Well Replacement
- Preview-First Workflow
- Contact-Sheet Inspection Algorithm
- Validation
- iPhone Test
- Platform Notes
- Troubleshooting
## When To Use Live Photo
Use Live Photo when the user explicitly asks for `Live Photo` / `实况照片`, or when they ask to make a social card from video素材 and the motion is the proof.
The normal production path uses user-supplied videos, screen recordings, or product demos. Web-sourced free videos are only for making our own demo/promo cases, or when the user explicitly asks for sourced example material.
Core principle: Live Photo does not tell a complete long story. It proves one moment, one small process, one before/after change, or a compact set of parallel results.
Audience-facing copy should name the real scene in the video. Do not put internal production labels in the H1 or main copy: `3s`, `5s`, `Live Photo`, `triple collage`, `long-video intake`, `information budget`, `speed-up`, `highlight detection`, and `one action point` belong in planning notes, filenames, QA summaries, or delivery text. The visible card should read like the user posted it for their audience, not like the agent is explaining how it was made.
User problems this feature solves:
- **Compress motion into a carousel card**: a user can publish code generation, UI interaction, game demo, before/after state, or scroll reveal inside a normal image carousel instead of switching to a pure video post.
- **Preserve the Skill's layout quality**: the poster still follows the existing 3:4 visual system; only the image well becomes a video well.
- **Preview before spending render/model budget**: the agent can extract the video first frame, place it as a still image, and confirm the layout before producing a MOV or `.pvt`.
- **Handle overlong clips**: the agent can decide whether to trim, speed up, or combine both, instead of forcing the user to pre-edit every source.
- **Make evidence readable**: the agent crops video like it crops images, enlarging the important UI/counter/result area instead of dropping in a tiny full-screen recording.
- **Avoid platform gotchas**: the agent outputs `.pvt` for iPhone/AirDrop and respects platform duration limits.
- **Explain the publish path**: users learn that `.pvt` must be transferred to iPhone and published from the mobile app path, not from desktop/web upload.
- **Provide graceful fallbacks**: if WeChat web upload is required, the same source can become GIF/short video while the static card remains usable.
## Key Finding
Do not AirDrop a plain `JPG + MOV` pair as separate files and expect iPhone Photos to merge them automatically.
Even when both files contain matching Apple Live Photo metadata, the normal multi-file AirDrop path can import them as separate assets. For reliable testing and transfer, package the pair as a single `.pvt` bundle.
## Recommended Output
For each Live Photo, generate these deliverables:
```text
local-tests/<slug>/output/
IMG_<slug>_LIVE.JPG # 1080 x 1440 key photo / cover frame
IMG_<slug>_LIVE.MOV # platform-duration paired video
IMG_<slug>_LIVE.pvt/ # AirDrop-friendly Live Photo package
IMG_<slug>_LIVE.JPG
IMG_<slug>_LIVE.MOV
metadata.plist
```
Use the `.pvt` package for AirDrop tests. Use the `JPG + MOV` pair as source/debug artifacts.
If both Xiaohongshu and WeChat are requested, export separate platform files so the user knows what to upload:
```text
IMG_<slug>_XHS_5S_LIVE.*
IMG_<slug>_WECHAT_3S_LIVE.*
```
## Visual Format
Default Xiaohongshu Live Photo card:
- Canvas: `1080 x 1440`
- Ratio: `3:4`
- Duration: `5s`
- FPS: `24-30` for screen recordings; `12-15` is acceptable for simple animated graphics.
- Codec: H.264 MOV
- First frame: a valid static social card state with no excessive crop.
- Key photo: a readable representative frame from the confirmed layout. It may be the first frame or a later stronger result frame, but it must use the same crop and composition.
Default WeChat Official Account Live Photo card:
- Canvas: `1080 x 1440`
- Ratio: `3:4`
- Duration: `3s`
- Upload path: iPhone only; web upload should use GIF/short-video fallback.
For a "triple test" card:
- Top: title and short thesis.
- Middle: three horizontal result strips.
- Each strip should show one generated result, not only code.
- Add compact labels such as `01 / 3D SPACE GAME`, `02 / AI INFERENCE WEBSITE`, `03 / SPACE TRAVEL WEBSITE`.
- The static key photo must be useful even when motion is not supported.
## Live Photo Information Budget
Judge how much source material can fit before designing the layout.
| Format | Time budget | Best for | Avoid |
| --- | --- | --- | --- |
| WeChat single Live Photo | `3s` | one action point, one tap result, one before/after reveal | multi-step tutorials, long scrolls, three unrelated clips |
| Xiaohongshu single Live Photo | `5s` | one small process, short product interaction, key game move, generation-to-result proof | full story arcs, dense narration, long setup before the result |
| Triple Live Photo collage | `3-5s` per exported asset, three wells inside one card | three parallel results, three viewpoints, three examples, wrong/fix/result | a sequential process that needs the viewer to read all three wells in order |
| Material-first puzzle | `3-5s` | high-quality footage where the image is the content: single video, two-grid, three-grid, or four-grid motion cards | forcing explanatory text, labels, or diagrams onto beautiful footage |
| Long source video | source is much longer than target | diagnosis, trim, speed-up, split, or user-specified range | blindly compressing the entire clip into `3s` or `5s` |
Use this motion-role vocabulary when planning:
- **Operation proof**: show the exact action and resulting state.
- **Process compression**: show a small transformation from start to result.
- **Before/after**: make the difference credible by showing the transition.
- **Parallel evidence**: show multiple outcomes side by side.
- **Atmosphere supplement**: static card carries the information; motion adds place, texture, or realism.
If the source video carries more than one of these roles, pick the strongest one or split it. Do not overfill a single Live Photo.
## Triple Live Photo Collage
Use a triple collage when the user has three short videos, three generated results, three product states, three gameplay clips, or three viewpoints that are stronger together than as separate pages.
Recommended structures:
1. **Pure three-column / three-strip video collage**: almost no text; use when the clips themselves are the content.
2. **Title + three videos**: one hook at top, three labeled wells below; good for Rednote first-card use.
3. **Editorial mixed collage**: asymmetric wells plus labels, short conclusions, or numbered captions; good for tutorials, guides, and product updates.
Judge the relationship between the three clips before choosing geometry:
- **Parallel comparison**: A / B / C.
- **Process stages**: before / during / after.
- **Result showcase**: three generated or tested outputs.
- **Correction flow**: wrong / fix / result.
- **Multi-view**: overview / detail / result.
Keep labels short: `01 / SETUP`, `02 / ACTION`, `03 / RESULT`. Do not turn the collage into three tiny full-screen recordings. Crop each well around the evidence area.
## Material-First Puzzle Layouts
Use this when the user supplies good footage and wants the Live Photo to behave like a moving photo layout, not a text card. The key choice is geometry, not copywriting.
Supported shapes:
- **Single video**: crop one clip to `3:4` full canvas. Add at most one short headline, placed in a confirmed quiet zone away from faces, bodies, hands, products, or key motion. If text is added, it must follow the M16 Image-Led Cover / text-on-image typography: Editorial serif/Songti, regular-medium weight, paper-cream color, restrained tracking, and no default full-canvas mask. Do not invent extra kicker, meta, hairlines, labels, or badges just to make the overlay feel designed. If only one headline is available, make the typography carry the layout: phrase-aware line break, restrained size, tracking, alignment, and placement.
- **Two-grid**: stack two clips vertically. Use no added text unless the user asks.
- **Three-grid**: stack three clips vertically. Use no added text; the rhythm comes from the footage.
- **Four-grid**: use a `2 x 2` grid of four `3:4` wells (`540 x 720` each inside a `1080 x 1440` card). Crop each source as a real `3:4` material, not as a tiny landscape thumbnail.
Rules:
- Do not add template filler: no labels, numeric tags, rulers, badges, subtitles, or explanatory production copy.
- Source-embedded text is part of the footage. Do not add new text on top of it unless requested.
- Keep gutters at `0-12px`; choose no gutter when the user asks for pure 拼图 or the source already has strong edges.
- Generate source contact sheets first, then choose crop positions. For people, protect faces and upper body; for objects, protect the focal feature.
- For single-video text, follow `image-overlay.md`: subject map first, no full-canvas mask, title in one quiet zone, and thumbnail readability check.
- Do not render single-video text as generic subtitles. It should look like an Editorial image-led cover title, not video captioning.
- Do not confuse "minimal copy" with permission to invent copy. Minimal copy still needs typography and a deliberate relation to the subject, but the agent must not add new visible words unless the user asks.
- If a crop cannot preserve the subject, prefer a different puzzle geometry over forcing a bad crop.
## Long Video Intake
For long user videos, do not promise precise automatic highlight detection. Use a low-cost diagnosis first, then make a recommendation.
Low-cost diagnosis:
1. Probe duration, resolution, frame rate, and audio presence with `ffprobe`.
2. Generate a sparse contact sheet:
- `8-12` frames for an initial long-source scan.
- `12-15` frames for a candidate 5-second window.
- `8-12` frames for a candidate 3-second window.
3. Inspect only the contact sheet unless the user asks for deeper analysis.
4. Identify obvious structure only: black/blank intro, static stretches, large section changes, visible result frame, or multiple separate moments.
Recommendation types:
- **Trim**: use one stable time window at normal speed.
- **Speed-up + trim**: preserve a short process when readability is still acceptable.
- **Split / triple collage**: use multiple parallel moments instead of compressing a long story.
- **Ask for range**: when the contact sheet cannot reveal the intended highlight.
- **Reject Live Photo**: when the material needs narration, audio, or a long sequence; suggest short video/GIF instead.
Useful response pattern:
```text
我先粗看了这个视频的信息量。它不适合整段压进 5 秒。
建议有两种:
A. 裁 00:04-00:09,保留关键操作和结果
B. 拆成三连 Live Photo:准备 / 操作 / 结果
你想走哪一种?
```
## Tooling
Primary tool:
- [`RhetTbull/makelive`](https://github.com/RhetTbull/makelive)
Why this tool:
- It writes the Apple Live Photo content identifier metadata into both photo and video resources.
- It can create a `.pvt` package via `save_live_photo_pair_as_pvt`.
- The `.pvt` package is specifically useful for AirDrop and other transfer paths that may not preserve a loose photo/video association.
Install or run with `uv`:
```bash
uvx makelive IMG_TEST_LIVE.JPG IMG_TEST_LIVE.MOV
```
For `.pvt`, use the Python API:
```bash
uvx --from makelive python -c "from makelive import save_live_photo_pair_as_pvt; asset_id, pvt_path = save_live_photo_pair_as_pvt('IMG_TEST_LIVE.JPG', 'IMG_TEST_LIVE.MOV'); print(asset_id); print(pvt_path)"
```
If running inside a restricted sandbox, set writable cache/tool directories:
```bash
UV_CACHE_DIR=/private/tmp/uv-cache \
UV_TOOL_DIR=/private/tmp/uv-tools \
uvx --from makelive python -c "from makelive import save_live_photo_pair_as_pvt; asset_id, pvt_path = save_live_photo_pair_as_pvt('IMG_TEST_LIVE.JPG', 'IMG_TEST_LIVE.MOV'); print(asset_id); print(pvt_path)"
```
This repo also includes a generic wrapper:
```bash
UV_CACHE_DIR=/private/tmp/uv-cache \
UV_TOOL_DIR=/private/tmp/uv-tools \
uvx --from makelive python scripts/package-live-photo.py IMG_TEST_LIVE.JPG IMG_TEST_LIVE.MOV
```
Use this wrapper when available so every run prints the asset id and `.pvt` path consistently.
## Intake Decision Tree
When the user provides video assets:
1. Identify target platform:
- Xiaohongshu / Rednote → `5s`
- WeChat Official Account → `3s`
2. Classify the likely format:
- single Live Photo
- triple Live Photo collage
- long-video intake
3. Probe each source:
- duration
- width / height
- frame rate
- whether the clip contains multiple sections, transitions, or scroll boundaries
4. If the source is long or ambiguous, generate a sparse contact sheet first. Do not inspect dense frame sequences unless the user explicitly needs precision.
5. If the source is longer than the platform duration, give a recommendation and ask one concise question unless the intended moment is obvious:
```text
这个视频比平台 Live Photo 时长长。粗看更适合:
A. 裁一段,保持原速
B. 加速 + 裁切,保留更多过程
C. 拆成三连 Live Photo
```
6. If the source is shorter than the platform duration, do not silently pad and call it dynamic. Choose one:
- ask for a longer source
- use the shorter platform-safe duration
- hold the final frame and state that it is a hold
7. If the source has a visible section transition or page scroll boundary, choose a stable time window or slow the segment slightly. Do not let two sections occupy a shallow crop at once.
8. If the important visual focus is ambiguous, do not guess. Ask whether the user wants readability-focused enlargement, full-context preservation, or a balanced crop.
Useful short-source prompt:
```text
这个素材短于目标 Live Photo 时长。你想:
A. 换一个更长素材
B. 直接做成较短的 Live Photo
C. 允许末帧停住 / 轻微慢放,并在交付时说明
```
Useful focus/crop prompt:
```text
这个视频可以有两种处理:
A. 放大关键区域,数字 / UI 更清楚,但会少一点全局上下文
B. 保留完整画面,信息更全,但手机上细节会小一些
你更想突出哪一个?
```
## Video As Image-Well Replacement
Treat video wells like image wells:
- Keep the original card layout, margins, labels, typography, and style mode.
- Apply the same crop logic to video that you would apply to a still image.
- The video first frame must look like a finished static card image. Before rendering the MOV, extract the first frame, place it in the card, and verify:
- no important UI/text/product/detail is over-cropped
- the crop follows the original image-well rule and safe area
- the surrounding title/body/layout still match the chosen recipe
- the card works as a still image if motion is unavailable
- Use `object-fit: cover` equivalent behavior with ffmpeg: crop first, then scale to the image well.
- Use `object-fit: contain` equivalent behavior when dense UI text must remain readable: scale with letterboxing or enlarge the well rather than cutting important UI.
- Choose a representative key frame after the video crop is final; the still image must be useful even if motion playback fails.
- Treat "可图性提升" as an option, not an automatic fix. If the source would benefit from zooming in, sharpening, slowing, or making a video well larger, suggest it and wait for the user when the focal priority is not obvious.
## Preview-First Workflow
Always confirm the static version before producing the Live Photo package. This saves tokens and render time because the expensive motion path runs only after the layout/crop is accepted.
1. Extract the first frame from each video source or chosen time window:
```bash
ffmpeg -y -ss <start_seconds> -i source.mp4 \
-frames:v 1 -q:v 2 assets/source-first-frame.jpg
```
2. Use that frame exactly like a supplied image inside the normal `3:4` card template. Apply the same crop, object position, title spacing, safe area, and density rules as static cards.
3. Render a static PNG preview of the card. Inspect it and, when working interactively, show it to the user before rendering video.
4. Only after the user accepts the preview, render the platform-duration MOV using the same crop coordinates and layout geometry.
5. Extract the key JPG from the rendered MOV. Prefer a frame that is readable and strong as a static card while remaining faithful to the confirmed crop.
Do not use a separate "video layout" that differs from the static preview. The preview is the contract.
## Contact-Sheet Inspection Algorithm
Use contact sheets to inspect motion cheaply. Generate the tiled frame strip with command-line tools, then inspect only the final composite image. This avoids loading many individual frames into the model context and makes visual QA much faster.
Algorithm:
1. Probe the output duration with `ffprobe`.
2. Choose the inspection window:
- final MOV → full platform duration
- source clip with suspected issue → the candidate trim window
3. Choose `N` frames. Use `8-12` for a 3-second asset and `12-15` for a 5-second asset.
4. Compute sample rate as `N / window_duration`.
5. Use ffmpeg to sample, scale each frame to a fixed thumbnail width, and tile into `cols x rows`.
6. Inspect the resulting JPG/PNG for:
- over-cropped first frame
- section-boundary overlap
- unreadable counters or UI text
- frozen areas caused by padding short clips
- unexpected black bars or aspect-ratio changes
Generic command:
```bash
ffmpeg -y -ss <start_seconds> -t <window_seconds> -i input.mov \
-vf "fps=<frame_count>/<window_seconds>,scale=320:-1:flags=lanczos,tile=<cols>x<rows>:padding=12:margin=12:color=white" \
-frames:v 1 contact-sheet.jpg
```
Examples:
```bash
# 3-second WeChat Live Photo, 9 frames
ffmpeg -y -t 3 -i IMG_LIVE.MOV \
-vf "fps=9/3,scale=320:-1:flags=lanczos,tile=3x3:padding=12:margin=12:color=white" \
-frames:v 1 contact-wechat.jpg
# 5-second Xiaohongshu Live Photo, 15 frames
ffmpeg -y -t 5 -i IMG_LIVE.MOV \
-vf "fps=15/5,scale=320:-1:flags=lanczos,tile=5x3:padding=12:margin=12:color=white" \
-frames:v 1 contact-xhs.jpg
```
This repo includes a wrapper with the same logic:
```bash
python scripts/make-video-contact-sheet.py IMG_LIVE.MOV contact-xhs.jpg --frames 15 --cols 5 --duration 5
```
## Example: Render A 3:4 MOV
This example combines three existing landscape screen recordings into one vertical `3:4` Live Photo source movie.
```bash
ffmpeg -y \
-ss 145 -t 3 -i "Area测试 01.mp4" \
-ss 49 -t 3 -i "Area 测试 2.mp4" \
-ss 154 -t 3 -i "Area 测试 3.mp4" \
-i overlay.png \
-filter_complex "color=c=0x030611:s=1080x1440:d=3:r=15[bg];[0:v]fps=15,crop=1580:980:344:60,scale=1000:330:force_original_aspect_ratio=increase,crop=1000:330,setsar=1[g];[1:v]fps=15,scale=1000:330:force_original_aspect_ratio=increase,crop=1000:330,setsar=1[a];[2:v]fps=15,scale=1000:330:force_original_aspect_ratio=increase,crop=1000:330,setsar=1[s];[bg][g]overlay=40:190[p1];[p1][a]overlay=40:590[p2];[p2][s]overlay=40:990[p3];[p3][3:v]overlay=0:0" \
-t 3 -r 15 -an -c:v libx264 -pix_fmt yuv420p -movflags +faststart \
IMG_TEST_LIVE_RAW.mov
```
If `ffmpeg` lacks `drawtext`, create text and border overlays as a transparent PNG with ImageMagick, then overlay that PNG in the video render.
## Extract The Key Photo
Pick the final readable frame as the still image:
```bash
ffmpeg -y \
-ss 2.7 -i IMG_TEST_LIVE_RAW.mov \
-frames:v 1 -q:v 2 \
IMG_TEST_LIVE.JPG
```
Copy or rename the raw MOV:
```bash
cp IMG_TEST_LIVE_RAW.mov IMG_TEST_LIVE.MOV
```
Then create the `.pvt` package:
```bash
uvx --from makelive python -c "from makelive import save_live_photo_pair_as_pvt; asset_id, pvt_path = save_live_photo_pair_as_pvt('IMG_TEST_LIVE.JPG', 'IMG_TEST_LIVE.MOV'); print(asset_id); print(pvt_path)"
```
## Validation
Check dimensions and duration:
```bash
ffprobe -v error \
-show_entries stream=width,height,r_frame_rate \
-show_entries format=duration,size \
-of default=noprint_wrappers=1 \
IMG_TEST_LIVE.MOV
```
Expected:
```text
width=1080
height=1440
r_frame_rate=<chosen fps>
duration=<platform duration>
```
Inspect the `.pvt` package:
```bash
find IMG_TEST_LIVE.pvt -maxdepth 2 -type f -print
plutil -p IMG_TEST_LIVE.pvt/metadata.plist
```
Expected package shape:
```text
IMG_TEST_LIVE.pvt/IMG_TEST_LIVE.JPG
IMG_TEST_LIVE.pvt/IMG_TEST_LIVE.MOV
IMG_TEST_LIVE.pvt/metadata.plist
```
Then create a contact sheet from the final MOV and inspect that one image:
```bash
python scripts/make-video-contact-sheet.py IMG_TEST_LIVE.MOV contact-final.jpg --frames 15 --cols 5 --duration 5
```
For WeChat `3s`, use fewer frames:
```bash
python scripts/make-video-contact-sheet.py IMG_TEST_LIVE.MOV contact-final.jpg --frames 9 --cols 3 --duration 3
```
The first tile should satisfy the static card rules. The later tiles should show motion without section-boundary overlap, unintended freezes, or unreadable focal content.
## iPhone Test
Use AirDrop with the package:
1. In Finder, select `IMG_TEST_LIVE.pvt` as one item.
2. AirDrop it to the iPhone.
3. Open Photos and verify that it imports as one Live Photo asset.
4. Press and hold to confirm motion playback.
5. Publish from the iPhone app path: Xiaohongshu app for Rednote, or the iPhone Official Account article editing path for WeChat.
Avoid selecting these together for the transfer:
```text
IMG_TEST_LIVE.JPG
IMG_TEST_LIVE.MOV
IMG_TEST_LIVE_RAW.mov
```
That path may produce three separate iPhone Photos assets.
Do not promise desktop publishing for `.pvt`. Current practical path:
```text
Mac Finder -> AirDrop .pvt as one item -> iPhone Photos -> publish from mobile app
```
Computer-side upload paths generally treat `.pvt` as an unknown package or separate source files, so they cannot be used as the primary Live Photo publishing workflow.
## Platform Notes
### Xiaohongshu / Rednote
Use Live Photo as the first card when the motion itself is the hook.
Duration:
- Use `5s` unless the user asks for a shorter asset.
- If a source is longer, prefer trim-only for readable demos; use speed-up + trim when the user wants to preserve more process.
- If a source is shorter, ask for more source or clearly document any hold frame.
Good first-card structure:
- Static cover is clear at thumbnail size.
- Motion reveals code speed first, then runnable results.
- The final frame is the strongest result state.
For a mixed post:
1. Static `3:4` cover.
2. Text + image explanation card.
3. Triple-result Live Photo.
4. Individual result cards.
5. Speed / evaluation matrix.
### WeChat Official Account
WeChat Official Account Live Photo support is upload-path dependent.
Observed behavior:
- iPhone upload can preserve Live Photo.
- Web upload does not expose the same Live Photo path.
- Keep Live Photo duration at or below `3s`.
Recommended fallback:
- Keep static `3:4` images for article body cards.
- Use GIF or short video when a desktop/web publishing path is required.
- Use Live Photo from iPhone when the motion card is important.
## Optional Local Scripts
This repo includes two small Swift helpers created during testing:
- `scripts/add-livephoto-maker-note.swift`
- `scripts/add-livephoto-mov-metadata.swift`
They can write Apple-style identifiers and timed metadata manually, but the recommended production path is still `makelive`, because it also creates the `.pvt` package needed for AirDrop-style transfer tests.
Use the Swift helpers only when debugging metadata or when `makelive` is unavailable.
## Troubleshooting
If iPhone Photos shows separate JPG and MOV assets:
- You probably AirDropped loose files, not the `.pvt` package.
- Recreate the `.pvt` using `save_live_photo_pair_as_pvt`.
- AirDrop only the `.pvt` item.
If the Live Photo imports but does not animate:
- Keep duration within the target platform limit: `5s` for Xiaohongshu, `3s` for WeChat Official Account.
- Use H.264 MOV and `yuv420p`; use `24-30fps` for screen recordings and `12-15fps` for simple graphics.
- Make sure the key photo and MOV came from the same canvas ratio and dimensions.
- Rebuild with `makelive` so identifiers are consistent.
If `uvx` fails inside a sandbox:
- Use writable `UV_CACHE_DIR` and `UV_TOOL_DIR`.
- If the failure is network or macOS system-configuration related, run it outside the sandbox with user approval.
If the result/demo region looks like two videos overlap:
- First inspect the source clip contact sheet. If the source scrolls between webpage sections, a shallow crop can show the end of one section and start of the next at the same time.
- Do not fix this by adding masks or more overlays.
- Choose a better time window, make the result well taller, or slow a shorter stable segment into the target duration.
- For webpage demos, keep Hero visible briefly, then allow controlled scroll. Avoid spending the whole Live Photo on Hero unless the Hero itself is the result.
## Case Study: MIMO UltraSpeed Card Lessons
This section records the issues found while producing the MIMO UltraSpeed Xiaohongshu / WeChat Live Photo card. Keep it as the reusable checklist for future screen-recording based Live Photos.
### Final Working Layout
The accepted card used a Swiss International treatment:
- Canvas: `1080 x 1440`, `3:4`
- FPS: `30`
- Codec: H.264 MOV, `yuv420p`
- Background: off-white `#fafaf8`
- Accent: IKB blue `#002FA7`
- Structure:
- Top: short title and one-line context.
- Middle-small: process / token speed strip.
- Bottom-large: result demo strip.
- Footer: compact factual stats.
The accepted geometry was:
```text
token strip: x=64, y=360, w=956, h=327
result strip: x=64, y=744, w=956, h=595
```
The important editorial decision: **the result demo should be the larger evidence block**. The token strip only needs to prove speed and motion; it should not dominate the page.
### Do Not Add Synthetic Metrics
For speed tests, do not invent a large extra number beside the source recording.
Bad:
- Add a separate animated counter.
- Re-type `tokens/s` as a designed KPI block while the real platform counter is already visible.
Good:
- Crop the real platform speed panel from the source video.
- If clarity is poor, enlarge the crop and sharpen it lightly.
- Keep the source UI as evidence; do not replace it with a decorative metric.
### Static Preview Before Live Photo
For video cards, first generate a static preview frame and confirm the crop. Do not jump straight to `.pvt`.
Recommended preview process:
```bash
mkdir -p /private/tmp/mimo-check
ffmpeg -y -i "process.mp4" \
-vf "fps=1,scale=481:-1,tile=6x1" \
-frames:v 1 /private/tmp/mimo-check/process_contact.jpg
ffmpeg -y -i "result.mp4" \
-vf "fps=1,scale=481:-1,tile=6x1" \
-frames:v 1 /private/tmp/mimo-check/result_contact.jpg
```
Inspect these contact sheets before choosing crop coordinates. For this MIMO case, skipping this step caused several rounds of wrong crops.
### Token Strip Crop
Problem observed:
- The first token strip crop preserved too much browser whitespace.
- The actual `tokens/s` number was tiny and blurry.
- A later crop preserved only the number, but lost the sense that code was being generated.
Accepted compromise:
- Crop the bottom of the code pane plus the real `tokens/s` panel.
- Keep a little generated text above the speed number.
- Avoid the later part of the recording if it switches into the right-side preview pane.
Final crop used for the `1924 x 1080` process recording:
```text
crop=760:260:690:715
scale=956:327
unsharp=5:5:0.45
```
Example filter:
```bash
ffmpeg -y -t 5 -i "5 秒-过程.mp4" \
-vf "fps=30,trim=0:5,setpts=PTS-STARTPTS,crop=760:260:690:715,scale=956:327:flags=lanczos,unsharp=5:5:0.45,setsar=1" \
token-preview.mov
```
Notes:
- If the source recording is low bitrate, enlarging helps composition but cannot restore true detail.
- Prefer a fresh high-bitrate screen recording over over-sharpening.
- If the target duration is longer than the usable process segment, ask for a longer source or explicitly document any hold frame. Do not silently pad still frames and describe it as fully dynamic.
### Result Demo Crop
Problem observed:
- Cropping the result demo into a short horizontal strip made webpage section transitions look like overlapping videos.
- The source webpage scroll can naturally show two sections at once near boundaries. A very shallow crop exaggerates this and makes it look broken.
- Scaling with `force_original_aspect_ratio=increase` plus a tight vertical crop can change the apparent scroll rhythm.
Accepted approach:
- Give the result demo a larger region.
- Preserve the full vertical structure of the source as much as possible.
- For `16:10` style recordings, use full-frame scaling when possible.
- If the source width changes, center-crop only the horizontal edges, not the vertical content.
- Time-window selection matters as much as crop. If a 5-second source contains Hero → scroll → data section, choose a window that shows the intended story without crossing section boundaries too abruptly.
For the newer `1924 x 1080` 5-second result source, the accepted crop was:
```text
crop=1736:1080:88:0
scale=956:595
```
This center-crops the wider screen recording to match the previous 16:10-ish view before scaling into the card.
For already-cropped `1736 x 1080` result sources, do not apply the `x=88` center crop again. Use:
```text
crop=1736:1080:0:0
scale=956:595
```
Observed fixes from the MIMO tests:
- Test 1 had a title/start screen before the game. Fix: skip the first `0.35s` so the Live Photo starts on the real game result.
- Test 2 had a long Hero plus scroll. A full `0-5s` window made section boundaries look overlapped; a pure `0-2.8s` window removed the scroll. Fix: take `1.0-5.2s` and lightly stretch it to `5s`, preserving Hero briefly and then scrolling.
- Test 3 was stable because its first seconds stayed mostly on the Hero/result state.
Example timing filters:
```text
# Trim only, normal speed
trim=start=0:end=5,setpts=PTS-STARTPTS
# Skip an intro frame before a game/demo starts
trim=start=0.35:end=5.35,setpts=PTS-STARTPTS,tpad=stop_mode=clone:stop_duration=0.5
# Preserve a later webpage scroll while fitting 4.2s into a 5s Live Photo
trim=start=1.0:end=5.2,setpts=(5/4.2)*(PTS-STARTPTS)
```
### Duration Rule
Always verify source duration before promising a Live Photo duration:
```bash
ffprobe -v error \
-show_entries format=filename,duration,size \
-show_entries stream=index,codec_type,codec_name,width,height,r_frame_rate,duration \
-of json "source.mp4"
```
For a 5-second Live Photo, both visual tracks should ideally be at least 5 seconds.
Acceptable alternatives only if stated clearly:
- Top process video holds the final frame after its usable segment.
- Top process video continues into preview mode.
- Use a shorter Live Photo duration.
Do not call a file "true 5 seconds" if one track is mostly padded. Validate the output with frame counting:
```bash
ffprobe -v error -count_frames -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate,duration,nb_read_frames \
-show_entries format=duration,size \
-of default=noprint_wrappers=1 \
IMG_MIMO_AD_V3_5S_LIVE.MOV
```
Expected for 5 seconds at 30fps:
```text
width=1080
height=1440
r_frame_rate=30/1
duration=5.000000
nb_read_frames=150
```
### Final 5-Second MIMO Command Pattern
This pattern combines a 5-second process recording and a 5-second result recording into the accepted card layout.
```bash
ffmpeg -y \
-t 5 -i "5 秒-过程.mp4" \
-t 5 -i "5 秒-结果.mp4" \
-i overlay.png \
-filter_complex "color=c=0xfafaf8:s=1080x1440:d=5:r=30[bg];[0:v]fps=30,trim=0:5,setpts=PTS-STARTPTS,crop=760:260:690:715,scale=956:327:flags=lanczos,unsharp=5:5:0.45,setsar=1[token];[1:v]fps=30,trim=0:5,setpts=PTS-STARTPTS,crop=1736:1080:88:0,scale=956:595:flags=lanczos,setsar=1[result];[bg][token]overlay=64:360[p1];[p1][result]overlay=64:744[p2];[p2][2:v]overlay=0:0" \
-t 5 -r 30 -an -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart \
IMG_MIMO_AD_V3_5S_LIVE.MOV
```
Then extract the key image and create `.pvt`:
```bash
ffmpeg -y -ss 0.8 -i IMG_MIMO_AD_V3_5S_LIVE.MOV \
-frames:v 1 -q:v 2 IMG_MIMO_AD_V3_5S_LIVE.JPG
UV_CACHE_DIR=/private/tmp/uv-cache \
UV_TOOL_DIR=/private/tmp/uv-tools \
uvx --from makelive python -c "from makelive import save_live_photo_pair_as_pvt; asset_id, pvt_path = save_live_photo_pair_as_pvt('IMG_MIMO_AD_V3_5S_LIVE.JPG', 'IMG_MIMO_AD_V3_5S_LIVE.MOV'); print(asset_id); print(pvt_path)"
```
### Failure Modes And Fixes
| Failure | Likely cause | Fix |
| --- | --- | --- |
| Result demo appears to overlap | Crop is too shallow; source page is crossing section boundaries | Make result area taller; preserve full vertical source; inspect contact sheet |
| Result demo feels frozen | A short source window was stretched too much | Use a later/larger time window, or trim only |
| Token number is unreadable | Crop contains too much empty browser space or source is low bitrate | Crop closer around the real platform counter; request higher-bitrate source if needed |
| Token strip loses "generation" feeling | Crop only shows the numeric panel | Include the bottom of the generated code pane above the speed number |
| 5-second output plays but one area freezes | One source clip is shorter than target duration or was padded with `tpad` | Use true 5-second source clips or explicitly label the hold |
| iPhone imports separate files | Loose JPG/MOV were AirDropped | AirDrop only the `.pvt` package |
| Style feels off-brand | The card ignores this skill's Swiss rules | Use off-white, black, one IKB accent, straight modules, no shadow/glass/rounded cards |
### Reusable Rule Of Thumb
For social-card Live Photos built from screen recordings:
1. Inspect source duration, resolution, and bitrate first.
2. Create contact sheets before choosing crops.
3. Produce a static preview before `.pvt`.
4. Make the result/demo area the main visual evidence.
5. Keep the process/token area small but legible.
6. Preserve real UI counters instead of designing fake ones.
7. Validate frame count after rendering.
8. Package with `.pvt` for AirDrop tests.
+20
View File
@@ -22,6 +22,15 @@ Recommended set:
- 1 cover. - 1 cover.
- 4-8 content pages. - 4-8 content pages.
- 1 summary or checklist page if the article benefits from it. - 1 summary or checklist page if the article benefits from it.
- Optional Live Photo card for motion-heavy evidence. Use `references/live-photo-production.md` when making `.pvt` packages for AirDrop and iPhone testing.
Live Photo:
- Duration: up to `5s`.
- Keep the same `1080 x 1440` / `3:4` canvas and safe area as static Rednote cards.
- Use Live Photo when motion is the evidence: fast generation, before/after UI, product interaction, app/game demo, scroll reveal, or process-to-result comparison.
- Package as `.pvt` for iPhone/AirDrop testing. Loose JPG/MOV pairs often import as separate assets.
- Publish path: AirDrop the `.pvt` package as one item to iPhone, then publish from the Xiaohongshu app. Desktop/web upload cannot be treated as the primary Live Photo publishing path.
Naming: Naming:
@@ -101,6 +110,17 @@ output/wechat-1x1-cover.png
output/wechat-cover-pair-preview.png output/wechat-cover-pair-preview.png
``` ```
## WeChat Official Account Article Live Photo
Observed support is upload-path dependent:
- iPhone upload can preserve Live Photo in Official Account article editing.
- Web upload does not expose the same Live Photo path.
- Keep Live Photo duration at or below `3s`.
- Publish path: AirDrop the `.pvt` package as one item to iPhone, then upload from the iPhone Official Account article editing path.
Use the same `3:4` card standard as Rednote when the user wants a WeChat article body card. If the publisher must use the desktop/web path, export a GIF or short video fallback instead of promising Live Photo support.
## Screenshot And Photo Placement ## Screenshot And Photo Placement
For supplied screenshots/photos: For supplied screenshots/photos:
+7 -5
View File
@@ -2,10 +2,10 @@
## Recommended Folder Shape ## Recommended Folder Shape
Create a task folder under the current workspace: Create a task folder under `local-tests/` by default:
```text ```text
social-card-<slug>/ local-tests/<slug>/
index.html index.html
render.cjs render.cjs
assets/ assets/
@@ -14,9 +14,11 @@ social-card-<slug>/
Use descriptive slugs: Use descriptive slugs:
- `social-card-doubao-input` - `local-tests/social-card-doubao-input`
- `social-card-hiking-outfit` - `local-tests/social-card-hiking-outfit`
- `wechat-ai-card-skill-cover` - `local-tests/wechat-ai-card-skill-cover`
If the user explicitly names another output folder, use that folder instead. Do not create generated task folders or rendered assets in the skill root next to `SKILL.md`. Root-level folders such as `social-card-*`, `livephoto-*`, `wechat-*`, `output/`, and loose `.png` / `.jpg` / `.mov` / `.pvt` artifacts are not allowed for new work.
## HTML/CSS Rendering Pattern ## HTML/CSS Rendering Pattern
+27
View File
@@ -28,6 +28,23 @@ Run this before final delivery.
- Images and screenshots align to the grid. - Images and screenshots align to the grid.
- Screenshot pages give the screenshot enough area. - Screenshot pages give the screenshot enough area.
- 1:1 WeChat cover uses a simplified short title and is composed separately, not blindly cropped. - 1:1 WeChat cover uses a simplified short title and is composed separately, not blindly cropped.
- If a page overflows, measure the overflow before editing. Do not remove content until the measured overflow requires it.
- Titles need visible breathing room before the next content block. Main display titles should usually keep at least 28px below; local headings should keep at least 16px.
### Overflow Correction Ladder
Use validator output before changing the layout:
- `1-40px` over: nudge the content group up or tighten one gap/padding by 20-40px. Do not delete content.
- `40-90px` over: compact local gaps/padding and reduce one block height. Avoid cutting copy.
- `90-160px` over: reduce a display title slightly or compress one paragraph before deleting content.
- `160px+` over: switch to a higher-capacity recipe, merge modules, or remove content intentionally.
After fixing overflow, check for over-correction:
- If `R8` reports large bottom whitespace, restore some spacing, move the content down slightly, or expand the final block.
- Do not accept a fix that turns a small overflow into a large empty lower band.
- Prefer one measured adjustment per render pass, then re-run validation.
### 4-band Density Check (3:4 only — run after render) ### 4-band Density Check (3:4 only — run after render)
@@ -45,6 +62,16 @@ A poster passes when:
If failing: don't shrink the canvas, don't add decorative blobs. Either expand copy (more ledger items, longer paragraphs, supporting evidence row, marginalia column) or switch recipe (M07 → M04 for genuinely-short content; M03 → M11 to add a marginal column; thin ledger → M08 Tall Ledger with bigger rows). If failing: don't shrink the canvas, don't add decorative blobs. Either expand copy (more ledger items, longer paragraphs, supporting evidence row, marginalia column) or switch recipe (M07 → M04 for genuinely-short content; M03 → M11 to add a marginal column; thin ledger → M08 Tall Ledger with bigger rows).
### Programmatic Layout Checks
`validate-social-deck.mjs` reports:
- `R1` / `R8`: DOM overflow and visual overflow, including measured pixel amount and the lowest overflowing element.
- `R8`: bottom whitespace and active content height, which catches "fixed too much" layouts.
- `R9`: title-to-content gap, which catches titles visually touching the next block.
Use these numbers as the first diagnosis. Multimodal inspection is still useful for taste, but overflow, empty lower bands, and title gaps should be handled from measured output first.
## Style ## Style
- Package uses one visual system. - Package uses one visual system.
+44
View File
@@ -0,0 +1,44 @@
import Foundation
import ImageIO
guard CommandLine.arguments.count == 4 else {
fputs("Usage: swift add-livephoto-maker-note.swift <input.jpg> <output.jpg> <asset-id>\n", stderr)
exit(2)
}
let inputURL = URL(fileURLWithPath: CommandLine.arguments[1])
let outputURL = URL(fileURLWithPath: CommandLine.arguments[2])
let assetIdentifier = CommandLine.arguments[3]
guard let source = CGImageSourceCreateWithURL(inputURL as CFURL, nil) else {
fputs("Could not open input image\n", stderr)
exit(1)
}
guard let imageType = CGImageSourceGetType(source) else {
fputs("Could not determine image type\n", stderr)
exit(1)
}
let imageCount = CGImageSourceGetCount(source)
guard let destination = CGImageDestinationCreateWithURL(outputURL as CFURL, imageType, imageCount, nil) else {
fputs("Could not create output image\n", stderr)
exit(1)
}
for index in 0..<imageCount {
var properties = (CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [String: Any]) ?? [:]
var makerApple = (properties[kCGImagePropertyMakerAppleDictionary as String] as? [String: Any]) ?? [:]
makerApple["17"] = assetIdentifier
properties[kCGImagePropertyMakerAppleDictionary as String] = makerApple
CGImageDestinationAddImageFromSource(destination, source, index, properties as CFDictionary)
}
guard CGImageDestinationFinalize(destination) else {
fputs("Could not finalize output image\n", stderr)
exit(1)
}
print("Wrote Apple MakerNote asset identifier: \(assetIdentifier)")
+140
View File
@@ -0,0 +1,140 @@
import Foundation
import AVFoundation
import CoreMedia
guard CommandLine.arguments.count == 4 else {
fputs("Usage: swift add-livephoto-mov-metadata.swift <input.mov> <output.mov> <asset-id>\n", stderr)
exit(2)
}
let inputURL = URL(fileURLWithPath: CommandLine.arguments[1])
let outputURL = URL(fileURLWithPath: CommandLine.arguments[2])
let assetIdentifier = CommandLine.arguments[3]
try? FileManager.default.removeItem(at: outputURL)
func quickTimeMetadataItem(key: String, value: NSCopying & NSObjectProtocol, dataType: String) -> AVMutableMetadataItem {
let item = AVMutableMetadataItem()
item.keySpace = .quickTimeMetadata
item.key = key as NSString
item.value = value
item.dataType = dataType
return item
}
let asset = AVURLAsset(url: inputURL)
let semaphore = DispatchSemaphore(value: 0)
var didFail = false
Task {
do {
let tracks = try await asset.loadTracks(withMediaType: .video)
guard let videoTrack = tracks.first else {
throw NSError(domain: "LivePhotoMetadata", code: 1, userInfo: [NSLocalizedDescriptionKey: "No video track"])
}
let reader = try AVAssetReader(asset: asset)
let readerOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: nil)
readerOutput.alwaysCopiesSampleData = false
guard reader.canAdd(readerOutput) else {
throw NSError(domain: "LivePhotoMetadata", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot add reader output"])
}
reader.add(readerOutput)
let writer = try AVAssetWriter(outputURL: outputURL, fileType: .mov)
let formatDescriptions = try await videoTrack.load(.formatDescriptions)
let sourceFormatHint = formatDescriptions.first
let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: nil, sourceFormatHint: sourceFormatHint)
writerInput.expectsMediaDataInRealTime = false
guard writer.canAdd(writerInput) else {
throw NSError(domain: "LivePhotoMetadata", code: 3, userInfo: [NSLocalizedDescriptionKey: "Cannot add writer input"])
}
writer.add(writerInput)
let contentIdentifier = quickTimeMetadataItem(
key: "com.apple.quicktime.content.identifier",
value: assetIdentifier as NSString,
dataType: "com.apple.metadata.datatype.UTF-8"
)
writer.metadata = [contentIdentifier]
let metadataSpec: [String: Any] = [
kCMMetadataFormatDescriptionMetadataSpecificationKey_Identifier as String: "mdta/com.apple.quicktime.still-image-time",
kCMMetadataFormatDescriptionMetadataSpecificationKey_DataType as String: "com.apple.metadata.datatype.int8"
]
var metadataDescription: CMFormatDescription?
let status = CMMetadataFormatDescriptionCreateWithMetadataSpecifications(
allocator: kCFAllocatorDefault,
metadataType: kCMMetadataFormatType_Boxed,
metadataSpecifications: [metadataSpec] as CFArray,
formatDescriptionOut: &metadataDescription
)
guard status == noErr, let metadataDescription else {
throw NSError(domain: "LivePhotoMetadata", code: 4, userInfo: [NSLocalizedDescriptionKey: "Cannot create metadata description"])
}
let metadataInput = AVAssetWriterInput(mediaType: .metadata, outputSettings: nil, sourceFormatHint: metadataDescription)
let metadataAdaptor = AVAssetWriterInputMetadataAdaptor(assetWriterInput: metadataInput)
guard writer.canAdd(metadataInput) else {
throw NSError(domain: "LivePhotoMetadata", code: 5, userInfo: [NSLocalizedDescriptionKey: "Cannot add metadata input"])
}
writer.add(metadataInput)
guard writer.startWriting() else {
throw writer.error ?? NSError(domain: "LivePhotoMetadata", code: 6)
}
guard reader.startReading() else {
throw reader.error ?? NSError(domain: "LivePhotoMetadata", code: 7)
}
writer.startSession(atSourceTime: .zero)
let stillImageTime = quickTimeMetadataItem(
key: "com.apple.quicktime.still-image-time",
value: NSNumber(value: Int8(-1)),
dataType: "com.apple.metadata.datatype.int8"
)
let stillGroup = AVTimedMetadataGroup(
items: [stillImageTime],
timeRange: CMTimeRange(start: .zero, duration: CMTime(value: 1, timescale: 600))
)
guard metadataAdaptor.append(stillGroup) else {
throw writer.error ?? NSError(domain: "LivePhotoMetadata", code: 8, userInfo: [NSLocalizedDescriptionKey: "Could not append still image time metadata"])
}
metadataInput.markAsFinished()
let queue = DispatchQueue(label: "livephoto.video.writer")
writerInput.requestMediaDataWhenReady(on: queue) {
while writerInput.isReadyForMoreMediaData {
if let sample = readerOutput.copyNextSampleBuffer() {
if !writerInput.append(sample) {
didFail = true
reader.cancelReading()
writerInput.markAsFinished()
writer.cancelWriting()
semaphore.signal()
return
}
} else {
writerInput.markAsFinished()
writer.finishWriting {
semaphore.signal()
}
return
}
}
}
} catch {
didFail = true
fputs("Error: \(error.localizedDescription)\n", stderr)
semaphore.signal()
}
}
semaphore.wait()
if didFail {
exit(1)
}
print("Wrote QuickTime Live Photo metadata: \(assetIdentifier)")
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env node
import fs from "node:fs";
const checks = [];
function fileText(file) {
return fs.readFileSync(file, "utf8");
}
function mustInclude(file, needle, label) {
checks.push(() => {
const ok = fileText(file).includes(needle);
return {
ok,
label,
detail: ok ? `${file} includes "${needle}"` : `${file} must include "${needle}"`,
};
});
}
function mustNotMatch(file, pattern, label) {
checks.push(() => {
const ok = !pattern.test(fileText(file));
return {
ok,
label,
detail: ok ? `${file} does not match ${pattern}` : `${file} must not match ${pattern}`,
};
});
}
mustInclude(
"SKILL.md",
"Generated work must live in a task folder, not in the skill root.",
"root output guardrail in SKILL.md",
);
mustInclude(
"SKILL.md",
"local-tests/<slug>/",
"default task folder in SKILL.md",
);
mustInclude(
"SKILL.md",
"Triple Live Photo",
"triple Live Photo capability in SKILL.md",
);
mustInclude(
"SKILL.md",
"material-first Live Photo puzzle layouts",
"material-first puzzle capability in SKILL.md",
);
mustInclude(
"SKILL.md",
"use the M16 Image-Led Cover / text-on-image rules",
"single-video text uses M16 overlay rules",
);
mustInclude(
"SKILL.md",
"Do not invent extra kicker, meta, hairlines",
"single-video text must not invent extra overlay copy",
);
mustInclude(
"SKILL.md",
"platform limits (`5s` Xiaohongshu, `3s` WeChat Official Account)",
"platform publishing reminder in SKILL.md",
);
mustInclude(
"SKILL.md",
"Audience-facing copy must describe the user's actual scene",
"audience-facing copy guardrail in SKILL.md",
);
mustInclude(
"SKILL.md",
"Do not add non-template ornaments just to satisfy an automated density warning",
"non-template ornament guardrail in SKILL.md",
);
mustNotMatch(
"SKILL.md",
/Create a task folder in the current workspace/,
"no root-level task-folder instruction in SKILL.md",
);
mustInclude(
"references/production-workflow.md",
"Create a task folder under `local-tests/` by default",
"production workflow uses local-tests",
);
mustInclude(
"references/production-workflow.md",
"Do not create generated task folders or rendered assets in the skill root",
"production workflow forbids skill-root outputs",
);
mustInclude(
"references/live-photo-production.md",
"## Live Photo Information Budget",
"Live Photo information budget section",
);
mustInclude(
"references/live-photo-production.md",
"## Triple Live Photo Collage",
"triple collage section",
);
mustInclude(
"references/live-photo-production.md",
"## Material-First Puzzle Layouts",
"material-first puzzle section",
);
mustInclude(
"references/live-photo-production.md",
"Do not render single-video text as generic subtitles.",
"single-video text is not subtitle-style",
);
mustInclude(
"references/live-photo-production.md",
"Do not invent extra kicker, meta, hairlines",
"single-video overlay must not invent extra copy",
);
mustInclude(
"references/live-photo-production.md",
"Four-grid",
"four-grid puzzle guidance",
);
mustInclude(
"references/live-photo-production.md",
"## Long Video Intake",
"long video intake section",
);
mustInclude(
"references/live-photo-production.md",
"Web-sourced free videos are only for making our own demo/promo cases",
"user-supplied video is the normal path",
);
mustInclude(
"references/live-photo-production.md",
"Audience-facing copy should name the real scene in the video.",
"Live Photo visible-copy guardrail",
);
mustInclude(
"references/category-cookbook.md",
"## Live Photo Scene Library",
"category scene library section",
);
mustInclude(
"references/category-cookbook.md",
"This is not a fixed template list.",
"category library stays heuristic",
);
mustInclude(
"PRODUCT.md",
"## 10. Live Photo 复盘(2026-07-01)",
"product doc records Live Photo retrospective",
);
mustInclude(
"PRODUCT.md",
"把内部制作要求写成观众可见文案",
"product doc records visible-copy failure mode",
);
mustInclude(
"HANDOFF.md",
"### v0.15 · 2026-07-01",
"handoff records Live Photo version history",
);
mustInclude(
"HANDOFF.md",
"不要把制作要求当成观众可见内容",
"handoff records Live Photo execution pitfall",
);
let failed = 0;
for (const run of checks) {
const result = run();
const marker = result.ok ? "PASS" : "FAIL";
console.log(`${marker} ${result.label}`);
if (!result.ok) {
failed += 1;
console.log(` ${result.detail}`);
}
}
if (failed > 0) {
console.error(`\n${failed} skill doc check(s) failed.`);
process.exit(1);
}
console.log(`\n${checks.length} skill doc checks passed.`);
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Create a tiled contact sheet from a video for cheap Live Photo QA."""
from __future__ import annotations
import argparse
import json
import math
import subprocess
from pathlib import Path
def run(cmd: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(cmd, check=True, text=True, capture_output=True)
def probe_duration(video: Path) -> float:
result = run(
[
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"json",
str(video),
]
)
data = json.loads(result.stdout)
duration = float(data["format"]["duration"])
if duration <= 0:
raise SystemExit(f"invalid duration for {video}: {duration}")
return duration
def main() -> None:
parser = argparse.ArgumentParser(description="Make a tiled frame strip for video QA.")
parser.add_argument("video", type=Path, help="Input video path.")
parser.add_argument("output", type=Path, help="Output JPG/PNG contact sheet path.")
parser.add_argument("--frames", type=int, default=12, help="Number of frames to sample.")
parser.add_argument("--cols", type=int, default=4, help="Tile columns.")
parser.add_argument("--start", type=float, default=0.0, help="Start time in seconds.")
parser.add_argument("--duration", type=float, help="Inspection window duration in seconds.")
parser.add_argument("--thumb-width", type=int, default=320, help="Thumbnail width in pixels.")
parser.add_argument("--padding", type=int, default=12, help="Padding between tiles.")
parser.add_argument("--margin", type=int, default=12, help="Outer tile margin.")
parser.add_argument("--color", default="white", help="Tile background color.")
args = parser.parse_args()
video = args.video.expanduser().resolve()
output = args.output.expanduser().resolve()
if not video.exists():
raise SystemExit(f"missing video: {video}")
if args.frames < 1:
raise SystemExit("--frames must be >= 1")
if args.cols < 1:
raise SystemExit("--cols must be >= 1")
source_duration = probe_duration(video)
window = args.duration if args.duration is not None else max(0.1, source_duration - args.start)
if window <= 0:
raise SystemExit("--duration must be > 0")
output.parent.mkdir(parents=True, exist_ok=True)
rows = math.ceil(args.frames / args.cols)
fps_expr = f"{args.frames}/{window:.6f}"
vf = (
f"fps={fps_expr},"
f"scale={args.thumb_width}:-1:flags=lanczos,"
f"tile={args.cols}x{rows}:padding={args.padding}:margin={args.margin}:color={args.color}"
)
cmd = ["ffmpeg", "-y"]
if args.start > 0:
cmd += ["-ss", f"{args.start:.6f}"]
cmd += ["-t", f"{window:.6f}", "-i", str(video), "-vf", vf, "-frames:v", "1", str(output)]
run(cmd)
print(output)
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Package a JPG/MOV pair as an AirDrop-friendly Live Photo .pvt bundle.
Run with:
UV_CACHE_DIR=/private/tmp/uv-cache \
UV_TOOL_DIR=/private/tmp/uv-tools \
uvx --from makelive python scripts/package-live-photo.py IMG.JPG IMG.MOV
"""
from __future__ import annotations
import argparse
from pathlib import Path
from makelive import save_live_photo_pair_as_pvt
def main() -> None:
parser = argparse.ArgumentParser(description="Package a JPG/MOV pair as a Live Photo .pvt bundle.")
parser.add_argument("jpg", type=Path, help="Key photo path, usually 1080x1440 JPG.")
parser.add_argument("mov", type=Path, help="Paired H.264 MOV path.")
args = parser.parse_args()
jpg = args.jpg.expanduser().resolve()
mov = args.mov.expanduser().resolve()
if not jpg.exists():
raise SystemExit(f"missing JPG: {jpg}")
if not mov.exists():
raise SystemExit(f"missing MOV: {mov}")
asset_id, pvt_path = save_live_photo_pair_as_pvt(str(jpg), str(mov))
print(asset_id)
print(pvt_path)
if __name__ == "__main__":
main()
+188 -2
View File
@@ -15,6 +15,8 @@
* R5 4-band density on 3:4 boards: <75% filled OR any under-filled band > 216px * R5 4-band density on 3:4 boards: <75% filled OR any under-filled band > 216px
* R6 h-xl hard cap display title lines/chars exceed the per-board cap * R6 h-xl hard cap display title lines/chars exceed the per-board cap
* R7 figure margin drift browser-default <figure> margin offsets media alignment * R7 figure margin drift browser-default <figure> margin offsets media alignment
* R8 visual bounds measured top/bottom blank space and visual overflow
* R9 title gap display title touches the next content block
*/ */
import { chromium } from "playwright"; import { chromium } from "playwright";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -91,6 +93,20 @@ const HXL_CAPS = {
const DISPLAY_CLASSES = ["h-xl", "h-hero", "h-statement", "h-display", "num-mega", "num-xl"]; const DISPLAY_CLASSES = ["h-xl", "h-hero", "h-statement", "h-display", "num-mega", "num-xl"];
function overflowFix(px) {
const n = Math.round(px);
if (n <= 40) {
return `only ${n}px over: nudge content up or tighten one gap/padding by 20-40px; do not delete content`;
}
if (n <= 90) {
return `${n}px over: compact local gaps/padding and reduce one block height; avoid cutting copy`;
}
if (n <= 160) {
return `${n}px over: reduce a display title slightly or compress one paragraph before deleting content`;
}
return `${n}px over: switch to a higher-capacity recipe or remove/merge content intentionally`;
}
for (const s of sections) { for (const s of sections) {
const meta = await s.evaluate(el => { const meta = await s.evaluate(el => {
const w = el.clientWidth, h = el.clientHeight; const w = el.clientWidth, h = el.clientHeight;
@@ -112,7 +128,106 @@ for (const s of sections) {
// R1 overflow // R1 overflow
const overflow = meta.scrollH - meta.clientH; const overflow = meta.scrollH - meta.clientH;
if (overflow > 4) { if (overflow > 4) {
fails.push({ rule: "R1", msg: `overflow ${overflow}px (scrollH ${meta.scrollH} > clientH ${meta.clientH})`, fix: "tighten content or switch to a higher-capacity recipe" }); fails.push({ rule: "R1", msg: `overflow ${overflow}px (scrollH ${meta.scrollH} > clientH ${meta.clientH})`, fix: overflowFix(overflow) });
}
// R8 visual bounds — measures the true rendered content extent, not just scrollHeight.
const visualBounds = await s.evaluate(el => {
const er = el.getBoundingClientRect();
const H = el.clientHeight;
const W = el.clientWidth;
const posterArea = H * W;
const TRANSPARENT = /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0?\s*\)|transparent/;
const hasDirectText = n => {
for (const c of n.childNodes) {
if (c.nodeType === 3 && c.textContent.trim().length > 0) return true;
}
return false;
};
const cssColorVisible = c => c && !TRANSPARENT.test(c);
const meaningful = [];
for (const n of el.querySelectorAll("*")) {
if (n.closest("script, style")) continue;
const tag = n.tagName;
const cs = getComputedStyle(n);
const r = n.getBoundingClientRect();
if (r.width < 6 || r.height < 6) continue;
if (n === el || n.classList.contains("content")) continue;
if (cs.position === "absolute" && r.width * r.height >= posterArea * 0.85) continue;
const isText = hasDirectText(n);
const isMedia = tag === "IMG" || tag === "CANVAS" || tag === "SVG";
const isRule = tag === "HR" || (r.height <= 4 && (
parseFloat(cs.borderTopWidth) >= 1 ||
parseFloat(cs.borderBottomWidth) >= 1 ||
cssColorVisible(cs.backgroundColor)
));
const hasBoxFill = cssColorVisible(cs.backgroundColor) &&
r.width * r.height >= 1600 &&
!["MAIN", "SECTION"].includes(tag);
const hasBorderBox = (parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) +
parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth)) >= 1 &&
r.width * r.height >= 1600;
if (!isText && !isMedia && !isRule && !hasBoxFill && !hasBorderBox) continue;
meaningful.push({
top: r.top - er.top,
bottom: r.bottom - er.top,
left: r.left - er.left,
right: r.right - er.left,
cls: n.className ? "." + String(n.className).split(" ").filter(Boolean).join(".") : tag.toLowerCase(),
text: n.textContent.trim().replace(/\s+/g, " ").slice(0, 32),
});
}
if (meaningful.length === 0) return null;
let top = Infinity, bottom = -Infinity;
let topNode = null, bottomNode = null;
for (const item of meaningful) {
if (item.top < top) {
top = item.top;
topNode = item;
}
if (item.bottom > bottom) {
bottom = item.bottom;
bottomNode = item;
}
}
return {
top: Math.round(top),
bottom: Math.round(bottom),
activeHeight: Math.round(bottom - top),
activeRatio: (bottom - top) / H,
topGap: Math.max(0, Math.round(top)),
bottomGap: Math.max(0, Math.round(H - bottom)),
topOverflow: Math.max(0, Math.round(-top)),
bottomOverflow: Math.max(0, Math.round(bottom - H)),
topNode,
bottomNode,
count: meaningful.length,
};
});
if (visualBounds) {
if (visualBounds.bottomOverflow > 4) {
fails.push({
rule: "R8",
msg: `visual bottom overflow ${visualBounds.bottomOverflow}px (lowest: ${visualBounds.bottomNode.cls}${visualBounds.bottomNode.text ? ` "${visualBounds.bottomNode.text}"` : ""})`,
fix: overflowFix(visualBounds.bottomOverflow),
});
}
if (visualBounds.topOverflow > 4) {
fails.push({
rule: "R8",
msg: `visual top overflow ${visualBounds.topOverflow}px (highest: ${visualBounds.topNode.cls}${visualBounds.topNode.text ? ` "${visualBounds.topNode.text}"` : ""})`,
fix: "move the content group down by the measured overflow plus 16-24px",
});
}
const bottomGapLimit = meta.board === "wide" ? 130 : meta.board === "square" ? 160 : 190;
if (visualBounds.bottomGap > bottomGapLimit && visualBounds.activeRatio < 0.78) {
warns.push({
rule: "R8",
msg: `bottom whitespace ${visualBounds.bottomGap}px; active content height ${Math.round(visualBounds.activeRatio * 100)}%`,
fix: "use the measured blank space to expand the last block or move the content down slightly; avoid over-tightening after an overflow fix",
});
}
} }
// R2 footer collision — only flag leaf text or media nodes, not containers // R2 footer collision — only flag leaf text or media nodes, not containers
@@ -309,6 +424,77 @@ for (const s of sections) {
} }
} }
// R9 title gap — catches headings visually touching the next content block.
const titleGaps = await s.evaluate((el, board) => {
const er = el.getBoundingClientRect();
const TRANSPARENT = /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0?\s*\)|transparent/;
const hasDirectText = n => {
for (const c of n.childNodes) {
if (c.nodeType === 3 && c.textContent.trim().length > 0) return true;
}
return false;
};
const colorVisible = c => c && !TRANSPARENT.test(c);
const isMeaningful = n => {
const tag = n.tagName;
const cs = getComputedStyle(n);
const r = n.getBoundingClientRect();
if (r.width < 6 || r.height < 6) return false;
if (n === el || n.classList.contains("content")) return false;
if (hasDirectText(n)) return true;
if (tag === "IMG" || tag === "CANVAS" || tag === "SVG" || tag === "HR") return true;
if (colorVisible(cs.backgroundColor) && r.width * r.height >= 1600 && !["MAIN", "SECTION"].includes(tag)) return true;
const border = parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) +
parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth);
return border >= 1 && r.width * r.height >= 1600;
};
const label = n => {
const cls = n.className ? "." + String(n.className).split(" ").filter(Boolean).join(".") : n.tagName.toLowerCase();
const text = n.textContent.trim().replace(/\s+/g, " ").slice(0, 30);
return text ? `${cls} "${text}"` : cls;
};
const nodes = Array.from(el.querySelectorAll("*")).filter(isMeaningful);
const titles = Array.from(el.querySelectorAll(".h-xl, .h-hero, .h-display, .h-statement, .h-md, .row-title, .step-title"));
const out = [];
for (const title of titles) {
if (!title.textContent.trim()) continue;
const tr = title.getBoundingClientRect();
const isLocal = title.matches(".h-md, .row-title, .step-title");
const minGap = isLocal ? 16 : board === "wide" ? 24 : 28;
let nearest = null;
let nearestGap = Infinity;
for (const node of nodes) {
if (node === title || title.contains(node) || node.contains(title)) continue;
const nr = node.getBoundingClientRect();
const gap = nr.top - tr.bottom;
if (gap < -2) continue;
const overlap = Math.max(0, Math.min(tr.right, nr.right) - Math.max(tr.left, nr.left));
const overlapRatio = overlap / Math.min(tr.width, nr.width);
if (overlapRatio < 0.12 && gap < 96) continue;
if (gap < nearestGap) {
nearestGap = gap;
nearest = node;
}
}
if (nearest && nearestGap < minGap) {
out.push({
title: label(title),
next: label(nearest),
gap: Math.round(nearestGap),
minGap,
});
}
}
return out;
}, meta.board);
for (const g of titleGaps) {
warns.push({
rule: "R9",
msg: `${g.title} has ${g.gap}px gap before ${g.next} (min ${g.minGap}px)`,
fix: "add/restore measured spacing under the title before reducing copy or resizing the whole layout",
});
}
report.push({ meta, fails, warns }); report.push({ meta, fails, warns });
} }
@@ -353,7 +539,7 @@ for (const { meta, fails, warns } of report) {
} }
lines.push(""); lines.push("");
lines.push(`Legend: R1 overflow · R2 footer collision · R3 swiss bold display · R4 min font · R5 4-band density · R6 h-xl cap · R7 figure margin drift`); lines.push(`Legend: R1 overflow · R2 footer collision · R3 swiss bold display · R4 min font · R5 4-band density · R6 h-xl cap · R7 figure margin drift · R8 visual bounds · R9 title gap`);
lines.push(`Exit code 1 only on FAIL. Warnings are advisory.`); lines.push(`Exit code 1 only on FAIL. Warnings are advisory.`);
console.log(lines.join("\n")); console.log(lines.join("\n"));