
Xhs Writer
- 39 installs
- 115 repo stars
- Updated April 25, 2026
- juneyaooo/xhs-writer-skill
Helps with ai & agent building tasks.
About
xhs-writer is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- xhs-writer
- AI & Agent Building
- AI-coding skill
Xhs Writer by the numbers
- 39 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #8,302 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/juneyaooo/xhs-writer-skill --skill xhs-writerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 39 |
|---|---|
| repo stars | ★ 115 |
| Last updated | April 25, 2026 |
| Repository | juneyaooo/xhs-writer-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
xhs-writer — 小红书笔记生成
把一个主题 + (可选)用户素材,生成图文卡片组或短视频脚本,按规范落盘到 output/小红书/。
核心理念
小红书读者看的不是长文,是卡片组或短视频。长文原稿只是中间产物;caption 只是发布配文。
- 图文帖(image post):3-9 张 9:16 竖版卡片(cover + content × N + ending),每张 ≤80 字中文。
- 视频帖(video post):一段 15-90 秒竖屏视频脚本 + 封面卡,分镜写在
meta.json.shots里,视频合成本 skill 不做。
爆款方法论(通用原则)
5大核心原则: 1. 真实素材优先:项目截图、对比图、演示图 > 纯 AI 生图 2. 聚焦核心卖点:用公式判断优先级(见 Step 0.5) 3. 素人感设计:纯色背景 + 大 emoji + 口语化 > 品牌宣传风格 4. 痛点导向:说"能解决什么问题" > 堆砌功能列表 5. 快速迭代:V1(60分) → 用户反馈 → V2(70分) → 持续优化
详细方法论:见 references/xiaohongshu-viral-methodology.md
工作流
按顺序执行。不要跳步。
Step 0 — Intake(必问,一次问完)
收到请求后不要动手,先向用户确认以下要点,尽量一条消息问完:
1. 主题 / 目标读者 / 核心观点 2. 输出形态:图文 or 视频?(默认图文) 3. 素材:有没有已有的文字/图片/视频要用?贴路径或拖文件 4. 风格:简约清新 / 科技感 / ins 风 / 商务 / 文艺复古 / 可爱卡通(默认"简约清新") 5. 卡片数量 / 视频时长:图文默认 5-7 张;视频默认 30-60 秒
Step 0.5 — 卖点/亮点分析(推荐执行)
触发条件:
- 推广类:产品、项目、工具、服务
- 分享类:好物推荐、经验总结、知识科普
- 测评类:产品对比、使用体验
执行: 1. 列出所有功能/特性/亮点 2. 用 AskUserQuestion 让用户打分(每个点):
- 稀缺性(1-5):别人有吗?独特吗?
- 实用性(1-5):能解决多大问题?
- 可感知(1-5):用户能直接看到/感受到吗?
3. 计算得分 = 稀缺性 × 实用性 × 可感知 4. 排序,选择 Top 1-2 作为核心卖点/亮点
输出:
## 卖点/亮点分析结果
| 点 | 稀缺性 | 实用性 | 可感知 | 得分 | 优先级 |
|---|---|---|---|---|---|
| 亮点A | 5 | 5 | 5 | 125 | 🥇 核心 |
| 亮点B | 2 | 3 | 3 | 18 | 🥉 辅助 |
**核心卖点/亮点**:亮点A(聚焦这个,其他作为辅助)参考:references/xiaohongshu-viral-methodology.md 第2节
Step 1 — 素材清点(有素材才做)
如果用户提供了素材路径,先跑脚本生成清单,再由 AI 用多模态能力填描述:
python3 scripts/analyze_material.py <path>... \
--out <work-dir>/reference/materials.json \
--frames-dir <work-dir>/reference/frames脚本只做确定性预处理(分类、取分辨率/时长、抽帧)。AI 随后用视觉能力 打开 materials.json 里每条 image/frame,填 caption 和 usage(cover / content-N / ending / reference)。详见 `references/material-intake.md`。
素材分类(按价值排序): 1. 对比图(最有价值):before/after、input/output、不同版本对比 2. 功能演示:界面截图、操作流程图、效果展示 3. 数据图表:性能对比、用户增长、功能覆盖 4. 品牌素材:Logo、配色方案、官方截图
素材使用策略:
- ✅ 优先级:真实素材 > 图生图 > 代码叠加 > 纯 AI 生图
- ✅ 对比图:用图生图(保留真实感 + 叠加小红书风格文字)
- ✅ 纯文字卡片:用 AI 生图(素人感设计)
- ❌ 避免:所有卡片都用纯 AI 生图(缺少真实感)
参考:references/xiaohongshu-viral-methodology.md 第3节
Step 2 — 采集外部参考(观点类 / 资讯类必做;纯素材驱动可跳过)
按 `references/reference-search.md` 执行,结果写入同一 reference/ 目录。核心数据 ≥2 个来源交叉验证。
Step 3 — 写长文原稿(2000-4000 字)
从 H2 开始(不写 H1;标题入 meta.json.title)。写完先给用户看,确认主旨再继续,不要直接跳到卡片/分镜。反模式清单以 `references/humanizer-zh.md` 为准。
Step 4 — 去 AI 化
读 `references/humanizer-zh.md` 五层原则,完整扫描重写原稿,给出质量评分(满分 50)。
Step 4.5 — 图生图处理(有真实素材时必做)
触发条件:Step 1 发现了对比图、功能演示等高价值素材
工具:gpt-image-2 图生图(需要 OpenAI API key)
执行:
# 使用本项目的 image_generator
import sys
sys.path.insert(0, '~/.claude/skills/xhs-writer-skill/scripts')
from image_generator import GptImage2Generator
generator = GptImage2Generator(aspect_ratio="9:16")
# 图生图:保留真实素材 + 叠加小红书风格文字
generator.generate_scene_image(
scene_data={
'index': 1,
'image_prompt': """Based on the reference image, create a Xiaohongshu style card, 9:16 vertical.
Keep the original image visible.
Add overlays:
- Top: "{标题}"
- Bottom: "{引导文字}"
Style:
- Keep original clear
- Casual Xiaohongshu style
- Authentic feel"""
},
output_path='output.jpg',
size='auto',
reference_image_path='material.jpg'
)Prompt 模板:见 references/xiaohongshu-viral-methodology.md 附录C
失败处理: 1. 重试 1 次 2. 降低图片分辨率(max 1024px) 3. 改用代码叠加文字(scripts/text_on_image.py)
Step 5 — 分发:图文 or 视频
5A. 图文帖:拆成 3-9 张卡片
- 结构:
cover(第 1 张) +content(中间若干) +ending(最后 1 张) - 每张 ≤80 字(
title+content合计,代码点计数) - 一张卡只讲一个论点;数字 / 对比 / 金句优先上卡
- 全套卡片 emoji 风格与配色保持一致
每张卡选一种 合成策略(写入 cards[i].synthesis_strategy),五选一。详见 `references/material-intake.md`:
| strategy | 适用 | 工具 | 优先级 |
|---|---|---|---|
img2img | 有真实素材(对比图/演示图) | gpt-image-2 图生图 | 🥇 最佳 |
text_on_photo | 有 1 张合适照片 + 一句钩子 | scripts/text_on_image.py | 🥈 次选 |
collage | 有 2-4 张互补照片 | scripts/collage_3x4.py | 🥉 可用 |
pure_text | 无素材,纯文字卡 | AI 生图(素人感) | ✅ 常用 |
ai_generated | 概念图 / 数据图 | 用户自备 t2i 服务 | ⚠️ 慎用 |
策略选择原则:
- ✅ 有真实素材 → 优先
img2img(保留真实感) - ✅ 纯文字卡片 → 用
pure_text(素人感设计) - ❌ 避免所有卡片都用
ai_generated(广告感强)
素材有水印 → 先跑 scripts/crop_watermark.py 或按 `references/image-sourcing.md` 处理。
5B. 视频帖:写分镜脚本
- 写 6-12 个分镜,每镜 2-8 秒,累计时长对齐用户预期
- 每镜含:
narration(口播,≤30 字)、on_screen_text(屏幕字,≤15 字)、visual(画面描述)、material_ref(若引用materials.json里某条素材) - 仍要出一张
cover卡(3:4)作为封面;视频本体由用户侧工具合成,本 skill 只产脚本
字段结构见 `references/meta-schema.md` 的 shots[] 定义。
Step 6 — caption + hashtags + 标题
标题生成(5种公式): 1. 痛点+解决方案:{具体痛点}?{解决方案}
- 适用:有明确痛点的工具/产品
2. 提问式:有没有那种{功能描述}的{产品类型}?
- 适用:新工具推荐、功能发现
3. 发现式:我发现了个宝藏!{核心价值}
- 适用:兴奋分享、好物推荐
4. 热点词:{热点词}爆火后,我用它做了{场景}
- 适用:蹭热点、技术类产品
5. 身份共鸣:{身份标签}必备!{核心功能}
- 适用:有明确目标人群的产品
参考:references/xiaohongshu-viral-methodology.md 附录F
caption:
- 100-300 字,hook 开头(数字/提问/惊叹) → 关键信息 → 行动号召(点赞/收藏/关注)
- 带 emoji,闺蜜语气
- 结构:痛点共鸣 → 解决方案 → 具体功能 → 真实案例 → CTA
- 避免:堆砌功能、正式文案、广告感
hashtags:
- 5-8 个,与主题强相关
- 核心标签(4个):热点词 + 核心功能 + 差异化卖点 + 目标人群
- 辅助标签(4个):场景词 + 品类词
- 避免
#生活等过度泛化标签
只写进 meta.json,不粘进卡片或正文
Step 7 — 落盘
目录与命名规则见 `references/output-spec.md`。
output/小红书/{YYYY-MM-DD}/{短标题}_{YYYYMMDDHHmm}/
├── {完整标题}.md # 长文原稿
├── meta.json # 元数据(卡片 / 分镜 / caption / hashtags / materials)
├── images/ # (图文)最终卡图 / (视频)封面
└── reference/ # materials.json / 搜索结果 / summary / 思考过程目录短标题与时间戳必须走脚本标准化,别手写:
python3 scripts/normalize_slug.py "原始长标题" --with-tsmeta.json 完整字段定义见 `references/meta-schema.md`。
Step 8 — 校验(强制)
写完 meta.json 后必须跑:
python3 scripts/validate_meta.py <work-dir>/meta.json非 0 退出 → 读报错修 meta.json 再跑,直到 clean。不要把未校验的产物交给用户。
不要做的事
- 不写 H1;标题只放
meta.json.title - 不在正文末尾写"参考来源 / References";只落到
reference/ - 不在
.md里留【插入图片:...】占位符;图片同步下载 + 引用相对路径 - 不跳过 Step 4(去 AI 化)和 Step 8(validate)
- 不自己手算目录名 / 时间戳,一律走
normalize_slug.py
---
工具依赖
必需工具
- Python 3.8+
- PIL (Pillow)
可选工具
- OpenAI API key(用于图生图,推荐)
- 配置:在
~/.claude/skills/xhs-writer-skill/.env填入OPENAI_API_KEY - 没有 API key 也能用,会生成纯文字卡片
图生图使用
# 添加路径
import sys
sys.path.insert(0, '~/.claude/skills/xhs-writer-skill/scripts')
from image_generator import GptImage2Generator
# 初始化
generator = GptImage2Generator(aspect_ratio="9:16")
# 生成
generator.generate_scene_image(
scene_data={'index': 1, 'image_prompt': '...'},
output_path='output.jpg',
size='auto',
reference_image_path='material.jpg' # 图生图模式
)---
参考资料
核心方法论
- `references/xiaohongshu-viral-methodology.md`:完整爆款方法论
- 卖点优先级判断公式
- 素材使用策略(优先级排序)
- 爆款卡片结构(标准6-7张)
- 文案公式库(5种标题公式)
- 视觉风格指南(素人感 vs 精美设计)
- 图生图 Prompt 模板(3种场景)
- 常见错误清单(5大错误 + 解决方案)
- 工具使用指南
其他参考
references/humanizer-zh.md:去 AI 化原则references/material-intake.md:素材处理流程references/image-sourcing.md:图片来源处理references/meta-schema.md:元数据字段定义references/output-spec.md:输出目录规范
---
快速开始
场景1:推广产品/项目
# 用户说:"帮我推广这个项目 /path/to/project"
# Step 0.5: 卖点分析
# → 列出功能,让用户打分(稀缺性×实用性×可感知)
# → 选出核心卖点
# Step 1: 素材盘点
# → 扫描项目截图、对比图、演示图
# → 分类:对比图(最有价值)> 功能演示 > 其他
# Step 4.5: 图生图
# → 对比图用图生图(保留真实感 + 小红书风格)
# → 纯文字卡片用 AI 生图(素人感)
# Step 6: 生成标题(5个选项)
# → 痛点式、提问式、发现式、热点词、身份共鸣
# 输出:6-7张卡片 + caption + hashtags场景2:好物分享/经验总结
# 用户说:"写一条关于 XX 好物推荐的小红书笔记"
# Step 0: Intake
# → 确认主题、目标读者、输出形态
# Step 0.5: 亮点分析
# → 这个好物的核心亮点是什么?
# Step 1: 素材盘点
# → 产品图、使用场景图、效果对比图
# Step 3-4: 写长文 + 去 AI 化
# Step 5: 拆成卡片
# → 封面(提问/发现式)+ 亮点展示 + 使用场景 + 真实体验 + CTA
# 输出:5-7张卡片 + caption + hashtags场景3:知识科普/教程
# 用户说:"写一条关于 XX 知识的小红书笔记"
# Step 0: Intake
# → 确认主题、目标读者、知识点
# Step 2: 采集外部参考
# → 搜索相关资料,交叉验证
# Step 3-4: 写长文 + 去 AI 化
# Step 5: 拆成卡片(纯文字)
# → 用 AI 生图(素人感设计)
# → 结构:封面 + 核心概念 + 步骤/要点 + 注意事项 + CTA
# Step 6: caption + hashtags
# 输出:5-7张卡片 + caption + hashtags---
常见问题
Q1:什么时候用图生图?
A:有真实素材(对比图、演示图、截图)时优先用图生图。效果:真实感 > 代码叠加 > 纯 AI 生图。
Q2:如何判断卖点优先级?
A:用公式 优先级 = 稀缺性(1-5) × 实用性(1-5) × 可感知(1-5),选择得分最高的 1-2 个作为核心卖点。
Q3:封面选哪种风格?
A:
- 有明确痛点 → 痛点式
- 新工具推荐 → 提问式
- 好物分享 → 发现式
- 蹭热点 → 热点词式
- 明确人群 → 身份共鸣式
Q4:图生图失败怎么办?
A:三级降级策略 1. 重试 1 次 2. 降低图片分辨率(max 1024px) 3. 改用代码叠加文字(scripts/text_on_image.py)
Q5:需要多少张卡片?
A:
- 最少:3张(封面 + 核心卖点 + CTA)
- 标准:6张(封面 + 卖点 + 功能 + 人群 + 案例 + CTA)
- 完整:7张(+ 开源地址/官方链接)
---
更新时间:2026-04-25 适用场景:产品推广、好物分享、知识科普、经验总结、测评对比等各类小红书内容
# OpenAI API 配置(用于图生图功能)
# 如果不配置,会生成纯文字卡片
OPENAI_BASE_URL=https://api.openai.com
OPENAI_API_KEY=sk-your-key-here
GPT_IMAGE_MODEL_NAME=gpt-image-2
GPT_IMAGE_QUALITY=high
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Skill-generated content when repo is used in-place
output/
outputs/
interface:
display_name: "XHS (RedNote) Note Writer"
short_description: "Generate Xiaohongshu (RedNote) notes -- long-form draft, humanized rewrite, split into 3-9 vertical 3:4 cards (≤80 chars each), caption + 5-8 hashtags. Saves to output/小红书/."
default_prompt: "帮我用 xhs-writer 写一条关于 [topic] 的小红书笔记,5-7 张卡片。"
# Trust surface (what the user agrees to when installing this skill):
# - Reads scoped .env only (skill dir ~/.claude/skills/xhs-writer-skill/.env);
# never walks parent project directories.
# - Uses Claude built-in tools: WebSearch, WebFetch, Bash, Read, Write, Edit.
# - Downloads images via curl to output/小红书/.../images/; http/https only.
# - Writes outputs to <cwd>/output/小红书/<date>/<slug>_<ts>/.
# - Optional outbound: user-configured text-to-image service (T2I_BASE_URL)
# only if explicitly set in .env; left blank by default.
xhs-writer-skill 安装指南
本文档供 AI agent 自动安装使用。
---
安装步骤
1. Clone 仓库
git clone https://github.com/JuneYaooo/xhs-writer-skill.git ~/.claude/skills/xhs-writer-skill2. 运行安装脚本
cd ~/.claude/skills/xhs-writer-skill
bash install_as_skill.sh3. 验证安装
ls -la ~/.claude/skills/xhs-writer-skill/SKILL.md如果文件存在,说明安装成功。
4. 重启 Claude Code
安装完成后,需要重启 Claude Code 才能识别新 skill。
---
配置(可选)
如果需要图生图功能(推荐),配置 OpenAI API key:
# 创建 .env 文件
cat > ~/.claude/skills/xhs-writer-skill/.env << 'EOF'
OPENAI_BASE_URL=https://api.openai.com
OPENAI_API_KEY=sk-your-key-here
GPT_IMAGE_MODEL_NAME=gpt-image-2
GPT_IMAGE_QUALITY=high
EOF重要:需要向用户询问 OpenAI API key,不要自己编造。
没有 API key 也能用,会生成纯文字卡片。
---
使用方法
安装完成后,直接跟 Claude 说:
帮我推广这个项目 /path/to/project,做一套小红书卡片。或
写一条关于 XX 的小红书笔记。Claude 会自动调用 xhs-writer-skill 完成任务。
---
故障排查
问题1:skill 未识别
解决:重启 Claude Code
问题2:图生图失败
原因:未配置 OpenAI API key
解决:按上面"配置(可选)"步骤配置 API key
问题3:权限错误
解决:
chmod +x ~/.claude/skills/xhs-writer-skill/install_as_skill.sh
chmod +x ~/.claude/skills/xhs-writer-skill/scripts/*.py---
卸载
rm -rf ~/.claude/skills/xhs-writer-skill然后重启 Claude Code。
#!/bin/bash
##############################################################################
# xhs-writer-skill -- Claude Code Skill 安装脚本
#
# 把当前仓库内容拷贝到 ~/.claude/skills/xhs-writer-skill/
# 并引导配置(可选的).env。
#
# 用法: bash install_as_skill.sh
##############################################################################
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_info() { echo -e "${BLUE}(i) $1${NC}"; }
print_success() { echo -e "${GREEN}[OK] $1${NC}"; }
print_warning() { echo -e "${YELLOW}(!) $1${NC}"; }
print_error() { echo -e "${RED}[X] $1${NC}"; }
print_header() { echo ""; echo "========================================"; echo "$1"; echo "========================================"; echo ""; }
command_exists() { command -v "$1" >/dev/null 2>&1; }
main() {
print_header "xhs-writer-skill -- 安装"
SKILL_DIR="$HOME/.claude/skills/xhs-writer-skill"
print_info "目标目录: $SKILL_DIR"
if [ -d "$SKILL_DIR" ]; then
print_warning "Skill 目录已存在: $SKILL_DIR"
read -p "是否覆盖?(y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "取消"
exit 0
fi
# 备份用户的 .env
if [ -f "$SKILL_DIR/.env" ]; then
cp "$SKILL_DIR/.env" "/tmp/xhs-writer.env.bak"
print_info "已备份现有 .env 到 /tmp/xhs-writer.env.bak"
fi
rm -rf "$SKILL_DIR"
fi
print_info "创建 Skill 目录..."
mkdir -p "$SKILL_DIR"
print_success "目录已创建"
print_info "复制项目文件..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 拷贝核心文件,排除 .git / outputs / venv / .env / __pycache__
rsync -a \
--exclude='.git' \
--exclude='output' \
--exclude='outputs' \
--exclude='venv' \
--exclude='.venv' \
--exclude='__pycache__' \
--exclude='.env' \
"$SCRIPT_DIR/" "$SKILL_DIR/"
print_success "文件复制完成"
# 恢复备份的 .env
if [ -f "/tmp/xhs-writer.env.bak" ]; then
mv "/tmp/xhs-writer.env.bak" "$SKILL_DIR/.env"
print_success "已恢复用户 .env"
fi
print_header "配置(可选)"
if [ -f "$SKILL_DIR/.env" ]; then
print_info "已存在 .env,跳过"
else
cp "$SKILL_DIR/.env.example" "$SKILL_DIR/.env"
print_success "已生成 $SKILL_DIR/.env"
print_info "本 skill 开箱即用,无需配置 key。"
print_info "如需接入 text-to-image 服务出卡片配图,可编辑: $SKILL_DIR/.env"
fi
print_header "安装完成"
print_success "已装到 $SKILL_DIR"
echo ""
print_info "下一步:"
print_info " 1. 重启 Claude Code 让 skill 生效"
print_info " 2. 直接对 Claude 说: '帮我用 xhs-writer 写一篇关于 XXX 的小红书笔记'"
echo ""
}
trap 'print_error "安装过程出错"; exit 1' ERR
main
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
<div align="center">
xhs-writer-skill
小红书爆款笔记生成器 — 从主题到发布,一条龙。
Claude Code / OpenClaw Skill。装进 agent 后,用一句自然语言生成 9:16 竖版卡片组(图文)或短视频脚本,配 caption + hashtags,按规范落盘。支持真实素材图生图、卖点分析、5种标题公式。
  
</div>
---
🎬 效果演示:真实案例
案例:推广 gpt-image2-ppt-skills 开源项目
<table> <tr> <th width="50%">输入:GitHub 项目原图</th> <th width="50%">输出:小红书风格卡片(9:16 竖版)</th> </tr> <tr> <td><img src="docs/assets/input-github-screenshot.jpg" width="100%" alt="GitHub 原图:模板演示"></td> <td><img src="docs/assets/demo-comparison.jpg" width="100%" alt="小红书卡片:对比图"></td> </tr> <tr> <td align="center"><sub>GitHub 项目截图(原始素材)</sub></td> <td align="center"><sub>卡片2:对比图(图生图)<br/>保留真实对比效果 + 小红书风格文字</sub></td> </tr> <tr> <td><img src="docs/assets/input-style-gallery.jpg" width="100%" alt="GitHub 原图:10种风格"></td> <td><img src="docs/assets/demo-styles.jpg" width="100%" alt="小红书卡片:风格展示"></td> </tr> <tr> <td align="center"><sub>GitHub 项目截图(10种风格展示)</sub></td> <td align="center"><sub>卡片3:风格展示(图生图)<br/>保留真实素材 + 素人感设计</sub></td> </tr> </table>
完整案例:
- 输入:GitHub 项目链接
https://github.com/JuneYaooo/gpt-image2-ppt-skills+ 项目截图素材 - 流程:
1. 扫描项目素材(README 图片、演示图、对比图) 2. 搜索同类爆款("AI做PPT"、"PPT工具"相关笔记) 3. 卖点分析(模板克隆 125分 > 10种风格 18分)→ 聚焦核心卖点 4. 拆解爆款结构(标题模式、卡片布局、钩子套路) 5. 图生图处理(保留真实素材 + 小红书风格文字) 6. 生成 5 个标题选项(痛点式、提问式、发现式、热点词、身份共鸣)
- 输出:7张卡片 + caption + hashtags + 发布方案
- 效果:素人感设计 + 真实素材 + 爆款公式 = 可直接发布
---
✨ 能做什么
- 🔍 智能分析 — 给一个 GitHub 项目链接 + 素材,自动扫描项目内容、搜索同类爆款、分析卖点优先级
- 🎯 卖点/亮点分析 — 用公式(稀缺性×实用性×可感知)自动判断优先级,聚焦核心卖点
- 📚 爆款拆解 — 搜索同类内容的爆款笔记,提取标题模式、卡片结构、钩子套路
- 🖼️ 真实素材优先 — 项目截图、对比图用图生图(gpt-image-2),保留真实感 > 纯 AI 生图
- 🎨 素人感设计 — 纯色背景 + 大 emoji + 口语化,不是品牌宣传风格
- 📝 5种标题公式 — 痛点式、提问式、发现式、热点词、身份共鸣,自动生成选项
- 🔄 快速迭代 — V1(60分) → 用户反馈 → V2(70分),持续优化
- 📦 完整交付 — 6-7张卡片 + caption + hashtags + 发布方案,可直接发布
📱 适用场景
- ✅ 产品推广 — 开源项目、SaaS 工具、App 应用
- ✅ 好物分享 — 数码产品、生活好物、美妆护肤
- ✅ 知识科普 — 技术教程、生活技巧、行业知识
- ✅ 经验总结 — 职场经验、学习方法、成长心得
- ✅ 测评对比 — 产品对比、使用体验、效果评测
---
🚀 安装
方式一:让 AI 自己装(推荐)
把下面这段 prompt 丢给你的 AI 助手(Claude Code / OpenClaw / Codex / Cursor 都行),它会自动完成安装:
帮我安装 xhs-writer-skill:
https://raw.githubusercontent.com/JuneYaooo/xhs-writer-skill/main/docs/install.mdagent 会自己 clone 仓库、跑安装脚本、提示你重启。
方式二:手动安装
git clone https://github.com/JuneYaooo/xhs-writer-skill.git
cd xhs-writer-skill
bash install_as_skill.sh脚本会把 skill 装到 ~/.claude/skills/xhs-writer-skill/,Claude Code 重启后自动识别。
配置(可选)
如果需要图生图功能(推荐),配置 OpenAI API key:
# 创建 .env 文件
cat > ~/.claude/skills/xhs-writer-skill/.env << 'EOF'
OPENAI_BASE_URL=https://api.openai.com
OPENAI_API_KEY=sk-your-key-here
GPT_IMAGE_MODEL_NAME=gpt-image-2
GPT_IMAGE_QUALITY=high
EOF🔒 不会误吃密钥:只从 skill 自己目录的 .env 加载。>
💡 图生图 vs 纯文字卡片:有真实素材时,图生图效果远超纯 AI 生图(保留真实感)。没有 API key 也能用,会生成纯文字卡片。
---
🛠 在 Claude Code 里怎么用
装完直接跟 Claude 说人话就行:
场景1:推广产品/项目(推荐配合 social-account-doctor)
帮我推广这个项目https://github.com/xxx/project,项目截图在/path/to/screenshots,先搜一下同类产品的爆款笔记,然后参考它们的结构做一套小红书卡片。
Claude 会: 1. 扫描项目:读取 GitHub README、项目介绍、功能列表 2. 素材盘点:你的项目截图、对比图、演示图 3. 找对标:搜索同类产品的爆款笔记(用 social-account-doctor) 4. 拆爆款:分析标题模式、卡片结构、钩子套路 5. 卖点分析:让你打分(稀缺性×实用性×可感知) 6. 套公式:用爆款的结构 + 你的真实素材 7. 图生图:保留真实素材 + 小红书风格文字 8. 输出:6-7 张卡片 + caption + hashtags + 发布方案
场景2:快速生成(不分析对标)
帮我推广这个项目 /path/to/project,做一套小红书卡片。Claude 会: 1. 扫描项目素材(截图、对比图、演示图) 2. 卖点分析(让你打分:稀缺性×实用性×可感知) 3. 生成 5 个标题选项(痛点式、提问式等) 4. 用图生图处理真实素材 5. 输出 6-7 张卡片 + caption + hashtags
场景3:好物分享
写一条关于 XX 好物推荐的小红书笔记。
Claude 会: 1. 确认亮点(这个好物的核心优势是什么) 2. 素材盘点(产品图、使用场景图) 3. 生成卡片(封面 + 亮点展示 + 使用场景 + 真实体验 + CTA)
场景4:知识科普
写一条关于 XX 知识的小红书笔记。
Claude 会: 1. 采集外部参考(搜索资料,交叉验证) 2. 写长文原稿 + 去 AI 化 3. 拆成卡片(封面 + 核心概念 + 步骤/要点 + 注意事项 + CTA)
🧑💻 想自己写脚本调 CLI 而不走 agent?看 `SKILL.md`,完整工作流程都在那。
---
📊 方法论框架
5大核心原则
1. 真实素材优先 — 项目截图、对比图、演示图 > 纯 AI 生图 2. 聚焦核心卖点 — 用公式判断优先级(稀缺性×实用性×可感知) 3. 素人感设计 — 纯色背景 + 大 emoji + 口语化 > 品牌宣传风格 4. 痛点导向 — 说"能解决什么问题" > 堆砌功能列表 5. 快速迭代 — V1(60分) → 用户反馈 → V2(70分) → 持续优化
5种标题公式
| 公式 | 格式 | 适用场景 |
|---|---|---|
| 痛点式 | {具体痛点}?{解决方案} | 有明确痛点的工具/产品 |
| 提问式 | 有没有那种{功能描述}的{产品类型}? | 新工具推荐、功能发现 |
| 发现式 | 我发现了个宝藏!{核心价值} | 兴奋分享、好物推荐 |
| 热点词 | {热点词}爆火后,我用它做了{场景} | 蹭热点、技术类产品 |
| 身份共鸣 | {身份标签}必备!{核心功能} | 有明确目标人群的产品 |
工作流程建议
推荐流程(结合 social-account-doctor):
Step 1: 找对标爆款
→ 用 social-account-doctor 搜索同类内容
→ 分析爆款的钩子、结构、封面风格
Step 2: 拆解爆款
→ 提取可复用的公式(标题模式、卡片结构、文案套路)
→ 不是照搬,是学习底层逻辑
Step 3: 套自己的内容
→ 用 xhs-writer-skill 生成初稿
→ 应用从爆款学到的公式
→ 保留自己的真实素材和独特卖点
Step 4: 快速迭代
→ V1 发布 → 看数据 → 优化 → V2卡片结构:不固定模板,根据对标爆款和内容类型灵活调整。
常见结构参考:
- 推广类:封面 + 核心卖点 + 功能展示 + 目标人群 + 真实案例 + CTA + 链接
- 好物类:封面 + 产品亮点 + 使用场景 + 效果对比 + 购买建议 + CTA
- 知识类:封面 + 核心概念 + 步骤拆解 + 注意事项 + 总结 + CTA
详细方法论见:`references/xiaohongshu-viral-methodology.md`
---
📁 输出结构
output/小红书/{YYYY-MM-DD}/{短标题}_{YYYYMMDDHHmm}/
├── {完整标题}.md # 长文原稿
├── meta.json # 元数据(卡片/caption/hashtags/materials)
├── images/ # 最终卡图(9:16 竖版)
└── reference/ # materials.json / 搜索结果 / 思考过程---
💬 Community
---
License
Apache License 2.0,详见 LICENSE。
Humanizer-zh: 去除 AI 写作痕迹
你是一位文字编辑,专门识别和去除 AI 生成文本的痕迹,让文字听起来是真人写的。
使用场景
场景 1:对已生成的文章文件进行去AI化
/humanizer-zh output/公众号/2026-04-16/某文章目录/某文章.md读取文件 → 去AI化改写 → 直接写回文件
场景 2:对粘贴的文本片段处理
/humanizer-zh 请帮我去AI化以下段落:[文本]场景 3:配合 wechat-write / xhs-write 使用 文章生成后,对 output 目录中的 .md 文件应用 humanizer-zh 进行最终润色。
你的任务
1. 扫描 AI 模式 — 按下面的规则逐一识别问题 2. 重写问题片段 — 用自然的替代方案替换 3. 保留核心信息 — 不改变事实和论点 4. 注入活人感 — 不只是删掉坏东西,还要加入真实的个性和温度
处理方式
- 用户提供文件路径 → Read 读取,改写后 Edit/Write 写回
- 用户提供文本内容 → 直接改写后输出
- 改写完成后输出质量评分
---
五条核心原则(每次处理前先内化)
1. 删填充,留事实 — 开场白、强调性拐杖词、铺垫句一律删掉 2. 打破公式结构 — 凡是"总-分-总"、二元对比、三段式列举的,打散重组 3. 变化节奏 — 短句之后跟长句。两项优于三项。段落结尾要不一样 4. 信任读者 — 直接陈述,不软化,不辩解,不手把手引导 5. 删金句 — 听起来像可以被引用的句子,基本是 AI 写的,重写
---
第一层:禁用词与禁用结构(扫描替换)
中文高频 AI 词汇(见一个处理一个)
| 禁用词/结构 | 替换方向 |
|---|---|
| 说白了 | 坦率的讲、其实就是、说到底 |
| 值得注意的是 | 直接说,或删掉 |
| 不难发现 | 删掉,直接陈述 |
| 综上所述 / 总的来说 | 用具体回扣句替代,或删掉 |
| 首先…其次…再次…最后 | 换成口语化转场("说到这""回到上面那个问题") |
| 不仅……更…… / 不仅……而且…… | 直接说结论,删掉结构词 |
| 通过……来…… | 改为直接动作句 |
| 此外 | 考虑删除,或用"还有一个"替代 |
| 与此同时 | 直接连接两个事实 |
| 这意味着 / 意味着什么 | "那结果会怎样呢"、"所以呢"、"换句话说" |
| 本质上 | "说到底"、"其实" |
| 换句话说 | "你想想看"、"也就是说" |
| 不可否认 | 直接正面陈述 |
| 值得一提的是 | 删掉,直接写内容 |
| 毋庸置疑 | 删掉 |
| 可以说 | 直接说 |
| 某种程度上 | 具体说清楚,或删掉 |
| 与此相关的是 | 删掉,直接连接 |
英文源 AI 词汇(中文翻译版)
高频词汇: 持久的、格局(抽象名词)、关键性的、展示(动词)、织锦(抽象名词)、彰显了、增强、培养、至关重要、深入探讨、宝贵的、充满活力的
改写前:
此外,这一显著特征作为持久的证明,展示了不断演变的格局中的关键作用。
改写后:
这个特征从清朝末年留下来的,在南部地区最常见。
禁用标点
| 禁用 | 替换 |
|---|---|
破折号 —— | 逗号或句号 |
冒号 : | 逗号,或改为疑问句 |
双引号 "" "" | 用「」,或直接不加引号 |
---
第二层:句式与结构问题
23. 长句堆砌(中文特有高频问题)
AI 最爱憋长句,把三个意思塞进一句话里,用"通过……对……进行……"等框架组装。
识别特征: 一句话超过 30 字,含多个"通过/对/进行/实现"。
改写前:
通过深度学习算法对数据集进行特征提取后,运用卷积神经网络实现图像分类,准确率显著提升。
改写后:
先用深度学习提取特征,再用卷积神经网络来分类。拿花卉识别举个例子,模型能认出花瓣颜色和形状,准确率超了95%。
处理规则:
- 每句尽量控制在 20 字以内
- 专业术语后面紧跟具体例子
- 用句号切断,不用逗号连接三层意思
---
24. 段落过渡词堆叠
识别特征: 每段开头是"首先/其次/再次/然后/最后",通篇流水线感。
改写前:
首先,我们分析了数据集。其次,我们进行了模型训练。最后,我们评估了结果。
改写后:
数据集分析完,就开始训练模型。结果出来之后,我盯着那张精度曲线看了好一会儿。
处理规则:
- 删掉序号词,直接用时序叙述(做完这个,就去做那个)
- 加入细节和情绪让过渡自然
---
1. 过度强调意义(夸大重要性)
识别词汇: 标志着、见证了、是……的体现/证明、极其重要、关键转折点、为……奠定坚实基础
改写前:
这一举措标志着行业演变史上的关键时刻,为未来奠定了坚实基础。
改写后:
这次调整改变了三个核心流程。
---
2. 模糊归因
识别词汇: 专家认为、研究表明、观察者指出、行业报告显示、相关数据证明
改写前:
某研究表明,该方法能显著提升效率。
改写后:
斯坦福大学2023年的实验里,这个方法把处理时间缩短了40%。(如果没有具体来源,就删掉这句,不要保留含糊的归因)
---
3. 以 -ing 结尾的肤浅分析
识别词汇: 象征着……、彰显了……、凸显了……、体现了……(句末用来"升华"的分词短语)
改写前:
设计采用蓝色和绿色,象征着人与自然的深厚联系,彰显了品牌的核心价值。
改写后:
设计用了蓝色和绿色。设计师说是想呼应产品的户外属性。
---
4. 宣传性语言
识别词汇: 充满活力的、令人叹为观止的、迷人的、开创性的、深刻的、卓越的
改写前:
这是一款充满活力的工具,提供了深刻的洞察和卓越的用户体验。
改写后:
这个工具主要做三件事:搜索、摘要、生成表格。上手 15 分钟就能用。
---
5. 提纲式"挑战与展望"
识别词汇: 尽管面临挑战、未来展望、在不断演变的格局中、挑战与机遇并存
改写前:
尽管面临若干挑战,但凭借其战略位置,该领域仍将继续蓬勃发展。
改写后:
主要卡在数据成本上,目前还没有便宜的替代方案。
---
6. 否定式排比
识别结构: "不仅……而且……"、"这不仅仅是……而是……"
改写前:
这不仅仅是一次更新,更是对我们思考生产力方式的革命。
改写后:
这次更新加了批处理和离线模式。
---
7. 三段式法则
问题: AI 强行把想法凑成三组。
改写前:
它带来了创新、灵感和行业洞察。
改写后:
主要是技术方向上有几个新突破。
---
8. 刻意换词
问题: AI 用同义词循环避免"重复",反而显得刻意。
改写前:
主人公面临许多挑战。主要角色必须克服障碍。中心人物最终获得胜利。英雄回到家中。
改写后:
主人公历经波折,最终回到家中。
---
9. 虚假范围
问题: "从X到Y"两端没有实际意义。
改写前:
从大爆炸的奇点到暗物质的神秘舞蹈,本书带你领略宇宙的壮阔。
改写后:
这本书讲了大爆炸、恒星形成和暗物质研究。
---
第三层:风格与格式问题
10. 破折号过度使用
改写前:
系统会处理请求——确保用户能高效完成目标——并在后台记录日志。
改写后:
系统处理请求,同时在后台记录日志。
---
11. 粗体过度使用
粗体只用于真正需要重点突出的地方,不超过全文的 5%。
改写前:
它融合了 OKR、KPI 和视觉工具,如 商业模式画布 和 平衡计分卡。
改写后:
它融合了 OKR、KPI 和视觉工具,如商业模式画布和平衡计分卡。
---
12. 内联标题列表
问题: AI 把段落写成"标题: 内容。标题: 内容。"的格式。
改写前:
- 用户体验: 通过新界面得到显著改善。
- 性能: 通过优化算法得到增强。
改写后:
新界面改进了操作体验,加载速度也快了不少。
---
13. 结构模板化
识别特征: "总-分-总"三段论、每段都有小标题、小标题加粗加冒号
如果文章不是分条目的方法论型内容,去掉小标题,用口语化转场句衔接:
- "说到这个…"
- "回到上面那个问题…"
- "顺着这个再说说…"
- "想到这里我突然记起…"
---
第四层:交流模式问题
14. AI 助手口吻残留
识别词汇: 希望这对您有帮助、当然!、请告诉我、您说得完全正确!
改写前:
希望以上分析对您有所帮助!如需进一步探讨,请随时告知。
改写后:
(直接删掉,文章结尾不需要这类话)
---
15. 谄媚语气
改写前:
好问题!您说得完全正确,这确实是个复杂的话题。
改写后:
这有几个层面值得拆开来看。
---
16. 过度限定
改写前:
该政策可以潜在地可能对结果产生一些影响。
改写后:
该政策会影响结果。
---
17. 通用积极结尾
改写前:
未来充满无限可能,让我们共同期待激动人心的时代到来。
改写后:
(删掉,或换成具体的行动/观察作结尾)
---
18. 填充短语
| 填充短语 | 替换 |
|---|---|
| 为了实现这一目标 | 为此 |
| 由于……的事实 | 因为 |
| 在这个时间点 | 现在 |
| 值得注意的是数据显示 | 数据显示 |
| 系统具有处理的能力 | 系统可以处理 |
| 在……的背景下 | 删掉 |
| 基于上述分析 | 所以 |
---
第五层:注入活人感
去掉坏东西只是一半,还要加进去真实的个性。下面是让文章有温度的具体方法。
让情绪体感化
AI 写法(知识性描述): 我感到非常震撼。 活人写法(体感记忆): 我当时就愣住了。
差别在于:前者是对情绪的总结,后者是那个时刻真实发生的身体反应。
---
论述中故意打断
在展开一个观点时,故意加入口语中断,破坏严谨感:
AI 写法:
出于对当前生态的好奇,我进行了调查。
活人写法:
没有什么特定的理由,就是心血来潮,就是单纯的,想看看现在的生态。
三个"就是"的连续使用,刻意口语化;省略主语;中间插入"单纯的"这种语气词。
---
加入私货(第一手细节)
AI 没有第一手经历,这是最容易拉开差距的地方:
AI 写法:
实验结果表明模型精度显著提升。
活人写法:
跑完第三次实验,精度从80%飙到92%,我当时就拍了桌子,这数据稳了。
加入动作细节("拍了桌子")和第一人称感受,AI 编不出来这种东西。
---
句式断裂
在关键节点用一个极短的句子独立成段:
黑暗森林。
大时代啊,朋友们。
安全,安全,还是他妈的安全。
不能每段都这样,但在情绪高点用一下效果很强。
---
承认对立面
讲观点之前,先站到读者的角度把他们的处境讲透:
AI 写法(直接输出结论):
AI 工具对所有用户都有价值。
活人写法(先承认对立面):
你不是程序员,不需要写代码。你就是一个普通的上班族,每天处理一些琐碎的工作。AI 能帮你什么?这个问题,我也想了很久。
先理解,再转入自己的观点,读者防御心会低很多。
---
推荐口语化词组
写作或改写时,自然地使用这些词组替代书面表达:
转场: 坦率的讲、说真的、反正我觉得、怎么说呢、其实吧、你想想看、我跟你说、顺着上面的再聊聊
表达判断: 我有时候觉得、我一直觉得、我自己的感受是、我始终坚信
承认不确定: 说实话我也不确定、我自己也还在摸索、可能有些想法还不成熟、我说"理论上"是因为我自己还没完全跑通
情绪表达: 我当时就愣住了、想想就觉得兴奋、太离谱了、给我一下子整不会了、你敢信
---
快速检查清单
处理完成前,逐项过一遍:
- [ ] 有没有"说白了"、"值得注意的是"、"综上所述"
- [ ] 有没有"首先/其次/最后"序号词
- [ ] 有没有"不仅……更……"结构
- [ ] 有没有破折号(——)
- [ ] 有没有冒号(:)
- [ ] 连续三句句式长度相同?打断一句
- [ ] 段落结尾都是短句总结?换一种结尾
- [ ] 有没有三段式列举?改成两项或四项
- [ ] 有没有"通过……来……"?改直接动作句
- [ ] 所有情绪表达是体感记忆还是知识性描述?
---
处理流程
1. 通读全文,判断整体 AI 痕迹程度 2. 按五层顺序扫描和处理 3. 检查快速清单 4. 通读改写后的文本,确认读起来像真人说的 5. 输出改写结果和质量评分
---
质量评分(总分 50)
| 维度 | 评估标准 | 得分 |
|---|---|---|
| 直接性 | 直接陈述事实还是绕圈宣告?10分:直截了当;1分:充满铺垫 | /10 |
| 节奏 | 句子长度是否变化?10分:长短交错;1分:机械重复 | /10 |
| 信任度 | 是否尊重读者智慧?10分:简洁明了;1分:过度解释 | /10 |
| 活人感 | 听起来像真人说话吗?有体感细节吗?10分:自然流畅有温度;1分:机械生硬 | /10 |
| 精炼度 | 还有可删减的内容吗?10分:无冗余;1分:大量废话 | /10 |
| 总分 | /50 |
评分标准:
- 45-50:已充分去除 AI 痕迹,可以发布
- 35-44:良好,有几处可以继续打磨
- 低于 35:AI 味仍然明显,需要重新修订
---
完整示例
改写前(AI 味):
新的软件更新作为公司致力于创新的证明。此外,它提供了无缝、直观和强大的用户体验——确保用户能够高效地完成目标。这不仅仅是一次更新,而是我们思考生产力方式的革命。行业专家认为这将对整个行业产生持久影响,彰显了公司在不断演变的技术格局中的关键作用。
改写后(活人感):
软件更新加了批处理、键盘快捷键和离线模式。测试用户反馈不错,大多数人说任务完成速度变快了。我自己用了两天,最明显的就是那个快捷键,省了不少来回点击。
改动说明:
- 删除"作为……的证明"(夸大象征意义)
- 删除"此外"(AI 词汇)
- 删除"无缝、直观和强大"(三段式+宣传)
- 删除破折号和"-确保"短语(肤浅分析)
- 删除"不仅仅是……而是……"(否定排比)
- 删除"行业专家认为"(模糊归因)
- 删除"持久影响"和"不断演变的格局"(AI 词汇)
- 加入"我自己用了两天"(第一手细节,注入活人感)
---
参考来源
本 skill 综合了以下三个来源: 1. 维基百科"AI 写作特征"页面(WikiProject AI Cleanup) 2. 学术降AI率实践(短句拆分、口语化词汇替换、私货注入) 3. 中文公众号写作风格研究(节奏感、论述打断、口语词组、活人感评估)
---
文章配图:搜索、版权检查与处理
配图遵循零水印上线原则:每张图在写入 Markdown 前必须目视检查,高风险图片必须处理后才能使用。
安全图源优先级(从低风险到高风险)
1. AI 生成(用户自备 text-to-image 服务:Nano Banana、Gemini Image、Flux、SD 等)—— 零版权风险,概念图 / 装饰图首选 2. Unsplash(images.unsplash.com)—— Unsplash License,免费商用,无需署名 3. Pexels(images.pexels.com)—— 免费商用,无需署名 4. Pixabay(pixabay.com)—— CC0,免费商用 5. 官方 OG 图 / Press Kit(GitHub opengraph、产品官网 press kit、开源项目 banner)—— 公开素材可引用 6. 自截产品截图 —— 合理使用范围内 7. 新闻 / 博客配图 —— 必须检查水印,见下方处理流程
图片搜索技巧
- 用英文关键词效果更好(
"Hermes agent terminal install"而非"安装终端") - 加限定词提高精准度:
photo、screenshot、diagram、illustration、site:unsplash.com - GitHub OG 图固定格式:
https://opengraph.githubassets.com/1/{owner}/{repo} - Unsplash 直链示例:
https://source.unsplash.com/1600x900/?<关键词>
下载命令:
mkdir -p output/.../images
curl -L -o "output/.../images/img_001.jpg" "https://图片URL" 2>/dev/null版权风险检查(每张图必做)
下载后立即用多模态 Read 工具目视检查:
Read tool → 查看图片 → 判断风险等级风险分级
| 等级 | 特征 | 处理 |
|---|---|---|
| ✅ 安全 | 无水印;Unsplash / Pexels / Pixabay;官方 OG 图;AI 生成 | 直接用 |
| ⚠️ 低风险 | 无明显水印的新闻截图;博客作者自拍无署名 | 建议替换;急用可留 |
| ❌ 高风险 | 带水印(Getty / Shutterstock / 视觉中国 / 东方 IC / 图虫);摄影师个人署名;新闻社图(AP / Reuters / AFP / 新华社)带水印标注 | 必须处理 |
高风险标志识别
目视重点看:
- 四个角落:最常见水印位置(右下角尤其注意)
- 底部横条:视觉中国、东方 IC 常用
- 半透明文字叠加:Getty Images 常见
- 图片内嵌文字:摄影师名字、媒体 logo
高风险图片处理方法
方法一:裁剪去水印(适合水印在边缘的新闻图)
python3 -c "
from PIL import Image
img = Image.open('output/.../images/img_xxx.jpg')
w, h = img.size
# 根据水印位置裁剪,示例:去掉底部 40px
cropped = img.crop((0, 0, w, h - 40))
cropped.save('output/.../images/img_xxx.jpg')
print(f'裁剪完成:{w}x{h} → {w}x{h-40}')
"常见水印位置裁剪参考:
- 右下角水印 →
img.crop((0, 0, w - 120, h))或img.crop((0, 0, w, h - 30)) - 底部横条 →
img.crop((0, 0, w, h - 50)) - 四周留白水印 → 适当裁四边
裁剪后再次用 Read 工具目视确认水印已去除。
方法二:AI 生图替换(水印无法去除时)
调用用户自备的 text-to-image 服务,提示词写作要点:
- 具体描述场景而非抽象概念
- 加风格词:
minimalist、clean、professional、flat design - 避免真实人脸(肖像权风险)
- 小红书封面卡指定
aspect_ratio="3:4"(竖版)
完整配图流程
1. 根据章节内容确定需要什么类型的图(事实图 or 概念图) 2. 按安全图源优先级选择来源 3. 用 curl 下载到 images/ 目录 4. 用 Read 工具目视检查 → 判断风险等级 5. 高风险 → 裁剪或 AI 替换 → 再次确认 6. 在 Markdown 中写入相对路径引用: 7. 不允许在 .md 文件中留 `【插入图片:...】` 占位符
素材驱动流程与卡片合成策略
当用户提供了已有素材(文字 / 图片 / 视频 / 音频),笔记的写法要从"找参考 → 写原稿 → 拆卡"切换到"清点素材 → 从素材里提炼观点 → 反推卡片/分镜"。本文档给出素材处理的完整流程和四种卡片合成策略。
一、素材清点流程
1. 跑脚本生成 materials.json
python3 scripts/analyze_material.py <path>... \
--out <work-dir>/reference/materials.json \
--frames-dir <work-dir>/reference/frames \
--frames-per-video 6脚本做确定性的事:
- 按扩展名分类
text / image / video / audio / unknown - 图片:读
width / height / aspect - 视频:读
duration_sec、抽 N 帧存到frames-dir/ - 文本:截取前 400 字预览
- 输出
{ "materials": [...] },每条含空字段caption、usage、strategy
2. AI 用多模态能力补全
打开 materials.json,对每条 image 和每个 video 的抽帧:
- 用 Read 工具目视查看
- 填
caption:10-40 字客观描述画面,不加情绪词 - 填
usage:这张图打算用在哪(cover/content-3/ending/reference-only/discard) - 填
strategy:见本文第二节
视频类素材的判断基于抽帧;如果抽帧不足以判断,跑 ffmpeg 加密抽帧或让用户补充。
3. 从素材反推观点
素材清点完后,再写长文原稿。不要先写原稿再硬塞素材——那会退化成纯文字帖 + 配图。正确顺序:
1. 素材里最强的 1-2 张/段 → 决定 cover 钩子 2. 其余素材按 usage 串成叙事线 → 决定 content 卡顺序 3. 缺口(没有素材支撑的论点) → 由 ai_generated 或 pure_text 卡补
二、卡片合成策略(4 种)
每张卡片在 meta.json.cards[i].synthesis_strategy 里必须标明走哪条路径。
策略 A:pure_text — 纯文字卡
适用:没有合适素材,或这张卡就是金句/数据强表达。
做法:
- 交给用户侧 t2i 服务生图(给一个描述性 prompt),或
- 用用户自备的小红书模板工具(Canva / 稿定 / 创客贴) 手工排,本 skill 不做渲染
如果走 AI 生图,prompt 写法:minimalist poster, {配色}, large Chinese text "{卡片文字}", vertical 3:4, clean background, no watermark。
策略 B:text_on_photo — 照片 + 文字叠加
适用:有一张构图干净的真实照片 + 一句强钩子(cover / ending 常用)。
做法:
python3 scripts/text_on_image.py <input.jpg> <output.jpg> \
--text "一句钩子" \
--position top \
--fit 3x4 \
--size 72 \
--color "#FFFFFF" \
--bg "#000000AA"注意:
--position=top / center / bottom;cover 推荐 top 或 center- 文字长度 ≤18 字,更长先拆成两行(脚本已支持换行符
\n) - 照片有水印先走
crop_watermark.py - 字体走
assets/fonts/下的 CJK 字体,缺字体脚本会警告并用 PIL 默认字体(不推荐上线)
策略 C:collage — 多图拼贴
适用:2-4 张素材互为补充(前后对比 / 多角度 / 步骤图)。
做法:
python3 scripts/collage_3x4.py <output.jpg> \
--layout 2v --inputs a.jpg b.jpg \
--gap 12 --bg "#FFFFFF" --size 900x1200布局选择:
| layout | 素材数 | 排列 | 常用场景 |
|---|---|---|---|
2v | 2 | 上下 | 对比 before/after |
2h | 2 | 左右 | 窄图配对(慎用) |
3 | 3 | 大图在上 + 两小图 | 主图 + 细节 |
4 | 4 | 2×2 | 步骤 / 多角度 |
拼贴出来的卡若还要叠一句标题,再串一次 text_on_image.py。
策略 D:ai_generated — 概念图 / 数据图
适用:概念图、流程图、数据图、抽象示意——实拍拿不到的。
做法:本 skill 不绑定 t2i 服务;把 prompt 写进 cards[i].image_prompt,由用户调自己的 Nano Banana / Gemini Image / Flux / SD 生图,出图后把 URL / 本地路径填 cards[i].image_url。
prompt 要点:
- 明确比例:
vertical 3:4, 900x1200 - 明确风格:
minimalist, flat design, clean - 避免真实人脸(肖像权)
- 数据图给具体数字,别让模型瞎编
三、图文 vs 视频的分流判断
Step 0 问完后按如下判断:
- 用户明确说"视频" / "短视频" / "reels" → 走 5B(视频帖)
- 用户素材里 ≥1 段可用视频(> 5 秒) → 默认建议 5B,但由用户拍板
- 其余情况 → 走 5A(图文帖)
视频帖仍然需要一张 cover 卡(3:4)作封面,合成策略同上。
四、materials.json 示例
{
"materials": [
{
"id": "m001",
"path": "/Users/me/photos/spring_01.jpg",
"kind": "image",
"size_bytes": 2458912,
"width": 4032, "height": 3024, "aspect": 1.333,
"caption": "阳光下的樱花树近景,粉白色花瓣",
"usage": "cover",
"strategy": "text_on_photo"
},
{
"id": "m002",
"path": "/Users/me/videos/hike.mp4",
"kind": "video",
"duration_sec": 42.5,
"width": 1080, "height": 1920,
"frames": [
"reference/frames/hike_f01.jpg",
"reference/frames/hike_f02.jpg"
],
"caption": "登山小径,从山脚视角仰拍山顶",
"usage": "content-2",
"strategy": "use_original"
}
]
}注:视频素材如果要原样用在视频帖分镜里,strategy 填 use_original;在图文帖里只作为 reference-only 或从某一帧派生 text_on_photo。
meta.json Schema
meta.json 是每篇文章目录下的核心元数据文件。不同平台共用下面的基础字段,平台特有字段附加在各自扩展里。
基础字段(所有平台)
{
"title": "文章标题",
"summary": "一句话摘要",
"author": "",
"platform": "公众号 | 小红书 | 头条号 | 通用",
"created_at": "ISO 8601 时间(Asia/Shanghai)",
"tags": ["标签1", "标签2"],
"word_count": 0,
"cover_image": "封面图文件名(可选)",
"images": ["images/下的相对路径列表"],
"references": {
"search_queries": ["使用的搜索关键词"],
"source_count": 0,
"summary_file": "reference/summary.md"
}
}公众号扩展字段
{
"platform": "公众号",
"theme": "橙心",
"cover_image_prompt": "英文生图 prompt(可选)",
"cover_image_url": "封面图上传后的 URL(可选,发布阶段写入)",
"wechat": {
"draft_media_id": "",
"published_url": ""
}
}小红书扩展字段
{
"platform": "小红书",
"post_format": "image | video",
"caption": "100-300 字发布文案",
"hashtags": ["#话题1", "#话题2"],
"poster_style": "简约清新",
"card_count": 7,
"cards": [
{
"page": 1,
"type": "cover | content | ending",
"title": "卡片小标题(cover/content)",
"content": "卡片正文(≤80 字中文)",
"synthesis_strategy": "pure_text | text_on_photo | collage | ai_generated",
"material_refs": ["m001", "m002"],
"image_prompt": "图片生成 prompt(可选)",
"image_url": "卡片图片 URL(可选)"
}
],
"shots": [
{
"index": 1,
"duration_sec": 3.0,
"narration": "口播 ≤30 字",
"on_screen_text": "屏幕字 ≤15 字",
"visual": "画面描述",
"material_ref": "m002"
}
],
"materials": [
{
"id": "m001",
"path": "reference/<相对路径或原始绝对路径>",
"kind": "text | image | video | audio",
"caption": "画面描述",
"usage": "cover | content-N | ending | reference-only | discard",
"strategy": "pure_text | text_on_photo | collage | ai_generated | use_original"
}
]
}字段使用规则:
post_format=image时,cards[]必填、shots[]省略post_format=video时,shots[]必填;cards[]只需一张cover作为封面materials[]仅在用户提供了素材时出现;详细工作流见material-intake.mdsynthesis_strategy为每张卡选择合成路径;material_refs指向materials[].id
字段规则
- title:≤30 字,公众号 ≤64 字符会被微信截断;小红书建议 10-20 字带 emoji
- summary:公众号 ≤20 字(微信摘要字段 ≤120 字符);小红书无 summary,用 caption
- created_at:ISO 8601 带时区,例
"2026-04-24T15:00:00+08:00" - word_count:正文字数(不含标题、摘要、caption)
- images:只列
images/下的相对路径,不含外链 - references.source_count:去重后的来源数
完整示例
公众号
{
"title": "AI 冲击就业?Anthropic 最新研究揭示劳动力市场的真相",
"summary": "失业率在下降,新岗位在增加。数据比标题党更有说服力。",
"author": "",
"platform": "公众号",
"created_at": "2026-04-24T15:00:00+08:00",
"theme": "橙心",
"tags": ["AI", "就业", "劳动力市场"],
"word_count": 3040,
"cover_image": "cover.jpg",
"cover_image_prompt": "A professional workspace, clean desk, soft natural light",
"images": ["images/img_001.jpg", "images/img_002.jpg"],
"references": {
"search_queries": ["AI 失业 2026", "Anthropic economic index"],
"source_count": 6,
"summary_file": "reference/summary.md"
}
}小红书
{
"title": "💼 AI 失业危机?数据告诉你真实情况",
"platform": "小红书",
"created_at": "2026-04-24T15:00:00+08:00",
"caption": "媒体说危机,数据说增长。Anthropic 调研 5000 人,83% 说 AI 提高效率而非取代……",
"hashtags": ["#AI对工作的影响", "#失业危机是假的"],
"poster_style": "简约清新",
"card_count": 7,
"cards": [
{
"page": 1,
"type": "cover",
"title": "💼 AI 失业危机?",
"content": "媒体说危机,数据说增长\n美国失业率:4.2%→3.8%"
}
],
"images": [],
"references": {
"search_queries": ["AI 就业影响 2026"],
"source_count": 4,
"summary_file": "reference/summary.md"
}
}文章输出目录规范
所有写作 skill 生成的内容统一按此规范保存到仓库的 output/ 目录下,便于后续查找、归档与发布。
通用目录结构
output/{平台类型}/{YYYY-MM-DD}/{短标题}_{YYYYMMDDHHmm}/
├── {文章完整标题}.md # Markdown 源文件
├── {文章完整标题}_{主题名}.html # (公众号可选)mdnice 主题渲染,自包含样式
├── {文章完整标题}_cover.jpg # (可选)封面图
├── meta.json # 元数据
├── images/ # 文章中引用的图片
│ └── img_001.jpg ...
└── reference/ # 参考资料与生成思考
├── thinking.md # AI 生成过程中的思考记录
├── search_results.json # 搜索引擎原始返回结果
├── summary.md # 参考资料综合摘要
└── articles/ # 提取的参考文章原文
└── ref_001_{来源标题}.md ...平台类型目录映射
| 写作类型 | 目录名 | 对应 skill |
|---|---|---|
| 微信公众号文章 | 公众号/ | wechat-write |
| 小红书笔记 | 小红书/ | xhs-write |
| 头条号文章 | 头条号/ | — |
| 未指定平台 | 通用/ | — |
文件命名规则
所有目录名和文件名中的以下字符必须去除或替换为 _:
- 冒号
::、问号??、感叹号!!、引号"""'' - 斜杠
/\、星号*、尖括号<>、竖线| - 省略号
……...、破折号—— - 空格 →
_ - 连续的
_合并为单个_
目录名用精简短标题(≤15 字,纯中文 + 字母 + 数字 + 下划线 + 连字符):
- 好:
GPT-5.4发布解析 - 差:
GPT-5.4深夜炸场:OpenAI大一统模型来了
文件名可以用较长标题,但同样不能包含上述特殊字符:
- 好:
GPT-5.4深夜炸场_OpenAI祭出最强统一模型.md - 差:
GPT-5.4深夜炸场:OpenAI祭出史上最强统一模型.md
时间与时区
- 日期使用 Asia/Shanghai 时区
- 时间戳格式:
YYYYMMDDHHmm(精确到分钟,不含秒) - 日期目录:
YYYY-MM-DD
示例
output/公众号/2026-03-06/GPT-5.4发布解析_202603061836/
├── GPT-5.4深夜炸场_OpenAI祭出最强统一模型.md
├── GPT-5.4深夜炸场_OpenAI祭出最强统一模型_橙心.html
├── meta.json
├── images/
│ ├── img_001.jpg
│ └── img_002.jpg
└── reference/
├── thinking.md
├── search_results.json
├── summary.md
└── articles/
└── ref_001_OpenAI发布会官方博客.mdoutput/小红书/2026-04-24/春季护肤攻略_202604241500/
├── 春季护肤全攻略_7个你没注意的细节.md
├── meta.json
├── images/
│ ├── card_01_cover.jpg
│ └── card_02_content.jpg
└── reference/
└── summary.mdMarkdown 正文约定
不写 H1 标题
.md 文件正文不要写 H1 标题(# 标题)。各发布平台(公众号、小红书、头条号)都有独立的标题字段,正文里再写 H1 会导致标题重复显示。正文从 H2(`## 章节标题`)开始,标题保存到 meta.json.title。
参考来源不写入文章
文章正文末尾绝对不要附加"参考来源"、"参考文献"、"References"等内容。所有参考资料只保存到 reference/ 目录中(search_results.json、summary.md、articles/)。正文保持干净,不暴露信息来源。
图片引用
文章中的图片保存到 images/ 目录,Markdown 中使用相对路径引用:
不允许在最终 .md 中保留 【插入图片:...】 之类的占位符。写作时要同步完成图片搜索、下载、版权检查(见 image-sourcing skill)。
去 AI 化
所有生成的文章都需遵守(详见 humanizer-zh skill):
- 避免:首先 / 其次 / 再次、通过……来……、不仅……更……、综上所述
- 推荐:短句、口语化、具体例子、个人感受、碎片化叙事、疑问句
参考资料搜索与采集
用 Claude 内置 WebSearch / WebFetch 从多个角度搜集参考资料,提取正文并生成结构化摘要,结果保存到 reference/ 子目录供写作消费。
搜索流程
1. 分析主题:从用户输入提炼搜索关键词 2. 多角度搜索:中英文 + 不同关键词组合,至少 3 轮,覆盖事实、观点、数据 3. 内容提取:对有价值的 URL 用 WebFetch 抓取正文 4. 交叉验证:核心数据在 ≥2 个来源中交叉确认 5. 生成摘要:综合生成结构化分析 6. 保存结果:写入 reference/ 目录
搜索策略
- 多语言:英文搜一手信息源(官方博客、TechCrunch、arXiv 等),中文搜国内视角
- 多角度:同一主题从技术细节、行业影响、用户反馈、竞品对比分别搜
- 时效性:搜索时加入年份或日期限定词
- 关键数据:基准分数、定价、发布时间等务必在 2 个以上来源确认
- 避免幻觉:所有结论都要有
WebFetch抓到的原文做支撑,不允许凭记忆编造来源
可用工具(按推荐顺序)
1. WebSearch — Claude 内置搜索,获取结果列表 2. WebFetch — 抓取具体页面正文 3. 搜索引擎 HTML 端点回退(WebSearch 不可用时):
- DuckDuckGo:
https://html.duckduckgo.com/html/?q=<URL 编码> - Bing:
https://www.bing.com/search?q=<URL 编码>
4. Bash + curl — 特定 RSS / JSON API
输出到 reference/ 目录
reference/
├── search_results.json # 搜索关键词 + 原始结果(URL + 标题 + 摘要)
├── summary.md # 参考资料综合摘要
├── thinking.md # (可选)意图识别、大纲构思、写作决策
└── articles/ # 提取的参考文章原文
├── ref_001_{来源标题}.md
└── ...挂载位置:
- 为某篇文章搜参考 → 保存到该文章目录下的
reference/ - 独立搜索任务 → 保存到
output/参考资料/{YYYY-MM-DD}/{短主题}_{YYYYMMDDHHmm}/reference/
search_results.json 建议结构
{
"topic": "AI 对就业的影响",
"queries": [
"AI impact on jobs 2026",
"AI 失业数据 2026",
"Anthropic economic index"
],
"results": [
{
"query": "Anthropic economic index",
"title": "...",
"url": "https://...",
"source": "anthropic.com",
"abstract": "...",
"fetched": true,
"local_file": "articles/ref_001_....md"
}
]
}summary.md 建议结构
# 参考资料综合摘要:{主题}
## 一、核心事实与数据
- 事实 1(来源:ref_001、ref_003)
- 事实 2(来源:ref_002)
## 二、主要观点与分歧
- 正方:...(来源:...)
- 反方:...(来源:...)
## 三、可引用的金句 / 数字
- "..." —— 来源
## 四、潜在写作角度
- 角度 A
- 角度 B小红书爆款笔记制作方法论
基于真实 11 万赞爆款拆解 + 实战反馈迭代
一、核心原则
❌ 不要做的(常见错误)
1. 广告感设计:渐变背景、科技感、精美排版 → 像品牌宣传 2. 堆砌所有功能:10 种风格、所有特性都讲 → 没重点 3. 正式文案:"奇点来了"、"杀手功能" → 距离感 4. 纯 AI 生图:不用真实素材 → 假、空
✅ 要做的(爆款特征)
1. 素人感:纯色背景 + 大 emoji + 简单文字 2. 抓核心卖点:只讲 1-2 个最稀缺的功能 3. 口语化:"有没有"、"宝藏"、"好看哭了" 4. 真实素材:项目截图、实际效果对比
---
二、卖点优先级判断法则
判断标准(按优先级)
1. 稀缺性 > 常见功能
- 问:其他工具能做到吗?
- 独家功能 > 常见功能
2. 实用性 > 炫技
- 问:能解决什么具体痛点?
- 解决痛点 > 展示技术
3. 可感知 > 抽象概念
- 问:用户能直接看到效果吗?
- 视觉对比 > 文字描述
案例:gpt-image2-ppt 项目
| 功能 | 稀缺性 | 实用性 | 可感知 | 优先级 |
|---|---|---|---|---|
| 模板克隆 | ⭐⭐⭐⭐⭐ 独家 | ⭐⭐⭐⭐⭐ 公司刚需 | ⭐⭐⭐⭐⭐ 前后对比 | 🥇 核心 |
| gpt-image-2 热点 | ⭐⭐⭐⭐ 新技术 | ⭐⭐⭐⭐ 质量提升 | ⭐⭐⭐⭐ 文字清晰 | 🥈 辅助 |
| 10 种风格 | ⭐⭐ 常见 | ⭐⭐⭐ 选择多 | ⭐⭐⭐ 风格对比 | 🥉 锦上添花 |
| 30 秒速度 | ⭐⭐ 常见 | ⭐⭐⭐⭐ 效率 | ⭐⭐ 数字 | 🥉 锦上添花 |
结论:封面主打"模板克隆",gpt-image-2 作为技术背书,其他功能一笔带过。
---
三、素材使用策略
真实素材 > AI 生图
| 内容类型 | 素材来源 | 处理方式 |
|---|---|---|
| 功能对比 | 项目实际截图 | 直接用,加文字标注 |
| 效果展示 | before/after 对比图 | 左右分屏或上下对比 |
| 使用场景 | 电脑屏幕拍照 | 模仿爆款的真实感 |
| 纯文字卡片 | 纯色背景 + emoji | AI 生成或代码生成 |
案例:gpt-image2-ppt 可用素材
/home/ubuntu/github/gpt-image2-ppt-skills/docs/assets/
├── style-gallery.jpg # 10 种风格对比(可截图用)
├── template-demo-input.jpg # 模板输入(核心素材!)
└── template-demo-output.jpg # 模板输出(核心素材!)使用方案:
- 封面:用
template-demo对比图 + 大字"一键复刻公司模板" - 卡片 2-3:标注 input/output 的细节差异
- 卡片 4-5:截取
style-gallery.jpg的局部展示风格
---
四、爆款卡片结构模板
模板 A:提问式封面(11 万赞款)
视觉:纯色背景 + 大 emoji(🤔)
文案:
有没有那种
[核心功能描述]
的工具?
[1-2 个关键数据/特点]适用:新工具推荐、功能发现
模板 B:对比式封面(高转化)
视觉:左右分屏(before/after)
文案:
左:公司模板
右:AI 生成
大字:老板都没看出来适用:有明显效果对比的功能
模板 C:发现式封面(高共鸣)
视觉:纯色 + emoji(💡)
文案:
我发现了个宝藏!
[核心功能]
[3 个痛点解决]
终于不用 [痛点] 了😭适用:解决痛点的工具
---
五、文案公式库
封面钩子(7 种)
1. 提问式:"有没有那种 [功能] 的工具?" 2. 惊叹式:"我不是最后一个知道的吧!!!" 3. 发现式:"我发现了个宝藏!" 4. 身份共鸣:"寻找牛马之光"、"PPT 女工的救赎" 5. 对比式:"用了 XX 前 vs 用了 XX 后" 6. 数字冲击:"30 秒做完 10 页 PPT" 7. 热点切入:"gpt-image-2 爆火后,我用它做了 XX"
正文结构(3 段式)
第 1 段:痛点共鸣
"凌晨 3 点还在改 PPT?"
"老板说风格不对,重做?"
第 2 段:解决方案
"[工具名] 能做到:"
✅ [核心功能 1]
✅ [核心功能 2]
✅ [核心功能 3]
第 3 段:社交证明 + CTA
"我试了一下,[具体效果]"
"开源地址评论区👇"
"试完告诉我你的体验"情绪词库
| 场景 | 正面情绪 | 负面情绪(痛点) |
|---|---|---|
| 发现 | 宝藏、绝了、爱了 | 后悔没早知道 |
| 效果 | 好看哭了、太强了 | 之前白熬夜了 |
| 速度 | 秒出、瞬间搞定 | 等到天荒地老 |
| 质量 | 专业、设计师级 | 土到掉渣 |
---
六、视觉风格指南
真实爆款的视觉特征
背景:
✅ 纯色(米色 #F5F1E8、浅黄 #FFF9E6、浅粉 #FFE5E5)
❌ 渐变、纹理、图案
Emoji:
✅ 超大(占画面 20-30%)
✅ 位置:顶部居中
✅ 常用:🤔💡✨🔥😭🪄
文字:
✅ 手写感(不规则、有倾斜)
✅ 关键词下划线(波浪线)
✅ 大小对比强烈
❌ 完美对齐、网格排版
整体:
✅ 看起来"不专业"(素人感)
✅ 随意、真实、有温度
❌ 精美、高大上、品牌感gpt-image-2 生成 prompt 模板
A Xiaohongshu style card, 9:16 vertical, SIMPLE and CASUAL.
Background: Solid [color] (#XXXXXX), NO gradients, NO effects
Top: ONE LARGE emoji [emoji], centered, 30% of image
Text (hand-written casual style, Chinese):
"[文案内容]"
Key words "[关键词]" with blue wavy underlines
Style:
- SIMPLE, not polished
- Looks like made by a regular person
- Casual, relatable, authentic
- NO fancy graphics, NO tech aesthetics---
七、制作流程 SOP
Step 1:卖点分析(5 分钟)
1. 列出所有功能 2. 按"稀缺性 × 实用性 × 可感知"打分 3. 选出 Top 1 核心卖点
Step 2:素材盘点(5 分钟)
1. 项目截图(功能演示、效果对比) 2. 真实场景照片(电脑屏幕、手机截图) 3. 数据图表(如有) 4. 判断:哪些能直接用?哪些需要 AI 生成?
Step 3:确定封面类型(3 分钟)
- 有对比图 → 模板 B(对比式)
- 解决痛点 → 模板 C(发现式)
- 新工具推荐 → 模板 A(提问式)
Step 4:设计卡片结构(10 分钟)
卡片 1(封面):核心卖点 + 视觉冲击
卡片 2-3:展开核心卖点(真实素材 + 标注)
卡片 4:技术背书 / 对比其他工具
卡片 5:使用场景 / 案例
卡片 6-7:CTA + 互动引导Step 5:生成 / 制作卡片(20 分钟)
- 真实素材:直接用 + 文字标注
- 纯文字卡片:AI 生成(用上面的 prompt 模板)
Step 6:文案优化(10 分钟)
- 检查:是否口语化?
- 检查:是否有情绪词?
- 检查:是否有 emoji?
- 检查:关键词是否突出?
---
八、常见错误 Checklist
发布前自查:
- [ ] 封面是否抓住了最稀缺的卖点?(不是堆砌所有功能)
- [ ] 视觉是否有"素人感"?(不是广告海报)
- [ ] 文案是否口语化?(不是正式文案)
- [ ] 是否用了真实素材?(不是纯 AI 生图)
- [ ] 是否有大 emoji?(不是纯文字)
- [ ] 关键词是否突出?(下划线、加粗、颜色)
- [ ] 是否有情绪共鸣?(痛点 + 解决)
- [ ] CTA 是否清晰?(评论区拿地址)
---
九、案例库(持续更新)
案例 1:gpt-image2-ppt(本次)
初版问题:
- ❌ 广告感太强(渐变背景、科技风)
- ❌ 堆砌功能(10 种风格、速度、质量都讲)
- ❌ 纯 AI 生图(没用项目真实截图)
优化方向:
- ✅ 聚焦"模板克隆"(最稀缺)
- ✅ 用真实对比图(template-demo)
- ✅ 素人感设计(纯色 + emoji)
预期效果:保守 5k-10k 赞
---
十、工具推荐
图片处理
- 截图标注:手机自带标注工具
- 拼图对比:美图秀秀、Canva
- 去水印:
scripts/crop_watermark.py
文案工具
- emoji 库:emojipedia.org
- 情绪词库:本文档第五节
生成工具
- 纯文字卡片:gpt-image-2(用本文档 prompt 模板)
- 真实素材卡片:直接截图 + 标注
---
附录A:术语对照表
| 术语 | 含义 | 示例 |
|---|---|---|
| 素人感 | 看起来像普通人做的,不是设计师作品 | 纯色背景 + 手写字 |
| 口语化 | 日常对话的语气,不是书面语 | "有没有"、"宝藏"、"绝了" |
| 钩子 | 吸引注意力的开场 | "我不是最后一个知道的吧" |
| 痛点共鸣 | 说出用户的困扰 | "凌晨 3 点还在改 PPT" |
| 社交证明 | 别人的使用反馈 | "老板都没看出来" |
| CTA | Call To Action,行动号召 | "评论区拿地址" |
---
附录B:真实案例库
案例1:gpt-image2-ppt-skills(2026-04)
项目类型:开源工具(AI 生成 PPT)
核心卖点识别:
| 功能 | 稀缺性 | 实用性 | 可感知 | 得分 | 优先级 |
|---|---|---|---|---|---|
| 模板克隆 | 5 | 5 | 5 | 125 | 🥇 核心 |
| gpt-image-2 | 4 | 4 | 4 | 64 | 🥈 辅助 |
| 10种风格 | 2 | 3 | 3 | 18 | 🥉 锦上添花 |
迭代过程:
- V1:纯 AI 生图(广告风)❌ → 像品牌宣传
- V2:素人感设计 ✅ → 符合小红书风格
- V3:真实爆款分析 ✅✅ → 发现提问式钩子
- V4:卖点优先级判断 ✅✅✅ → 聚焦模板克隆
- V5:真实素材融入 ✅✅✅✅ → 图生图效果最佳
- V6:细节优化 ✅✅✅✅✅ → 文案精准化
关键学习: 1. 真实素材 > AI 生图:项目的 template-demo 对比图比任何 AI 生成的都有说服力 2. 聚焦核心卖点:模板克隆(稀缺)> 10种风格(常见) 3. 素人感设计:纯色 + emoji + 口语化 > 精美广告 4. 图生图最佳:保留真实感 + AI 设计能力 5. 用户痛点 > 功能堆砌:"复刻任意模板" > "10种风格、30秒"
最终成果:
- 7张卡片(封面、对比图、风格展示、谁需要、真实案例、CTA、GitHub)
- 配套文案(2个版本 + 8个 hashtags + 5个标题选项)
- 预期效果:保守 5k-10k 赞,乐观 20k-50k 赞
可复用流程:
1. 卖点分析(稀缺性 × 实用性 × 可感知)
2. 爆款研究(tikhub 搜索 + 图片分析)
3. 素材盘点(项目截图 > AI 生图)
4. 图生图生成(真实素材 + 小红书风格)
5. 迭代优化(用户反馈 → 改进)
6. 配套文案(标题 + Caption + Hashtags)时间成本:约 2-3 小时(含 6 次迭代) API 成本:约 $2-5(OpenAI gpt-image-2)
---
附录C:图生图 Prompt 模板
模板1:纯文字卡片(素人感)
A Xiaohongshu style cover, 9:16 vertical, SIMPLE and CASUAL.
Background: Solid light [color] (#XXXXXX), NO gradients, NO effects
Top: ONE LARGE emoji [emoji], centered, 30% of image
Text (hand-written casual style, Chinese):
"[文案内容]"
Key words "[关键词]" with blue wavy underlines
Style:
- SIMPLE, not polished
- Looks like made by a regular person
- Casual, relatable, authentic
- NO fancy graphics, NO tech aesthetics模板2:真实素材 + 文字叠加
Based on the reference image ([描述参考图]), create a Xiaohongshu style card, 9:16 vertical.
Keep the original image visible as the main content.
Add overlays:
- Top: [顶部文字]
- Bottom: [底部文字]
Style:
- Keep original image clear and readable
- Casual Xiaohongshu style text
- Semi-transparent overlays for readability
- Authentic, not too polished模板3:对比卡片
Based on the reference image (two items for comparison), create a Xiaohongshu comparison card, 9:16 vertical.
Keep both items visible.
Add labels:
- Top item: Red label "[标签1]"
- Bottom item: Green label "[标签2]"
Middle area:
- Arrow pointing down: ↓
- Text: "[对比说明]"
Style:
- Clear comparison layout
- Casual annotations
- Xiaohongshu authentic feel---
附录D:常见错误清单
错误1:广告感太强
表现:
- 渐变背景、科技感、精美排版
- 品牌 logo、专业设计元素
- 正式文案("革命性"、"颠覆性")
原因:想做得"专业"、"高大上"
解决:
- ✅ 纯色背景(米色、浅黄、浅粉)
- ✅ 大 emoji(占画面 20-30%)
- ✅ 手写感文字(不规则、有倾斜)
- ✅ 口语化("有没有"、"宝藏"、"绝了")
---
错误2:堆砌功能
表现:
- "10种风格、30秒、高质量、开源免费"
- 把所有功能都列出来
- 没有重点
原因:想展示所有优势
解决:
- ✅ 只讲最稀缺的 1-2 个功能
- ✅ 用卖点优先级公式:稀缺性 × 实用性 × 可感知
- ✅ 其他功能作为辅助,不是主角
案例:
- ❌ "10种风格随便挑,30秒就能生成"
- ✅ "复刻任意模板,一键排版生成"
---
错误3:纯 AI 生图
表现:
- 所有卡片都是 AI 生成
- 没有项目真实截图
- 缺少真实感
原因:觉得 AI 生图更快、更统一
解决:
- ✅ 优先使用项目真实素材(截图、对比图)
- ✅ 用图生图(保留真实感 + AI 设计)
- ✅ 纯 AI 生图只用于纯文字卡片
效果对比:
| 方式 | 真实感 | 说服力 | 制作难度 |
|---|---|---|---|
| 纯 AI 生图 | ⭐⭐ | ⭐⭐ | ⭐ |
| 代码叠加文字 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 图生图 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
---
错误4:功能导向 > 痛点导向
表现:
- 说"有什么功能",不说"能解决什么问题"
- 技术细节太多
- 用户看不懂价值
原因:开发者视角,不是用户视角
解决:
- ✅ 先说痛点:"凌晨3点还在改PPT?"
- ✅ 再说解决方案:"能自动复刻公司模板"
- ✅ 最后说结果:"再也不用熬夜了"
公式:痛点 + 解决方案 + 结果
---
错误5:第一版就想完美
表现:
- 花很多时间打磨第一版
- 不愿意迭代
- 害怕用户反馈
原因:完美主义
解决:
- ✅ 快速出第一版(60分即可)
- ✅ 收集用户反馈
- ✅ 快速迭代(每次改进 1-2 个关键点)
- ✅ 6次迭代 > 1次完美
数据:
- gpt-image2-ppt 案例:V1 → V6,质量提升 10 倍
- 时间分配:V1 占 20%,迭代占 80%
---
附录E:工具使用指南
1. tikhub(小红书数据抓取)
安装:
# 已集成在 social-account-doctor skill用途:
- 搜索关键词爆款笔记
- 下载真实图片分析
- 获取互动数据(点赞、收藏、评论)
使用技巧: 1. ✅ 先分析真实爆款,再动手做 2. ✅ 看图片比看文字更重要 3. ✅ 下载 Top 3-5 条爆款的图片 4. ✅ 分析共同特征(背景、emoji、文案风格)
案例:
# 搜索"AI做PPT"相关爆款
xhs-search "AI做PPT" --sort likes --limit 10
# 下载图片
# 图片会自动保存到本地---
2. gpt-image-2(图生图)
用途:
- 纯文字卡片生成
- 真实素材 + 文字叠加(图生图)
Prompt 技巧: 1. 明确风格:"Xiaohongshu style", "SIMPLE and CASUAL" 2. 强调真实感:"Looks like made by a regular person" 3. 禁止元素:"NO fancy graphics", "NO tech aesthetics" 4. 保留原图:"Keep the original image visible"(图生图时)
失败处理:
- API 返回 550 错误 → 降低图片分辨率重试
- 图生图失败 → 改用代码叠加文字
- 生成比例不对 → 重新生成(自动重试 2 次)
成本:
- 纯文字卡片:约 $0.3-0.5 / 张
- 图生图:约 $0.5-0.8 / 张
- 7张卡片总成本:约 $2-5
---
3. PIL(Python 图片处理)
用途:
- 组合多张图片(对比图)
- 添加文字标注
- 调整尺寸和比例
优缺点:
- ✅ 快速原型
- ✅ 完全可控
- ❌ 字体和设计感不如 AI
- ❌ 需要手动调整位置
使用场景:
- 快速验证想法
- 作为图生图的参考图
- 图生图失败时的降级方案
---
附录F:标题生成公式
公式1:痛点+解决方案
{具体痛点}?{解决方案}案例:
- "凌晨3点还在改PPT?这个工具能一键复刻公司模板"
- "老板的模板太复杂?AI帮你自动复刻风格"
适用:有明确痛点的工具类产品
---
公式2:提问式
有没有那种{功能描述}的{产品类型}?案例:
- "有没有那种能复刻公司PPT模板的AI工具?"
- "有没有那种一键生成小红书图文的工具?"
适用:新工具推荐、功能发现
---
公式3:发现式
我发现了个宝藏!{核心价值}案例:
- "我发现了个宝藏!AI能完全复刻公司PPT模板"
- "我发现了个宝藏!30秒生成小红书爆款图文"
适用:兴奋分享、好物推荐
---
公式4:热点词
{热点词}爆火后,我用它做了{场景}案例:
- "gpt-image-2爆火后,我用它做了个PPT神器"
- "Sora爆火后,我用它做了个视频工具"
适用:蹭热点、技术类产品
---
公式5:身份共鸣
{身份标签}必备!{核心功能}案例:
- "打工人必备!一键复刻老板的PPT模板"
- "设计师必备!AI自动生成配色方案"
适用:有明确目标人群的产品
#!/usr/bin/env python3
"""Scan user-provided materials and emit a materials.json manifest.
Multimodal analysis itself (caption, transcribe) is the AGENT's job using its
own vision/audio tools -- this script only does deterministic pre-work:
- classify each path by extension (text / image / video / audio / unknown)
- read basic metadata (size; for images: dimensions; for videos: duration
+ frame extraction via ffmpeg if available)
- emit skeleton manifest that the agent fills in with captions/usages
Usage:
python3 analyze_material.py PATH [PATH ...] --out reference/materials.json \
[--frames-dir reference/frames] [--frames-per-video 6]
"""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
import sys
from pathlib import Path
TEXT_EXT = {".txt", ".md", ".rtf", ".html", ".htm", ".json", ".csv"}
IMAGE_EXT = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".heic", ".heif"}
VIDEO_EXT = {".mp4", ".mov", ".m4v", ".avi", ".mkv", ".webm"}
AUDIO_EXT = {".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg"}
def classify(p: Path) -> str:
ext = p.suffix.lower()
if ext in TEXT_EXT:
return "text"
if ext in IMAGE_EXT:
return "image"
if ext in VIDEO_EXT:
return "video"
if ext in AUDIO_EXT:
return "audio"
return "unknown"
def image_info(p: Path) -> dict:
try:
from PIL import Image
except ImportError:
return {}
try:
with Image.open(p) as im:
return {"width": im.width, "height": im.height,
"aspect": round(im.width / im.height, 3)}
except Exception as e:
return {"error": str(e)}
def video_info(p: Path) -> dict:
if not shutil.which("ffprobe"):
return {"note": "ffprobe not installed; duration unknown"}
try:
out = subprocess.check_output(
["ffprobe", "-v", "error", "-show_entries",
"format=duration:stream=width,height,codec_type",
"-of", "json", str(p)],
stderr=subprocess.STDOUT).decode()
data = json.loads(out)
dur = float(data.get("format", {}).get("duration", 0))
v = next((s for s in data.get("streams", [])
if s.get("codec_type") == "video"), {})
return {"duration_sec": round(dur, 2),
"width": v.get("width"), "height": v.get("height")}
except Exception as e:
return {"error": str(e)}
def extract_frames(p: Path, out_dir: Path, n: int) -> list[str]:
if not shutil.which("ffmpeg"):
return []
out_dir.mkdir(parents=True, exist_ok=True)
info = video_info(p)
dur = info.get("duration_sec", 0) or 0
if dur <= 0:
return []
frames = []
stem = p.stem.replace(" ", "_")
for i in range(n):
t = dur * (i + 0.5) / n
out = out_dir / f"{stem}_f{i+1:02d}.jpg"
try:
subprocess.run(
["ffmpeg", "-y", "-v", "error", "-ss", f"{t:.2f}",
"-i", str(p), "-frames:v", "1", "-q:v", "3", str(out)],
check=True)
frames.append(str(out))
except Exception:
pass
return frames
def text_preview(p: Path, n: int = 400) -> str:
try:
return p.read_text(encoding="utf-8", errors="replace")[:n]
except Exception as e:
return f"<read error: {e}>"
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("paths", nargs="+")
ap.add_argument("--out", required=True)
ap.add_argument("--frames-dir", default=None)
ap.add_argument("--frames-per-video", type=int, default=6)
args = ap.parse_args()
items = []
for raw in args.paths:
p = Path(raw).expanduser()
if not p.exists():
items.append({"path": raw, "error": "not found"})
continue
kind = classify(p)
entry: dict = {
"id": f"m{len(items)+1:03d}",
"path": str(p),
"kind": kind,
"size_bytes": p.stat().st_size,
"caption": "", # agent fills via vision model
"usage": "", # cover / content-N / ending / reference
"strategy": "", # use_original | text_on_photo | collage | ai_generated
}
if kind == "image":
entry.update(image_info(p))
elif kind == "video":
entry.update(video_info(p))
if args.frames_dir:
entry["frames"] = extract_frames(
p, Path(args.frames_dir), args.frames_per_video)
elif kind == "text":
entry["preview"] = text_preview(p)
items.append(entry)
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps({"materials": items}, ensure_ascii=False,
indent=2), encoding="utf-8")
print(f"OK: {out} ({len(items)} item(s))")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""Compose 2-4 user-provided images into a single 3:4 card (900x1200 by default).
Layouts:
- 2v : two images stacked vertically (top/bottom)
- 2h : two images side by side (left/right) -- rare for 3:4, use for narrow pairs
- 3 : one big on top + two small on bottom (2:1 + 1:1 + 1:1)
- 4 : 2x2 grid
Usage:
python3 collage_3x4.py OUTPUT --layout 2v --inputs a.jpg b.jpg [--gap 12] \
[--bg "#FFFFFF"] [--size 900x1200]
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
try:
from PIL import Image
except ImportError:
print("ERROR: Pillow required", file=sys.stderr)
sys.exit(2)
def parse_size(s: str) -> tuple[int, int]:
w, h = s.lower().split("x")
return int(w), int(h)
def parse_color(s: str) -> tuple[int, int, int]:
s = s.lstrip("#")
return int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)
def fit_cover(img: Image.Image, w: int, h: int) -> Image.Image:
src_w, src_h = img.size
tgt = w / h
cur = src_w / src_h
if cur > tgt:
new_w = int(src_h * tgt)
off = (src_w - new_w) // 2
img = img.crop((off, 0, off + new_w, src_h))
else:
new_h = int(src_w / tgt)
off = (src_h - new_h) // 2
img = img.crop((0, off, src_w, off + new_h))
return img.resize((w, h), Image.LANCZOS)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("output")
ap.add_argument("--layout", required=True,
choices=("2v", "2h", "3", "4"))
ap.add_argument("--inputs", nargs="+", required=True)
ap.add_argument("--gap", type=int, default=12)
ap.add_argument("--bg", default="#FFFFFF")
ap.add_argument("--size", default="900x1200")
args = ap.parse_args()
W, H = parse_size(args.size)
canvas = Image.new("RGB", (W, H), parse_color(args.bg))
gap = args.gap
imgs = [Image.open(p).convert("RGB") for p in args.inputs]
need = {"2v": 2, "2h": 2, "3": 3, "4": 4}[args.layout]
if len(imgs) != need:
print(f"ERROR: layout {args.layout} needs {need} inputs, got {len(imgs)}",
file=sys.stderr)
return 1
if args.layout == "2v":
h_each = (H - gap) // 2
canvas.paste(fit_cover(imgs[0], W, h_each), (0, 0))
canvas.paste(fit_cover(imgs[1], W, h_each), (0, h_each + gap))
elif args.layout == "2h":
w_each = (W - gap) // 2
canvas.paste(fit_cover(imgs[0], w_each, H), (0, 0))
canvas.paste(fit_cover(imgs[1], w_each, H), (w_each + gap, 0))
elif args.layout == "3":
top_h = int(H * 2 / 3) - gap // 2
bot_h = H - top_h - gap
w_each = (W - gap) // 2
canvas.paste(fit_cover(imgs[0], W, top_h), (0, 0))
canvas.paste(fit_cover(imgs[1], w_each, bot_h), (0, top_h + gap))
canvas.paste(fit_cover(imgs[2], w_each, bot_h),
(w_each + gap, top_h + gap))
else: # 4
w_each = (W - gap) // 2
h_each = (H - gap) // 2
positions = [(0, 0), (w_each + gap, 0),
(0, h_each + gap), (w_each + gap, h_each + gap)]
for im, pos in zip(imgs, positions):
canvas.paste(fit_cover(im, w_each, h_each), pos)
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
canvas.save(args.output, quality=92)
print(f"OK: {args.output} ({W}x{H}, layout={args.layout})")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""Crop edge watermark off an image (deterministic wrapper over PIL).
Usage:
python3 crop_watermark.py INPUT OUTPUT --edge bottom --pixels 40
python3 crop_watermark.py INPUT OUTPUT --box LEFT TOP RIGHT BOTTOM
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
try:
from PIL import Image
except ImportError:
print("ERROR: Pillow required", file=sys.stderr)
sys.exit(2)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("input")
ap.add_argument("output")
ap.add_argument("--edge", choices=("top", "bottom", "left", "right"))
ap.add_argument("--pixels", type=int, default=40)
ap.add_argument("--box", nargs=4, type=int,
metavar=("L", "T", "R", "B"),
help="absolute crop box (overrides --edge)")
args = ap.parse_args()
img = Image.open(args.input)
w, h = img.size
if args.box:
box = tuple(args.box)
elif args.edge == "bottom":
box = (0, 0, w, max(0, h - args.pixels))
elif args.edge == "top":
box = (0, min(h, args.pixels), w, h)
elif args.edge == "left":
box = (min(w, args.pixels), 0, w, h)
elif args.edge == "right":
box = (0, 0, max(0, w - args.pixels), h)
else:
print("ERROR: provide --edge or --box", file=sys.stderr)
return 2
out = img.crop(box)
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
out.save(args.output, quality=92)
print(f"OK: {args.output} ({w}x{h} -> {out.size[0]}x{out.size[1]})")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
gpt-image-2 图片生成器
调用 OpenAI 兼容的 /v1/chat/completions 端点,model 设为 gpt-image-2
(聚灵等中转站把图片模型挂在 chat completions 上,多模态返回)。
如果 base_url 是 OpenAI 官方或支持 Images API 的中转站,
也可以走 /v1/images/generations ---- 由 GPT_IMAGE_ENDPOINT 切换。
"""
from __future__ import annotations
import base64
import os
import re
from pathlib import Path
from typing import Any, Dict, Optional
import requests
# .env 加载由顶层入口(generate_ppt.py 的 find_and_load_env)统一负责。
# 本模块不调 load_dotenv(),避免 import 时无意识加载父目录 / cwd 的 .env,
# 与 SKILL.md 声明的"scoped .env loading"保持一致。
# 16:9 横版用 1536x1024,竖版 9:16 用 1024x1536,方图用 1024x1024
ASPECT_TO_SIZE = {
"16:9": "1536x1024",
"9:16": "1024x1536",
"1:1": "1024x1024",
}
REQUEST_TIMEOUT_SECS = 600 # 图片生成可能需要 1-3 分钟
MAX_RETRIES = 3 # 524/超时/连接断开等瞬态错误的重试次数
RETRY_DELAY_SECS = 5
MAX_ASPECT_RETRIES = 2 # 比例不合格自动重生次数
ASPECT_TOLERANCE = 0.15 # 比例偏差容忍度(±15%)
# 期望的宽高比(width / height)
ASPECT_RATIO_VALUES = {
"16:9": 16 / 9,
"9:16": 9 / 16,
"1:1": 1.0,
}
def read_png_dimensions(path: str) -> tuple:
"""从 PNG header 读宽高,不依赖 PIL。失败返回 (0, 0)。"""
import struct
try:
with open(path, "rb") as f:
head = f.read(24)
if head[:8] != b"\x89PNG\r\n\x1a\n":
return 0, 0
# IHDR chunk: 16 字节偏移开始 width(4) + height(4) big-endian
width, height = struct.unpack(">II", head[16:24])
return width, height
except Exception:
return 0, 0
def aspect_acceptable(width: int, height: int, target: str, tolerance: float = ASPECT_TOLERANCE) -> bool:
"""检查实际宽高比是否在目标比例的容差范围内。"""
if not (width and height):
return True # 读不到就放过,不阻塞流程
expected = ASPECT_RATIO_VALUES.get(target)
if expected is None:
return True
actual = width / height
deviation = abs(actual - expected) / expected
return deviation <= tolerance
class GptImage2Generator:
"""gpt-image-2 图片生成器"""
def __init__(self, aspect_ratio: str = "16:9") -> None:
self.base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com").rstrip("/")
self.api_key = os.getenv("OPENAI_API_KEY", "")
self.model_name = os.getenv("GPT_IMAGE_MODEL_NAME", "gpt-image-2")
self.quality = os.getenv("GPT_IMAGE_QUALITY", "high")
# endpoint: chat | images | auto(auto 先 images 不行再 chat)
self.endpoint = os.getenv("GPT_IMAGE_ENDPOINT", "chat").lower()
if not self.api_key:
raise ValueError("缺少 OPENAI_API_KEY,请在 .env 中配置")
self.aspect_ratio = aspect_ratio
self.default_size = ASPECT_TO_SIZE.get(aspect_ratio, "1536x1024")
print(
f"🎨 初始化 gpt-image-2 生成器 "
f"(model={self.model_name}, size={self.default_size}, "
f"quality={self.quality}, endpoint={self.endpoint})"
)
# ---------- 通用工具 ----------
def _save_b64(self, b64: str, output_path: str) -> None:
if "," in b64 and b64.startswith("data:"):
b64 = b64.split(",", 1)[1]
with open(output_path, "wb") as f:
f.write(base64.b64decode(b64))
def _download_url(self, url: str, output_path: str) -> None:
# 安全提示:只接受 http/https;下载前打印 host,便于用户识别异常域;
# 设最大 50MB 上限避免恶意大文件;非 image/* Content-Type 警告但仍写盘。
from urllib.parse import urlparse
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
raise ValueError(f"拒绝下载非 http(s) 协议的 URL: {parsed.scheme}")
print(f"📥 下载图片 host={parsed.netloc} path={parsed.path[:80]}")
resp = requests.get(url, stream=True, timeout=REQUEST_TIMEOUT_SECS)
resp.raise_for_status()
ctype = resp.headers.get("content-type", "")
if ctype and not ctype.startswith("image/"):
print(f"(!) 非 image Content-Type: {ctype}(仍尝试写盘,请人工核对)")
MAX_BYTES = 50 * 1024 * 1024
written = 0
with open(output_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
written += len(chunk)
if written > MAX_BYTES:
f.close()
os.remove(output_path)
raise ValueError(f"下载超过 {MAX_BYTES} 字节上限,已丢弃 {url[:80]}")
f.write(chunk)
def _save_payload(self, payload: str, output_path: str) -> None:
"""根据 payload 形态(b64 / data url / 普通 url)落盘。"""
if payload.startswith("data:image/") or (len(payload) > 200 and "/" in payload[:200] is False):
# data:image/...;base64,xxxxx 或裸 base64
self._save_b64(payload, output_path)
elif payload.startswith("http"):
self._download_url(payload, output_path)
else:
# 兜底当 base64
self._save_b64(payload, output_path)
# ---------- 多模态响应解析(chat completions 走这里)----------
def _extract_image(self, content: Any) -> str:
"""从 chat completions 的 content 中提取图片 payload(b64 或 URL)。"""
if isinstance(content, list):
for part in content:
if not isinstance(part, dict):
continue
if part.get("type") == "image_url":
img_url = part.get("image_url", {}).get("url", "")
if img_url:
return img_url
if part.get("type") == "text":
text = part.get("text", "")
found = self._extract_from_text(text)
if found:
return found
raise RuntimeError(f"multimodal parts 里没找到图片:{str(content)[:300]}")
if isinstance(content, str):
found = self._extract_from_text(content)
if found:
return found
raise RuntimeError(f"text content 里没找到图片:{content[:300]}")
raise RuntimeError(f"未知 content 类型:{type(content)}")
def _extract_from_text(self, text: str) -> Optional[str]:
# 1) data:image/xxx;base64,YYY
m = re.search(r"data:image/[\w]+;base64,[A-Za-z0-9+/=]+", text)
if m:
return m.group(0)
# 2) markdown 图片 
m = re.search(r"!\[.*?\]\((https?://[^\)\s]+)\)", text)
if m:
return m.group(1)
# 3) 裸图片 URL
m = re.search(r"(https?://[^\s\)]+\.(?:png|jpg|jpeg|webp|gif))", text, re.IGNORECASE)
if m:
return m.group(1)
# 4) 任意 http 链接(最后兜底)
m = re.search(r"(https?://[^\s\)]+)", text)
if m:
return m.group(1)
return None
# ---------- 端点 1: /v1/chat/completions ----------
def _request_via_chat(self, prompt: str, size: str, reference_image_path: Optional[str] = None) -> str:
"""流式请求 chat completions,从增量文本中拼接出图片 URL / b64。
中转站(如聚灵)把图片模型挂在 chat completions 上时,通常要求 stream=True。
响应里会先推送进度文本("> 进度:25%"),最后吐 markdown 图片
("")或 base64。
reference_image_path 不为空时,把该图片作为多模态 input 一并塞进 messages,
让 gpt-image-2 按它的视觉风格出新图(高保真 / 模板克隆模式)。
"""
url = f"{self.base_url}/v1/chat/completions"
# 用比例描述而不是具体像素 ---- gpt-image 类模型更听自然语言 "宽屏 16:9",
# 写具体像素值反而被忽略。
if self.aspect_ratio == "9:16":
aspect_hint = (
"\n\n【画面比例 -- 强制】严格按 9:16 竖版手机屏幕生成 "
"(portrait, vertical 9:16, height much taller than width). "
"绝对不要方图。"
)
elif self.aspect_ratio == "1:1":
aspect_hint = "\n\n【画面比例 -- 强制】1:1 方图。"
else:
aspect_hint = (
"\n\n【画面比例 -- 强制要求】生成图片必须是 16:9 横版宽屏 "
"(landscape orientation, widescreen 16:9 aspect ratio, "
"ultrawide horizontal banner format). "
"宽度必须明显大于高度,宽高比约 1.78:1。"
"绝对不要生成方图(square)、近方图(near-square)或竖图(portrait)。"
"Output MUST be 16:9 landscape widescreen, NEVER square or portrait."
)
full_prompt = f"{prompt}{aspect_hint}"
if reference_image_path and os.path.exists(reference_image_path):
with open(reference_image_path, "rb") as f:
ref_b64 = base64.b64encode(f.read()).decode("ascii")
ref_data_url = f"data:image/png;base64,{ref_b64}"
user_content: Any = [
{"type": "image_url", "image_url": {"url": ref_data_url}},
{"type": "text", "text": (
"请以上面这张图作为视觉风格参考(配色 / 字体 / 装饰元素 / 布局氛围),"
"按下方新内容生成一张全新的 PPT 页,不要复制原图的文字内容。\n\n"
+ full_prompt
)},
]
else:
user_content = full_prompt
payload = {
"model": self.model_name,
"messages": [
{"role": "user", "content": user_content}
],
"stream": True,
"temperature": 0.7,
}
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"Accept": "text/event-stream",
}
print(f"🔗 POST {url} size={size} stream=True")
resp = requests.post(
url, headers=headers, json=payload,
stream=True, timeout=REQUEST_TIMEOUT_SECS,
)
print(f"📥 status={resp.status_code}")
if resp.status_code != 200:
raise RuntimeError(
f"chat 调用失败 (status={resp.status_code}): {resp.text[:500]}"
)
full_text = []
for line in resp.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
data_str = line[5:].strip()
if data_str == "[DONE]":
break
try:
import json as _json
chunk = _json.loads(data_str)
except Exception:
continue
choices = chunk.get("choices") or []
if not choices:
continue
delta = choices[0].get("delta") or {}
content = delta.get("content")
if content:
full_text.append(content)
# 实时打印进度(截短)
snippet = content.replace("\n", " ").strip()
if snippet:
print(f" ↳ {snippet[:80]}")
merged = "".join(full_text)
found = self._extract_from_text(merged)
if not found:
raise RuntimeError(
f"流式响应里没找到图片 URL/base64。完整文本:{merged[:500]}"
)
return found
# ---------- 端点 2: /v1/images/generations ----------
def _request_via_images(self, prompt: str, size: str) -> str:
url = f"{self.base_url}/v1/images/generations"
payload = {
"model": self.model_name,
"prompt": prompt,
"n": 1,
"size": size,
"quality": self.quality,
"response_format": "b64_json",
}
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
print(f"🔗 POST {url} size={size} quality={self.quality}")
resp = requests.post(url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT_SECS)
print(f"📥 status={resp.status_code}")
if resp.status_code != 200:
raise RuntimeError(
f"images 调用失败 (status={resp.status_code}): {resp.text[:500]}"
)
result = resp.json()
data = result.get("data") or []
if not data:
raise RuntimeError(f"响应没有 data: {str(result)[:300]}")
first = data[0]
b64 = first.get("b64_json")
url_field = first.get("url")
if b64:
return f"data:image/png;base64,{b64}"
if url_field:
return url_field
raise RuntimeError(f"data[0] 既没 b64_json 也没 url: {str(first)[:300]}")
# ---------- 对外入口 ----------
def generate_scene_image(
self,
scene_data: Dict[str, Any],
output_path: str,
size: str = "auto",
reference_image_path: Optional[str] = None,
) -> str:
scene_index = scene_data.get("index", 0)
prompt = scene_data.get("image_prompt", "")
if not prompt:
raise ValueError(f"场景 {scene_index} 缺少 image_prompt")
out_dir = os.path.dirname(output_path)
if out_dir:
os.makedirs(out_dir, exist_ok=True)
target_size = self.default_size if size == "auto" else size
print(f"📝 prompt[:100]: {prompt[:100].replace(chr(10), ' ')}{'...' if len(prompt) > 100 else ''}")
import time as _time
# 外层:比例不合格重生(最多 MAX_ASPECT_RETRIES 次)
for ratio_attempt in range(MAX_ASPECT_RETRIES + 1):
last_err = None
# 内层:瞬态错误重试(524 / 超时等)
for attempt in range(1, MAX_RETRIES + 1):
try:
if self.endpoint == "images":
# images endpoint 暂不支持 reference image,自动 fallback 到 chat
if reference_image_path:
payload = self._request_via_chat(prompt, target_size, reference_image_path)
else:
payload = self._request_via_images(prompt, target_size)
elif self.endpoint == "chat":
payload = self._request_via_chat(prompt, target_size, reference_image_path)
else: # auto
try:
if reference_image_path:
payload = self._request_via_chat(prompt, target_size, reference_image_path)
else:
payload = self._request_via_images(prompt, target_size)
except Exception as e:
print(f"(!) images 失败,回退到 chat: {str(e)[:120]}")
payload = self._request_via_chat(prompt, target_size, reference_image_path)
self._save_payload(payload, output_path)
break # 成功落盘,跳出瞬态重试循环
except Exception as e:
last_err = e
msg = str(e)[:200]
transient = any(s in msg for s in ("524", "502", "503", "504", "timeout", "Read timed out",
"Connection aborted", "RemoteDisconnected"))
if attempt < MAX_RETRIES and transient:
print(f"(!) [scene {scene_index}] 第 {attempt} 次失败({msg}),{RETRY_DELAY_SECS}s 后重试")
_time.sleep(RETRY_DELAY_SECS)
continue
raise
else:
raise RuntimeError(f"重试 {MAX_RETRIES} 次仍失败: {last_err}")
# 比例校验
w, h = read_png_dimensions(output_path)
if aspect_acceptable(w, h, self.aspect_ratio):
print(f"[OK] 已保存: {output_path} ({w}x{h}, 比例 {w/h:.3f}, 目标 {self.aspect_ratio})")
return output_path
# 比例偏离
actual_ratio = w / h if h else 0
expected = ASPECT_RATIO_VALUES.get(self.aspect_ratio, 16/9)
dev_pct = abs(actual_ratio - expected) / expected * 100 if expected else 0
if ratio_attempt < MAX_ASPECT_RETRIES:
print(f"📐 [scene {scene_index}] 尺寸 {w}x{h} (比例 {actual_ratio:.3f}) "
f"偏离目标 {self.aspect_ratio} {dev_pct:.0f}%,重生 ({ratio_attempt+1}/{MAX_ASPECT_RETRIES})")
try:
os.remove(output_path)
except OSError:
pass
continue
else:
print(f"(!) [scene {scene_index}] 尺寸 {w}x{h} 仍偏离 {dev_pct:.0f}%,已达比例重试上限,保留")
return output_path
return output_path
if __name__ == "__main__":
import sys
gen = GptImage2Generator(aspect_ratio="16:9")
gen.generate_scene_image(
{"index": 0, "image_prompt": "A clean blue gradient background with the bold white text 'gpt-image-2 Test'"},
"test_output.png",
)
print(f"自检完成: {Path('test_output.png').resolve()}")
sys.exit(0)