Compare commits

...

45 Commits

Author SHA1 Message Date
Jim Liu 宝玉 9977ff520c chore: release v1.107.0 2026-04-15 21:34:22 -05:00
Jim Liu 宝玉 03bd68d3a4 docs: add Cocoon-AI credit for diagram skill design 2026-04-15 21:33:59 -05:00
Jim Liu 宝玉 39792f4360 feat(baoyu-diagram): add SVG-to-PNG @2x conversion script and consolidate references 2026-04-15 21:33:56 -05:00
Kayue Yeung 25b8a7f73d fix(claude-plugin): allow inline marketplace manifest (#130)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 19:38:38 -05:00
Jim Liu 宝玉 e13092ba2a chore: release v1.106.0 2026-04-14 11:33:44 -05:00
Jim Liu 宝玉 acbcf19ba2 feat(baoyu-diagram): add architecture enrichment and structural layout patterns 2026-04-14 11:32:55 -05:00
Jim Liu 宝玉 dcd0f81433 chore: release v1.105.0 2026-04-13 19:51:57 -05:00
Jim Liu 宝玉 c938396efc feat(baoyu-diagram): unify to analyze→confirm→generate workflow
Remove single/multi mode split. Skill now analyzes any input material,
recommends diagram types and splitting strategy, confirms once, then generates.
2026-04-13 19:42:29 -05:00
Jim Liu 宝玉 1ba68c9a9c chore: release v1.104.0 2026-04-13 14:24:04 -05:00
Jim Liu 宝玉 b3f5c0a8aa fix(baoyu-post-to-wechat): verify editor focus before paste operations 2026-04-13 14:23:41 -05:00
Jim Liu 宝玉 9c45ede0c7 feat(baoyu-diagram): add Mermaid sketch step before SVG generation 2026-04-13 14:23:39 -05:00
Jim Liu 宝玉 30d2ac98ce chore: release v1.103.1 2026-04-13 11:13:06 -05:00
Jim Liu 宝玉 bfdd64bd4e chore: sync baoyu-md vendor to baoyu-markdown-to-html and baoyu-post-to-weibo 2026-04-12 20:18:05 -05:00
Jim Liu 宝玉 434d4857da chore: release v1.103.0 2026-04-12 20:17:25 -05:00
Jim Liu 宝玉 990fea4f7b fix(baoyu-post-to-wechat): decode HTML entities and strip tags from article summary
Add cleanSummaryText() to baoyu-md package: decodes HTML entities (&amp;, &lt;, &#x..., etc.)
and strips HTML tags before using frontmatter description/summary as WeChat article digest
2026-04-12 20:16:53 -05:00
Jim Liu 宝玉 517ff566a1 fix(baoyu-image-cards): prevent color names from appearing as visible text in images 2026-04-12 20:16:48 -05:00
Jim Liu 宝玉 4c9af7d92f fix(baoyu-cover-image): prevent color names and hex codes from appearing as visible text in images
Add semantic constraint to all palette references and prompt template:
color values are rendering guidance only and must not be displayed as text labels
2026-04-12 20:16:44 -05:00
Jim Liu 宝玉 46c4859d48 fix(baoyu-article-illustrator): prevent color names and hex codes from appearing as visible text in images
Add semantic constraint to palette references and prompt-construction rules:
color values are rendering guidance only and must not be displayed as text labels
2026-04-12 20:16:40 -05:00
Jim Liu 宝玉 f3f886217b feat(baoyu-diagram): add multi-diagram mode for article-wide diagram generation
- New multi-diagram mode: analyze article content and generate diagrams at identified positions
- New options: --density (minimal/balanced/per-section/rich), --mode (single/multi/auto)
- Auto mode detection: file path or multi-paragraph content → multi, short topic → single
- Output structure: diagram/{article-slug}/NN-{type}-{slug}/diagram.svg + outline.md
- Step 7: insert diagram image links into article at identified positions
- Modification guide: regenerate/add/remove individual diagrams
- Version bumped to 1.1.0 in SKILL.md
2026-04-12 20:16:36 -05:00
Jim Liu 宝玉 4d465d55d0 chore: release v1.102.0 2026-04-12 02:15:47 -05:00
Jim Liu 宝玉 11d80eeaa9 feat(baoyu-imagine): add OpenAI-compatible image API dialect support
Add --imageApiDialect flag, OPENAI_IMAGE_API_DIALECT env var, and
default_image_api_dialect config for gateways that expect aspect-ratio
size plus metadata.resolution instead of pixel size.
2026-04-12 02:14:18 -05:00
Jim Liu 宝玉 58ba4579ef chore: release v1.101.0 2026-04-12 01:20:43 -05:00
Jim Liu 宝玉 67a45a57a0 Improve baoyu-imagine Replicate compatibility (#125)
* Align Replicate image behavior with the models we actually support

Replicate image generation in baoyu-imagine no longer assumes that every model
accepts the nano-banana request schema. The Replicate provider now defaults to
google/nano-banana-2, routes supported model families through family-specific
builders and validators, blocks misleading multi-output requests before they
reach the API, and updates user-facing docs/config guidance to match the actual
contract.

Constraint: Replicate model families expose different input schemas
Constraint: Current Replicate path only saves one output image per request
Constraint: Must not change non-Replicate providers
Rejected: Keep one nano-banana-style payload for all Replicate models | triggers remote schema errors on Seedream and Wan
Rejected: Continue accepting multi-image Replicate requests and save only the first result | silently drops outputs
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Add a family-specific validator and input builder before exposing more Replicate model IDs or multi-output flags
Tested: npm test
Tested: node --test skills/baoyu-imagine/scripts/providers/replicate.test.ts skills/baoyu-imagine/scripts/main.test.ts
Not-tested: Live Replicate API calls against production models
Co-authored-by: justnode <justnode@users.noreply.github.com>

* Preserve Replicate compatibility when shared defaults leak across providers

Addressed the new PR review findings by teaching baoyu-imagine to track
where aspect-ratio defaults came from, mirroring the earlier imageSize fix,
so unsupported Replicate models can still run prompt-only requests when the
value was inherited from shared config. Also corrected Seedream 4.5 custom
size encoding to use the API's custom width/height schema instead of sending
literal WxH strings.

Constraint: Shared EXTEND defaults still need to apply globally for providers that support them
Constraint: Seedream 4.5 custom sizes must follow Replicate's documented custom size schema
Rejected: Ignore all aspect ratios for unknown Replicate models | would hide explicit unsupported CLI/task input
Rejected: Keep Seedream custom sizes as literal strings | validated locally but fails against the provider API
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Any future inherited-default validation for provider-specific flags should record the source explicitly before rejecting it
Tested: node --import tsx --test skills/baoyu-imagine/scripts/main.test.ts skills/baoyu-imagine/scripts/providers/replicate.test.ts
Tested: npm test
Not-tested: Live Replicate API calls for Seedream 4.5 custom-size requests

---------

Co-authored-by: justnode <justnode@users.noreply.github.com>
2026-04-12 01:16:32 -05:00
Jim Liu 宝玉 0b8ac256f4 chore: release v1.100.0 2026-04-12 00:30:55 -05:00
Jim Liu 宝玉 eaa0f1aa11 feat(baoyu-imagine): add Z.AI GLM-Image provider
Adds the Z.AI (智谱) provider supporting glm-image and cogview-4-250304
models via the Z.AI sync image API. Configure with ZAI_API_KEY (or
BIGMODEL_API_KEY for backward compat). Reference images are not supported yet.
2026-04-12 00:30:49 -05:00
Jim Liu 宝玉 ec5f4ffcc9 chore: release v1.99.1 2026-04-11 21:02:36 -05:00
Jim Liu 宝玉 d206e1674d fix(baoyu-article-illustrator): omit model field from batch tasks when unspecified
When --model is not provided, build-batch no longer writes a model field
into batch task entries; baoyu-imagine resolves the default from env or
EXTEND config instead of inheriting a hardcoded script default.

Adds tests covering both the omit-by-default and explicit-override paths.
2026-04-11 21:02:18 -05:00
Jim Liu 宝玉 b62ad26098 feat: add baoyu-diagram skill and baoyu-article-illustrator ink-notes style (v1.99.0)
- baoyu-diagram: new skill for generating publication-ready SVG diagrams (flowchart, structural, illustrative) with embedded styles and auto dark-mode
- baoyu-article-illustrator: add ink-notes style and mono-ink palette with presets for before/after, manifesto, and framework diagrams
2026-04-11 20:54:32 -05:00
Jim Liu 宝玉 31b2929d1c chore: release v1.98.0 2026-04-10 10:58:50 -05:00
Jim Liu 宝玉 7c2253dd3e feat(baoyu-xhs-images): add sketch-notes style, palette system, and new presets
- Restore baoyu-xhs-images as active skill (remove deprecated warning)
- Add sketch-notes style: hand-drawn educational infographic with macaron pastels
- Add palette system (macaron/warm/neon) as optional color override dimension
- Add 3 new presets: hand-drawn-edu, sketch-card, sketch-summary
- Add palette gallery documentation and palette override workflow
- Update style-presets.md with palette column
- Update prompt-assembly.md with palette override logic
- Update READMEs to reference baoyu-xhs-images
2026-04-10 10:58:29 -05:00
Jim Liu 宝玉 8be98c5afe chore: release v1.97.1 2026-04-09 13:20:21 -05:00
Jim Liu 宝玉 fe1a854bed fix(baoyu-image-cards): rename palette Zone labels to prevent text rendering in images 2026-04-09 13:20:14 -05:00
Jim Liu 宝玉 eeba585315 chore: release v1.97.0 2026-04-09 13:05:50 -05:00
Jim Liu 宝玉 1ca99ae5ae chore: add .omx to gitignore and update lockfile 2026-04-09 13:05:04 -05:00
Jim Liu 宝玉 c5df9d01bf fix(baoyu-xhs-images): improve deprecated skill description for better routing 2026-04-09 13:05:00 -05:00
Jim Liu 宝玉 ee1fc3c823 feat(baoyu-image-cards): add sketch-notes style, palette system, and 3 new presets 2026-04-09 13:04:57 -05:00
Jim Liu 宝玉 b7725a4ba8 chore: release v1.96.0 2026-04-09 10:47:51 -05:00
Jim Liu 宝玉 a596c653d3 feat(baoyu-image-cards): add image card series skill migrated from baoyu-xhs-images
Rename baoyu-xhs-images to baoyu-image-cards to decouple from Xiaohongshu platform.
Deprecate baoyu-xhs-images with migration notice pointing to baoyu-image-cards.
2026-04-09 10:47:28 -05:00
Jim Liu 宝玉 3017bfe423 chore: release v1.95.1 2026-04-09 10:00:43 -05:00
Jim Liu 宝玉 ae1d619ab2 fix(baoyu-slide-deck): add pptxgenjs dependency and detect image format by magic bytes 2026-04-09 10:00:29 -05:00
Jim Liu 宝玉 4821508c34 chore: release v1.95.0 2026-04-08 22:10:03 -05:00
Jim Liu 宝玉 f1042c8a6c feat(baoyu-slide-deck): add hand-drawn-edu preset and macaron mood 2026-04-08 22:06:54 -05:00
Jim Liu 宝玉 fa0fe441f5 feat(baoyu-infographic): add hand-drawn-edu style with macaron palette 2026-04-08 22:06:50 -05:00
Jim Liu 宝玉 90b2205914 chore: release v1.94.0 2026-04-08 17:58:33 -05:00
Jim Liu 宝玉 8e111c17b3 feat(baoyu-cover-image): add macaron palette and hand-drawn-edu style preset 2026-04-08 17:58:29 -05:00
114 changed files with 7497 additions and 212 deletions
+4 -3
View File
@@ -6,14 +6,14 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.93.0"
"version": "1.107.0"
},
"plugins": [
{
"name": "baoyu-skills",
"description": "Content generation, AI backends, and utility tools for daily work efficiency",
"source": "./",
"strict": true,
"strict": false,
"skills": [
"./skills/baoyu-article-illustrator",
"./skills/baoyu-comic",
@@ -21,6 +21,7 @@
"./skills/baoyu-cover-image",
"./skills/baoyu-danger-gemini-web",
"./skills/baoyu-danger-x-to-markdown",
"./skills/baoyu-diagram",
"./skills/baoyu-format-markdown",
"./skills/baoyu-imagine",
"./skills/baoyu-infographic",
@@ -31,7 +32,7 @@
"./skills/baoyu-slide-deck",
"./skills/baoyu-translate",
"./skills/baoyu-url-to-markdown",
"./skills/baoyu-xhs-images",
"./skills/baoyu-image-cards",
"./skills/baoyu-youtube-transcript"
]
}
+2
View File
@@ -154,6 +154,7 @@ illustrations/
comic/
translate/
posts/
diagram/
### IntelliJ IDEA ###
.idea
*.iws
@@ -167,3 +168,4 @@ posts/
.release-artifacts/
.worktrees/
youtube-transcript/
.omx/
+111
View File
@@ -2,6 +2,117 @@
English | [中文](./CHANGELOG.zh.md)
## 1.107.0 - 2026-04-15
### Features
- `baoyu-diagram`: add SVG-to-PNG @2x conversion script — auto-converts generated SVG diagrams to @2x PNG using Sharp; consolidate reference files and add `{baseDir}` path resolution for portable skill loading
### Fixes
- `claude-plugin`: allow inline marketplace manifest (#130)
## 1.106.0 - 2026-04-14
### Features
- `baoyu-diagram`: add architecture enrichment rules — automatically expand architecture diagrams with multiple client types, per-service tech stacks, database tiers, message buses, and color-coded categories; add full structural layout patterns, architecture-specific pitfalls, network topology templates, and layout math for complex diagrams
## 1.105.0 - 2026-04-13
### Features
- `baoyu-diagram`: unify to analyze→confirm→generate workflow — remove single/multi mode split; skill now analyzes any input material, recommends diagram types and splitting strategy, confirms once, then generates all diagrams
## 1.104.0 - 2026-04-13
### Features
- `baoyu-diagram`: add Mermaid sketch step (6d-0) before SVG generation — write a Mermaid code block as structural intent; add MermaidSVG consistency check in step 6f
### Fixes
- `baoyu-post-to-wechat`: verify editor focus before paste and type operations to prevent silent paste failures
## 1.103.1 - 2026-04-13
### Fixes
- `baoyu-markdown-to-html`: decode HTML entities and strip tags from article summary
- `baoyu-post-to-weibo`: decode HTML entities and strip tags from article summary
## 1.103.0 - 2026-04-12
### Features
- `baoyu-diagram`: add multi-diagram mode — analyze article content and generate multiple diagrams at identified positions; new `--density` option (`minimal`, `balanced`, `per-section`, `rich`) and `--mode` option (`single`, `multi`, `auto`); auto-detects mode from input (file path → multi, short topic → single); inserts diagram image links into article; output structure `diagram/{article-slug}/NN-{type}-{slug}/`
### Fixes
- `baoyu-article-illustrator`: prevent color names and hex codes from appearing as visible text in generated images — add semantic constraint to all palette references and prompt construction rules
- `baoyu-cover-image`: prevent color names and hex codes from appearing as visible text in generated images — add constraint to all palette references and prompt template
- `baoyu-image-cards`: prevent color names from appearing as visible text in generated images
- `baoyu-post-to-wechat`: decode HTML entities and strip HTML tags from article summary before using as WeChat article digest
## 1.102.0 - 2026-04-12
### Features
- `baoyu-imagine`: add OpenAI-compatible image API dialect — new `--imageApiDialect` flag, `OPENAI_IMAGE_API_DIALECT` env var, and `default_image_api_dialect` config for gateways that expect aspect-ratio `size` plus `metadata.resolution` instead of pixel `size`
## 1.101.0 - 2026-04-12
### Features
- `baoyu-imagine`: improve Replicate provider compatibility — route models through family-specific input builders and validators (nano-banana, Seedream 4.5, Seedream 5 Lite, Wan 2.7 Image); update default model to `google/nano-banana-2`; fix Seedream 4.5 custom size encoding to use width/height schema; fix aspect-ratio default inheritance for unsupported Replicate models; block multi-output requests before they reach the API (by @justnode)
## 1.100.0 - 2026-04-12
### Features
- `baoyu-imagine`: add Z.AI GLM-Image provider — supports `glm-image` and `cogview-4-250304` models via the Z.AI sync image API; configure with `ZAI_API_KEY` (or `BIGMODEL_API_KEY` for backward compatibility)
## 1.99.1 - 2026-04-11
### Fixes
- `baoyu-article-illustrator`: omit `model` field from batch tasks when `--model` is not specified, letting `baoyu-imagine` resolve the default from env/config
## 1.99.0 - 2026-04-10
### Features
- `baoyu-diagram`: add new skill for generating publication-ready SVG diagrams — flowcharts, structural/architecture diagrams, and illustrative intuition diagrams. Claude writes real SVG code directly following a cohesive design system; output is a single self-contained `.svg` file with embedded styles and auto dark-mode, ready to embed in articles, WeChat posts, slides, and docs
## 1.98.0 - 2026-04-10
### Features
- `baoyu-xhs-images`: Restore as active skill (remove deprecated warning)
- `baoyu-xhs-images`: Add `sketch-notes` style — hand-drawn educational infographic with macaron pastels, wobble lines, and warm cream background
- `baoyu-xhs-images`: Add palette system (`macaron`, `warm`, `neon`) as optional `--palette` color override dimension
- `baoyu-xhs-images`: Add 3 new presets: `hand-drawn-edu`, `sketch-card`, `sketch-summary`
## 1.97.1 - 2026-04-09
### Fixes
- `baoyu-image-cards`: rename palette color roles from "Zone N" to "Block Color" to prevent AI rendering labels as visible text in images
## 1.97.0 - 2026-04-09
### Features
- `baoyu-image-cards`: add `sketch-notes` style, palette system (`macaron`, `warm`, `neon`), and 3 new presets (`hand-drawn-edu`, `sketch-card`, `sketch-summary`)
### Fixes
- `baoyu-xhs-images`: improve deprecated skill description for better routing
## 1.96.0 - 2026-04-09
### Features
- `baoyu-image-cards`: add image card series skill migrated from `baoyu-xhs-images`, decoupled from Xiaohongshu platform
- `baoyu-xhs-images`: deprecated, migrated to `baoyu-image-cards`
## 1.95.1 - 2026-04-09
### Fixes
- `baoyu-slide-deck`: add `pptxgenjs` dependency and detect image format by magic bytes instead of file extension in PDF merge
## 1.95.0 - 2026-04-08
### Features
- `baoyu-infographic`: add `hand-drawn-edu` style — macaron pastels, hand-drawn wobble, stick figures
- `baoyu-slide-deck`: add `hand-drawn-edu` preset and `macaron` mood dimension with pastel color palette
## 1.94.0 - 2026-04-08
### Features
- `baoyu-cover-image`: add macaron palette and hand-drawn-edu style preset
## 1.93.0 - 2026-04-08
### Features
+111
View File
@@ -2,6 +2,117 @@
[English](./CHANGELOG.md) | 中文
## 1.107.0 - 2026-04-15
### 新功能
- `baoyu-diagram`:新增 SVG 转 @2x PNG 转换脚本 —— 使用 Sharp 自动将生成的 SVG 图表转为 @2x PNG;精简合并参考文件,新增 `{baseDir}` 路径解析以支持可移植的技能加载
### 修复
- `claude-plugin`:支持内联 marketplace manifest (#130)
## 1.106.0 - 2026-04-14
### 新功能
- `baoyu-diagram`:新增架构图丰富化规则 —— 自动扩展架构图,补充多客户端类型、各服务技术栈、数据库分层、消息总线和分色分类;新增完整结构布局模式、架构专用陷阱提示、网络拓扑模板和复杂图表布局计算
## 1.105.0 - 2026-04-13
### 新功能
- `baoyu-diagram`:统一为分析→确认→生成工作流 —— 移除单图/多图模式区分;技能现在分析任意输入素材,推荐图表类型和拆分策略,一次确认后批量生成所有图表
## 1.104.0 - 2026-04-13
### 新功能
- `baoyu-diagram`:新增 Mermaid 草图步骤(6d-0),在生成 SVG 前先写 Mermaid 代码块作为结构意图;在步骤 6f 新增 Mermaid–SVG 一致性检查
### 修复
- `baoyu-post-to-wechat`:在粘贴和输入操作前校验编辑器焦点,避免粘贴静默失败
## 1.103.1 - 2026-04-13
### 修复
- `baoyu-markdown-to-html`:修复文章摘要中 HTML 实体未解码及 HTML 标签未剥离的问题
- `baoyu-post-to-weibo`:修复文章摘要中 HTML 实体未解码及 HTML 标签未剥离的问题
## 1.103.0 - 2026-04-12
### 新功能
- `baoyu-diagram`:新增多图模式 —— 分析文章内容,在识别出的位置批量生成图表;新增 `--density` 参数(`minimal``balanced``per-section``rich`)和 `--mode` 参数(`single``multi``auto`);根据输入自动判断模式(文件路径→多图,短主题→单图);自动在文章中插入图表链接;输出目录结构 `diagram/{article-slug}/NN-{type}-{slug}/`
### 修复
- `baoyu-article-illustrator`:修复生成图像中出现颜色名称和色值文字的问题 —— 在所有调色板参考文件和提示构建规则中添加语义约束
- `baoyu-cover-image`:修复生成图像中出现颜色名称和色值文字的问题 —— 在所有调色板参考文件和提示模板中添加约束
- `baoyu-image-cards`:修复生成图像中出现颜色名称文字的问题
- `baoyu-post-to-wechat`:修复文章摘要中 HTML 实体未解码及 HTML 标签未剥离的问题,避免微信文章摘要显示乱码
## 1.102.0 - 2026-04-12
### 新功能
- `baoyu-imagine`:新增 OpenAI 兼容图像 API 方言支持 —— 新增 `--imageApiDialect` 参数、`OPENAI_IMAGE_API_DIALECT` 环境变量及 `default_image_api_dialect` 配置项,用于对接期望宽高比格式 `size``metadata.resolution` 的兼容网关
## 1.101.0 - 2026-04-12
### 新功能
- `baoyu-imagine`:改进 Replicate 服务商兼容性 —— 针对不同模型系列(nano-banana、Seedream 4.5、Seedream 5 Lite、Wan 2.7 Image)实现专属输入构建器和验证器;将默认模型更新为 `google/nano-banana-2`;修复 Seedream 4.5 自定义尺寸编码(改用 width/height schema);修复不支持的 Replicate 模型的宽高比默认值继承问题;在请求到达 API 前拦截多图请求 (by @justnode)
## 1.100.0 - 2026-04-12
### 新功能
- `baoyu-imagine`:新增 Z.AI GLM-Image 服务商支持,支持 `glm-image``cogview-4-250304` 模型,通过 Z.AI 同步图像 API 调用;配置 `ZAI_API_KEY`(或 `BIGMODEL_API_KEY` 向后兼容)
## 1.99.1 - 2026-04-11
### 修复
- `baoyu-article-illustrator`:未指定 `--model` 时,批处理任务中不再写入 `model` 字段,改由 `baoyu-imagine` 从环境变量或配置中解析默认值
## 1.99.0 - 2026-04-10
### 新功能
- `baoyu-diagram`:新增技能,用于生成可直接发布的 SVG 图表 —— 包括流程图、架构/结构图、示意图(直觉图解)。Claude 直接输出符合统一设计规范的真实 SVG 代码,产物是单个自包含的 `.svg` 文件,内嵌样式并自动支持深色模式,可直接嵌入文章、微信公众号、幻灯片和文档中
## 1.98.0 - 2026-04-10
### 新功能
- `baoyu-xhs-images`:恢复为正式技能(移除废弃警告)
- `baoyu-xhs-images`:新增 `sketch-notes` 风格 —— 手绘教育信息图,马卡龙配色,波动线条,暖奶油背景
- `baoyu-xhs-images`:新增配色系统(`macaron``warm``neon`),支持 `--palette` 参数覆盖风格默认颜色
- `baoyu-xhs-images`:新增 3 个预设:`hand-drawn-edu``sketch-card``sketch-summary`
## 1.97.1 - 2026-04-09
### 修复
- `baoyu-image-cards`:将配色方案中 "Zone N" 角色名改为 "Block Color",防止 AI 将标签文字渲染到图片中
## 1.97.0 - 2026-04-09
### 新功能
- `baoyu-image-cards`:新增 `sketch-notes` 风格、配色系统(`macaron``warm``neon`)及 3 个新预设(`hand-drawn-edu``sketch-card``sketch-summary`
### 修复
- `baoyu-xhs-images`:优化已弃用技能描述以改善路由匹配
## 1.96.0 - 2026-04-09
### 新功能
- `baoyu-image-cards`:新增图片卡片系列技能,从 `baoyu-xhs-images` 迁移,解除小红书平台绑定
- `baoyu-xhs-images`:已弃用,迁移至 `baoyu-image-cards`
## 1.95.1 - 2026-04-09
### 修复
- `baoyu-slide-deck`:添加 `pptxgenjs` 依赖,PDF 合并时通过魔数字节检测图片格式替代文件扩展名判断
## 1.95.0 - 2026-04-08
### 新功能
- `baoyu-infographic`:新增 `hand-drawn-edu` 风格 — 马卡龙柔和色块、手绘线条、火柴人角色
- `baoyu-slide-deck`:新增 `hand-drawn-edu` 预设和 `macaron` 色调维度,含柔和马卡龙色板
## 1.94.0 - 2026-04-08
### 新功能
- `baoyu-cover-image`:新增马卡龙色板和 hand-drawn-edu 风格预设
## 1.93.0 - 2026-04-08
### 新功能
+2 -1
View File
@@ -1,6 +1,6 @@
# CLAUDE.md
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.93.0**.
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.107.0**.
## Architecture
@@ -55,6 +55,7 @@ Priority: project `skills/` → `$HOME/.baoyu-skills/` → system-level.
| Skill | Note |
|-------|------|
| `baoyu-image-gen` | Migrated to `baoyu-imagine`. Do NOT add to `.claude-plugin/marketplace.json`. Do NOT update README for this skill. |
| `baoyu-xhs-images` | Migrated to `baoyu-image-cards`. Do NOT add to `.claude-plugin/marketplace.json`. Do NOT update README for this skill. |
## Release Process
+84 -15
View File
@@ -99,7 +99,7 @@ Content generation and publishing skills.
#### baoyu-xhs-images
Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-10 cartoon-style infographics with **Style × Layout** two-dimensional system.
Xiaohongshu image card series generator. Breaks down content into 1-10 cartoon-style image cards with **Style × Layout** system and optional palette override.
```bash
# Auto-select style and layout
@@ -112,7 +112,10 @@ Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-1
/baoyu-xhs-images posts/ai-future/article.md --layout dense
# Combine style and layout
/baoyu-xhs-images posts/ai-future/article.md --style tech --layout list
/baoyu-xhs-images posts/ai-future/article.md --style notion --layout list
# Override palette
/baoyu-xhs-images posts/ai-future/article.md --style notion --palette macaron
# Direct content input
/baoyu-xhs-images 今日星座运势
@@ -122,7 +125,9 @@ Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-1
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
```
**Styles** (visual aesthetics): `cute` (default), `fresh`, `warm`, `bold`, `minimal`, `retro`, `pop`, `notion`, `chalkboard`
**Styles** (visual aesthetics): `cute` (default), `fresh`, `warm`, `bold`, `minimal`, `retro`, `pop`, `notion`, `chalkboard`, `study-notes`, `screen-print`, `sketch-notes`
**Palettes** (optional color override): `macaron`, `warm`, `neon`
**Style Previews**:
@@ -156,7 +161,7 @@ Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-1
#### baoyu-infographic
Generate professional infographics with 20 layout types and 17 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics.
Generate professional infographics with 21 layout types and 21 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics.
```bash
# Auto-recommend combinations based on content
@@ -267,9 +272,46 @@ Generate professional infographics with 20 layout types and 17 visual styles. An
| ![knolling](./screenshots/infographic-styles/knolling.webp) | ![lego-brick](./screenshots/infographic-styles/lego-brick.webp) | |
| knolling | lego-brick | |
#### baoyu-diagram
Generate publication-ready SVG diagrams from source material — flowcharts, sequence/protocol diagrams, structural/architecture diagrams, and illustrative intuition diagrams. Analyzes input material to recommend diagram type(s) and splitting strategy, confirms the plan once, then generates all diagrams. Claude writes real SVG code directly following a cohesive design system. Output is self-contained `.svg` files with embedded styles and auto dark-mode.
```bash
# Topic string — skill analyzes and proposes a plan
/baoyu-diagram "how JWT authentication works"
/baoyu-diagram "Kubernetes architecture" --type structural
/baoyu-diagram "OAuth 2.0 flow" --type sequence
# File path — skill reads, analyzes, and proposes a plan
/baoyu-diagram path/to/article.md
# Language and output path
/baoyu-diagram "微服务架构" --lang zh
/baoyu-diagram "build pipeline" --out docs/build-pipeline.svg
```
**Options**:
| Option | Description |
|--------|-------------|
| `--type <name>` | `flowchart`, `sequence`, `structural`, `illustrative`, `class`, `auto` (default). Skips type recommendation. |
| `--lang <code>` | Output language (en, zh, ja, ...) |
| `--out <path>` | Output file path. Generates exactly one diagram focused on the most important aspect. |
**Diagram types**:
| Type | Reader need | Verbs that trigger it |
|------|-------------|------------------------|
| `flowchart` | Walk me through the steps in order | walk through, steps, process, lifecycle, workflow, state machine |
| `sequence` | Who talks to whom, in what order | protocol, handshake, auth flow, OAuth, TCP, request/response |
| `structural` | Show me what's inside what, how it's organised | architecture, components, topology, layout, what's inside |
| `illustrative` | Give me the intuition — draw the mechanism | how does X work, explain X, intuition for, why does X do Y |
| `class` | What are the types and how are they related | class diagram, UML, inheritance, interface, schema |
Not an image-generation skill — no LLM image model is called. Claude writes the SVG by hand with hand-computed layout math, so every diagram honors the design system. Embedded `<style>` block with `@media (prefers-color-scheme: dark)` means the same file renders correctly in both light and dark mode anywhere it's embedded.
#### baoyu-cover-image
Generate cover images for articles with 5 dimensions: Type × Palette × Rendering × Text × Mood. Combines 9 color palettes with 6 rendering styles for 54 unique combinations.
Generate cover images for articles with 5 dimensions: Type × Palette × Rendering × Text × Mood. Combines 11 color palettes with 7 rendering styles for 77 unique combinations.
```bash
# Auto-select all dimensions based on content
@@ -294,8 +336,8 @@ Generate cover images for articles with 5 dimensions: Type × Palette × Renderi
**Five Dimensions**:
- **Type**: `hero`, `conceptual`, `typography`, `metaphor`, `scene`, `minimal`
- **Palette**: `warm`, `elegant`, `cool`, `dark`, `earth`, `vivid`, `pastel`, `mono`, `retro`
- **Rendering**: `flat-vector`, `hand-drawn`, `painterly`, `digital`, `pixel`, `chalk`
- **Palette**: `warm`, `elegant`, `cool`, `dark`, `earth`, `vivid`, `pastel`, `mono`, `retro`, `duotone`, `macaron`
- **Rendering**: `flat-vector`, `hand-drawn`, `painterly`, `digital`, `pixel`, `chalk`, `screen-print`
- **Text**: `none`, `title-only` (default), `title-subtitle`, `text-rich`
- **Mood**: `subtle`, `balanced` (default), `bold`
@@ -702,15 +744,24 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
# DashScope with custom size
/baoyu-imagine --prompt "为咖啡品牌设计一张 21:9 横幅海报,包含清晰中文标题" --image banner.png --provider dashscope --model qwen-image-2.0-pro --size 2048x872
# Z.AI GLM-Image
/baoyu-imagine --prompt "一张带清晰中文标题的科技海报" --image out.png --provider zai
# MiniMax
/baoyu-imagine --prompt "A fashion editorial portrait by a bright studio window" --image out.jpg --provider minimax
# MiniMax with subject reference
/baoyu-imagine --prompt "A girl stands by the library window, cinematic lighting" --image out.jpg --provider minimax --model image-01 --ref portrait.png --ar 16:9
# Replicate
# Replicate (default: google/nano-banana-2)
/baoyu-imagine --prompt "A cat" --image cat.png --provider replicate
# Replicate Seedream 4.5
/baoyu-imagine --prompt "A studio portrait" --image portrait.png --provider replicate --model bytedance/seedream-4.5 --ar 3:2
# Replicate Wan 2.7 Image Pro
/baoyu-imagine --prompt "A concept frame" --image frame.png --provider replicate --model wan-video/wan-2.7-image-pro --size 2048x1152
# Jimeng (即梦)
/baoyu-imagine --prompt "一只可爱的猫" --image cat.png --provider jimeng
@@ -732,14 +783,15 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
| `--image` | Output image path (required) |
| `--batchfile` | JSON batch file for multi-image generation |
| `--jobs` | Worker count for batch mode |
| `--provider` | `google`, `openai`, `azure`, `openrouter`, `dashscope`, `minimax`, `jimeng`, `seedream`, or `replicate` |
| `--model`, `-m` | Model ID or deployment name. Azure uses deployment name; OpenRouter uses full model IDs; MiniMax uses `image-01` / `image-01-live` |
| `--provider` | `google`, `openai`, `azure`, `openrouter`, `dashscope`, `zai`, `minimax`, `jimeng`, `seedream`, or `replicate` |
| `--model`, `-m` | Model ID or deployment name. Azure uses deployment name; OpenRouter uses full model IDs; Z.AI uses `glm-image`; MiniMax uses `image-01` / `image-01-live` |
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size` | Size (e.g., `1024x1024`) |
| `--quality` | `normal` or `2k` (default: `2k`) |
| `--imageSize` | `1K`, `2K`, or `4K` for Google/OpenRouter |
| `--ref` | Reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate, MiniMax, or Seedream 5.0/4.5/4.0) |
| `--n` | Number of images per request |
| `--imageApiDialect` | `openai-native` or `ratio-metadata` for OpenAI-compatible gateways |
| `--ref` | Reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate supported families, MiniMax, or Seedream 5.0/4.5/4.0) |
| `--n` | Number of images per request (`replicate` currently requires `--n 1`) |
| `--json` | JSON output |
**Environment Variables** (see [Environment Configuration](#environment-configuration) for setup):
@@ -751,6 +803,8 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
| `GOOGLE_API_KEY` | Google API key | - |
| `GEMINI_API_KEY` | Alias for `GOOGLE_API_KEY` | - |
| `DASHSCOPE_API_KEY` | DashScope API key (Aliyun) | - |
| `ZAI_API_KEY` | Z.AI API key | - |
| `BIGMODEL_API_KEY` | Backward-compatible alias for Z.AI API key | - |
| `MINIMAX_API_KEY` | MiniMax API key | - |
| `REPLICATE_API_TOKEN` | Replicate API token | - |
| `JIMENG_ACCESS_KEY_ID` | Jimeng Volcengine access key | - |
@@ -762,11 +816,14 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
| `OPENROUTER_IMAGE_MODEL` | OpenRouter model | `google/gemini-3.1-flash-image-preview` |
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope model | `qwen-image-2.0-pro` |
| `ZAI_IMAGE_MODEL` | Z.AI model | `glm-image` |
| `BIGMODEL_IMAGE_MODEL` | Backward-compatible alias for Z.AI model | `glm-image` |
| `MINIMAX_IMAGE_MODEL` | MiniMax model | `image-01` |
| `REPLICATE_IMAGE_MODEL` | Replicate model | `google/nano-banana-pro` |
| `REPLICATE_IMAGE_MODEL` | Replicate model | `google/nano-banana-2` |
| `JIMENG_IMAGE_MODEL` | Jimeng model | `jimeng_t2i_v40` |
| `SEEDREAM_IMAGE_MODEL` | Seedream model | `doubao-seedream-5-0-260128` |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | - |
| `OPENAI_IMAGE_API_DIALECT` | OpenAI-compatible image API dialect (`openai-native` or `ratio-metadata`) | `openai-native` |
| `OPENAI_IMAGE_USE_CHAT` | Use `/chat/completions` for OpenAI image generation | `false` |
| `AZURE_OPENAI_BASE_URL` | Azure resource or deployment endpoint | - |
| `AZURE_API_VERSION` | Azure image API version | `2025-04-01-preview` |
@@ -775,6 +832,8 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
| `OPENROUTER_TITLE` | Optional app name for OpenRouter attribution | - |
| `GOOGLE_BASE_URL` | Custom Google endpoint | - |
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | - |
| `ZAI_BASE_URL` | Custom Z.AI endpoint | `https://api.z.ai/api/paas/v4` |
| `BIGMODEL_BASE_URL` | Backward-compatible alias for Z.AI endpoint | - |
| `MINIMAX_BASE_URL` | Custom MiniMax endpoint | `https://api.minimax.io` |
| `REPLICATE_BASE_URL` | Custom Replicate endpoint | - |
| `JIMENG_BASE_URL` | Custom Jimeng endpoint | `https://visual.volcengineapi.com` |
@@ -787,16 +846,20 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
**Provider Notes**:
- Azure OpenAI: `--model` means Azure deployment name, not the underlying model family.
- DashScope: `qwen-image-2.0-pro` is the recommended default for custom `--size`, `21:9`, and strong Chinese/English text rendering.
- Z.AI: `glm-image` is recommended for posters, diagrams, and text-heavy Chinese/English images. Reference images are not supported.
- MiniMax: `image-01` supports documented custom `width` / `height`; `image-01-live` is lower latency and works best with `--ar`.
- MiniMax reference images are sent as `subject_reference`; the current API is specialized toward character / portrait consistency.
- Jimeng does not support reference images.
- Seedream reference images are supported by Seedream 5.0 / 4.5 / 4.0, not Seedream 3.0.
- Replicate defaults to `google/nano-banana-2`. `baoyu-imagine` only enables Replicate advanced options for `google/nano-banana*`, `bytedance/seedream-4.5`, `bytedance/seedream-5-lite`, `wan-video/wan-2.7-image`, and `wan-video/wan-2.7-image-pro`.
- Replicate currently saves exactly one output image per request. `--n > 1` is blocked locally instead of silently dropping extra results.
- Replicate model behavior is family-specific: nano-banana uses `--quality` / `--ar`, Seedream uses validated `--size` / `--ar`, and Wan uses validated `--size` (with `--ar` converted locally to a concrete size).
**Provider Auto-Selection**:
1. If `--provider` is specified → use it
2. If `--ref` is provided and no provider is specified → try Google, then OpenAI, Azure, OpenRouter, Replicate, Seedream, and finally MiniMax
3. If only one API key is available → use that provider
4. If multiple providers are available → default to Google
4. If multiple providers are available → default to Google, then OpenAI, Azure, OpenRouter, DashScope, Z.AI, MiniMax, Replicate, Jimeng, Seedream
#### baoyu-danger-gemini-web
@@ -1096,6 +1159,11 @@ DASHSCOPE_API_KEY=sk-xxx
DASHSCOPE_IMAGE_MODEL=qwen-image-2.0-pro
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
# Z.AI
ZAI_API_KEY=xxx
ZAI_IMAGE_MODEL=glm-image
# ZAI_BASE_URL=https://api.z.ai/api/paas/v4
# MiniMax
MINIMAX_API_KEY=xxx
MINIMAX_IMAGE_MODEL=image-01
@@ -1103,7 +1171,7 @@ MINIMAX_IMAGE_MODEL=image-01
# Replicate
REPLICATE_API_TOKEN=r8_xxx
REPLICATE_IMAGE_MODEL=google/nano-banana-pro
REPLICATE_IMAGE_MODEL=google/nano-banana-2
# REPLICATE_BASE_URL=https://api.replicate.com
# Jimeng (即梦)
@@ -1196,6 +1264,7 @@ This project was inspired by and builds upon the following open source projects:
- [doocs/md](https://github.com/doocs/md) by [@doocs](https://github.com/doocs) — Core implementation logic for Markdown to HTML conversion
- [High-density Infographic Prompt](https://waytoagi.feishu.cn/wiki/YG0zwalijihRREkgmPzcWRInnUg) by AJ@WaytoAGI — Inspiration for the infographic skill
- [qiaomu-mondo-poster-design](https://github.com/joeseesun/qiaomu-mondo-poster-design) by [@joeseesun](https://github.com/joeseesun)(乔木) — Inspiration for the Mondo style
- [architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) by [@Cocoon-AI](https://github.com/Cocoon-AI) — Inspiration for the diagram skill's design system
## License
+84 -15
View File
@@ -99,7 +99,7 @@ clawhub install baoyu-markdown-to-html
#### baoyu-xhs-images
小红书信息图系列生成器。将内容拆解为 1-10 张卡通风格信息图,支持 **风格 × 布局** 二维系统。
小红书图片卡片系列生成器。将内容拆解为 1-10 张卡通风格图片卡片,支持 **风格 × 布局** 系统和可选配色覆盖
```bash
# 自动选择风格和布局
@@ -112,7 +112,10 @@ clawhub install baoyu-markdown-to-html
/baoyu-xhs-images posts/ai-future/article.md --layout dense
# 组合风格和布局
/baoyu-xhs-images posts/ai-future/article.md --style tech --layout list
/baoyu-xhs-images posts/ai-future/article.md --style notion --layout list
# 覆盖配色
/baoyu-xhs-images posts/ai-future/article.md --style notion --palette macaron
# 直接输入内容
/baoyu-xhs-images 今日星座运势
@@ -122,7 +125,9 @@ clawhub install baoyu-markdown-to-html
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
```
**风格**(视觉美学):`cute`(默认)、`fresh``warm``bold``minimal``retro``pop``notion``chalkboard`
**风格**(视觉美学):`cute`(默认)、`fresh``warm``bold``minimal``retro``pop``notion``chalkboard``study-notes``screen-print``sketch-notes`
**配色**(可选颜色覆盖):`macaron``warm``neon`
**风格预览**
@@ -156,7 +161,7 @@ clawhub install baoyu-markdown-to-html
#### baoyu-infographic
专业信息图生成器,支持 20 种布局和 17 种视觉风格。分析内容后推荐布局×风格组合,生成可发布的信息图。
专业信息图生成器,支持 21 种布局和 21 种视觉风格。分析内容后推荐布局×风格组合,生成可发布的信息图。
```bash
# 根据内容自动推荐组合
@@ -267,9 +272,46 @@ clawhub install baoyu-markdown-to-html
| ![knolling](./screenshots/infographic-styles/knolling.webp) | ![lego-brick](./screenshots/infographic-styles/lego-brick.webp) | |
| knolling | lego-brick | |
#### baoyu-diagram
从源素材生成可直接发布的 SVG 图表 —— 包括流程图、时序/协议图、架构/结构图、示意图(直觉图解)。分析输入素材,推荐图表类型和拆分策略,一次确认后批量生成。Claude 直接输出符合统一设计规范的真实 SVG 代码,产物是自包含的 `.svg` 文件,内嵌样式并自动支持深色模式。
```bash
# 主题描述 —— 技能分析并提出方案
/baoyu-diagram "JWT 认证流程是怎么工作的"
/baoyu-diagram "Kubernetes 架构" --type structural
/baoyu-diagram "OAuth 2.0 流程" --type sequence
# 文件路径 —— 技能读取、分析并提出方案
/baoyu-diagram path/to/article.md
# 语言和输出路径
/baoyu-diagram "微服务架构" --lang zh
/baoyu-diagram "build pipeline" --out docs/build-pipeline.svg
```
**参数**
| 参数 | 说明 |
|------|------|
| `--type <name>` | `flowchart``sequence``structural``illustrative``class``auto`(默认)。跳过类型推荐直接生成。 |
| `--lang <code>` | 输出语言(en、zh、ja 等) |
| `--out <path>` | 输出文件路径。生成聚焦于最重要内容的单张图表。 |
**五种图表类型**
| 类型 | 适用场景 | 触发动词 |
|------|----------|----------|
| `flowchart` | 按顺序走一遍流程 | 流程、步骤、工作流、生命周期、状态机 |
| `sequence` | 谁和谁通信、按什么顺序 | 协议、握手、认证流程、OAuth、TCP、请求/响应 |
| `structural` | 展示什么包含什么、如何组织 | 架构、组件、拓扑、布局、什么在什么里面 |
| `illustrative` | 建立直觉 —— 画出机制本身 | 怎么工作、原理、为什么、直观解释 |
| `class` | 类型是什么、它们如何关联 | 类图、UML、继承、接口、数据模型 |
本技能不调用任何图像生成模型 —— Claude 通过手算坐标直接写 SVG 代码,确保每个图表都遵守设计规范。内嵌的 `<style>` 块包含 `@media (prefers-color-scheme: dark)`,同一个文件在浅色和深色模式下均正确渲染,可嵌入到任意支持 SVG 的宿主环境中。
#### baoyu-cover-image
为文章生成封面图,支持五维定制系统:类型 × 配色 × 渲染 × 文字 × 氛围。9 种配色方案与 6 种渲染风格组合,提供 54 种独特效果。
为文章生成封面图,支持五维定制系统:类型 × 配色 × 渲染 × 文字 × 氛围。11 种配色方案与 7 种渲染风格组合,提供 77 种独特效果。
```bash
# 根据内容自动选择所有维度
@@ -294,8 +336,8 @@ clawhub install baoyu-markdown-to-html
**五个维度**
- **类型 (Type)**`hero``conceptual``typography``metaphor``scene``minimal`
- **配色 (Palette)**`warm``elegant``cool``dark``earth``vivid``pastel``mono``retro`
- **渲染 (Rendering)**`flat-vector``hand-drawn``painterly``digital``pixel``chalk`
- **配色 (Palette)**`warm``elegant``cool``dark``earth``vivid``pastel``mono``retro``duotone``macaron`
- **渲染 (Rendering)**`flat-vector``hand-drawn``painterly``digital``pixel``chalk``screen-print`
- **文字 (Text)**`none``title-only`(默认)、`title-subtitle``text-rich`
- **氛围 (Mood)**`subtle``balanced`(默认)、`bold`
@@ -702,15 +744,24 @@ AI 驱动的生成后端。
# DashScope 自定义尺寸
/baoyu-imagine --prompt "为咖啡品牌设计一张 21:9 横幅海报,包含清晰中文标题" --image banner.png --provider dashscope --model qwen-image-2.0-pro --size 2048x872
# Z.AI GLM-Image
/baoyu-imagine --prompt "一张带清晰中文标题的科技海报" --image out.png --provider zai
# MiniMax
/baoyu-imagine --prompt "A fashion editorial portrait by a bright studio window" --image out.jpg --provider minimax
# MiniMax + 角色参考图
/baoyu-imagine --prompt "A girl stands by the library window, cinematic lighting" --image out.jpg --provider minimax --model image-01 --ref portrait.png --ar 16:9
# Replicate
# Replicate(默认:google/nano-banana-2
/baoyu-imagine --prompt "一只猫" --image cat.png --provider replicate
# Replicate Seedream 4.5
/baoyu-imagine --prompt "一张影棚人像" --image portrait.png --provider replicate --model bytedance/seedream-4.5 --ar 3:2
# Replicate Wan 2.7 Image Pro
/baoyu-imagine --prompt "一张概念分镜" --image frame.png --provider replicate --model wan-video/wan-2.7-image-pro --size 2048x1152
# 即梦(Jimeng
/baoyu-imagine --prompt "一只可爱的猫" --image cat.png --provider jimeng
@@ -732,14 +783,15 @@ AI 驱动的生成后端。
| `--image` | 输出图片路径(必需) |
| `--batchfile` | 多图批量生成的 JSON 文件 |
| `--jobs` | 批量模式的并发 worker 数 |
| `--provider` | `google``openai``azure``openrouter``dashscope``minimax``jimeng``seedream``replicate` |
| `--model`, `-m` | 模型 ID 或部署名。Azure 使用部署名;OpenRouter 使用完整模型 IDMiniMax 使用 `image-01` / `image-01-live` |
| `--provider` | `google``openai``azure``openrouter``dashscope``zai``minimax``jimeng``seedream``replicate` |
| `--model`, `-m` | 模型 ID 或部署名。Azure 使用部署名;OpenRouter 使用完整模型 IDZ.AI 使用 `glm-image`MiniMax 使用 `image-01` / `image-01-live` |
| `--ar` | 宽高比(如 `16:9``1:1``4:3` |
| `--size` | 尺寸(如 `1024x1024` |
| `--quality` | `normal``2k`(默认:`2k` |
| `--imageSize` | Google/OpenRouter 使用的 `1K``2K``4K` |
| `--ref` | 参考图片(Google、OpenAI、Azure OpenAI、OpenRouter、Replicate、MiniMax 或 Seedream 5.0/4.5/4.0 |
| `--n` | 单次请求生成图片数量 |
| `--imageApiDialect` | OpenAI 兼容网关的图像 API 方言(`openai-native``ratio-metadata` |
| `--ref` | 参考图片(Google、OpenAI、Azure OpenAI、OpenRouter、Replicate 支持的模型家族、MiniMax 或 Seedream 5.0/4.5/4.0 |
| `--n` | 单次请求生成图片数量(`replicate` 当前只支持 `--n 1` |
| `--json` | 输出 JSON 结果 |
**环境变量**(配置方法见[环境配置](#环境配置)):
@@ -751,6 +803,8 @@ AI 驱动的生成后端。
| `GOOGLE_API_KEY` | Google API 密钥 | - |
| `GEMINI_API_KEY` | `GOOGLE_API_KEY` 的别名 | - |
| `DASHSCOPE_API_KEY` | DashScope API 密钥(阿里云) | - |
| `ZAI_API_KEY` | Z.AI API 密钥 | - |
| `BIGMODEL_API_KEY` | Z.AI API 密钥向后兼容别名 | - |
| `MINIMAX_API_KEY` | MiniMax API 密钥 | - |
| `REPLICATE_API_TOKEN` | Replicate API Token | - |
| `JIMENG_ACCESS_KEY_ID` | 即梦火山引擎 Access Key | - |
@@ -762,11 +816,14 @@ AI 驱动的生成后端。
| `OPENROUTER_IMAGE_MODEL` | OpenRouter 模型 | `google/gemini-3.1-flash-image-preview` |
| `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope 模型 | `qwen-image-2.0-pro` |
| `ZAI_IMAGE_MODEL` | Z.AI 模型 | `glm-image` |
| `BIGMODEL_IMAGE_MODEL` | Z.AI 模型向后兼容别名 | `glm-image` |
| `MINIMAX_IMAGE_MODEL` | MiniMax 模型 | `image-01` |
| `REPLICATE_IMAGE_MODEL` | Replicate 模型 | `google/nano-banana-pro` |
| `REPLICATE_IMAGE_MODEL` | Replicate 模型 | `google/nano-banana-2` |
| `JIMENG_IMAGE_MODEL` | 即梦模型 | `jimeng_t2i_v40` |
| `SEEDREAM_IMAGE_MODEL` | 豆包模型 | `doubao-seedream-5-0-260128` |
| `OPENAI_BASE_URL` | 自定义 OpenAI 端点 | - |
| `OPENAI_IMAGE_API_DIALECT` | OpenAI 兼容图像 API 方言(`openai-native``ratio-metadata` | `openai-native` |
| `OPENAI_IMAGE_USE_CHAT` | OpenAI 改走 `/chat/completions` | `false` |
| `AZURE_OPENAI_BASE_URL` | Azure 资源或部署端点 | - |
| `AZURE_API_VERSION` | Azure 图像 API 版本 | `2025-04-01-preview` |
@@ -775,6 +832,8 @@ AI 驱动的生成后端。
| `OPENROUTER_TITLE` | OpenRouter 归因用应用名 | - |
| `GOOGLE_BASE_URL` | 自定义 Google 端点 | - |
| `DASHSCOPE_BASE_URL` | 自定义 DashScope 端点 | - |
| `ZAI_BASE_URL` | 自定义 Z.AI 端点 | `https://api.z.ai/api/paas/v4` |
| `BIGMODEL_BASE_URL` | Z.AI 端点向后兼容别名 | - |
| `MINIMAX_BASE_URL` | 自定义 MiniMax 端点 | `https://api.minimax.io` |
| `REPLICATE_BASE_URL` | 自定义 Replicate 端点 | - |
| `JIMENG_BASE_URL` | 自定义即梦端点 | `https://visual.volcengineapi.com` |
@@ -787,16 +846,20 @@ AI 驱动的生成后端。
**Provider 说明**
- Azure OpenAI`--model` 表示 Azure deployment name,不是底层模型家族名。
- DashScope`qwen-image-2.0-pro` 是自定义 `--size``21:9` 和中英文排版的推荐默认模型。
- Z.AI`glm-image` 适合海报、图表和中英文排版密集的图片生成,暂不支持参考图。
- MiniMax`image-01` 支持官方文档里的自定义 `width` / `height``image-01-live` 更偏低延迟,适合配合 `--ar` 使用。
- MiniMax 参考图会走 `subject_reference`,当前能力更偏角色 / 人像一致性。
- 即梦不支持参考图。
- 豆包参考图能力仅适用于 Seedream 5.0 / 4.5 / 4.0,不适用于 Seedream 3.0。
- Replicate 默认模型改为 `google/nano-banana-2``baoyu-imagine` 目前只对 `google/nano-banana*``bytedance/seedream-4.5``bytedance/seedream-5-lite``wan-video/wan-2.7-image``wan-video/wan-2.7-image-pro` 开启本地能力识别与校验。
- Replicate 当前只保存单张输出图,`--n > 1` 会在本地直接报错,避免多图结果被静默丢弃。
- Replicate 的参数能力按模型家族区分:nano-banana 走 `--quality` / `--ar`Seedream 走校验后的 `--size` / `--ar`Wan 走校验后的 `--size``--ar` 会先在本地换算成具体尺寸)。
**服务商自动选择**
1. 如果指定了 `--provider` → 使用指定的
2. 如果传了 `--ref` 且未指定 provider → 依次尝试 Google、OpenAI、Azure、OpenRouter、Replicate、Seedream,最后是 MiniMax
3. 如果只有一个 API 密钥 → 使用对应服务商
4. 如果多个可用 → 默认使用 Google
4. 如果多个可用 → 默认使用 Google,然后依次为 OpenAI、Azure、OpenRouter、DashScope、Z.AI、MiniMax、Replicate、即梦、豆包
#### baoyu-danger-gemini-web
@@ -1096,6 +1159,11 @@ DASHSCOPE_API_KEY=sk-xxx
DASHSCOPE_IMAGE_MODEL=qwen-image-2.0-pro
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
# Z.AI
ZAI_API_KEY=xxx
ZAI_IMAGE_MODEL=glm-image
# ZAI_BASE_URL=https://api.z.ai/api/paas/v4
# MiniMax
MINIMAX_API_KEY=xxx
MINIMAX_IMAGE_MODEL=image-01
@@ -1103,7 +1171,7 @@ MINIMAX_IMAGE_MODEL=image-01
# Replicate
REPLICATE_API_TOKEN=r8_xxx
REPLICATE_IMAGE_MODEL=google/nano-banana-pro
REPLICATE_IMAGE_MODEL=google/nano-banana-2
# REPLICATE_BASE_URL=https://api.replicate.com
# 即梦(Jimeng
@@ -1196,6 +1264,7 @@ HTTP_PROXY=http://127.0.0.1:7890 HTTPS_PROXY=http://127.0.0.1:7890 /baoyu-danger
- [doocs/md](https://github.com/doocs/md) by [@doocs](https://github.com/doocs) — Markdown 转 HTML 的核心实现逻辑
- [高密度信息图 Prompt](https://waytoagi.feishu.cn/wiki/YG0zwalijihRREkgmPzcWRInnUg) by AJ@WaytoAGI — 信息图技能的灵感来源
- [qiaomu-mondo-poster-design](https://github.com/joeseesun/qiaomu-mondo-poster-design) by [@joeseesun](https://github.com/joeseesun)(乔木) — Mondo 风格的灵感来源
- [architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) by [@Cocoon-AI](https://github.com/Cocoon-AI) — 图表技能设计体系的灵感来源
## 许可证
BIN
View File
Binary file not shown.
+196
View File
@@ -8,6 +8,10 @@
"workspaces": [
"packages/*"
],
"dependencies": {
"pdf-lib": "^1.17.1",
"pptxgenjs": "^4.0.1"
},
"devDependencies": {
"@mozilla/readability": "^0.6.0",
"linkedom": "^0.18.12",
@@ -1010,6 +1014,24 @@
"node": ">= 8"
}
},
"node_modules/@pdf-lib/standard-fonts": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.6"
}
},
"node_modules/@pdf-lib/upng": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.10"
}
},
"node_modules/@types/bun": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.11.tgz",
@@ -1302,6 +1324,12 @@
"node": ">=18"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1912,6 +1940,12 @@
"node": ">= 14"
}
},
"node_modules/https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
"license": "ISC"
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -1957,6 +1991,33 @@
"node": ">= 4"
}
},
"node_modules/image-size": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
"queue": "6.0.2"
},
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=16.x"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
@@ -2058,6 +2119,12 @@
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2127,6 +2194,18 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/juice": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/juice/-/juice-11.1.1.tgz",
@@ -2147,6 +2226,15 @@
"node": ">=18.17"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lighthouse-logger": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz",
@@ -3239,6 +3327,12 @@
"quansync": "^0.2.7"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -3318,6 +3412,18 @@
"node": ">=8"
}
},
"node_modules/pdf-lib": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
"license": "MIT",
"dependencies": {
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
"pako": "^1.0.11",
"tslib": "^1.11.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -3348,6 +3454,27 @@
"node": ">=6"
}
},
"node_modules/pptxgenjs": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
"integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==",
"license": "MIT",
"dependencies": {
"@types/node": "^22.8.1",
"https": "^1.0.0",
"image-size": "^1.2.1",
"jszip": "^3.10.1"
}
},
"node_modules/pptxgenjs/node_modules/@types/node": {
"version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
@@ -3364,6 +3491,12 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3390,6 +3523,15 @@
],
"license": "MIT"
},
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"license": "MIT",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -3427,6 +3569,21 @@
"node": ">=6"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/reading-time": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz",
@@ -3564,6 +3721,12 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -3595,6 +3758,12 @@
"node": ">=10"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3667,6 +3836,15 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -3784,6 +3962,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
@@ -3849,6 +4033,12 @@
"node": ">=18.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -3933,6 +4123,12 @@
"node": ">= 4.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/valid-data-url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz",
+3 -1
View File
@@ -17,6 +17,8 @@
"tsx": "^4.20.5"
},
"dependencies": {
"pdf-lib": "^1.17.1"
"pdf-lib": "^1.17.1",
"pptxgenjs": "^4.0.1",
"sharp": "^0.34.5"
}
}
+17
View File
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
cleanSummaryText,
extractSummaryFromBody,
extractTitleFromMarkdown,
parseFrontmatter,
@@ -91,3 +92,19 @@ This is **the first paragraph** with [a link](https://example.com) and \`inline
"This is the first paragraph with a link and inline code that should...",
);
});
test("summary extraction normalizes raw HTML paragraphs to plain text", () => {
const summary = extractSummaryFromBody(
`
# Heading
<p style="font-size: 16px; color: #666; margin-bottom: 20px;">2026年初,一只“龙虾”搅动了整个科技圈。腾讯楼下排起近千人长队,只为让工程师领取一份福利。</p>
`,
120,
);
assert.equal(
summary,
"2026年初,一只“龙虾”搅动了整个科技圈。腾讯楼下排起近千人长队,只为让工程师领取一份福利。",
);
assert.equal(cleanSummaryText("<strong>Good&nbsp;text&#33;&apos;</strong>"), "Good text!'");
});
+43 -3
View File
@@ -46,6 +46,45 @@ export function stripWrappingQuotes(value: string): string {
return value.trim();
}
const HTML_ENTITIES: Record<string, string> = {
amp: "&",
apos: "'",
gt: ">",
lt: "<",
nbsp: " ",
quot: '"',
};
function decodeHtmlCodePoint(codePoint: number, fallback: string): string {
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {
return fallback;
}
return String.fromCodePoint(codePoint);
}
function decodeHtmlEntities(value: string): string {
return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (entity, body: string) => {
const normalized = body.toLowerCase();
if (normalized.startsWith("#x")) {
return decodeHtmlCodePoint(Number.parseInt(normalized.slice(2), 16), entity);
}
if (normalized.startsWith("#")) {
return decodeHtmlCodePoint(Number.parseInt(normalized.slice(1), 10), entity);
}
return HTML_ENTITIES[normalized] ?? entity;
});
}
export function cleanSummaryText(value: string): string {
return decodeHtmlEntities(stripWrappingQuotes(value))
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
.replace(/<br\s*\/?>/gi, " ")
.replace(/<\/?[a-z][a-z0-9:-]*(?:\s+[^>]*)?>/gi, " ")
.replace(/\s+/g, " ")
.trim();
}
export function toFrontmatterString(value: unknown): string | undefined {
if (typeof value === "string") {
return stripWrappingQuotes(value);
@@ -94,10 +133,11 @@ export function extractSummaryFromBody(body: string, maxLen: number): string {
.replace(/\*(.+?)\*/g, "$1")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/`([^`]+)`/g, "$1");
const summaryText = cleanSummaryText(cleanText);
if (cleanText.length > 20) {
if (cleanText.length <= maxLen) return cleanText;
return `${cleanText.slice(0, maxLen - 3)}...`;
if (summaryText.length > 20) {
if (summaryText.length <= maxLen) return summaryText;
return `${summaryText.slice(0, maxLen - 3)}...`;
}
}
@@ -39,6 +39,22 @@ test("buildHtmlDocument includes optional meta tags and code theme CSS", () => {
assert.match(html, /<article>Hello<\/article>/);
});
test("buildHtmlDocument escapes head metadata attributes", () => {
const html = buildHtmlDocument(
{
title: `Doc <draft>`,
author: `Bao"yu`,
description: `<p style="color: red">Summary & notes</p>`,
},
"",
"",
);
assert.match(html, /<title>Doc &lt;draft&gt;<\/title>/);
assert.match(html, /meta name="author" content="Bao&quot;yu"/);
assert.match(html, /meta name="description" content="&lt;p style=&quot;color: red&quot;&gt;Summary &amp; notes&lt;\/p&gt;"/);
});
test("normalizeCssText and normalizeInlineCss replace variables and strip declarations", () => {
const rawCss = `
:root { --md-primary-color: #000; --md-font-size: 12px; --foreground: 0 0% 5%; }
+8 -3
View File
@@ -45,19 +45,24 @@ export function loadCodeThemeCss(themeName: string): string {
}
export function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string, codeThemeCss?: string): string {
const escapeHtmlAttribute = (value: string) => value
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const lines = [
"<!doctype html>",
"<html>",
"<head>",
' <meta charset="utf-8" />',
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
` <title>${meta.title}</title>`,
` <title>${escapeHtmlAttribute(meta.title)}</title>`,
];
if (meta.author) {
lines.push(` <meta name="author" content="${meta.author}" />`);
lines.push(` <meta name="author" content="${escapeHtmlAttribute(meta.author)}" />`);
}
if (meta.description) {
lines.push(` <meta name="description" content="${meta.description}" />`);
lines.push(` <meta name="description" content="${escapeHtmlAttribute(meta.description)}" />`);
}
lines.push(` <style>${css}</style>`);
if (codeThemeCss) {
@@ -24,6 +24,10 @@ Soft macaron pastel color blocks on warm cream
Coral Red (#E8655A) for key data, warnings, and emphasis highlights. Use sparingly — one or two elements per illustration.
## Semantic Constraint
Soft pastel macaron color palette. Use block colors as rounded card backgrounds for distinct information sections. Accent coral red sparingly for emphasis on key terms only. Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Educational content, knowledge sharing, concept explainers, tutorials, tech summaries, onboarding materials
@@ -0,0 +1,42 @@
# mono-ink
Black ink on pure white with sparse semantic accent colors
## Background
- Color: Pure White (#FFFFFF)
- Texture: Clean, no grain, no tint
## Colors
| Role | Color | Hex | Usage |
|------|-------|-----|-------|
| Background | Pure White | #FFFFFF | Canvas |
| Primary | Near Black | #1A1A1A | All lines, text, figures, arrows |
| Accent (risk/emphasis) | Coral Red | #E8655A | Risk, problem, gap, key emphasis |
| Accent (positive) | Muted Teal | #5FA8A8 | Positive, solution, "after" state |
| Accent (neutral tag) | Dusty Lavender | #9B8AB5 | Neutral tags, category labels |
| Soft Fill | Pale Gray | #F0F0F0 | Subtle zone backgrounds (optional) |
## Accent
Use black ink for all structural elements — lines, text, figures. Accent colors appear only for semantic highlighting: coral red for risks/gaps/problems, muted teal for positive/solution/after-states, dusty lavender for neutral category tags. Total colored pixels must remain under 10% of canvas. Pale gray may back a subtle zone but must never dominate.
## Semantic Constraint
Black ink on white canvas. Accent colors for semantic highlighting only — total colored pixels under 10% of canvas. Do NOT render color names, hex codes, or role labels as visible text in the image.
## Compatible With
- `ink-notes` (primary, default pairing)
- `minimal` (strict monochrome variation, drops the style's built-in accent)
- `sketch` (pencil + ink hybrid look)
## Not Recommended With
- `sketch-notes` — its "no pure white backgrounds" rule conflicts
- `warm`, `elegant`, `watercolor`, `fantasy-animation` — color-heavy by design, mono-ink strips their identity
## Best For
Professional visual notes, Before/After essays, tech manifestos, framework analogies, whiteboard-presentation explainers
@@ -24,6 +24,10 @@ Vibrant neon colors on dark backgrounds
Hot Pink (#FF1493) for primary emphasis. High contrast neon-on-dark creates immediate visual impact.
## Semantic Constraint
Vibrant neon-on-dark palette. High contrast, immediate visual impact. Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Gaming, retro tech, 80s/90s nostalgic content, bold editorial, trend and pop culture
@@ -23,6 +23,10 @@ Warm earth tones on soft peach, no cool colors
Warm Orange (#ED8936) for primary emphasis. Warm-only palette — no cool colors (no green, blue, purple). Modern-retro feel.
## Semantic Constraint
Warm earth tone palette. Warm-only — no cool colors (no green, blue, purple). Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Product showcases, team introductions, feature grids, brand content, personal growth, lifestyle
@@ -67,6 +67,17 @@ STYLE (from reference):
---
## Color Specification Rules
Colors in prompts use hex codes for **rendering guidance only** — they tell the model which colors to use, NOT what text to display.
**⚠️ CRITICAL**: Image generation models sometimes render color names and hex values as visible text labels in the image (e.g., painting "Macaron Blue #A8D8EA" as a label). This must be prevented.
**Add to ALL prompts that contain a COLORS section**:
> Color values (#hex) and color names are rendering guidance only — do NOT display color names, hex codes, or palette labels as visible text in the image.
---
## Character Rendering
When depicting people:
@@ -193,6 +204,22 @@ ELEMENTS: Rounded cards with dashed/solid borders, wavy hand-drawn arrows with l
STYLE: Color fills don't completely fill outlines, hand-drawn lettering, generous white space
```
**Flowchart + ink-notes + mono-ink palette**:
```
Professional hand-drawn visual-note flowchart on pure white. Black ink line work
with slight wobble, à la Mike Rohde sketchnoting.
PALETTE: mono-ink — black ink dominant, sparse semantic accents
COLORS: Pure White background (#FFFFFF), Near Black (#1A1A1A) for all lines,
text, and figures; Coral Red (#E8655A) only for risk/emphasis,
Muted Teal (#5FA8A8) only for positive/solution states
ELEMENTS: Left-to-right stage boxes with rounded-rect frames, wavy hand-drawn
arrows between stages, simple stick-figure characters with role
labels above (e.g., "ML Engineer", "Team Lead"), dashed-border box
for future/empty stage, small doodle icons per stage
STYLE: Hand-lettered titles (bold, oversized), handwritten stage labels and
annotations, generous white space, bottom tagline summarizing takeaway
```
### Comparison
```
@@ -227,6 +254,28 @@ COLORS: Left side Warm Orange (#ED8936), Right side Terracotta (#C05621),
ELEMENTS: Bold icons, black outlines, centered divider line
```
**Comparison + ink-notes + mono-ink palette** (Before/After, Traditional vs New):
```
Professional hand-drawn sketchnote comparison on pure white. Black ink line work
with slight wobble, à la Mike Rohde sketchnoting.
PALETTE: mono-ink — black ink dominant, sparse semantic accents
COLORS: Pure White background (#FFFFFF), Near Black (#1A1A1A) for all outlines,
text, figures, arrows; Coral Red (#E8655A) reserved for risks/gaps
(left/Before side); Muted Teal (#5FA8A8) reserved for positives
(right/After side). Color accents under 10% of canvas.
LAYOUT: Left | Right split with vertical hand-drawn divider. Hand-lettered
"Before" label (top-left) and "After" label (top-right).
LEFT SIDE: Stick figure(s) with role label above, speech bubble showing the
pain point, bulleted pain-point list in handwritten text.
RIGHT SIDE: Stick figure(s) showing the new state, bulleted improvement list,
small positive-action icons.
BRIDGE: Curved hand-drawn "mindset shift" arrow bridging left → right with
small inline label describing the shift.
BOTTOM: Single-line hand-lettered tagline summarizing the takeaway.
STYLE: Hand-lettered headings (bold, oversized), handwritten body annotations,
generous white space, no computer fonts, no gradients, no shadows.
```
### Framework
```
@@ -259,6 +308,27 @@ COLORS: Soft Peach background (#FFECD2), nodes in Warm Orange (#ED8936),
ELEMENTS: Rounded rectangles or circles for nodes, thick connecting lines
```
**Framework + ink-notes + mono-ink palette** (command center, OS analogy):
```
Professional hand-drawn sketchnote framework on pure white. Black ink line work
with slight wobble, à la Mike Rohde sketchnoting.
PALETTE: mono-ink — black ink dominant, sparse semantic accents
COLORS: Pure White background (#FFFFFF), Near Black (#1A1A1A) for all lines,
text, figures; Dusty Lavender (#9B8AB5) for neutral category tags only;
Coral Red (#E8655A) for emphasis sparingly. Color accents under 10%.
STRUCTURE: Central rounded-rectangle frame as "the system" with hand-lettered
title inside. Inner layer of labeled sub-components (node labels
above each). Outer layer of feeder arrows from stick-figure
operators/users with role labels.
ELEMENTS: Stick figures at the edges with role tags ("Team Lead", "Operator"),
wavy hand-drawn connector arrows with small inline labels, small
doodle icons per component, dashed-border placeholder(s) for
future/empty capabilities.
BOTTOM: Single-line hand-lettered tagline.
STYLE: Hand-lettered headings, handwritten annotations, generous white space,
no computer fonts, no gradients.
```
### Timeline
```
@@ -24,6 +24,9 @@
| `warm-knowledge` | `infographic` | `vector-illustration` | `warm` | Product showcases, team intros, feature cards, brand content |
| `edu-visual` | `infographic` | `vector-illustration` | `macaron` | Knowledge summaries, concept explainers, educational articles |
| `hand-drawn-edu` | `flowchart` | `sketch-notes` | `macaron` | Hand-drawn educational diagrams, process explainers, onboarding visuals |
| `ink-notes-compare` | `comparison` | `ink-notes` | `mono-ink` | Before/After essays, Traditional vs New, OS-style comparisons, mindset-shift narratives |
| `ink-notes-flow` | `flowchart` | `ink-notes` | `mono-ink` | Professional process explainers, workforce pipelines, hand-drawn technical walkthroughs |
| `ink-notes-framework` | `framework` | `ink-notes` | `mono-ink` | System analogies, command-center diagrams, architecture-as-metaphor, tech manifestos |
### Data & Analysis
@@ -60,7 +63,8 @@ Use this table during Step 3 to recommend presets based on Step 2 content analys
| Tutorial | `tutorial` | `process-flow`, `knowledge-base`, `edu-visual` |
| Methodology / Framework | `system-design` | `architecture`, `process-flow` |
| Data / Metrics | `data-report` | `versus`, `tech-explainer` |
| Comparison / Review | `versus` | `business-compare`, `editorial-poster` |
| Comparison / Review | `versus` | `business-compare`, `editorial-poster`, `ink-notes-compare` |
| Manifesto / Mindset shift / Professional visual note | `ink-notes-compare` | `ink-notes-framework`, `ink-notes-flow` |
| Narrative / Personal | `storytelling` | `lifestyle`, `evolution` |
| Opinion / Editorial | `opinion-piece` | `cinematic`, `editorial-poster` |
| Historical / Timeline | `history` | `evolution` |
@@ -43,6 +43,7 @@ Use Core Styles for most cases. See full Style Gallery below for granular contro
| `sketch` | Raw pencil notebook style | Brainstorming, creative exploration |
| `screen-print` | Bold poster art, halftone textures, limited colors | Opinion, editorial, cultural, cinematic |
| `sketch-notes` | Soft hand-drawn warm notes | Educational, warm notes |
| `ink-notes` | Black ink on pure white, sparse semantic accents, hand-lettered (à la Mike Rohde's sketchnoting) | Before/After essays, tech manifestos, framework analogies |
| `vintage` | Aged parchment historical | Historical, heritage |
Full specifications: `references/styles/<style>.md`
@@ -81,6 +82,7 @@ Full specifications: `references/styles/<style>.md`
| How-to, steps, workflow, process, tutorial | flowchart | vector-illustration, notion |
| Framework, model, architecture, principles | framework | blueprint, vector-illustration |
| vs, pros/cons, before/after, alternatives | comparison | vector-illustration, notion |
| Manifesto, mindset shift, workforce, OS, whiteboard, professional visual note | comparison / framework | ink-notes |
| Story, emotion, journey, experience, personal | scene | warm, watercolor |
| History, timeline, progress, evolution | timeline | elegant, warm |
| Productivity, SaaS, tool, app, software | infographic | notion, vector-illustration |
@@ -206,6 +208,7 @@ Palettes override a style's default colors. Combine any style with any palette:
| `macaron` | Soft pastel blocks (blue, mint, lavender, peach) on warm cream | Educational, knowledge, tutorials |
| `warm` | Warm earth tones (orange, terracotta, gold) on soft peach, no cool colors | Brand, product, lifestyle |
| `neon` | Vibrant neon (pink, cyan, yellow) on dark purple | Gaming, retro, pop culture |
| `mono-ink` | Black ink on pure white with sparse semantic accents (coral red, muted teal, dusty lavender) | Professional visual notes, Before/After, manifestos |
Full specifications: `references/palettes/<palette>.md`
@@ -0,0 +1,90 @@
# ink-notes
Professional black-ink visual notes on pure white, in the tradition of Mike Rohde's sketchnoting
## Compared to sketch-notes
`ink-notes` and `sketch-notes` are distinct styles. Pick the right one:
| | `sketch-notes` | `ink-notes` |
|---|---|---|
| Background | Warm Off-White #FAF8F0 with paper grain | Pure White #FFFFFF, clean, no texture |
| Palette | Soft warm accents (orange, mustard, sage, light blue) | Black ink dominant + sparse semantic accents |
| Feel | Soft, warm, educational, approachable | Professional, structured, whiteboard-presentation |
| Best For | Friendly tutorials, onboarding, casual explainers | Before/After essays, tech manifestos, framework analogies |
When in doubt: warm & friendly → `sketch-notes`. Disciplined & professional → `ink-notes`.
## Design Aesthetic
Disciplined hand-drawn visual note. Confident black ink line work with slight wobble, hand-lettered typography, and sparse color accents used only for semantic emphasis. Feels like a skilled visual notetaker's whiteboard presentation — clean, structured, intentionally hand-drawn rather than decorative.
## Background
- Color: Pure White (#FFFFFF)
- Texture: Clean, no grain, no tint
## Color Palette
| Role | Color | Hex | Usage |
|------|-------|-----|-------|
| Background | Pure White | #FFFFFF | Canvas |
| Primary Ink | Near Black | #1A1A1A | All lines, text, figures, arrows |
| Accent Warm | Coral Red | #E8655A | Risk, problem, gap, emphasis |
| Accent Cool | Muted Teal | #5FA8A8 | Positive, solution, "after" state |
| Accent Neutral | Dusty Lavender | #9B8AB5 | Neutral tags, category labels |
| Soft Fill | Pale Gray | #F0F0F0 | Subtle zone backgrounds (optional) |
Color accents must remain under 10% of canvas area and only carry semantic meaning. Black ink does the structural work.
## Visual Elements
- Black ink line work with intentional slight wobble on all strokes
- Hand-lettered titles (bold, oversized) and handwritten body annotations
- Simple stick-figure characters with expressive poses (pointing, thinking, walking)
- Role labels above characters (e.g., "Tech Lead", "Compliance Officer")
- Thought bubbles and speech bubbles with hand-drawn outlines
- Rounded-rectangle frames for content groupings
- Dashed-border rectangles for placeholder, "coming next", or empty states
- Curvy hand-drawn arrows with small inline labels
- Vertical or horizontal dividers between comparison zones ("Before" | "After")
- "Mindset shift" curved arrow bridging two zones
- Bottom tagline: single-line hand-lettered conclusion that points the takeaway
- Stars, asterisks, underlines for emphasis — used sparingly
## Style Rules
### Do
- Keep background pure white with no texture or tint
- Let black ink dominate outlines, text, and figures
- Use accent colors only for semantic highlighting
- Keep all type hand-lettered — no computer-generated fonts
- Maintain confident line quality (wobble, not mess)
- Include a bottom tagline summarizing the main takeaway
- Structure content into clear zones with visible dividers
- Use dashed boxes for future, empty, or placeholder states
### Don't
- Use warm off-white or paper-textured backgrounds (that is sketch-notes' territory)
- Fill large zones with color blocks
- Use more than 3 accent colors per image
- Use perfect geometric shapes — preserve hand-drawn wobble
- Clutter with decorative doodles; every element must carry meaning
- Use gradients, shadows, or computer-generated fonts
## Type Compatibility
| Type | Rating | Notes |
|------|--------|-------|
| comparison | ✓✓ | Best fit — Before/After, Traditional vs New, side-by-side contrasts |
| framework | ✓✓ | OS-style command centers, layered architectures, organizational models |
| flowchart | ✓✓ | Process explainers with labeled stages, workforce pipelines |
| infographic | ✓ | Multi-zone technical summaries, manifesto-style posters |
| timeline | ✓ | Hand-drawn horizontal arrow with era markers and milestones |
| scene | ✗ | Not recommended — lacks scenic space |
## Best For
Product and engineering essays, tech manifestos, framework introductions, Before/After narratives, OS-level comparisons, workforce and organizational analogies, visual summaries of talks, thought-leadership articles
@@ -0,0 +1,85 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import test from "node:test";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(import.meta.dirname, "..", "..", "..");
const scriptPath = path.join(repoRoot, "skills", "baoyu-article-illustrator", "scripts", "build-batch.ts");
async function makeFixture(): Promise<{
root: string;
outlinePath: string;
promptsDir: string;
outputPath: string;
}> {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "baoyu-article-illustrator-build-batch-"));
const outlinePath = path.join(root, "outline.md");
const promptsDir = path.join(root, "prompts");
const outputPath = path.join(root, "batch.json");
await fs.mkdir(promptsDir, { recursive: true });
await fs.writeFile(
outlinePath,
`## Illustration 1
**Position**: demo
**Purpose**: demo
**Visual Content**: demo
**Filename**: 01-demo.png
`,
);
await fs.writeFile(path.join(promptsDir, "01-demo.md"), "A demo prompt\n");
return { root, outlinePath, promptsDir, outputPath };
}
async function runBuildBatch(args: string[]): Promise<void> {
await execFileAsync(process.execPath, ["--import", "tsx", scriptPath, ...args], {
cwd: repoRoot,
});
}
test("build-batch omits default model so baoyu-imagine can resolve env or EXTEND defaults", async () => {
const fixture = await makeFixture();
await runBuildBatch([
"--outline",
fixture.outlinePath,
"--prompts",
fixture.promptsDir,
"--output",
fixture.outputPath,
]);
const batch = JSON.parse(await fs.readFile(fixture.outputPath, "utf8")) as {
tasks: Array<Record<string, unknown>>;
};
assert.equal(batch.tasks.length, 1);
assert.equal(batch.tasks[0]?.provider, "replicate");
assert.equal(Object.hasOwn(batch.tasks[0]!, "model"), false);
});
test("build-batch preserves explicit model overrides", async () => {
const fixture = await makeFixture();
await runBuildBatch([
"--outline",
fixture.outlinePath,
"--prompts",
fixture.promptsDir,
"--output",
fixture.outputPath,
"--model",
"acme/custom-model",
]);
const batch = JSON.parse(await fs.readFile(fixture.outputPath, "utf8")) as {
tasks: Array<Record<string, unknown>>;
};
assert.equal(batch.tasks[0]?.model, "acme/custom-model");
});
@@ -8,7 +8,7 @@ type CliArgs = {
outputPath: string | null;
imagesDir: string | null;
provider: string;
model: string;
model: string | null;
aspectRatio: string;
quality: string;
jobs: number | null;
@@ -30,7 +30,7 @@ Options:
--output <path> Path to output batch.json
--images-dir <path> Directory for generated images
--provider <name> Provider for baoyu-imagine batch tasks (default: replicate)
--model <id> Model for baoyu-imagine batch tasks (default: google/nano-banana-pro)
--model <id> Explicit model for baoyu-imagine batch tasks (default: resolved by baoyu-imagine config/env)
--ar <ratio> Aspect ratio for all tasks (default: 16:9)
--quality <level> Quality for all tasks (default: 2k)
--jobs <count> Recommended worker count metadata (optional)
@@ -44,7 +44,7 @@ function parseArgs(argv: string[]): CliArgs {
outputPath: null,
imagesDir: null,
provider: "replicate",
model: "google/nano-banana-pro",
model: null,
aspectRatio: "16:9",
quality: "2k",
jobs: null,
@@ -132,15 +132,16 @@ async function main(): Promise<void> {
}
const imageDir = args.imagesDir ?? path.dirname(args.outputPath);
tasks.push({
const task: Record<string, unknown> = {
id: `illustration-${String(entry.index).padStart(2, "0")}`,
promptFiles: [promptFile],
image: path.join(imageDir, entry.filename),
provider: args.provider,
model: args.model,
ar: args.aspectRatio,
quality: args.quality,
});
};
if (args.model) task.model = args.model;
tasks.push(task);
}
const output: Record<string, unknown> = { tasks };
+4 -4
View File
@@ -1,6 +1,6 @@
---
name: baoyu-cover-image
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 10 color palettes and 7 rendering styles. Supports cinematic (2.35:1), widescreen (16:9), and square (1:1) aspects. Use when user asks to "generate cover image", "create article cover", or "make cover".
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 11 color palettes and 7 rendering styles. Supports cinematic (2.35:1), widescreen (16:9), and square (1:1) aspects. Use when user asks to "generate cover image", "create article cover", or "make cover".
version: 1.56.1
metadata:
openclaw:
@@ -39,7 +39,7 @@ Generate elegant cover images for articles with 5-dimensional customization.
| Option | Description |
|--------|-------------|
| `--type <name>` | hero, conceptual, typography, metaphor, scene, minimal |
| `--palette <name>` | warm, elegant, cool, dark, earth, vivid, pastel, mono, retro, duotone |
| `--palette <name>` | warm, elegant, cool, dark, earth, vivid, pastel, mono, retro, duotone, macaron |
| `--rendering <name>` | flat-vector, hand-drawn, painterly, digital, pixel, chalk, screen-print |
| `--style <name>` | Preset shorthand (see [Style Presets](references/style-presets.md)) |
| `--text <level>` | none, title-only, title-subtitle, text-rich |
@@ -56,7 +56,7 @@ Generate elegant cover images for articles with 5-dimensional customization.
| Dimension | Values | Default |
|-----------|--------|---------|
| **Type** | hero, conceptual, typography, metaphor, scene, minimal | auto |
| **Palette** | warm, elegant, cool, dark, earth, vivid, pastel, mono, retro, duotone | auto |
| **Palette** | warm, elegant, cool, dark, earth, vivid, pastel, mono, retro, duotone, macaron | auto |
| **Rendering** | flat-vector, hand-drawn, painterly, digital, pixel, chalk, screen-print | auto |
| **Text** | none, title-only, title-subtitle, text-rich | title-only |
| **Mood** | subtle, balanced, bold | balanced |
@@ -69,7 +69,7 @@ Auto-selection rules: [references/auto-selection.md](references/auto-selection.m
**Types**: hero, conceptual, typography, metaphor, scene, minimal
→ Details: [references/types.md](references/types.md)
**Palettes**: warm, elegant, cool, dark, earth, vivid, pastel, mono, retro, duotone
**Palettes**: warm, elegant, cool, dark, earth, vivid, pastel, mono, retro, duotone, macaron
→ Details: [references/palettes/](references/palettes/)
**Renderings**: flat-vector, hand-drawn, painterly, digital, pixel, chalk, screen-print
@@ -27,6 +27,7 @@ When a dimension is omitted, select based on content signals.
| Zen, focus, essential, pure, simple | `mono` |
| History, vintage, retro, classic, exploration | `retro` |
| Movie poster, album cover, concert, cinematic, dramatic, two-color | `duotone` |
| Education, tutorial, knowledge, onboarding, concept explainer | `macaron` |
## Auto Rendering Selection
@@ -18,7 +18,7 @@ watermark:
preferred_type: null # hero|conceptual|typography|metaphor|scene|minimal or null for auto-select
preferred_palette: null # warm|elegant|cool|dark|earth|vivid|pastel|mono|retro or null for auto-select
preferred_palette: null # warm|elegant|cool|dark|earth|vivid|pastel|mono|retro|duotone|macaron or null for auto-select
preferred_rendering: null # flat-vector|hand-drawn|painterly|digital|pixel|chalk or null for auto-select
@@ -21,6 +21,10 @@ Technical, professional, precise
- Technical schematics and diagrams
- Geometric precision elements
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Architecture, system design, API, technical documentation, engineering, data analysis
@@ -21,6 +21,10 @@ Cinematic, premium, atmospheric
- Silhouettes with backlit edges
- Subtle gradient backgrounds
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Entertainment, premium brands, cinematic storytelling, dark mode, gaming, night themes
@@ -34,6 +34,10 @@ Choose ONE pair based on content mood. The two colors dominate the entire image:
- Minimal use of third color (only for small highlights)
- High contrast figure-ground relationships
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Movie posters, album covers, concert prints, dramatic announcements, cinematic content, bold branding, editorial covers, artistic campaigns
@@ -21,6 +21,10 @@ Natural, organic, grounded
- Botanical illustrations
- Earthy textures and natural patterns
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Nature, wellness, eco, organic, travel, sustainability, outdoor topics, slow living
@@ -21,6 +21,10 @@ Sophisticated, refined, understated luxury
- Refined geometric patterns
- Balanced, symmetrical compositions
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Business, professional, thought leadership, luxury, corporate communications
@@ -0,0 +1,30 @@
# macaron
Soft macaron pastel color blocks on warm cream
## Color Palette
| Role | Color | Hex |
|------|-------|-----|
| Primary 1 | Sky Blue | #A8D8EA |
| Primary 2 | Mint Green | #B5E5CF |
| Primary 3 | Lavender | #D5C6E0 |
| Background | Warm Cream | #F5F0E8 |
| Background Alt | Peach | #FFD5C2 |
| Accent 1 | Coral Red | #E8655A |
| Accent 2 | Deep Charcoal | #2D2D2D |
## Decorative Hints
- Rounded pastel color blocks
- Subtle warm paper grain texture
- Soft shadows, no hard edges
- Gentle gradient transitions between zones
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Educational content, knowledge sharing, concept explainers, tutorials, tech summaries, onboarding materials
@@ -21,6 +21,10 @@ Clean, focused, essential
- Single focal point emphasis
- Stark contrast between elements
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Zen, focus, essential concepts, pure, simple, minimalist philosophy, clean design
@@ -21,6 +21,10 @@ Gentle, whimsical, soft
- Soft shadows and gentle highlights
- Storybook-style elements
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Fantasy, children, gentle content, creative, whimsical, casual, beginner guides
@@ -25,6 +25,10 @@ Nostalgic, vintage, classic
- Pill-shaped clouds, small dots and stars
- Classic icons and retro motifs
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
History, vintage, retro, classic, exploration, retrospectives, throwback content, creative proposals, educational
@@ -21,6 +21,10 @@ Energetic, bold, attention-grabbing
- Dramatic lighting effects
- High-energy visual compositions
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Product launch, gaming, promotion, event, marketing, announcements, brand showcases
@@ -21,6 +21,10 @@ Friendly, approachable, human-centered
- Hearts, smiling faces, friendly icons
- Warm gradient overlays
## Semantic Constraint
Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best For
Personal growth, lifestyle, education, human stories, emotion, community
@@ -23,6 +23,7 @@
| `vintage` | `retro` | `hand-drawn` |
| `warm` | `warm` | `hand-drawn` |
| `warm-flat` | `warm` | `flat-vector` |
| `hand-drawn-edu` | `macaron` | `hand-drawn` |
| `watercolor` | `earth` | `painterly` |
| `poster-art` | `retro` | `screen-print` |
| `mondo` | `mono` | `screen-print` |
@@ -62,6 +62,7 @@ Visual composition:
- Decorative: [palette-specific elements that reinforce content theme]
Color scheme: [primary, background, accent from palette definition, adjusted by mood]
Color constraint: Color values (#hex) and color names are rendering guidance only — do NOT display color names, hex codes, or palette labels as visible text in the image.
Rendering notes: [key characteristics from rendering definition — lines, texture, depth, element style]
Type notes: [key characteristics from type definition]
Palette notes: [key characteristics from palette definition]
+247
View File
@@ -0,0 +1,247 @@
---
name: baoyu-diagram
description: Create professional, dark-themed SVG diagrams of any type — architecture diagrams, flowcharts, sequence diagrams, structural diagrams, mind maps, timelines, illustrative/conceptual diagrams, and more. Use this skill whenever the user asks for any kind of technical or conceptual diagram, visualization of a system, process flow, data flow, component relationship, network topology, decision tree, org chart, state machine, or any visual representation of structure/logic/process. Also trigger when the user says "画个图" "画一个架构图" "diagram" "flowchart" "sequence diagram" "draw me a ..." or uploads content and asks to visualize it. Output is always a standalone .svg file.
---
# Diagram Generator
Create professional SVG diagrams across multiple diagram types. All output is a single self-contained `.svg` file with embedded styles and fonts.
## Supported Diagram Types
| Type | When to Use | Key Characteristics |
|------|-------------|-------------------|
| **Architecture** | System components & relationships | Grouped boxes, connection arrows, region boundaries |
| **Flowchart** | Decision logic, process steps | Diamond decisions, rounded step boxes, directional flow |
| **Sequence** | Time-ordered interactions between actors | Vertical lifelines, horizontal messages, activation bars |
| **Structural** | Class diagrams, ER diagrams, org charts | Compartmented boxes, typed relationships (inheritance, composition) |
| **Mind Map** | Brainstorming, topic exploration | Central node, radiating branches, organic layout |
| **Timeline** | Chronological events | Horizontal/vertical axis, event markers, period spans |
| **Illustrative** | Conceptual explanations, comparisons | Free-form layout, icons, annotations, visual metaphors |
| **State Machine** | State transitions, lifecycle | Rounded state nodes, labeled transitions, start/end markers |
| **Data Flow** | Data transformation pipelines | Process bubbles, data stores, external entities |
## Design System
### Color Palette
Semantic colors for component categories:
| Category | Fill (rgba) | Stroke | Use For |
|----------|-------------|--------|---------|
| Primary | `rgba(8, 51, 68, 0.4)` | `#22d3ee` (cyan) | Frontend, user-facing, inputs |
| Secondary | `rgba(6, 78, 59, 0.4)` | `#34d399` (emerald) | Backend, services, processing |
| Tertiary | `rgba(76, 29, 149, 0.4)` | `#a78bfa` (violet) | Database, storage, persistence |
| Accent | `rgba(120, 53, 15, 0.3)` | `#fbbf24` (amber) | Cloud, infrastructure, regions |
| Alert | `rgba(136, 19, 55, 0.4)` | `#fb7185` (rose) | Security, errors, warnings |
| Connector | `rgba(251, 146, 60, 0.3)` | `#fb923c` (orange) | Buses, queues, middleware |
| Neutral | `rgba(30, 41, 59, 0.5)` | `#94a3b8` (slate) | External, generic, unknown |
| Highlight | `rgba(59, 130, 246, 0.3)` | `#60a5fa` (blue) | Active state, focus, current step |
For flowcharts and sequence diagrams, assign colors by role (actor, decision, process) rather than by technology.
### Typography
Use embedded SVG `@font-face` or system monospace fallback:
```svg
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&amp;display=swap');
text { font-family: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace; }
</style>
```
Font sizes by role:
- **Title:** 16px, weight 700
- **Component name:** 11-12px, weight 600
- **Sublabel / description:** 9px, weight 400, color `#94a3b8`
- **Annotation / note:** 8px, weight 400
- **Tiny label (on arrows):** 7-8px
### Core Visual Elements
**Background:** `#0f172a` (slate-900) with subtle grid:
```svg
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#0f172a"/>
<rect width="100%" height="100%" fill="url(#grid)"/>
```
**Arrowhead marker (standard):**
```svg
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b"/>
</marker>
```
**Arrowhead marker (colored) — create per-color as needed:**
```svg
<marker id="arrow-cyan" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#22d3ee"/>
</marker>
```
**Open arrowhead (for async/return messages):**
```svg
<marker id="arrow-open" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polyline points="0 0, 10 3.5, 0 7" fill="none" stroke="#64748b" stroke-width="1.5"/>
</marker>
```
### SVG Structure & Layering
Draw elements in this order to get correct z-ordering (SVG paints back-to-front):
1. Background fill + grid pattern
2. Region/group boundaries (dashed outlines)
3. Connection arrows and lines
4. Opaque masking rects (same position as component boxes, `fill="#0f172a"`)
5. Component boxes (semi-transparent fill + stroke)
6. Text labels
7. Legend (bottom-right or bottom area, outside all boundaries)
8. Title block (top-left)
The opaque masking rect trick is essential — semi-transparent component fills will show arrows underneath without it:
```svg
<!-- Mask layer: opaque background to hide arrows -->
<rect x="100" y="100" width="160" height="60" rx="6" fill="#0f172a"/>
<!-- Visual layer: styled component -->
<rect x="100" y="100" width="160" height="60" rx="6" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<text x="180" y="125" fill="white" font-size="11" font-weight="600" text-anchor="middle">API Gateway</text>
<text x="180" y="141" fill="#94a3b8" font-size="9" text-anchor="middle">Kong / Nginx</text>
```
### Spacing Rules
These prevent overlapping — follow them strictly:
- **Component box height:** 50-70px (standard), 80-120px (large/complex)
- **Minimum gap between components:** 40px vertical, 30px horizontal
- **Arrow label clearance:** 10px from any box edge
- **Region boundary padding:** 20px inside edges around contained components
- **Legend placement:** At least 20px below the lowest diagram element
- **Title block:** 20px from top-left, outside diagram content area
- **viewBox:** Always extend to fit all content + 30px padding on all sides
### Component Patterns
**Standard box (service/process):**
```svg
<rect x="X" y="Y" width="160" height="60" rx="6" fill="#0f172a"/>
<rect x="X" y="Y" width="160" height="60" rx="6" fill="FILL" stroke="STROKE" stroke-width="1.5"/>
<text x="CX" y="Y+24" fill="white" font-size="11" font-weight="600" text-anchor="middle">Name</text>
<text x="CX" y="Y+40" fill="#94a3b8" font-size="9" text-anchor="middle">description</text>
```
**Decision diamond (flowchart):**
```svg
<g transform="translate(CX, CY)">
<polygon points="0,-35 50,0 0,35 -50,0" fill="#0f172a"/>
<polygon points="0,-35 50,0 0,35 -50,0" fill="rgba(120,53,15,0.3)" stroke="#fbbf24" stroke-width="1.5"/>
<text y="4" fill="white" font-size="10" font-weight="600" text-anchor="middle">Condition?</text>
</g>
```
**Database cylinder:**
```svg
<g transform="translate(X, Y)">
<rect x="0" y="10" width="120" height="50" rx="2" fill="#0f172a"/>
<ellipse cx="60" cy="10" rx="60" ry="12" fill="#0f172a"/>
<ellipse cx="60" cy="60" rx="60" ry="12" fill="#0f172a"/>
<rect x="0" y="10" width="120" height="50" fill="rgba(76,29,149,0.4)"/>
<ellipse cx="60" cy="10" rx="60" ry="12" fill="rgba(76,29,149,0.4)" stroke="#a78bfa" stroke-width="1.5"/>
<ellipse cx="60" cy="60" rx="60" ry="12" fill="rgba(76,29,149,0.4)" stroke="#a78bfa" stroke-width="1.5"/>
<line x1="0" y1="10" x2="0" y2="60" stroke="#a78bfa" stroke-width="1.5"/>
<line x1="120" y1="10" x2="120" y2="60" stroke="#a78bfa" stroke-width="1.5"/>
<text x="60" y="40" fill="white" font-size="11" font-weight="600" text-anchor="middle">PostgreSQL</text>
</g>
```
**Region boundary:**
```svg
<rect x="X" y="Y" width="W" height="H" rx="12" fill="none" stroke="#fbbf24" stroke-width="1" stroke-dasharray="8,4"/>
<text x="X+12" y="Y+16" fill="#fbbf24" font-size="9" font-weight="600">AWS us-east-1</text>
```
**Security group:**
```svg
<rect x="X" y="Y" width="W" height="H" rx="8" fill="none" stroke="#fb7185" stroke-width="1" stroke-dasharray="4,4"/>
<text x="X+10" y="Y+14" fill="#fb7185" font-size="8" font-weight="500">VPC / Security Group</text>
```
## Type-Specific Layout Guidance
Determine this SKILL.md file's directory path as `{baseDir}`. Read the reference file for the specific diagram type before starting layout. Reference files are located at `{baseDir}/references/` and contain detailed layout algorithms and examples.
### Architecture Diagrams
→ Read `{baseDir}/references/architecture.md`
Key points: left-to-right or top-to-bottom data flow. Group related services in region boundaries. Use buses/connectors between layers. Place databases at the bottom or right.
### Flowcharts
→ Read `{baseDir}/references/flowchart.md`
Key points: top-to-bottom primary flow. Diamonds for decisions with Yes/No labels on exit arrows. Rounded rectangles for start/end. Use the Highlight color for the happy path.
### Sequence Diagrams
→ Read `{baseDir}/references/sequence.md`
Key points: actors as boxes at top, vertical dashed lifelines, horizontal arrows for messages (solid=sync, dashed=return). Time flows downward. Activation bars show processing. Number messages if complex.
### Structural Diagrams
→ Read `{baseDir}/references/structural.md`
Key points: compartmented boxes (name / attributes / methods for class diagrams). Relationship lines: solid with filled diamond=composition, solid with empty diamond=aggregation, dashed arrow=dependency, solid triangle=inheritance.
### Mind Maps
Free-form radiating layout from a central concept. Use organic curves (`<path>` with cubic beziers) for branches. Vary branch colors using the palette. Larger font for central node, decreasing as you go outward.
### Timelines
Horizontal or vertical axis line. Event markers as circles or diamonds on the axis. Description text offset to alternating sides to avoid overlap. Use color to categorize event types.
### State Machines
Rounded-rect states with double-border for composite states. Filled circle for initial state, bullseye for final state. Curved arrows for self-transitions. Label all transitions with `event [guard] / action` format.
## Output Rules
1. Output a **single `.svg` file** — no external dependencies except the Google Fonts import
2. Set `viewBox` to fit all content with 30px padding; do NOT set fixed `width`/`height` attributes (let the SVG scale responsively)
3. Include `xmlns="http://www.w3.org/2000/svg"` on the root `<svg>` element
4. Put all `<style>`, `<defs>`, markers, and patterns at the top of the SVG
5. Use `text-anchor="middle"` for centered labels; ensure text doesn't overflow boxes
6. **Chinese text support:** When labels contain Chinese characters, use `font-family: 'JetBrains Mono', 'Noto Sans SC', 'PingFang SC', sans-serif'` and increase box widths — CJK characters are wider
7. **Save location:** If the input is a file, save to `{inputFileDir}/diagram/`. Otherwise save to `{projectDir}/diagram/{topic-slug}/`. Create the directory if it doesn't exist
## Script
Determine this SKILL.md file's directory path as `{baseDir}`. Script path: `{baseDir}/scripts/main.ts`.
Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun.
### SVG → @2x PNG
After saving the SVG, convert it to a @2x PNG:
```bash
${BUN_X} {baseDir}/scripts/main.ts <svg-path> [options]
```
Options:
- `-s, --scale <n>` — Scale factor (default: 2)
- `-o, --output <path>` — Custom output path (default: `<input>@2x.png`)
- `--json` — JSON output
## Process
1. Identify the diagram type from the user's request
2. Read the relevant reference file if one exists for that type
3. Plan the layout: list all components, determine grouping and flow direction, calculate positions
4. Write the SVG following the layering order above
5. Verify spacing rules — no overlaps, legends outside boundaries, viewBox large enough
6. Save the SVG file
7. Run `${BUN_X} {baseDir}/scripts/main.ts <svg-path>` to generate @2x PNG
8. Present both files to the user
@@ -0,0 +1,74 @@
# Architecture Diagram Layout
## Flow Direction
Choose one primary direction:
- **Left-to-Right (LTR):** Best for data pipelines, request flows. Users/clients on left, data stores on right.
- **Top-to-Bottom (TTB):** Best for layered architectures. Clients at top, infrastructure at bottom.
## Layout Algorithm
1. **Identify layers:** Group components by role (clients, gateways, services, data, infrastructure)
2. **Assign columns (LTR) or rows (TTB):** One layer per column/row
3. **Within each layer:** Stack components vertically (LTR) or horizontally (TTB), 40px gap minimum
4. **Region boundaries:** Draw around groups that share infrastructure (e.g., "AWS us-east-1", "Kubernetes Cluster")
5. **Connectors:** Route arrows between layers. For buses/queues between layers, place a thin connector bar in the gap.
## Typical Layer Structure (LTR)
```
Col 1 (x=40) Col 2 (x=250) Col 3 (x=460) Col 4 (x=670)
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │────▶│ Gateway │─────▶│ Services │─────▶│ Database │
│ Layer │ │ Layer │ │ Layer │ │ Layer │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
```
Column spacing: 200-220px between column starts. Adjust if components are wider.
## Typical Layer Structure (TTB)
```
Row 1 (y=60): [ Browser ] [ Mobile App ] [ API Client ]
Row 2 (y=160): [ Load Balancer / API Gateway ]
Row 3 (y=280): [ Auth Svc ] [ User Svc ] [ Order Svc ]
Row 4 (y=400): [ Redis ] [ PostgreSQL ] [ S3 Bucket ]
```
Row spacing: 120-140px between row starts.
## Connection Routing
- Prefer straight horizontal or vertical lines
- For connections that would cross components, use two-segment (L-shaped) paths:
```svg
<path d="M x1,y1 L midX,y1 L midX,y2" fill="none" stroke="#64748b" marker-end="url(#arrow)"/>
```
- For busy diagrams, use `stroke-opacity="0.6"` on less important connections
- Label important connections with a text element near the midpoint
## Message Bus / Event Bus Pattern
When services communicate through a shared bus, draw it as a horizontal bar between the service layer:
```
Services: [ Svc A ] [ Svc B ] [ Svc C ]
│ │ │
Bus: ════╪══════════════╪════════════╪═══════
│ │ │
Data: [ DB A ] [ DB B ] [ Cache ]
```
Use the Connector color (orange) for the bus bar.
## Multi-Region / Multi-Cloud
Nest region boundaries:
- Outer boundary: Cloud provider (AWS, GCP)
- Inner boundary: Region or VPC
- Innermost: Availability zones or subnets
Use different dash patterns to distinguish nesting levels:
- Outer: `stroke-dasharray="12,4"`
- Middle: `stroke-dasharray="8,4"`
- Inner: `stroke-dasharray="4,4"`
@@ -0,0 +1,60 @@
# Flowchart Layout
## Shape Vocabulary
| Shape | Meaning | SVG Element |
|-------|---------|-------------|
| Rounded rect (large radius) | Start / End | `<rect rx="25">` |
| Rectangle | Process / Action | `<rect rx="6">` |
| Diamond | Decision | `<polygon>` rotated 45° |
| Parallelogram | Input / Output | `<polygon>` with skew |
| Cylinder | Data store | Ellipse + rect combo |
## Flow Direction
Primary flow: **top to bottom**. Branch flows go left/right from decisions.
## Layout Algorithm
1. **Identify the main path** (happy path / most common flow) — this runs straight down the center
2. **Branch from decisions:** "Yes" continues down center, "No" branches right (or left if space is tight)
3. **Merge paths:** Route branches back to the main path using L-shaped connectors
4. **Loop-backs:** Route upward on the far left/right side of the diagram with curved paths
## Spacing
- Step-to-step vertical gap: 60-80px (enough for arrow + optional label)
- Decision diamond height: 70px (point to point)
- Decision diamond width: 100px (point to point)
- Branch horizontal offset: 200px from center
- Merge connector clearance: 20px from any box
## Decision Labels
Place "Yes" / "No" (or "True" / "False", "是" / "否") labels directly on the exit arrows, 10px from the diamond edge:
```svg
<!-- Decision diamond at center (400, 200) -->
<!-- Yes: downward -->
<line x1="400" y1="235" x2="400" y2="300" stroke="#64748b" marker-end="url(#arrow)"/>
<text x="412" y="260" fill="#34d399" font-size="8">Yes</text>
<!-- No: rightward -->
<line x1="450" y1="200" x2="550" y2="200" stroke="#64748b" marker-end="url(#arrow)"/>
<text x="480" y="193" fill="#fb7185" font-size="8">No</text>
```
## Coloring Strategy
- **Start/End nodes:** Highlight color (blue)
- **Process steps:** Primary (cyan) or Secondary (emerald)
- **Decision diamonds:** Accent (amber) — they draw the eye naturally
- **Error/exception paths:** Alert (rose) dashed arrows
- **Happy path arrows:** Slightly brighter than branch arrows (`stroke-opacity` difference)
## Complex Flowcharts
For flowcharts with 10+ steps:
- Group related steps into swim lanes (vertical columns with header bars)
- Add a "phase" row header at the top of each swim lane
- Use the region boundary pattern from Architecture for swim lanes
@@ -0,0 +1,88 @@
# Sequence Diagram Layout
## Core Elements
| Element | Visual | Description |
|---------|--------|-------------|
| Actor/Participant | Box at top + dashed vertical lifeline | Each entity in the interaction |
| Sync message | Solid arrow → | Request or call |
| Async message | Open arrowhead → | Fire-and-forget |
| Return message | Dashed arrow ← | Response |
| Activation bar | Narrow filled rect on lifeline | Entity is processing |
| Self-message | Arrow looping back to same lifeline | Internal processing |
| Note | Rounded rect with folded corner | Annotation |
| Alt/Opt frame | Dashed boundary with label tab | Conditional block |
| Loop frame | Dashed boundary with "loop" tab | Repetition |
## Layout Algorithm
1. **Place actors** horizontally across the top, evenly spaced (150-200px apart)
2. **Draw lifelines** as vertical dashed lines from each actor box downward
3. **Place messages** as horizontal arrows between lifelines, top to bottom in time order
4. **Vertical spacing** between messages: 40-50px
5. **Activation bars:** 10px wide, centered on lifeline, spanning from incoming to outgoing message
## Actor Box
```svg
<!-- Actor box -->
<rect x="X" y="20" width="130" height="45" rx="6" fill="#0f172a"/>
<rect x="X" y="20" width="130" height="45" rx="6" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<text x="CX" y="47" fill="white" font-size="11" font-weight="600" text-anchor="middle">Actor Name</text>
<!-- Lifeline -->
<line x1="CX" y1="65" x2="CX" y2="BOTTOM" stroke="#334155" stroke-width="1" stroke-dasharray="6,4"/>
```
## Message Arrows
```svg
<!-- Sync message (solid arrow) -->
<line x1="FROM_CX" y1="Y" x2="TO_CX" y2="Y" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="MID_X" y="Y-8" fill="#e2e8f0" font-size="9" text-anchor="middle">methodCall()</text>
<!-- Return message (dashed arrow, reversed direction) -->
<line x1="TO_CX" y1="Y" x2="FROM_CX" y2="Y" stroke="#64748b" stroke-width="1" stroke-dasharray="6,3" marker-end="url(#arrow)"/>
<text x="MID_X" y="Y-8" fill="#94a3b8" font-size="8" text-anchor="middle" font-style="italic">response</text>
<!-- Self-message (loop arrow) -->
<path d="M CX,Y L CX+40,Y L CX+40,Y+25 L CX,Y+25" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="CX+45" y="Y+15" fill="#e2e8f0" font-size="8">process()</text>
```
## Activation Bar
```svg
<rect x="CX-5" y="START_Y" width="10" height="H" rx="2" fill="rgba(8,51,68,0.6)" stroke="#22d3ee" stroke-width="1"/>
```
## Conditional / Loop Frames
```svg
<!-- Frame boundary -->
<rect x="X" y="Y" width="W" height="H" rx="4" fill="none" stroke="#64748b" stroke-width="1" stroke-dasharray="4,3"/>
<!-- Frame label tab -->
<rect x="X" y="Y" width="50" height="18" rx="4" fill="rgba(30,41,59,0.8)" stroke="#64748b" stroke-width="1"/>
<text x="X+25" y="Y+13" fill="#94a3b8" font-size="8" font-weight="600" text-anchor="middle">alt</text>
<!-- Condition text -->
<text x="X+60" y="Y+13" fill="#94a3b8" font-size="8" font-style="italic">[condition]</text>
<!-- Divider line for else -->
<line x1="X" y1="MID_Y" x2="X+W" y2="MID_Y" stroke="#64748b" stroke-width="1" stroke-dasharray="4,3"/>
<text x="X+10" y="MID_Y+13" fill="#94a3b8" font-size="8" font-style="italic">[else]</text>
```
## Numbering
For complex sequences (8+ messages), number each message:
```svg
<circle cx="FROM_CX-15" cy="Y" r="8" fill="rgba(59,130,246,0.3)" stroke="#60a5fa" stroke-width="1"/>
<text x="FROM_CX-15" y="Y+3" fill="#60a5fa" font-size="7" font-weight="600" text-anchor="middle">1</text>
```
## Color Assignment
Assign each actor a distinct color from the palette. Use that color for:
- Actor box stroke
- Activation bar on that lifeline
- Outgoing arrows from that actor (optional, for visual clarity in complex diagrams)
@@ -0,0 +1,100 @@
# Structural Diagram Layout
Covers: class diagrams, ER diagrams, component diagrams, package diagrams, org charts.
## Class Diagram
### Class Box (3-compartment)
```svg
<g transform="translate(X, Y)">
<!-- Mask -->
<rect width="180" height="120" rx="6" fill="#0f172a"/>
<!-- Box -->
<rect width="180" height="120" rx="6" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<!-- Class name compartment -->
<text x="90" y="24" fill="white" font-size="11" font-weight="700" text-anchor="middle">ClassName</text>
<!-- Divider 1 -->
<line x1="0" y1="35" x2="180" y2="35" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.5"/>
<!-- Attributes -->
<text x="10" y="52" fill="#94a3b8" font-size="8">- id: int</text>
<text x="10" y="64" fill="#94a3b8" font-size="8">- name: string</text>
<!-- Divider 2 -->
<line x1="0" y1="75" x2="180" y2="75" stroke="#22d3ee" stroke-width="0.5" stroke-opacity="0.5"/>
<!-- Methods -->
<text x="10" y="92" fill="#94a3b8" font-size="8">+ getName(): string</text>
<text x="10" y="104" fill="#94a3b8" font-size="8">+ setName(s: string)</text>
</g>
```
For abstract classes, italicize the class name. For interfaces, add `«interface»` above the name in smaller font.
### Relationship Lines
| Relationship | Line Style | Arrow/End |
|-------------|------------|-----------|
| Inheritance | Solid | Empty triangle (▷) pointing to parent |
| Implementation | Dashed | Empty triangle pointing to interface |
| Composition | Solid | Filled diamond (◆) at owner end |
| Aggregation | Solid | Empty diamond (◇) at owner end |
| Dependency | Dashed | Open arrowhead at dependency target |
| Association | Solid | Open arrowhead or none |
**Markers:**
```svg
<!-- Inheritance triangle -->
<marker id="inherit" markerWidth="12" markerHeight="10" refX="12" refY="5" orient="auto">
<polygon points="0 0, 12 5, 0 10" fill="#0f172a" stroke="#94a3b8" stroke-width="1.5"/>
</marker>
<!-- Composition diamond -->
<marker id="composition" markerWidth="12" markerHeight="8" refX="0" refY="4" orient="auto">
<polygon points="0 4, 6 0, 12 4, 6 8" fill="#94a3b8"/>
</marker>
<!-- Aggregation diamond -->
<marker id="aggregation" markerWidth="12" markerHeight="8" refX="0" refY="4" orient="auto">
<polygon points="0 4, 6 0, 12 4, 6 8" fill="#0f172a" stroke="#94a3b8" stroke-width="1.5"/>
</marker>
```
### Cardinality Labels
Place at each end of the relationship line, offset 5-8px from the box edge:
```svg
<text x="X" y="Y" fill="#94a3b8" font-size="8">1..*</text>
```
## ER Diagram
Similar to class diagrams but:
- Use 2-compartment boxes (entity name + attributes)
- Mark primary keys with `PK` prefix and bold
- Mark foreign keys with `FK` prefix
- Relationship lines use crow's foot notation:
```svg
<!-- One end (single line) -->
<line x1="X1" y1="Y" x2="X1+15" y2="Y" stroke="#94a3b8" stroke-width="1.5"/>
<!-- Many end (crow's foot) -->
<line x1="X2-15" y1="Y-6" x2="X2" y2="Y" stroke="#94a3b8" stroke-width="1.5"/>
<line x1="X2-15" y1="Y+6" x2="X2" y2="Y" stroke="#94a3b8" stroke-width="1.5"/>
<line x1="X2-15" y1="Y" x2="X2" y2="Y" stroke="#94a3b8" stroke-width="1.5"/>
```
## Org Chart
- Top-down tree layout
- Root at top center
- Each level evenly spaced (100-120px vertical gap)
- Siblings evenly distributed horizontally
- Connection lines: vertical from parent bottom center to horizontal bar, then vertical down to each child top center
- Use color to indicate departments or hierarchy levels
## Layout Tips
- Start by counting the widest level to determine total diagram width
- Center the tree horizontally in the viewBox
- For deep trees (5+ levels), consider horizontal layout instead
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bun
import { existsSync, readFileSync, mkdirSync } from "fs";
import { basename, dirname, extname, join, resolve } from "path";
interface Options {
input: string;
output?: string;
scale: number;
json: boolean;
}
function parseViewBox(svg: string): { width: number; height: number } | null {
const vb = svg.match(/viewBox\s*=\s*"([^"]+)"/);
if (vb) {
const parts = vb[1].split(/[\s,]+/).map(Number);
if (parts.length >= 4 && parts[2] > 0 && parts[3] > 0) return { width: parts[2], height: parts[3] };
}
const w = svg.match(/\bwidth\s*=\s*"(\d+(?:\.\d+)?)"/);
const h = svg.match(/\bheight\s*=\s*"(\d+(?:\.\d+)?)"/);
if (w && h) return { width: Number(w[1]), height: Number(h[1]) };
return null;
}
function getOutputPath(input: string, scale: number, custom?: string): string {
if (custom) return resolve(custom);
const dir = dirname(input);
const base = basename(input, extname(input));
const suffix = scale === 1 ? "" : `@${scale}x`;
return join(dir, `${base}${suffix}.png`);
}
async function convert(input: string, opts: Options): Promise<{ output: string; width: number; height: number }> {
const svg = readFileSync(input);
const svgStr = svg.toString("utf-8");
const dims = parseViewBox(svgStr);
if (!dims) throw new Error("Cannot determine SVG dimensions from viewBox or width/height attributes");
const width = Math.round(dims.width * opts.scale);
const height = Math.round(dims.height * opts.scale);
const sharp = (await import("sharp")).default;
const output = getOutputPath(input, opts.scale, opts.output);
mkdirSync(dirname(output), { recursive: true });
await sharp(svg, { density: 72 * opts.scale })
.resize(width, height)
.png()
.toFile(output);
return { output, width, height };
}
function printHelp() {
console.log(`Usage: bun main.ts <input.svg> [options]
Convert SVG to @2x PNG.
Options:
-o, --output <path> Output path (default: <input>@2x.png)
-s, --scale <n> Scale factor (default: 2)
--json JSON output
-h, --help Show help`);
}
function parseArgs(args: string[]): Options | null {
const opts: Options = { input: "", scale: 2, json: false };
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") { printHelp(); process.exit(0); }
else if (arg === "-o" || arg === "--output") opts.output = args[++i];
else if (arg === "-s" || arg === "--scale") {
const s = Number(args[++i]);
if (isNaN(s) || s <= 0) { console.error(`Invalid scale: ${args[i]}`); return null; }
opts.scale = s;
} else if (arg === "--json") opts.json = true;
else if (!arg.startsWith("-") && !opts.input) opts.input = arg;
}
if (!opts.input) { console.error("Error: Input SVG file required"); printHelp(); return null; }
return opts;
}
async function main() {
const opts = parseArgs(process.argv.slice(2));
if (!opts) process.exit(1);
const input = resolve(opts.input);
if (!existsSync(input)) { console.error(`Error: ${input} not found`); process.exit(1); }
if (extname(input).toLowerCase() !== ".svg") { console.error("Error: Input must be an SVG file"); process.exit(1); }
try {
const r = await convert(input, opts);
if (opts.json) console.log(JSON.stringify({ input, ...r }, null, 2));
else console.log(`${input}${r.output} (${r.width}×${r.height})`);
} catch (e) {
console.error(`Error: ${(e as Error).message}`);
process.exit(1);
}
}
main();
+641
View File
@@ -0,0 +1,641 @@
---
name: baoyu-image-cards
description: Generates infographic image card series with 12 visual styles, 8 layouts, and 3 color palettes. Breaks content into 1-10 cartoon-style image cards optimized for social media engagement. Use when user mentions "小红书图片", "小红书种草", "小绿书", "微信图文", "微信贴图", "image cards", "图片卡片", or wants social media infographic series.
version: 1.56.1
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-image-cards
---
# Image Card Series Generator
Break down complex content into eye-catching image card series with multiple style options.
## Usage
```bash
# Auto-select style and layout based on content
/baoyu-image-cards posts/ai-future/article.md
# Specify style
/baoyu-image-cards posts/ai-future/article.md --style notion
# Specify layout
/baoyu-image-cards posts/ai-future/article.md --layout dense
# Combine style and layout
/baoyu-image-cards posts/ai-future/article.md --style notion --layout list
# Specify palette (override style colors)
/baoyu-image-cards posts/ai-future/article.md --style notion --palette macaron
# Use preset (style + layout + optional palette shorthand)
/baoyu-image-cards posts/ai-future/article.md --preset knowledge-card
# Preset with override
/baoyu-image-cards posts/ai-future/article.md --preset poster --layout quadrant
# Preset with palette override
/baoyu-image-cards posts/ai-future/article.md --preset hand-drawn-edu --palette warm
# Direct content input
/baoyu-image-cards
[paste content]
# Direct input with options
/baoyu-image-cards --style bold --layout comparison
[paste content]
# Non-interactive (for scheduled tasks / automation)
/baoyu-image-cards posts/ai-future/article.md --yes
/baoyu-image-cards posts/ai-future/article.md --yes --preset knowledge-card
```
## Options
| Option | Description |
|--------|-------------|
| `--style <name>` | Visual style (see Style Gallery) |
| `--layout <name>` | Information layout (see Layout Gallery) |
| `--palette <name>` | Color palette override (see Palette Gallery) |
| `--preset <name>` | Style + layout + optional palette shorthand (see [Style Presets](references/style-presets.md)) |
| `--yes` | Non-interactive mode: skip all confirmations. Uses EXTEND.md preferences if found, otherwise uses defaults (no watermark, auto style/layout). Auto-confirms recommended plan (Path A). Suitable for scheduled tasks and automation. |
## Dimensions
| Dimension | Controls | Options |
|-----------|----------|---------|
| **Style** | Visual aesthetics: lines, decorations, rendering | cute, fresh, warm, bold, minimal, retro, pop, notion, chalkboard, study-notes, screen-print, sketch-notes |
| **Layout** | Information structure: density, arrangement | sparse, balanced, dense, list, comparison, flow, mindmap, quadrant |
| **Palette** (optional) | Color override: replaces style's default colors | macaron, warm, neon |
Style × Layout can be freely combined, with optional palette override. Example: `--style notion --layout dense` creates an intellectual-looking knowledge card with high information density. Add `--palette macaron` to swap colors to soft pastels while keeping notion's rendering style.
Or use presets: `--preset knowledge-card` → style + layout in one flag. See [Style Presets](references/style-presets.md).
**Palette behavior**:
- No `--palette` → style uses its built-in colors (or its `default_palette` if defined)
- `--palette macaron` → overrides any style's colors with macaron palette
- Palette replaces colors only; style rendering rules (line treatment, elements, textures) stay unchanged
- Some styles declare a `default_palette` (e.g., sketch-notes defaults to macaron)
## Style Gallery
| Style | Description |
|-------|-------------|
| `cute` (Default) | Sweet, adorable, girly aesthetic |
| `fresh` | Clean, refreshing, natural |
| `warm` | Cozy, friendly, approachable |
| `bold` | High impact, attention-grabbing |
| `minimal` | Ultra-clean, sophisticated |
| `retro` | Vintage, nostalgic, trendy |
| `pop` | Vibrant, energetic, eye-catching |
| `notion` | Minimalist hand-drawn line art, intellectual |
| `chalkboard` | Colorful chalk on black board, educational |
| `study-notes` | Realistic handwritten photo style, blue pen + red annotations + yellow highlighter |
| `screen-print` | Bold poster art, halftone textures, limited colors, symbolic storytelling |
| `sketch-notes` | Hand-drawn educational infographic, macaron pastels on warm cream, wobble lines |
Detailed style definitions: `references/presets/<style>.md`
## Preset Gallery
Quick-start presets by content scenario. Use `--preset <name>` or recommend during Step 2.
**Knowledge & Learning**:
| Preset | Style | Layout | Best For |
|--------|-------|--------|----------|
| `knowledge-card` | notion | dense | 干货知识卡、概念科普 |
| `checklist` | notion | list | 清单、排行榜、必备清单 |
| `concept-map` | notion | mindmap | 概念图、知识脉络 |
| `swot` | notion | quadrant | SWOT分析、四象限分类 |
| `tutorial` | chalkboard | flow | 教程步骤、操作流程 |
| `classroom` | chalkboard | balanced | 课堂笔记、知识讲解 |
| `study-guide` | study-notes | dense | 学习笔记、考试重点 |
| `hand-drawn-edu` | sketch-notes | flow | 手绘教程、流程图解 |
| `sketch-card` | sketch-notes | dense | 手绘知识卡、概念科普 |
| `sketch-summary` | sketch-notes | balanced | 手绘总结、图文笔记 |
**Lifestyle & Sharing**:
| Preset | Style | Layout | Best For |
|--------|-------|--------|----------|
| `cute-share` | cute | balanced | 少女风分享、日常种草 |
| `girly` | cute | sparse | 甜美封面、氛围感 |
| `cozy-story` | warm | balanced | 生活故事、情感分享 |
| `product-review` | fresh | comparison | 产品对比、测评 |
| `nature-flow` | fresh | flow | 健康流程、自然主题 |
**Impact & Opinion**:
| Preset | Style | Layout | Best For |
|--------|-------|--------|----------|
| `warning` | bold | list | 避坑指南、重要提醒 |
| `versus` | bold | comparison | 正反对比、强烈对照 |
| `clean-quote` | minimal | sparse | 金句、极简封面 |
| `pro-summary` | minimal | balanced | 专业总结、商务内容 |
**Trend & Entertainment**:
| Preset | Style | Layout | Best For |
|--------|-------|--------|----------|
| `retro-ranking` | retro | list | 复古排行、经典盘点 |
| `throwback` | retro | balanced | 怀旧分享、老物件 |
| `pop-facts` | pop | list | 趣味冷知识、好玩的事 |
| `hype` | pop | sparse | 炸裂封面、惊叹分享 |
**Poster & Editorial**:
| Preset | Style | Layout | Best For |
|--------|-------|--------|----------|
| `poster` | screen-print | sparse | 海报风封面、影评书评 |
| `editorial` | screen-print | balanced | 观点文章、文化评论 |
| `cinematic` | screen-print | comparison | 电影对比、戏剧张力 |
Full preset definitions: [references/style-presets.md](references/style-presets.md)
## Layout Gallery
| Layout | Description |
|--------|-------------|
| `sparse` (Default) | Minimal information, maximum impact (1-2 points) |
| `balanced` | Standard content layout (3-4 points) |
| `dense` | High information density, knowledge card style (5-8 points) |
| `list` | Enumeration and ranking format (4-7 items) |
| `comparison` | Side-by-side contrast layout |
| `flow` | Process and timeline layout (3-6 steps) |
| `mindmap` | Center radial mind map layout (4-8 branches) |
| `quadrant` | Four-quadrant / circular section layout |
Detailed layout definitions: `references/elements/canvas.md`
## Palette Gallery
Optional color override. Replaces style's built-in colors while preserving rendering rules.
| Palette | Background | Zone Colors | Accent | Feel |
|---------|------------|-------------|--------|------|
| `macaron` | Warm cream #F5F0E8 | Blue #A8D8EA, Lavender #D5C6E0, Mint #B5E5CF, Peach #F8D5C4 | Coral #E8655A | Soft, educational, approachable |
| `warm` | Soft Peach #FFECD2 | Orange #ED8936, Terracotta #C05621, Golden #F6AD55, Rose #D4A09A | Sienna #A0522D | Cozy, earth tones, no cool colors |
| `neon` | Dark Purple #1A1025 | Cyan #00F5FF, Magenta #FF00FF, Green #39FF14, Pink #FF6EC7 | Yellow #FFFF00 | High-energy, futuristic |
Detailed palette definitions: `references/palettes/<palette>.md`
## Auto Selection
| Content Signals | Style | Layout | Recommended Preset |
|-----------------|-------|--------|--------------------|
| Beauty, fashion, cute, girl, pink | `cute` | sparse/balanced | `cute-share`, `girly` |
| Health, nature, clean, fresh, organic | `fresh` | balanced/flow | `product-review`, `nature-flow` |
| Life, story, emotion, feeling, warm | `warm` | balanced | `cozy-story` |
| Warning, important, must, critical | `bold` | list/comparison | `warning`, `versus` |
| Professional, business, elegant, simple | `minimal` | sparse/balanced | `clean-quote`, `pro-summary` |
| Classic, vintage, old, traditional | `retro` | balanced | `throwback`, `retro-ranking` |
| Fun, exciting, wow, amazing | `pop` | sparse/list | `hype`, `pop-facts` |
| Knowledge, concept, productivity, SaaS | `notion` | dense/list | `knowledge-card`, `checklist` |
| Education, tutorial, learning, teaching, classroom | `chalkboard` | balanced/dense | `tutorial`, `classroom` |
| Notes, handwritten, study guide, knowledge, realistic, photo | `study-notes` | dense/list/mindmap | `study-guide` |
| Movie, album, concert, poster, opinion, editorial, dramatic, cinematic | `screen-print` | sparse/comparison | `poster`, `editorial`, `cinematic` |
| Hand-drawn, infographic, diagram, visual summary, 手绘, 图解, workflow, process | `sketch-notes` | flow/balanced/dense | `hand-drawn-edu`, `sketch-card`, `sketch-summary` |
## Outline Strategies
Three differentiated outline strategies for different content goals:
### Strategy A: Story-Driven (故事驱动型)
| Aspect | Description |
|--------|-------------|
| **Concept** | Personal experience as main thread, emotional resonance first |
| **Features** | Start from pain point, show before/after change, strong authenticity |
| **Best for** | Reviews, personal shares, transformation stories |
| **Structure** | Hook → Problem → Discovery → Experience → Conclusion |
### Strategy B: Information-Dense (信息密集型)
| Aspect | Description |
|--------|-------------|
| **Concept** | Value-first, efficient information delivery |
| **Features** | Clear structure, explicit points, professional credibility |
| **Best for** | Tutorials, comparisons, product reviews, checklists |
| **Structure** | Core conclusion → Info card → Pros/Cons → Recommendation |
### Strategy C: Visual-First (视觉优先型)
| Aspect | Description |
|--------|-------------|
| **Concept** | Visual impact as core, minimal text |
| **Features** | Large images, atmospheric, instant appeal |
| **Best for** | High-aesthetic products, lifestyle, mood-based content |
| **Structure** | Hero image → Detail shots → Lifestyle scene → CTA |
## File Structure
Each session creates an independent directory named by content slug:
```
image-cards/{topic-slug}/
├── source-{slug}.{ext} # Source files (text, images, etc.)
├── analysis.md # Deep analysis + questions asked
├── outline-strategy-a.md # Strategy A: Story-driven
├── outline-strategy-b.md # Strategy B: Information-dense
├── outline-strategy-c.md # Strategy C: Visual-first
├── outline.md # Final selected/merged outline
├── prompts/
│ ├── 01-cover-[slug].md
│ ├── 02-content-[slug].md
│ └── ...
├── 01-cover-[slug].png
├── 02-content-[slug].png
└── NN-ending-[slug].png
```
**Slug Generation**:
1. Extract main topic from content (2-4 words, kebab-case)
2. Example: "AI工具推荐" → `ai-tools-recommend`
**Conflict Resolution**:
If `image-cards/{topic-slug}/` already exists:
- Append timestamp: `{topic-slug}-YYYYMMDD-HHMMSS`
- Example: `ai-tools` exists → `ai-tools-20260118-143052`
**Source Files**:
Copy all sources with naming `source-{slug}.{ext}`:
- `source-article.md`, `source-photo.jpg`, etc.
- Multiple sources supported: text, images, files from conversation
## Workflow
### Progress Checklist
Copy and track progress:
```
Image Card Series Progress:
- [ ] Step 0: Check preferences (EXTEND.md) ⛔ BLOCKING (--yes: use defaults if not found)
- [ ] Found → load preferences → continue
- [ ] Not found → run first-time setup → MUST complete before Step 1 (--yes: skip setup, use defaults)
- [ ] Step 1: Analyze content → analysis.md
- [ ] Step 2: Smart Confirm ⚠️ REQUIRED (--yes: auto-confirm Path A)
- [ ] Path A: Quick confirm → generate recommended outline
- [ ] Path B: Customize → adjust then generate outline
- [ ] Path C: Detailed → 3 outlines → second confirm → generate outline
- [ ] Step 3: Generate images (sequential)
- [ ] Step 4: Completion report
```
### Flow
```
Input → [--yes?] ─┬─ Yes → [Step 0: Load or defaults] → Analyze → Auto-confirm → Generate → Complete
└─ No → [Step 0: Preferences] ─┬─ Found → Continue
└─ Not found → First-Time Setup ⛔ BLOCKING
└─ Complete setup → Save EXTEND.md → Continue
┌─────────────────────────────────────────────────────────────────────────────────────┘
Analyze → [Smart Confirm] ─┬─ Quick: confirm recommended → outline.md → Generate → Complete
├─ Customize: adjust options → outline.md → Generate → Complete
└─ Detailed: 3 outlines → [Confirm 2] → outline.md → Generate → Complete
```
### Step 0: Load Preferences (EXTEND.md) ⛔ BLOCKING
**Purpose**: Load user preferences or run first-time setup.
**`--yes` mode**: If EXTEND.md found → load it. If not found → use built-in defaults (no watermark, style/layout auto-select, language from content). Do NOT run first-time setup, do NOT create EXTEND.md, do NOT ask any questions. Proceed directly to Step 1.
**CRITICAL** (interactive mode only): If EXTEND.md not found, MUST complete first-time setup before ANY other questions or steps. Do NOT proceed to content analysis, do NOT ask about style, do NOT ask about layout — ONLY complete the preferences setup first.
Check EXTEND.md existence (priority order):
```bash
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-image-cards/EXTEND.md && echo "project"
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-image-cards/EXTEND.md" && echo "xdg"
test -f "$HOME/.baoyu-skills/baoyu-image-cards/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-image-cards/EXTEND.md) { "project" }
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
if (Test-Path "$xdg/baoyu-skills/baoyu-image-cards/EXTEND.md") { "xdg" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-image-cards/EXTEND.md") { "user" }
```
┌────────────────────────────────────────────────────┬───────────────────┐
│ Path │ Location │
├────────────────────────────────────────────────────┼───────────────────┤
│ .baoyu-skills/baoyu-image-cards/EXTEND.md │ Project directory │
├────────────────────────────────────────────────────┼───────────────────┤
│ $HOME/.baoyu-skills/baoyu-image-cards/EXTEND.md │ User home │
└────────────────────────────────────────────────────┴───────────────────┘
┌───────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Result │ Action │
├───────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Found │ Read, parse, display summary → Continue to Step 1 │
├───────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Not found │ ⛔ BLOCKING: Run first-time setup ONLY (see below) → Complete and save EXTEND.md → Then Step 1 │
└───────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────┘
**First-Time Setup** (when EXTEND.md not found):
**Language**: Use user's input language or saved language preference.
Use AskUserQuestion with ALL questions in ONE call. See `references/config/first-time-setup.md` for question details.
**EXTEND.md Supports**: Watermark | Preferred style/layout | Custom style definitions | Language preference
Schema: `references/config/preferences-schema.md`
### Step 1: Analyze Content → `analysis.md`
Read source content, save it if needed, and perform deep analysis.
**Actions**:
1. **Save source content** (if not already a file):
- If user provides a file path: use as-is
- If user pastes content: save to `source.md` in target directory
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
2. Read source content
3. **Deep analysis** following `references/workflows/analysis-framework.md`:
- Content type classification (种草/干货/测评/教程/避坑...)
- Hook analysis (爆款标题潜力)
- Target audience identification
- Engagement potential (收藏/分享/评论)
- Visual opportunity mapping
- Swipe flow design
4. Detect source language
5. Determine recommended image count (2-10)
6. **Auto-recommend** best strategy + style + layout + palette based on content signals
7. **Save to `analysis.md`**
### Step 2: Smart Confirm ⚠️
**Purpose**: Present auto-recommended plan, let user confirm or adjust.
**`--yes` mode**: Skip this entire step. Use auto-recommended strategy + style + layout + palette from Step 1 analysis (or `--style`/`--layout`/`--palette`/`--preset` if provided). Generate outline directly using Path A logic → save to `outline.md` → proceed to Step 3. No AskUserQuestion calls.
**Interactive mode**: Do NOT skip.
**Auto-Recommendation Logic**:
1. Use Auto Selection table to match content signals → best strategy + style + layout + palette
2. Infer optimal image count from content density
3. Load style's default elements from preset (apply palette override if applicable)
**Display** (analysis summary + recommended plan):
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 内容分析
主题:[topic] | 类型:[content_type]
要点:[key points summary]
受众:[target audience]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎨 推荐方案(自动匹配)
策略:[A/B/C] [strategy name][reason]
风格:[style] · 布局:[layout] · 配色:[palette or "默认"] · 预设:[preset]
图片:[N]张(封面+[N-2]内容+结尾)
元素:[background] / [decorations] / [emphasis]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
**Use AskUserQuestion** with single question:
| Option | Description |
|--------|-------------|
| 1. ✅ 确认,直接生成(推荐) | Trust auto-recommendation, proceed immediately |
| 2. 🎛️ 自定义调整 | Modify strategy/style/layout/count in one step |
| 3. 📋 详细模式 | Generate 3 outlines, then choose (two confirmations) |
#### Path A: Quick Confirm (Option 1)
Generate single outline using recommended strategy + style → save to `outline.md` → Step 3.
#### Path B: Customize (Option 2)
**Use AskUserQuestion** with adjustable options (leave blank = keep recommended):
1. **策略风格**: Current: [strategy + style]. Options: A Story-Driven(warm) | B Information-Dense(notion) | C Visual-First(screen-print). Or specify style directly: cute/fresh/warm/bold/minimal/retro/pop/notion/chalkboard/study-notes/screen-print/sketch-notes. Or use preset: knowledge-card / checklist / tutorial / poster / hand-drawn-edu / etc.
2. **布局**: Current: [layout]. Options: sparse | balanced | dense | list | comparison | flow | mindmap | quadrant
3. **配色**: Current: [palette or "默认"]. Options: 默认 | macaron | warm | neon
4. **图片数量**: Current: [N]. Range: 2-10
5. **补充说明**(可选): Selling point emphasis, audience adjustment, custom color preference, etc.
**After response**: Generate single outline with user's choices → save to `outline.md` → Step 3.
#### Path C: Detailed Mode (Option 3)
Full two-confirmation flow for maximum control:
**Step 2a: Content Understanding**
**Use AskUserQuestion** for:
1. Core selling point (multiSelect: true)
2. Target audience
3. Style preference: Authentic sharing / Professional review / Aesthetic mood / Auto
4. Additional context (optional)
**After response**: Update `analysis.md`.
**Step 2b: Generate 3 Outline Variants**
| Strategy | Filename | Outline | Recommended Style |
|----------|----------|---------|-------------------|
| A | `outline-strategy-a.md` | Story-driven: emotional, before/after | warm, cute, fresh |
| B | `outline-strategy-b.md` | Information-dense: structured, factual | notion, minimal, chalkboard |
| C | `outline-strategy-c.md` | Visual-first: atmospheric, minimal text | bold, pop, retro, screen-print |
**Outline format** (YAML front matter + content):
```yaml
---
strategy: a # a, b, or c
name: Story-Driven
style: warm # recommended style for this strategy
palette: ~ # optional palette override (macaron, warm, neon, or ~ for style default)
style_reason: "Warm tones enhance emotional storytelling and personal connection"
elements: # from style preset, can be customized
background: solid-pastel
decorations: [clouds, stars-sparkles]
emphasis: star-burst
typography: highlight
layout: balanced # primary layout
image_count: 5
---
## P1 Cover
**Type**: cover
**Hook**: "入冬后脸不干了🥹终于找到对的面霜"
**Visual**: Product hero shot with cozy winter atmosphere
**Layout**: sparse
## P2 Problem
**Type**: pain-point
**Message**: Previous struggles with dry skin
**Visual**: Before state, relatable scenario
**Layout**: balanced
...
```
**Differentiation requirements**:
- Each strategy MUST have different outline structure AND different recommended style
- Adapt page count: A typically 4-6, B typically 3-5, C typically 3-4
- Include `style_reason` explaining why this style fits the strategy
Reference: `references/workflows/outline-template.md`
**Step 2c: Outline & Style Selection**
**Use AskUserQuestion** with three questions:
**Q1: Outline Strategy**: A / B / C / Combine (specify pages from each)
**Q2: Visual Style**: Use recommended | Select preset | Select style | Custom description
**Q3: Visual Elements**: Use defaults (Recommended) | Adjust background | Adjust decorations | Custom
**After response**: Save selected/merged outline to `outline.md` with confirmed style and elements → Step 3.
### Step 3: Generate Images
With confirmed outline + style + layout:
**Visual Consistency — Reference Image Chain**:
To ensure character/style consistency across all images in a series:
1. **Generate image 1 (cover) FIRST** — without `--ref`
2. **Use image 1 as `--ref` for ALL remaining images** (2, 3, ..., N)
- This anchors the character design, color rendering, and illustration style
- Command pattern: `--ref <path-to-image-01.png>` added to every subsequent generation
This is critical for styles that use recurring characters, mascots, or illustration elements. Image 1 becomes the visual anchor for the entire series.
**For each image (cover + content + ending)**:
1. Save prompt to `prompts/NN-{type}-[slug].md` (in user's preferred language)
- **Backup rule**: If prompt file exists, rename to `prompts/NN-{type}-[slug]-backup-YYYYMMDD-HHMMSS.md`
2. Generate image:
- **Image 1**: Generate without `--ref` (this establishes the visual anchor)
- **Images 2+**: Generate with `--ref <image-01-path>` for consistency
- **Backup rule**: If image file exists, rename to `NN-{type}-[slug]-backup-YYYYMMDD-HHMMSS.png`
3. Report progress after each generation
**Watermark Application** (if enabled in preferences):
Add to each image generation prompt:
```
Include a subtle watermark "[content]" positioned at [position].
The watermark should be legible but not distracting from the main content.
```
Reference: `references/config/watermark-guide.md`
**Image Generation Skill Selection**:
- Check available image generation skills
- If multiple skills available: ask user preference (interactive) or use first available skill (`--yes` mode)
**Session Management**:
If image generation skill supports `--sessionId`:
1. Generate unique session ID: `cards-{topic-slug}-{timestamp}`
2. Use same session ID for all images
3. Combined with reference image chain, ensures maximum visual consistency
### Step 4: Completion Report
```
Image Card Series Complete!
Topic: [topic]
Mode: [Quick / Custom / Detailed]
Strategy: [A/B/C/Combined]
Style: [style name]
Palette: [palette name or "default"]
Layout: [layout name or "varies"]
Location: [directory path]
Images: N total
✓ analysis.md
✓ outline.md
✓ outline-strategy-a/b/c.md (detailed mode only)
Files:
- 01-cover-[slug].png ✓ Cover (sparse)
- 02-content-[slug].png ✓ Content (balanced)
- 03-content-[slug].png ✓ Content (dense)
- 04-ending-[slug].png ✓ Ending (sparse)
```
## Image Modification
| Action | Steps |
|--------|-------|
| **Edit** | **Update prompt file FIRST** → Regenerate with same session ID |
| **Add** | Specify position → Create prompt → Generate → Renumber subsequent files (NN+1) → Update outline |
| **Delete** | Remove files → Renumber subsequent (NN-1) → Update outline |
**IMPORTANT**: When updating images, ALWAYS update the prompt file (`prompts/NN-{type}-[slug].md`) FIRST before regenerating. This ensures changes are documented and reproducible.
## Content Breakdown Principles
1. **Cover (Image 1)**: Hook + visual impact → `sparse` layout
2. **Content (Middle)**: Core value per image → `balanced`/`dense`/`list`/`comparison`/`flow`
3. **Ending (Last)**: CTA / summary → `sparse` or `balanced`
**Style × Layout Matrix** (✓✓ = highly recommended, ✓ = works well):
| | sparse | balanced | dense | list | comparison | flow | mindmap | quadrant |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| cute | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
| fresh | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓ | ✓ |
| warm | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ |
| bold | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ |
| minimal | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| retro | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
| pop | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ |
| notion | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
| chalkboard | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
| study-notes | ✗ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ |
| screen-print | ✓✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ | ✗ | ✓✓ |
| sketch-notes | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
## References
Detailed templates in `references/` directory:
**Elements** (Visual building blocks):
- `elements/canvas.md` - Aspect ratios, safe zones, grid layouts
- `elements/image-effects.md` - Cutout, stroke, filters
- `elements/typography.md` - Decorated text (花字), tags, text direction
- `elements/decorations.md` - Emphasis marks, backgrounds, doodles, frames
**Presets** (Style presets):
- `presets/<name>.md` - Element combination definitions (cute, notion, warm...)
- `style-presets.md` - Preset shortcuts (style + layout + palette combos)
**Palettes** (Color overrides):
- `palettes/<name>.md` - Color palette definitions (macaron, warm, neon)
**Workflows** (Process guides):
- `workflows/analysis-framework.md` - Content analysis framework
- `workflows/outline-template.md` - Outline template with layout guide
- `workflows/prompt-assembly.md` - Prompt assembly guide
**Config** (Settings):
- `config/preferences-schema.md` - EXTEND.md schema
- `config/first-time-setup.md` - First-time setup flow
- `config/watermark-guide.md` - Watermark configuration
## Notes
- Auto-retry once on failure | Cartoon alternatives for sensitive figures
- Use confirmed language preference | Maintain style consistency
- **Smart Confirm required** (Step 2) - do not skip; detailed mode uses two sub-confirmations
## Extension Support
Custom configurations via EXTEND.md. See **Step 0** for paths and supported options.
@@ -0,0 +1,122 @@
---
name: first-time-setup
description: First-time setup flow for baoyu-xhs-images preferences
---
# First-Time Setup
## Overview
When no EXTEND.md is found, guide user through preference setup.
**⛔ BLOCKING OPERATION**: This setup MUST complete before ANY other workflow steps. Do NOT:
- Ask about content/article
- Ask about style or layout
- Ask about target audience
- Proceed to content analysis
ONLY ask the questions in this setup flow, save EXTEND.md, then continue.
## Setup Flow
```
No EXTEND.md found
┌─────────────────────┐
│ AskUserQuestion │
│ (all questions) │
└─────────────────────┘
┌─────────────────────┐
│ Create EXTEND.md │
└─────────────────────┘
Continue to Step 1
```
## Questions
**Language**: Use user's input language or saved language preference.
Use single AskUserQuestion with multiple questions (AskUserQuestion auto-adds "Other" option):
### Question 1: Watermark
```
header: "Watermark"
question: "Watermark text for generated images? Type your watermark content (e.g., name, @handle)"
options:
- label: "No watermark (Recommended)"
description: "No watermark, can enable later in EXTEND.md"
```
Position defaults to bottom-right.
### Question 2: Preferred Style
```
header: "Style"
question: "Default visual style preference? Or type another style name or your custom style"
options:
- label: "None (Recommended)"
description: "Auto-select based on content analysis"
- label: "cute"
description: "Sweet, adorable - classic XHS aesthetic"
- label: "notion"
description: "Minimalist hand-drawn, intellectual"
```
### Question 3: Save Location
```
header: "Save"
question: "Where to save preferences?"
options:
- label: "Project"
description: ".baoyu-skills/ (this project only)"
- label: "User"
description: "~/.baoyu-skills/ (all projects)"
```
## Save Locations
| Choice | Path | Scope |
|--------|------|-------|
| Project | `.baoyu-skills/baoyu-xhs-images/EXTEND.md` | Current project |
| User | `~/.baoyu-skills/baoyu-xhs-images/EXTEND.md` | All projects |
## After Setup
1. Create directory if needed
2. Write EXTEND.md with frontmatter
3. Confirm: "Preferences saved to [path]"
4. Continue to Step 1
## EXTEND.md Template
```yaml
---
version: 1
watermark:
enabled: [true/false]
content: "[user input or empty]"
position: bottom-right
opacity: 0.7
preferred_style:
name: [selected style or null]
description: ""
preferred_layout: null
language: null
custom_styles: []
---
```
## Modifying Preferences Later
Users can edit EXTEND.md directly or run setup again:
- Delete EXTEND.md to trigger setup
- Edit YAML frontmatter for quick changes
- Full schema: `config/preferences-schema.md`
@@ -0,0 +1,118 @@
---
name: preferences-schema
description: EXTEND.md YAML schema for baoyu-xhs-images user preferences
---
# Preferences Schema
## Full Schema
```yaml
---
version: 1
watermark:
enabled: false
content: ""
position: bottom-right # bottom-right|bottom-left|bottom-center|top-right
preferred_style:
name: null # Built-in or custom style name
description: "" # Override/notes
preferred_layout: null # sparse|balanced|dense|list|comparison|flow
language: null # zh|en|ja|ko|auto
custom_styles:
- name: my-style
description: "Style description"
color_palette:
primary: ["#FED7E2", "#FEEBC8"]
background: "#FFFAF0"
accents: ["#FF69B4", "#FF6B6B"]
visual_elements: "Hearts, stars, sparkles"
typography: "Rounded, bubbly hand lettering"
best_for: "Lifestyle, beauty"
---
```
## Field Reference
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `version` | int | 1 | Schema version |
| `watermark.enabled` | bool | false | Enable watermark |
| `watermark.content` | string | "" | Watermark text (@username or custom) |
| `watermark.position` | enum | bottom-right | Position on image |
| `preferred_style.name` | string | null | Style name or null |
| `preferred_style.description` | string | "" | Custom notes/override |
| `preferred_layout` | string | null | Layout preference or null |
| `language` | string | null | Output language (null = auto-detect) |
| `custom_styles` | array | [] | User-defined styles |
## Position Options
| Value | Description |
|-------|-------------|
| `bottom-right` | Lower right corner (default, most common) |
| `bottom-left` | Lower left corner |
| `bottom-center` | Bottom center |
| `top-right` | Upper right corner |
## Custom Style Fields
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Unique style identifier (kebab-case) |
| `description` | Yes | What the style conveys |
| `color_palette.primary` | No | Main colors (array) |
| `color_palette.background` | No | Background color |
| `color_palette.accents` | No | Accent colors (array) |
| `visual_elements` | No | Decorative elements |
| `typography` | No | Font/lettering style |
| `best_for` | No | Recommended content types |
## Example: Minimal Preferences
```yaml
---
version: 1
watermark:
enabled: true
content: "@myusername"
preferred_style:
name: notion
---
```
## Example: Full Preferences
```yaml
---
version: 1
watermark:
enabled: true
content: "@myxhsaccount"
position: bottom-right
preferred_style:
name: notion
description: "Clean knowledge cards for tech content"
preferred_layout: dense
language: zh
custom_styles:
- name: corporate
description: "Professional B2B style"
color_palette:
primary: ["#1E3A5F", "#4A90D9"]
background: "#F5F7FA"
accents: ["#00B4D8", "#48CAE4"]
visual_elements: "Clean lines, subtle gradients, geometric shapes"
typography: "Modern sans-serif, professional"
best_for: "Business, SaaS, enterprise"
---
```
@@ -0,0 +1,62 @@
---
name: watermark-guide
description: Watermark configuration guide for baoyu-xhs-images
---
# Watermark Guide
## Position Diagram
```
┌─────────────────────────────┐
│ [top-right]│
│ │
│ │
│ IMAGE CONTENT │
│ │
│ │
│[bottom-left][bottom-center][bottom-right]│
└─────────────────────────────┘
```
## Position Recommendations
| Position | Best For | Avoid When |
|----------|----------|------------|
| `bottom-right` | Default choice, most common | Key info in bottom-right |
| `bottom-left` | Right-heavy layouts | Key info in bottom-left |
| `bottom-center` | Centered designs | Text-heavy bottom area |
| `top-right` | Bottom-heavy content | Title/header in top-right |
## Content Format
| Format | Example | Style |
|--------|---------|-------|
| Handle | `@username` | Most common for XHS |
| Text | `MyBrand` | Simple branding |
| Chinese | `小红书:用户名` | Platform specific |
| URL | `myblog.com` | Cross-platform |
## Best Practices
1. **Consistency**: Use same watermark across all images in series
2. **Legibility**: Ensure watermark readable on both light/dark areas
3. **Size**: Keep subtle - should not distract from content
## Prompt Integration
When watermark is enabled, add to image generation prompt:
```
Include a subtle watermark "[content]" positioned at [position].
The watermark should be legible but not distracting from the main content.
```
## Common Issues
| Issue | Solution |
|-------|----------|
| Watermark invisible | Adjust position or check contrast |
| Watermark too prominent | Change position or reduce size |
| Watermark overlaps content | Change position |
| Inconsistent across images | Use session ID for consistency |
@@ -0,0 +1,122 @@
# Canvas & Layout
Core canvas specifications and layout grids for Xiaohongshu infographics.
## Aspect Ratios
| Name | Ratio | Pixels | Note |
|------|-------|--------|------|
| portrait-3-4 | 3:4 | 1242×1660 | Highest traffic on XHS (recommended) |
| square | 1:1 | 1242×1242 | Second recommended |
| portrait-2-3 | 2:3 | 1242×1863 | Taller format |
**Default**: portrait-3-4 for maximum engagement.
## Safe Zones
Avoid placing critical content in these areas:
| Zone | Position | Reason |
|------|----------|--------|
| bottom-overlay | Bottom 10% | Title bar overlay on mobile |
| top-right | Top-right corner | Like/share button overlay |
| bottom-right | Bottom-right corner | Watermark position |
```
┌─────────────────────────────┐
│ [like/share]│ ← top-right: avoid
│ │
│ │
│ ✓ SAFE CONTENT AREA │
│ │
│ │
│ [title bar overlay area] │ ← bottom 10%: avoid key info
└─────────────────────────────┘
```
## Grid Layouts
### Density-Based Layouts
| Layout | Info Density | Whitespace | Points/Image | Best For |
|--------|--------------|------------|--------------|----------|
| sparse | Low | 60-70% | 1-2 | Covers, quotes, impactful statements |
| balanced | Medium | 40-50% | 3-4 | Standard content, tutorials |
| dense | High | 20-30% | 5-8 | Knowledge cards, cheat sheets |
### Structure-Based Layouts
| Layout | Structure | Items | Best For |
|--------|-----------|-------|----------|
| list | Vertical enumeration | 4-7 | Rankings, checklists, step guides |
| comparison | Left vs Right | 2 sections | Before/after, pros/cons |
| flow | Connected nodes | 3-6 steps | Processes, timelines, workflows |
| mindmap | Center radial | 4-8 branches | Concept maps, brainstorming, topic overview |
| quadrant | 4-section grid | 4 sections | SWOT analysis, priority matrix, classification |
## Layout by Position
| Position | Recommended Layout | Why |
|----------|-------------------|-----|
| Cover | sparse | Maximum visual impact, clear title |
| Setup | balanced | Context without overwhelming |
| Core | balanced/dense/list | Based on content density |
| Payoff | balanced/list | Clear takeaways |
| Ending | sparse | Clean CTA, memorable close |
## Grid Cells
For multi-element compositions:
| Name | Cells | Use Case |
|------|-------|----------|
| single | 1 | Hero image, maximum impact |
| dual | 2 | Before/after, comparison |
| triptych | 3 | Steps, process flow |
| quad | 4 | Product showcase |
| six-grid | 6 | Checklist, collection |
| nine-grid | 9 | Multi-image gallery |
## Visual Balance
### Sparse Layout
- Single focal point centered
- Breathing room on all sides
- Symmetrical composition
### Balanced Layout
- Top-weighted title
- Evenly distributed content below
- Clear visual hierarchy
### Dense Layout
- Organized grid structure
- Clear section boundaries
- Compact but readable spacing
### List Layout
- Left-aligned items
- Clear number/bullet hierarchy
- Consistent item format
### Comparison Layout
- Symmetrical left/right
- Clear visual contrast
- Divider between sections
### Flow Layout
- Directional flow (top→bottom or left→right)
- Connected nodes with arrows
- Clear progression indicators
### Mindmap Layout
- Central topic node
- Radial branches outward
- Hierarchical sub-branches
- Organic curved connections
### Quadrant Layout
- 4-section grid (2×2)
- Clear axis labels
- Each quadrant with distinct content
- Optional circular variant for cycles
@@ -0,0 +1,152 @@
# Decorative Assets
Visual embellishments and decorative elements for Xiaohongshu infographics.
## Emphasis Marks (强调标记)
Elements to draw attention to specific content.
| Name | Description | Use Case |
|------|-------------|----------|
| red-arrow | Red arrow pointing to target | Product features, key points |
| circle-mark | Circle highlight annotation | Highlighting details |
| underline | Straight or wavy underline | Text emphasis |
| star-burst | Starburst explosion effect | Special offers, wow factor |
| checkmark | Checkmark/tick symbol | Completed items, pros |
| cross-mark | X mark symbol | Cons, things to avoid |
| exclamation | Exclamation point decoration | Important warnings |
| question | Question mark decoration | FAQ, curiosity |
| numbering | Circled numbers | Steps, rankings |
| bracket | Bracket highlighting | Grouping, emphasis |
## Backgrounds (背景)
Base layer treatments.
| Name | Description | Use Case |
|------|-------------|----------|
| solid-saturated | High-saturation solid color | Bold, energetic |
| solid-pastel | Soft pastel solid color | Cute, gentle |
| gradient-linear | Linear color gradient | Modern, dynamic |
| gradient-radial | Radial color gradient | Spotlight effect |
| frosted-glass | Frosted glass blur effect | Layered compositions |
| paper-texture | Paper or craft texture | Handmade aesthetic |
| fabric-texture | Fabric/cloth texture | Cozy, tactile |
| chalkboard | Blackboard texture | Educational content |
| grid | Subtle grid pattern | Structured, organized |
| dots | Polka dot pattern | Playful, retro |
## Doodles & Emoji (涂鸦)
Hand-drawn decorative elements.
| Name | Description | Use Case |
|------|-------------|----------|
| hand-drawn-lines | Sketchy hand-drawn lines | Connections, borders |
| stars-sparkles | Stars and sparkle effects | Magic, excellence |
| flowers | Floral decorations | Beauty, feminine |
| hearts | Heart symbols | Love, favorites |
| clouds | Cloud shapes | Dreamy, thoughts |
| arrows-curvy | Curved directional arrows | Flow, direction |
| squiggles | Wavy squiggle lines | Energy, movement |
| confetti | Scattered confetti | Celebration |
| leaves | Leaf decorations | Nature, fresh |
| bubbles | Circular bubble shapes | Playful, light |
## Emoji Integration
| Category | Examples | Use Case |
|----------|----------|----------|
| Reactions | 🥹 😍 🤯 | Emotional emphasis |
| Objects | ✨ 💡 🎯 | Visual markers |
| Actions | 👇 👆 ➡️ | Directional cues |
| Nature | 🌸 🌿 ☀️ | Thematic decoration |
## Frames (边框)
Container and border treatments.
| Name | Description | Use Case |
|------|-------------|----------|
| polaroid | Instant photo frame | Photo showcase |
| film-strip | Film negative border | Cinematic, retro |
| phone-screenshot | Mobile device mockup | App/screen content |
| torn-paper | Torn paper edge effect | Scrapbook aesthetic |
| rounded-rect | Rounded rectangle border | Clean containers |
| decorative | Ornate decorative border | Premium, elegant |
| tape-corners | Washi tape corners | Crafty, casual |
| stamp-border | Stamp perforated edge | Vintage, postal |
## Dividers (分隔线)
Section separators.
| Name | Description | Use Case |
|------|-------------|----------|
| line-simple | Simple horizontal line | Clean separation |
| line-dashed | Dashed line | Subtle division |
| line-wavy | Wavy line | Playful separation |
| dots-row | Row of dots | Decorative division |
| ornamental | Decorative flourish | Elegant separation |
## Stickers (贴纸)
Pre-composed decorative elements.
| Name | Description | Use Case |
|------|-------------|----------|
| badge-new | "NEW" badge | New products |
| badge-hot | "HOT" badge | Trending items |
| badge-sale | Sale/discount badge | Promotions |
| seal-quality | Quality seal | Recommendations |
| ribbon-award | Award ribbon | Best picks |
| tag-price | Price tag shape | Pricing info |
## Style-Specific Decorations
### Cute Style
- Hearts, stars, sparkles
- Ribbon decorations, sticker-style
- Cute character elements
### Notion Style
- Simple line doodles
- Geometric shapes, stick figures
- Maximum whitespace, minimal decoration
### Warm Style
- Sun rays, coffee cups, cozy items
- Warm lighting effects
- Friendly, inviting decorations
### Fresh Style
- Plant leaves, clouds, water drops
- Simple geometric shapes
- Open, breathing composition
### Bold Style
- Exclamation marks, arrows
- Warning icons, strong shapes
- High contrast elements
### Pop Style
- Bold shapes, speech bubbles
- Comic-style effects, starburst
- Dynamic, energetic decorations
### Retro Style
- Halftone dots, vintage badges
- Classic icons, tape effects
- Aged texture overlays
### Chalkboard Style
- Chalk dust effects
- Hand-drawn doodles
- Mathematical formulas, simple icons
### Screen-Print Style
- Bold silhouettes, geometric shapes
- Halftone dot patterns, print grain
- No doodles — negative space does the work
- Stencil-cut edges, color block boundaries
- Vintage poster border treatments
@@ -0,0 +1,92 @@
# Image Processing Layer
Visual effects applied to image elements in Xiaohongshu infographics.
## AI Cutout (抠图)
Subject extraction styles for product/figure isolation.
| Name | Description | Use Case |
|------|-------------|----------|
| clean | Sharp edges, precise boundaries | Product photography, tech items |
| soft | Soft transition, feathered edges | Portrait cutout, organic subjects |
| stylized | Hand-drawn edge treatment | Artistic compositions |
## Stroke Effects (描边)
Border treatments for cutout elements.
| Name | Description | Use Case |
|------|-------------|----------|
| white-solid | White solid line border | Classic sticker feel, high contrast |
| colored-solid | Colored solid line border | Playful vibe, brand colors |
| dashed | Dashed/dotted border | Handmade aesthetic, casual |
| double | Double-layer stroke | Emphasis effect, premium feel |
| glow | Soft outer glow | Dreamy, soft aesthetic |
| shadow | Drop shadow effect | Depth, floating element |
**Stroke Width Guidelines**:
- Thin: 2-4px - Subtle, elegant
- Medium: 5-8px - Standard visibility
- Thick: 10-15px - Bold emphasis
## Filters (滤镜)
Color grading and mood presets popular on XHS.
| Name | Chinese | Description | Mood |
|------|---------|-------------|------|
| clear-glow | 清透感 | Transparent, radiant, luminous | Fresh, youthful |
| film-grain | 胶片感 | Vintage film aesthetic, grain texture | Nostalgic, artistic |
| cream-skin | 奶油肌 | Smooth, creamy complexion tones | Soft, flattering |
| japanese-magazine | 日杂感 | Lifestyle magazine aesthetic | Curated, aspirational |
| high-saturation | 高饱和 | Vibrant, punchy colors | Energetic, eye-catching |
| muted-tones | 莫兰迪 | Morandi-style desaturated palette | Sophisticated, calm |
| warm-tone | 暖色调 | Golden hour warmth | Cozy, inviting |
| cool-tone | 冷色调 | Blue-shifted coolness | Modern, clean |
## Texture Overlays
Additional texture effects.
| Name | Description | Use Case |
|------|-------------|----------|
| paper | Paper or fabric texture | Handmade feel |
| noise | Fine grain noise | Analog aesthetic |
| halftone | Dot pattern | Retro print style |
| scratch | Light scratch marks | Vintage wear |
## Blending Modes
For layered compositions.
| Mode | Effect | Use Case |
|------|--------|----------|
| multiply | Darken, merge | Shadow effects |
| screen | Lighten, glow | Light effects |
| overlay | Contrast boost | Vibrant compositions |
| soft-light | Subtle blending | Natural layering |
## Effect Combinations
Common effect stacks for different styles:
### Cute Style
- Filter: clear-glow or cream-skin
- Stroke: white-solid (medium)
- Texture: none
### Notion Style
- Filter: none or muted-tones
- Stroke: white-solid (thin) or none
- Texture: paper (subtle)
### Retro Style
- Filter: film-grain
- Stroke: double or dashed
- Texture: halftone, scratch
### Bold Style
- Filter: high-saturation
- Stroke: colored-solid (thick)
- Texture: none
@@ -0,0 +1,96 @@
# Typography System
Text styling elements for Xiaohongshu infographics.
## Decorated Text (花字)
Stylized text treatments for emphasis and visual appeal.
| Name | Description | Use Case |
|------|-------------|----------|
| gradient | Gradient color fill | Title emphasis, modern feel |
| stroke-text | Outlined text with stroke | Cover headlines, high visibility |
| shadow-3d | 3D shadow/extrusion effect | Key terms, depth |
| highlight | Highlighter marker effect | Critical information, key points |
| neon | Neon glow effect | Tech content, night aesthetic |
| handwritten | Authentic handwritten style | Personal touch, casual |
| bubble | Rounded, inflated letterforms | Cute, playful content |
| brush | Brush stroke texture | Artistic, dynamic |
## Tags & Labels (标签)
Structured text containers.
| Name | Description | Use Case |
|------|-------------|----------|
| black-white | Black background, white text | Brand names, prices, categories |
| white-black | White background, black text | Clean labels, minimal style |
| bubble | Speech bubble style | Dialogue, annotations, callouts |
| pointer | Arrow pointer with label | Product callouts, pointing to features |
| ribbon | Ribbon/banner shape | Special offers, highlights |
| stamp | Stamp/seal style | Authenticity, recommendations |
| pill | Rounded pill shape | Tags, categories, keywords |
## Text Hierarchy
Recommended text sizing for visual hierarchy.
| Level | Role | Relative Size | Style |
|-------|------|---------------|-------|
| H1 | Main title | 100% | Bold, decorated |
| H2 | Section header | 70-80% | Semi-bold |
| H3 | Subsection | 50-60% | Medium weight |
| Body | Content text | 40-50% | Regular |
| Caption | Small notes | 30-35% | Light |
## Text Direction
| Direction | Description | Use Case |
|-----------|-------------|----------|
| horizontal | Standard left-to-right | Default for most content |
| vertical | Top-to-bottom columns | Magazine style, traditional Chinese |
| curved | Text following a curve | Decorative, around shapes |
| diagonal | Angled text | Dynamic compositions |
## Text Effects
| Effect | Description | Use Case |
|--------|-------------|----------|
| shadow | Drop shadow behind text | Readability on busy backgrounds |
| outline | Outline around letterforms | High contrast visibility |
| glow | Soft glow around text | Dreamy, emphasis |
| underline-wavy | Wavy underline decoration | Playful emphasis |
| strikethrough | Crossed out text | Before/after, corrections |
## Language Considerations
### Chinese Text (中文)
- Punctuation: 「」()、。!?
- Spacing: No spaces between characters
- Line height: 1.5-1.8x for readability
### Mixed Text
- English in Chinese context: Maintain consistent baseline
- Numbers: Use consistent number style (lining vs old-style)
## Style-Specific Typography
### Cute Style
- Rounded, bubbly hand lettering
- Soft shadows, playful decorations
- Pink/pastel color accents
### Notion Style
- Clean hand-drawn lettering
- Simple sans-serif labels
- Minimal decoration
### Bold Style
- Impactful hand lettering with shadows
- High contrast colors
- Strong outlines
### Chalkboard Style
- Chalk texture on all text
- Visible imperfections
- Multi-color chalk variety
@@ -0,0 +1,33 @@
# Macaron Palette
Soft pastel color blocks on warm cream background. Gentle, approachable, educational feel.
## Background
- Color: Warm cream (#F5F0E8)
- Texture: Subtle paper grain, warm tone
## Colors
| Role | Color | Hex | Usage |
|------|-------|-----|-------|
| Background | Warm Cream | #F5F0E8 | Primary background |
| Text | Deep Charcoal | #2C3E50 | Titles, main content |
| Secondary Text | Warm Gray | #6B6B6B | Annotations, labels |
| Block Color | Macaron Blue | #A8D8EA | Content block fill |
| Block Color | Macaron Lavender | #D5C6E0 | Content block fill |
| Block Color | Macaron Mint | #B5E5CF | Content block fill |
| Block Color | Macaron Peach | #F8D5C4 | Content block fill |
| Accent | Coral Red | #E8655A | Emphasis, highlights |
## Semantic Constraint
Soft pastel macaron color palette. Use block colors as rounded card backgrounds for distinct information sections. Accent coral red sparingly for emphasis on key terms only. All colors should feel gentle and approachable — no saturated or neon tones. Do NOT render color names or role labels as visible text in the image.
## Best Paired With
- `sketch-notes` — natural pairing for hand-drawn educational content
- `notion` — macaron accents soften the monochrome aesthetic
- `chalkboard` — pastel chalk tones replace standard chalk colors
- `warm` — reinforces the cozy, friendly feel
- `fresh` — complements the clean, natural aesthetic
@@ -0,0 +1,32 @@
# Neon Palette
Vibrant neon colors on dark background. High-energy, futuristic, eye-catching.
## Background
- Color: Dark Purple (#1A1025)
- Texture: Smooth, deep
## Colors
| Role | Color | Hex | Usage |
|------|-------|-----|-------|
| Background | Dark Purple | #1A1025 | Primary background |
| Text | Bright White | #F0F0F0 | Titles, main content |
| Secondary Text | Light Lavender | #B8B8D4 | Annotations, labels |
| Block Color | Neon Cyan | #00F5FF | Content block fill |
| Block Color | Neon Magenta | #FF00FF | Content block fill |
| Block Color | Neon Green | #39FF14 | Content block fill |
| Block Color | Neon Pink | #FF6EC7 | Content block fill |
| Accent | Electric Yellow | #FFFF00 | Emphasis, highlights |
## Semantic Constraint
Vibrant neon color palette on dark background. Colors should glow against the dark base. High contrast, futuristic feel. Use neon sparingly — too many glowing elements become chaotic. Let dark background breathe. Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best Paired With
- `bold` — amplifies high-impact energy
- `pop` — neon takes the vibrancy further
- `minimal` — neon accents on dark create striking contrast
- `notion` — futuristic knowledge card aesthetic
@@ -0,0 +1,32 @@
# Warm Palette
Warm earth tones on soft peach background. Cozy, inviting, no cool colors.
## Background
- Color: Soft Peach (#FFECD2)
- Texture: Warm, slightly textured
## Colors
| Role | Color | Hex | Usage |
|------|-------|-----|-------|
| Background | Soft Peach | #FFECD2 | Primary background |
| Text | Deep Brown | #744210 | Titles, main content |
| Secondary Text | Warm Brown | #9C6644 | Annotations, labels |
| Block Color | Warm Orange | #ED8936 | Content block fill |
| Block Color | Terracotta | #C05621 | Content block fill |
| Block Color | Golden Yellow | #F6AD55 | Content block fill |
| Block Color | Dusty Rose | #D4A09A | Content block fill |
| Accent | Burnt Sienna | #A0522D | Emphasis, highlights |
## Semantic Constraint
Warm-only color palette, no cool colors (no blue, green, purple). Earth tones throughout. Evokes comfort, warmth, and trust. All colors should feel like autumn sunlight. Do NOT render color names, hex codes, or role labels as visible text in the image.
## Best Paired With
- `warm` — natural pairing, amplifies cozy feel
- `cute` — warm pastels enhance the sweet aesthetic
- `retro` — earth tones complement vintage style
- `sketch-notes` — warm educational feel
@@ -0,0 +1,72 @@
---
name: bold
category: impact
---
# Bold Style
High impact, attention-grabbing aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: clean
stroke: colored-solid | double
filter: high-saturation
typography:
decorated: shadow-3d | stroke-text
tags: black-white | ribbon
direction: horizontal | diagonal
decorations:
emphasis: exclamation | star-burst | red-arrow
background: solid-saturated | gradient-linear
doodles: arrows-curvy | squiggles
frames: none
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Vibrant red, orange, yellow | #E53E3E, #DD6B20, #F6E05E |
| Background | Deep black, dark charcoal | #000000, #1A1A1A |
| Accents | White, neon yellow | #FFFFFF, #F7FF00 |
## Visual Elements
- Exclamation marks, arrows, warning icons
- Strong shapes, high contrast elements
- Dramatic compositions
- Bold geometric forms
## Typography
- Bold, impactful hand lettering with shadows
- High contrast text treatments
- Large, commanding headlines
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Impactful statements |
| balanced | ✓ | Warning content |
| dense | ✓ | Critical information cards |
| list | ✓✓ | Must-know lists, rankings |
| comparison | ✓✓ | Dramatic contrasts |
| flow | ✓ | Critical process steps |
## Best For
- Important tips and warnings
- Must-know content
- Critical announcements
- Rankings and comparisons
- Attention-grabbing hooks
@@ -0,0 +1,97 @@
---
name: chalkboard
category: educational
---
# Chalkboard Style
Black chalkboard background with colorful chalk drawing aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual | triptych
image_effects:
cutout: stylized
stroke: none
filter: none
typography:
decorated: handwritten
tags: none
direction: horizontal | vertical
decorations:
emphasis: underline | circle-mark | arrows-curvy
background: chalkboard
doodles: hand-drawn-lines | stars-sparkles
frames: none
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Background | Chalkboard black, green-black | #1A1A1A, #1C2B1C |
| Primary Text | Chalk white | #F5F5F5 |
| Accent 1 | Chalk yellow | #FFE566 |
| Accent 2 | Chalk pink | #FF9999 |
| Accent 3 | Chalk blue | #66B3FF |
| Accent 4 | Chalk green | #90EE90 |
| Accent 5 | Chalk orange | #FFB366 |
## Visual Elements
- Hand-drawn chalk illustrations with sketchy, imperfect lines
- Chalk dust effects around text and key elements
- Doodles: stars, arrows, underlines, circles, checkmarks
- Mathematical formulas and simple diagrams
- Eraser smudges and chalk residue textures
- Stick figures and simple icons
- Connection lines with hand-drawn feel
## Typography
- Hand-drawn chalk lettering style
- Visible chalk texture on all text
- Imperfect baseline adds authenticity
- White or bright colored chalk for emphasis
## Style Rules
### Do
- Maintain authentic chalk texture on all elements
- Use imperfect, hand-drawn quality throughout
- Add subtle chalk dust and smudge effects
- Create visual hierarchy with color variety
- Include playful doodles and annotations
### Don't
- Use perfect geometric shapes
- Create clean digital-looking lines
- Add photorealistic elements
- Use gradients or glossy effects
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Educational covers |
| balanced | ✓✓ | Standard lessons |
| dense | ✓✓ | Detailed tutorials |
| list | ✓✓ | Learning checklists |
| comparison | ✓ | Concept comparisons |
| flow | ✓✓ | Process explanations |
## Best For
- Educational content
- Tutorials and how-to's
- Classroom themes
- Teaching materials
- Workshops
- Informal learning sessions
- Knowledge sharing
@@ -0,0 +1,72 @@
---
name: cute
category: sweet
---
# Cute Style
Sweet, adorable, girly - classic Xiaohongshu aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual | quad
image_effects:
cutout: soft
stroke: white-solid | colored-solid
filter: clear-glow | cream-skin
typography:
decorated: bubble | highlight
tags: pill | bubble
direction: horizontal
decorations:
emphasis: star-burst | hearts
background: solid-pastel | gradient-linear
doodles: hearts | stars-sparkles | flowers
frames: polaroid | tape-corners
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Pink, peach, mint, lavender | #FED7E2, #FEEBC8, #C6F6D5, #E9D8FD |
| Background | Cream, soft pink | #FFFAF0, #FFF5F7 |
| Accents | Hot pink, coral | #FF69B4, #FF6B6B |
## Visual Elements
- Hearts, stars, sparkles, cute faces
- Ribbon decorations, sticker-style
- Cute stickers, emoji icons
- Soft, rounded shapes
## Typography
- Rounded, bubbly hand lettering
- Soft shadows, playful decorations
- Pink/pastel color accents on text
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Covers, emotional impact |
| balanced | ✓✓ | Standard cute content |
| dense | ✓ | Cute knowledge cards |
| list | ✓✓ | Checklists, cute rankings |
| comparison | ✓ | Before/after transformations |
| flow | ✓ | Cute step guides |
## Best For
- Lifestyle content
- Beauty and skincare
- Fashion and style
- Daily tips and hacks
- Personal shares
@@ -0,0 +1,72 @@
---
name: fresh
category: natural
---
# Fresh Style
Clean, refreshing, natural aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | triptych
image_effects:
cutout: soft
stroke: white-solid | none
filter: clear-glow | cool-tone
typography:
decorated: none | highlight
tags: pill | white-black
direction: horizontal
decorations:
emphasis: checkmark | circle-mark
background: solid-white | solid-pastel
doodles: leaves | clouds | bubbles
frames: rounded-rect | none
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Mint green, sky blue, light yellow | #9AE6B4, #90CDF4, #FAF089 |
| Background | Pure white, soft mint | #FFFFFF, #F0FFF4 |
| Accents | Leaf green, water blue | #48BB78, #4299E1 |
## Visual Elements
- Plant leaves, clouds, water drops
- Simple geometric shapes
- Breathing room, open composition
- Natural, organic elements
## Typography
- Clean, light hand lettering with breathing room
- Airy spacing
- Fresh color accents
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Clean covers |
| balanced | ✓✓ | Standard fresh content |
| dense | ✓ | Organized information |
| list | ✓ | Wellness tips |
| comparison | ✓ | Before/after health |
| flow | ✓✓ | Organic processes |
## Best For
- Health and wellness
- Minimalist lifestyle
- Self-care content
- Nature-related topics
- Clean living tips
@@ -0,0 +1,72 @@
---
name: minimal
category: elegant
---
# Minimal Style
Ultra-clean, sophisticated aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single
image_effects:
cutout: clean
stroke: none | white-solid
filter: none | muted-tones
typography:
decorated: none
tags: white-black | pill
direction: horizontal
decorations:
emphasis: underline | circle-mark
background: solid-white | solid-pastel
doodles: hand-drawn-lines
frames: none | rounded-rect
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Black, white | #000000, #FFFFFF |
| Background | Off-white, pure white | #FAFAFA, #FFFFFF |
| Accents | Single color (content-derived) | Blue, green, or coral |
## Visual Elements
- Single focal point, thin lines
- Maximum whitespace
- Simple, clean decorations
- Restrained visual elements
## Typography
- Clean, simple hand lettering
- Minimal weight variations
- Elegant spacing
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Elegant statements |
| balanced | ✓✓ | Professional content |
| dense | ✓✓ | Clean knowledge cards |
| list | ✓ | Simple lists |
| comparison | ✓ | Clean comparisons |
| flow | ✓ | Elegant processes |
## Best For
- Professional content
- Serious topics
- Elegant presentations
- High-end products
- Business content
@@ -0,0 +1,73 @@
---
name: notion
category: minimal
---
# Notion Style
Minimalist hand-drawn line art, intellectual aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: clean
stroke: none | white-solid
filter: none | muted-tones
typography:
decorated: none | handwritten
tags: black-white | pill
direction: horizontal
decorations:
emphasis: circle-mark | underline
background: solid-white | paper-texture
doodles: hand-drawn-lines | arrows-curvy
frames: none | rounded-rect
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Black, dark gray | #1A1A1A, #4A4A4A |
| Background | Pure white, off-white | #FFFFFF, #FAFAFA |
| Accents | Pastel blue, pastel yellow, pastel pink | #A8D4F0, #F9E79F, #FADBD8 |
## Visual Elements
- Simple line doodles, hand-drawn wobble effect
- Geometric shapes, stick figures
- Maximum whitespace, single-weight ink lines
- Clean, uncluttered compositions
## Typography
- Clean hand-drawn lettering
- Simple sans-serif labels
- Minimal decoration on text
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Concept covers |
| balanced | ✓✓ | Standard explanations |
| dense | ✓✓ | Knowledge cards, cheat sheets |
| list | ✓✓ | Productivity tips, tool lists |
| comparison | ✓✓ | Data comparisons |
| flow | ✓✓ | Process diagrams |
## Best For
- Knowledge sharing
- Concept explanations
- SaaS content
- Productivity tips
- Tech tutorials
- Professional content
@@ -0,0 +1,72 @@
---
name: pop
category: energetic
---
# Pop Style
Vibrant, energetic, eye-catching aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | quad
image_effects:
cutout: stylized
stroke: colored-solid | double
filter: high-saturation
typography:
decorated: stroke-text | shadow-3d
tags: bubble | ribbon
direction: horizontal | curved
decorations:
emphasis: star-burst | exclamation
background: solid-saturated | dots
doodles: stars-sparkles | confetti | squiggles
frames: none
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Bright red, yellow, blue, green | #F56565, #ECC94B, #4299E1, #48BB78 |
| Background | White, light gray | #FFFFFF, #F7FAFC |
| Accents | Neon pink, electric purple | #FF69B4, #9F7AEA |
## Visual Elements
- Bold shapes, speech bubbles
- Comic-style effects, starburst
- Dynamic, energetic compositions
- High-energy decorations
## Typography
- Dynamic, energetic hand lettering with outlines
- Bold color combinations
- Playful, expressive forms
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Exciting announcements |
| balanced | ✓✓ | Fun tutorials |
| dense | ✓ | Packed information |
| list | ✓✓ | Fun facts lists |
| comparison | ✓✓ | Dynamic comparisons |
| flow | ✓ | Energetic processes |
## Best For
- Exciting announcements
- Fun facts
- Engaging tutorials
- Entertainment content
- Youth-oriented content
@@ -0,0 +1,72 @@
---
name: retro
category: vintage
---
# Retro Style
Vintage, nostalgic, trendy aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: stylized
stroke: dashed | double
filter: film-grain | muted-tones
typography:
decorated: brush | handwritten
tags: stamp | ribbon
direction: horizontal
decorations:
emphasis: star-burst | numbering
background: paper-texture | dots
doodles: stars-sparkles | squiggles
frames: polaroid | film-strip | stamp-border
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Muted orange, dusty pink, faded teal | #E07A4D, #D4A5A5, #6B9999 |
| Background | Aged paper, sepia tones | #F5E6D3, #E8DCC8 |
| Accents | Faded red, vintage gold | #C55A5A, #B8860B |
## Visual Elements
- Halftone dots, vintage badges
- Classic icons, tape effects
- Aged texture overlays
- Nostalgic decorative elements
## Typography
- Vintage-style hand lettering
- Classic feel with imperfections
- Aged texture on text
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Vintage covers |
| balanced | ✓✓ | Classic content |
| dense | ✓ | Vintage knowledge cards |
| list | ✓✓ | Classic rankings |
| comparison | ✓ | Then vs now |
| flow | ✓ | Historical timelines |
## Best For
- Throwback content
- Classic tips
- Timeless advice
- Vintage aesthetics
- Nostalgic shares
@@ -0,0 +1,92 @@
---
name: screen-print
category: poster
---
# Screen-Print Style
Bold poster art with halftone textures, limited colors, and symbolic storytelling.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: silhouette
stroke: none
filter: halftone | print-grain
typography:
decorated: stroke-text | shadow-3d
tags: none
direction: horizontal
decorations:
emphasis: star-burst | numbering
background: solid-saturated | paper-texture
doodles: none
frames: none
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Burnt Orange, Deep Teal | #E8751A, #0A6E6E |
| Background | Off-Black, Warm Cream | #121212, #F5E6D0 |
| Accents | Crimson, Amber | #C0392B, #F4A623 |
**Duotone Pairs** (choose ONE based on content mood):
| Pair | Color A | Color B | Feel |
|------|---------|---------|------|
| Orange + Teal | #E8751A | #0A6E6E | Cinematic, action |
| Red + Cream | #C0392B | #F5E6D0 | Bold, classic |
| Blue + Gold | #1A3A5C | #D4A843 | Premium, prestigious |
| Crimson + Navy | #DC143C | #0D1B2A | Dramatic, noir |
| Magenta + Cyan | #C2185B | #00BCD4 | Vibrant, pop |
**Rule**: Use 2-5 colors maximum. Fewer colors = stronger impact.
## Visual Elements
- Bold silhouettes and symbolic shapes
- Halftone dot patterns within color fills
- Slight color layer misregistration (print offset effect)
- Geometric framing (circles, arches, triangles)
- Figure-ground inversion (negative space tells secondary story)
- Stencil-cut edges, no outlines — shapes defined by color boundaries
- Typography integrated as design element, not overlay
- Vintage poster border treatments
## Typography
- Bold condensed sans-serif or hand-drawn lettering
- Art Deco influences, vintage poster typography
- Typography as integral part of composition (not separate layer)
- High contrast with background for readability
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Iconic poster covers, dramatic statements |
| balanced | ✓✓ | Editorial compositions, opinion pieces |
| dense | ✗ | Too much info clashes with minimal poster aesthetic |
| list | ✓ | Bold rankings, top picks |
| comparison | ✓✓ | Duotone split compositions, before/after |
| flow | ✓ | Cinematic progression, timelines |
| mindmap | ✗ | Too complex for geometric poster style |
| quadrant | ✓✓ | Strong geometric division, classification |
## Best For
- Opinion pieces, cultural commentary
- Movie/music/book recommendations
- Dramatic announcements
- Before/after transformations
- Bold editorial content
- Event promotions
@@ -0,0 +1,100 @@
---
name: sketch-notes
category: educational
default_palette: macaron
---
# Sketch Notes Style
Hand-drawn educational infographic with slight line wobble, like a high-quality presentation visual summary.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: stylized
stroke: none
filter: none
typography:
decorated: handwritten
tags: rounded-badge
direction: horizontal
decorations:
emphasis: underline | circle-mark | arrows-curvy | star-burst
background: paper-texture
doodles: hand-drawn-lines | stars-sparkles | arrows-curvy | squiggles
frames: rounded-rect
```
## Color Palette
Default: **macaron** palette (see `palettes/macaron.md`)
When no `--palette` is specified, uses macaron colors: warm cream background (#F5F0E8), macaron blue/lavender/mint/peach zone blocks, coral red accent.
## Visual Elements
- Hand-drawn wobble on all lines and shapes
- Simple stick-figure characters at desks, working, thinking
- Rounded cards with pastel color blocks as information sections
- Color fills do NOT completely fill outlines (hand-painted feel)
- Doodle decorations: small stars, underlines, checkmarks, lock icons, clipboard icons
- Wavy hand-drawn arrows connecting zones with small text labels
- Thought bubbles and speech bubbles with sketchy outlines
- Simple conceptual icons (documents, lightbulbs, gears, arrows)
- Generous whitespace between zones for clean composition
## Typography
- Bold hand-drawn lettering for titles (large, prominent)
- Bold keywords within content zones
- Smaller annotations in secondary text color
- Hand-drawn quality on ALL text, no computer-generated fonts
- Clear information hierarchy: title > zone labels > body text > annotations
## Style Rules
### Do
- Maintain slight wobble on every line, shape, and border
- Use palette block colors as distinct section backgrounds
- Leave color fills intentionally incomplete at edges
- Include simple doodle icons relevant to content
- Keep generous whitespace between zones
- Use accent color sparingly for emphasis on key terms
- Draw connecting arrows with hand-drawn wavy feel
### Don't
- Use perfect geometric shapes or straight lines
- Create photorealistic elements
- Fill colors completely to edges (maintain hand-painted gap)
- Use dark or saturated backgrounds
- Overcrowd with too many decorative elements
- Use gradient fills or glossy effects
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓ | Simple covers with single zone |
| balanced | ✓✓ | Standard educational summaries |
| dense | ✓✓ | Knowledge cards, concept maps |
| list | ✓✓ | Step-by-step guides, checklists |
| comparison | ✓ | Side-by-side concept contrast |
| flow | ✓✓ | Process diagrams, workflows, tutorials |
| mindmap | ✓✓ | Concept maps, radial knowledge maps |
| quadrant | ✓ | Classification matrices |
## Best For
- Educational content, tutorials, how-to guides
- Process and workflow explanations
- Knowledge summaries, concept diagrams
- Technical explanations made approachable
- Visual summaries of articles or talks
- Onboarding materials, friendly guides
@@ -0,0 +1,115 @@
---
name: study-notes
category: realistic
---
# Study Notes Style
Realistic handwritten photo aesthetic - student notes style, dense and messy but readable.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single
image_effects:
cutout: none
stroke: none
filter: natural-photo
typography:
decorated: none
tags: none
direction: horizontal
decorations:
emphasis: circle-mark | underline | checkmark | cross | star-simple
background: lined-paper-white
doodles: arrows-simple | margin-notes | corrections | explanatory-diagrams
frames: none
```
## Color Palette (Three-Color Annotation System)
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Blue ballpoint, Black ink | #1E3A5F, #1A1A1A |
| Highlights | Yellow highlighter | #FFFF00 (50% opacity) |
| Accents | Red pen (circles, underlines) | #CC0000 |
| Background | White lined paper | #FFFFFF |
## Visual Elements
- Realistic photo perspective: top-down view of study desk
- Hand holding blue ballpoint pen, actively underlining
- Extremely dense handwritten content, filling entire page
- Red pen annotations: circles, underlines, stars, boxes
- Yellow highlighter marking key terms
- Correction marks, cramped notes squeezed into margins
- Simple hand-drawn symbols: → * ✓ ✗ !
- Varying pen pressure creating lighter and darker strokes
## Typography
- Authentic student handwriting
- Messy but readable, clear structure maintained
- Varying font sizes (large titles, small body, tiny margin notes)
- CJK optimized
## Content Structure
Three-section layout:
### Top Section
- Core topic (circled multiple times in red)
- First section title + 3-4 key points
- Arrow connections, red underlines
### Middle Section
- Second section title (red pen box)
- Numbered steps ①②③
- Specific methods and supplementary notes
### Bottom Section
- Third section title (red star)
- Time points / key metrics
- Key quotes / core tips (tiny corner notes)
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✗ | Not suitable - style requires dense content |
| balanced | ✓ | When content is lighter |
| dense | ✓✓ | Best fit - knowledge notes, summaries |
| list | ✓✓ | Step checklists, rankings |
| comparison | ✓ | Comparative analysis |
| flow | ✓ | Process flows |
| mindmap | ✓✓ | Mind map notes |
| quadrant | ✓ | Quadrant analysis |
## Best For
- Study guides, exam notes
- Knowledge organization, framework summaries
- Tutorial summaries, quick notes
- "Top student notes" style content
- Knowledge sharing requiring authentic feel
## Style Rules
### DO ✓
- Keep content extremely dense
- Use simple symbols (→ * ✓ ✗ !)
- Annotate key points with red pen
- Include correction marks
- Squeeze tiny notes into margins
### DON'T ✗
- Use complex emojis
- Leave too much whitespace
- Make neat, tidy layouts
- Add colorful decorations
- Include cartoon elements
@@ -0,0 +1,72 @@
---
name: warm
category: cozy
---
# Warm Style
Cozy, friendly, approachable aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: soft
stroke: white-solid | glow
filter: warm-tone | cream-skin
typography:
decorated: highlight | handwritten
tags: ribbon | bubble
direction: horizontal
decorations:
emphasis: star-burst | hearts
background: solid-pastel | gradient-radial
doodles: clouds | stars-sparkles
frames: polaroid | tape-corners
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Warm orange, golden yellow, terracotta | #ED8936, #F6AD55, #C05621 |
| Background | Cream, soft peach | #FFFAF0, #FED7AA |
| Accents | Deep brown, soft red | #744210, #E57373 |
## Visual Elements
- Sun rays, coffee cups, cozy items
- Warm lighting effects
- Friendly, inviting decorations
- Soft, comfortable shapes
## Typography
- Friendly, rounded hand lettering
- Warm color accents
- Comfortable, approachable feel
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Emotional covers |
| balanced | ✓✓ | Personal stories |
| dense | ✓ | Detailed experiences |
| list | ✓ | Life lessons |
| comparison | ✓✓ | Before/after stories |
| flow | ✓ | Journey narratives |
## Best For
- Personal stories
- Life lessons
- Emotional content
- Comfort and lifestyle
- Heartfelt shares
@@ -0,0 +1,43 @@
# Style Presets
`--preset X` expands to a style + layout + optional palette combination. Users can override any dimension.
| --preset | Style | Layout | Palette |
|----------|-------|--------|---------|
| `knowledge-card` | `notion` | `dense` | |
| `checklist` | `notion` | `list` | |
| `concept-map` | `notion` | `mindmap` | |
| `swot` | `notion` | `quadrant` | |
| `tutorial` | `chalkboard` | `flow` | |
| `classroom` | `chalkboard` | `balanced` | |
| `study-guide` | `study-notes` | `dense` | |
| `cute-share` | `cute` | `balanced` | |
| `girly` | `cute` | `sparse` | |
| `cozy-story` | `warm` | `balanced` | |
| `product-review` | `fresh` | `comparison` | |
| `nature-flow` | `fresh` | `flow` | |
| `warning` | `bold` | `list` | |
| `versus` | `bold` | `comparison` | |
| `clean-quote` | `minimal` | `sparse` | |
| `pro-summary` | `minimal` | `balanced` | |
| `retro-ranking` | `retro` | `list` | |
| `throwback` | `retro` | `balanced` | |
| `pop-facts` | `pop` | `list` | |
| `hype` | `pop` | `sparse` | |
| `poster` | `screen-print` | `sparse` | |
| `editorial` | `screen-print` | `balanced` | |
| `cinematic` | `screen-print` | `comparison` | |
| `hand-drawn-edu` | `sketch-notes` | `flow` | `macaron` |
| `sketch-card` | `sketch-notes` | `dense` | `macaron` |
| `sketch-summary` | `sketch-notes` | `balanced` | `macaron` |
Empty Palette = use style's built-in colors (or style's `default_palette` if defined in frontmatter).
## Override Examples
- `--preset knowledge-card --style chalkboard` = chalkboard style with dense layout
- `--preset poster --layout quadrant` = screen-print style with quadrant layout
- `--preset hand-drawn-edu --palette warm` = sketch-notes style with flow layout, warm palette instead of macaron
- `--style notion --palette macaron` = notion rendering rules with macaron colors
Explicit `--style`/`--layout`/`--palette` flags always override preset values.
@@ -0,0 +1,198 @@
# Xiaohongshu Content Analysis Framework
Deep analysis framework tailored for Xiaohongshu's unique engagement patterns.
## Purpose
Before creating infographics, thoroughly analyze the source material to:
- Maximize hook power and swipe motivation
- Identify save-worthy and share-worthy elements
- Plan the visual narrative arc
- Match content to optimal style/layout
## Platform Characteristics
Unlike other platforms, Xiaohongshu content must prioritize:
- **Hook Power**: First image decides 90% of engagement
- **Swipe Motivation**: Each image must compel users to continue
- **Save Value**: Content worth bookmarking for later
- **Share Triggers**: Emotional resonance that drives sharing
## Analysis Dimensions
### 1. Content Type Classification
| Type | Characteristics | Best Style | Best Layout |
|------|----------------|------------|-------------|
| 种草/安利 | Product recommendation, benefits focus | cute/fresh | balanced/list |
| 干货分享 | Knowledge, tips, how-to | notion | dense/list |
| 个人故事 | Personal experience, emotional | warm | balanced |
| 测评对比 | Review, comparison, pros/cons | bold/notion | comparison |
| 教程步骤 | Step-by-step guide | fresh/notion | flow/list |
| 避坑指南 | Warnings, mistakes to avoid | bold | list/comparison |
| 清单合集 | Collections, recommendations | cute/minimal | list/dense |
### 2. Hook Analysis (爆款标题潜力)
Evaluate title/hook potential using these patterns:
**Hook Types**:
- **数字钩子**: "5个方法", "3分钟学会", "99%的人不知道"
- **痛点钩子**: "踩过的坑", "后悔没早知道", "别再..."
- **好奇钩子**: "原来...", "竟然...", "没想到..."
- **利益钩子**: "省钱", "变美", "效率翻倍"
- **身份钩子**: "打工人必看", "学生党", "新手妈妈"
**Rating Scale**:
- ⭐⭐⭐⭐⭐ (5/5): Multiple strong hooks combined
- ⭐⭐⭐⭐ (4/5): Clear hook with room for enhancement
- ⭐⭐⭐ (3/5): Basic hook, needs strengthening
- ⭐⭐ (2/5): Weak hook, requires significant improvement
- ⭐ (1/5): No clear hook
### 3. Target Audience (用户画像)
| Audience | Interests | Preferred Style | Content Focus |
|----------|-----------|-----------------|---------------|
| 学生党 | 省钱、学习、校园 | cute/fresh | 平价、教程、学习方法 |
| 打工人 | 效率、职场、减压 | minimal/notion | 工具、技巧、摸鱼 |
| 宝妈 | 育儿、家居、省心 | warm/fresh | 实用、安全、经验 |
| 精致女孩 | 美妆、穿搭、仪式感 | cute/retro | 好看、氛围、品质 |
| 技术宅 | 工具、效率、极客 | notion/chalkboard | 深度、专业、新奇 |
| 美食爱好者 | 探店、食谱、测评 | warm/pop | 好吃、简单、颜值 |
| 旅行达人 | 攻略、打卡、小众 | fresh/retro | 省钱、避坑、拍照 |
### 4. Engagement Potential
**Save Value (收藏价值)**:
- Is it reference material? ✓ High save potential
- Is it a checklist or list? ✓ High save potential
- Is it a tutorial? ✓ High save potential
- Is it time-sensitive news? ✗ Low save potential
**Share Triggers (分享冲动)**:
- "我朋友也需要看这个" → High share potential
- "这说的就是我" → Identity resonance
- "太有用了必须分享" → Utility sharing
- "笑死,给朋友看看" → Entertainment sharing
**Comment Inducement (评论诱导)**:
- Open-ended questions: "你是哪种类型?"
- Experience sharing: "评论区说说你的经历"
- Debate triggers: "你觉得呢?"
- Help requests: "有更好的推荐吗?"
**Interaction Design (互动设计)**:
- Polls: "A还是B"
- Challenges: "你能做到几个?"
- Tags: "@你那个需要的朋友"
### 5. Visual Opportunity Map
| Content Element | Visual Treatment | Example |
|-----------------|------------------|---------|
| 数据/统计 | Highlighted numbers, simple charts | "节省80%时间" 大字突出 |
| 对比 | Before/after, side-by-side | 左右分屏对比图 |
| 步骤 | Numbered flow, arrows | 1→2→3 流程图 |
| 清单 | Checklist with icons | ✓/✗ 列表配图标 |
| 情感 | Character expressions, scenes | 卡通人物表情包 |
| 产品 | Product showcase, lifestyle | 产品实拍+使用场景 |
| 引用 | Quote cards, speech bubbles | 金句卡片设计 |
### 6. Swipe Flow Design
Plan the narrative arc across images:
| Position | Purpose | Hook Strategy |
|----------|---------|---------------|
| **Cover (封面)** | Stop scrolling | 最强视觉冲击 + 核心标题 |
| **Setup (铺垫)** | Build context | 痛点共鸣 / 好奇心 |
| **Core (核心)** | Deliver value | 干货内容,每页1-2个要点 |
| **Payoff (收获)** | Practical takeaway | 可执行的行动建议 |
| **Ending (结尾)** | Drive action | CTA + 互动引导 |
**Swipe Motivation Between Images**:
- End each image with a hook for the next
- Use "下一页更精彩" type transitions
- Create information gaps that require swiping
- Build anticipation through numbering ("第3个最重要")
## Output Format
Analysis results should be saved to `analysis.md` with:
```yaml
---
title: "5个让你效率翻倍的AI工具"
topic: 干货分享
content_type: 工具推荐
source_language: zh
user_language: zh
recommended_image_count: 6
---
## Target Audience
- **Primary**: 打工人、自由职业者 - 追求效率提升
- **Secondary**: 学生党 - 写论文、做作业需要
- **Tertiary**: 内容创作者 - 需要AI辅助
## Hook Analysis
**标题钩子评分**: ⭐⭐⭐⭐ (4/5)
- ✓ 数字钩子: "5个"
- ✓ 利益钩子: "效率翻倍"
- △ 可增强: 加入身份标签 "打工人必看"
**建议优化**:
- 原标题: "5个让你效率翻倍的AI工具"
- 优化: "打工人必看!5个让我效率翻倍的AI神器"
## Value Proposition
**为什么用户要看?**
1. **实用价值**: 直接可用的工具推荐
2. **省时省力**: 不用自己筛选,直接抄作业
3. **FOMO**: 别人都在用,我不能落后
**收藏理由**: 工具清单,需要时可以回来查
## Engagement Design
- **互动点**: 结尾问"你最常用哪个?"
- **评论诱导**: "还有什么好用的工具评论区分享"
- **分享触发**: 打工人会转发给同事
## Content Signals
- "AI工具" → notion + dense
- "效率" → notion + list
- "干货" → minimal + dense
## Swipe Flow
| Image | Position | Purpose | Hook |
|-------|----------|---------|------|
| 1 | Cover | 吸引停留 | 标题+视觉冲击 |
| 2 | Setup | 建立共鸣 | 为什么需要AI工具 |
| 3-5 | Core | 核心价值 | 每页1-2个工具详解 |
| 6 | Ending | 行动引导 | 总结+互动引导 |
## Recommended Approaches
1. **Notion + Dense** - 知识卡片风格,适合干货分享 (recommended)
2. **Notion + List** - 清爽知识卡片风格
3. **Minimal + Balanced** - 简约高端,适合职场人群
```
## Analysis Checklist
Before proceeding to outline generation:
- [ ] Can I identify the content type?
- [ ] Is the hook strong enough? (≥3 stars)
- [ ] Do I know the primary audience?
- [ ] Have I identified save/share triggers?
- [ ] Are there clear visual opportunities?
- [ ] Is the swipe flow planned?
- [ ] Have I identified the best style+layout recommendation?
@@ -0,0 +1,247 @@
# Xiaohongshu Outline Template
Template for generating infographic series outlines with layout specifications.
## File Naming
Outline files use strategy identifier in the name:
- `outline-strategy-a.md` - Story-driven variant
- `outline-strategy-b.md` - Information-dense variant
- `outline-strategy-c.md` - Visual-first variant
- `outline.md` - Final selected (copied from chosen variant)
## Image File Naming
Images use meaningful slugs for readability:
```
NN-{type}-[slug].png
NN-{type}-[slug].md (in prompts/)
```
| Type | Usage |
|------|-------|
| `cover` | First image (cover) |
| `content` | Middle content images |
| `ending` | Last image |
**Examples**:
- `01-cover-ai-tools.png`
- `02-content-why-ai.png`
- `03-content-chatgpt.png`
- `04-content-midjourney.png`
- `05-content-notion-ai.png`
- `06-ending-summary.png`
**Slug rules**:
- Derived from image content (kebab-case)
- Must be unique within the series
- Keep short but descriptive (2-4 words)
## Layout Selection Guide
### Density-Based Layouts
| Layout | When to Use | Info Points | Whitespace |
|--------|-------------|-------------|------------|
| sparse | Covers, quotes, impact statements | 1-2 | 60-70% |
| balanced | Standard content, tutorials | 3-4 | 40-50% |
| dense | Knowledge cards, cheat sheets | 5-8 | 20-30% |
### Structure-Based Layouts
| Layout | When to Use | Structure |
|--------|-------------|-----------|
| list | Rankings, checklists, steps | Numbered/bulleted vertical |
| comparison | Before/after, pros/cons | Left vs right split |
| flow | Processes, timelines | Connected nodes with arrows |
### Position-Based Recommendations
| Position | Recommended | Reasoning |
|----------|-------------|-----------|
| Cover | sparse | Maximum impact, clear title |
| Setup | balanced | Context without overwhelming |
| Core | balanced/dense/list | Match content density |
| Payoff | balanced/list | Clear takeaways |
| Ending | sparse | Clean CTA, memorable |
## Outline Format
```markdown
# Xiaohongshu Infographic Series Outline
---
strategy: a # a, b, or c
name: Story-Driven
style: notion
default_layout: dense
image_count: 6
generated: YYYY-MM-DD HH:mm
---
## Image 1 of 6
**Position**: Cover
**Layout**: sparse
**Hook**: 打工人必看!
**Slug**: ai-tools
**Filename**: 01-cover-ai-tools.png
**Text Content**:
- Title: 「5个AI神器让你效率翻倍」
- Subtitle: 亲测好用,建议收藏
**Visual Concept**:
科技感背景,多个AI工具图标环绕,中心大标题,
霓虹蓝+深色背景,未来感十足
**Swipe Hook**: 第一个就很强大👇
---
## Image 2 of 6
**Position**: Content
**Layout**: balanced
**Core Message**: 为什么你需要AI工具
**Slug**: why-ai
**Filename**: 02-content-why-ai.png
**Text Content**:
- Title: 「为什么要用AI?」
- Points:
- 重复工作自动化
- 创意辅助不卡壳
- 效率提升10倍
**Visual Concept**:
对比图:左边疲惫打工人,右边轻松使用AI的人
科技线条装饰,简洁有力
**Swipe Hook**: 接下来是具体工具推荐👇
---
## Image 3 of 6
**Position**: Content
**Layout**: dense
**Core Message**: ChatGPT使用技巧
**Slug**: chatgpt
**Filename**: 03-content-chatgpt.png
**Text Content**:
- Title: 「ChatGPT」
- Subtitle: 最强AI助手
- Points:
- 写文案:给出框架,秒出初稿
- 改文章:润色、翻译、总结
- 编程:写代码、找bug
- 学习:解释概念、出题练习
**Visual Concept**:
ChatGPT logo居中,四周放射状展示功能点
深色科技背景,霓虹绿点缀
**Swipe Hook**: 下一个更适合创意工作者👇
---
## Image 4 of 6
**Position**: Content
**Layout**: dense
**Core Message**: Midjourney绘图
**Slug**: midjourney
**Filename**: 04-content-midjourney.png
**Text Content**:
- Title: 「Midjourney」
- Subtitle: AI绘画神器
- Points:
- 输入描述,秒出图片
- 风格多样:写实/插画/3D
- 做封面、做头像、做素材
- 不会画画也能当设计师
**Visual Concept**:
展示几张MJ生成的不同风格图片
画框/画布元素装饰
**Swipe Hook**: 还有一个效率神器👇
---
## Image 5 of 6
**Position**: Content
**Layout**: balanced
**Core Message**: Notion AI笔记
**Slug**: notion-ai
**Filename**: 05-content-notion-ai.png
**Text Content**:
- Title: 「Notion AI」
- Subtitle: 智能笔记助手
- Points:
- 自动总结长文
- 头脑风暴出点子
- 整理会议记录
**Visual Concept**:
Notion界面风格,简洁黑白配色
展示笔记整理前后对比
**Swipe Hook**: 最后总结一下👇
---
## Image 6 of 6
**Position**: Ending
**Layout**: sparse
**Core Message**: 总结与互动
**Slug**: summary
**Filename**: 06-ending-summary.png
**Text Content**:
- Title: 「工具只是工具」
- Subtitle: 关键是用起来!
- CTA: 收藏备用 | 转发给需要的朋友
- Interaction: 你最常用哪个?评论区见👇
**Visual Concept**:
简洁背景,大字标题
底部互动引导文字
收藏/分享图标
---
```
## Swipe Hook Strategies
Each image should end with a hook for the next:
| Strategy | Example |
|----------|---------|
| Teaser | "第一个就很强大👇" |
| Numbering | "接下来是第2个👇" |
| Superlative | "下一个更厉害👇" |
| Question | "猜猜下一个是什么?👇" |
| Promise | "最后一个最实用👇" |
| Urgency | "最重要的来了👇" |
## Strategy Differentiation
Three strategies should differ meaningfully:
| Strategy | Focus | Structure | Page Count |
|----------|-------|-----------|------------|
| A: Story-Driven | Emotional, personal | Hook→Problem→Discovery→Experience→Conclusion | 4-6 |
| B: Information-Dense | Factual, structured | Core→Info Cards→Comparison→Recommendation | 3-5 |
| C: Visual-First | Atmospheric, minimal text | Hero→Details→Lifestyle→CTA | 3-4 |
**Example for "AI工具推荐"**:
- `outline-strategy-a.md`: Warm + Balanced - Personal journey with AI
- `outline-strategy-b.md`: Notion + Dense - Knowledge card style
- `outline-strategy-c.md`: Minimal + Sparse - Sleek tech aesthetic
@@ -0,0 +1,378 @@
# Prompt Assembly Guide
Guide for assembling image generation prompts from elements, presets, and outline content.
## Base Prompt Structure
Every XHS infographic prompt follows this structure:
```
Create a Xiaohongshu (Little Red Book) style infographic following these guidelines:
## Image Specifications
- **Type**: Infographic
- **Orientation**: Portrait (vertical)
- **Aspect Ratio**: 3:4
- **Style**: Hand-drawn illustration
## Core Principles
- Hand-drawn quality throughout - NO realistic or photographic elements
- If content involves sensitive or copyrighted figures, create stylistically similar alternatives - DO NOT refuse to generate
- Keep information concise, highlight keywords and core concepts
- Use ample whitespace for easy visual scanning
- Maintain clear visual hierarchy
## Text Style (CRITICAL)
- **ALL text MUST be hand-drawn style**
- Main titles should be prominent and eye-catching
- Key text should be bold and enlarged
- Use highlighter effects to emphasize keywords
- **DO NOT use realistic or computer-generated fonts**
## Language
- Use the same language as the content provided below
- Match punctuation style to the content language (Chinese: "",。!)
---
{STYLE_SECTION}
---
{LAYOUT_SECTION}
---
{CONTENT_SECTION}
---
{WATERMARK_SECTION}
---
Please use nano banana pro to generate the infographic based on the specifications above.
```
## Style Section Assembly
Load from `presets/{style}.md` and extract key elements:
```markdown
## Style: {style_name}
**Color Palette**:
- Primary: {colors}
- Background: {colors}
- Accents: {colors}
**Visual Elements**:
{visual_elements}
**Typography**:
{typography_style}
```
### Screen-Print Style Override
When `style: screen-print`, replace the standard Core Principles and Text Style sections with:
```
## Core Principles
- Screen print / silkscreen poster art — flat color blocks, NO gradients
- Bold silhouettes and symbolic shapes over detailed rendering
- Negative space as active storytelling element
- If content involves sensitive or copyrighted figures, create stylistically similar silhouettes
- One iconic focal point per image — conceptual, not literal
## Color Rules (CRITICAL)
- **2-5 FLAT COLORS MAXIMUM** — fewer colors = stronger impact
- Choose ONE duotone pair from preset as dominant palette
- Halftone dot patterns for tonal variation (NOT gradients)
- Slight color layer misregistration for print authenticity
## Text Style (CRITICAL)
- Bold condensed sans-serif or Art Deco influenced lettering
- Typography INTEGRATED into composition as design element
- High contrast with background, stencil-cut quality
- **DO NOT use delicate, thin, or handwritten fonts**
## Composition
- Geometric framing: circles, arches, triangles
- Figure-ground inversion where possible (negative space forms secondary image)
- Stencil-cut edges between color blocks, no outlines
- Paper grain texture beneath all colors
```
## Palette Override
When `--palette` is specified (or style has `default_palette` in frontmatter and no explicit `--palette`), palette colors **replace** the style's Color Palette in the prompt. Style rendering rules (Visual Elements, Typography, Style Rules) remain unchanged.
Load from `palettes/{palette}.md` and override:
```markdown
## Palette Override: {palette_name}
**Background**: {palette background color and hex}
**Colors**:
- Text: {text color and hex}
- Secondary: {secondary text color and hex}
- Zone 1: {zone color and hex}
- Zone 2: {zone color and hex}
- Zone 3: {zone color and hex}
- Zone 4: {zone color and hex}
- Accent: {accent color and hex}
**Constraint**: {semantic constraint from palette}
```
**Override rules**:
1. Palette Background **replaces** style's background color (keep style's texture description)
2. Palette Colors **replace** style's Color Palette section entirely
3. Palette Semantic Constraint is appended to the style section
4. If no `--palette` and style has `default_palette` → load that palette
5. If no `--palette` and no `default_palette` → use style's built-in colors (no override)
6. Explicit `--palette` always overrides style's `default_palette`
## Layout Section Assembly
Load from `elements/canvas.md` and extract relevant layout:
```markdown
## Layout: {layout_name}
**Information Density**: {density}
**Whitespace**: {percentage}
**Structure**:
{structure_description}
**Visual Balance**:
{balance_description}
```
## Content Section Assembly
From outline entry:
```markdown
## Content
**Position**: {Cover/Content/Ending}
**Core Message**: {message}
**Text Content**:
{text_list}
**Visual Concept**:
{visual_description}
```
## Watermark Section (if enabled)
```markdown
## Watermark
Include a subtle watermark "{content}" positioned at {position}. The watermark should
be legible but not distracting from the main content.
```
## Assembly Process
### Step 0: Resolve Style Preset (if `--preset` used)
If user specified `--preset`, resolve to style + layout + palette from `references/style-presets.md`:
```python
# e.g., --preset hand-drawn-edu → style=sketch-notes, layout=flow, palette=macaron
style, layout, palette = resolve_preset(preset_name)
```
Explicit `--style`/`--layout`/`--palette` flags override preset values.
### Step 1: Load Style Definition
```python
preset = load_preset(style_name) # e.g., "sketch-notes"
```
Extract:
- Color palette (may be overridden by palette)
- Visual elements
- Typography style
- Best practices (do/don't)
- `default_palette` from frontmatter (if present)
### Step 1.5: Apply Palette Override (if applicable)
```python
# Priority: explicit --palette > preset palette > style default_palette > none
palette = resolve_palette(cli_palette, preset_palette, style_default_palette)
if palette:
palette_def = load_palette(palette) # e.g., "macaron"
# Replace style colors with palette colors
# Keep style rendering rules (visual elements, typography, style rules)
```
### Step 2: Load Layout
```python
layout = get_layout_from_canvas(layout_name) # e.g., "dense"
```
Extract:
- Information density guidelines
- Whitespace percentage
- Structure description
- Visual balance rules
### Step 3: Format Content
From outline entry, format:
- Position context (Cover/Content/Ending)
- Text content with hierarchy
- Visual concept description
- Swipe hook (for context, not in prompt)
### Step 4: Add Watermark (if applicable)
If preferences include watermark:
- Add watermark section with content, position, opacity
### Step 5: Visual Consistency — Reference Image Chain
When generating multiple images in a series:
1. **Image 1 (cover)**: Generate without `--ref` — this establishes the visual anchor
2. **Images 2+**: Always pass image 1 as `--ref` to the installed image generation skill.
Read that skill's `SKILL.md` and use its documented interface rather than calling its scripts directly.
For each later image, use the assembled prompt file as input, set the output image path, keep aspect ratio `3:4`, use quality `2k`, and pass image 1 as the reference.
This ensures the AI maintains the same character design, illustration style, and color rendering across the series.
### Step 6: Combine
Assemble all sections into final prompt following base structure.
## Example: Assembled Prompt
```markdown
Create a Xiaohongshu (Little Red Book) style infographic following these guidelines:
## Image Specifications
- **Type**: Infographic
- **Orientation**: Portrait (vertical)
- **Aspect Ratio**: 3:4
- **Style**: Hand-drawn illustration
## Core Principles
- Hand-drawn quality throughout - NO realistic or photographic elements
- If content involves sensitive or copyrighted figures, create stylistically similar alternatives
- Keep information concise, highlight keywords and core concepts
- Use ample whitespace for easy visual scanning
- Maintain clear visual hierarchy
## Text Style (CRITICAL)
- **ALL text MUST be hand-drawn style**
- Main titles should be prominent and eye-catching
- Key text should be bold and enlarged
- Use highlighter effects to emphasize keywords
- **DO NOT use realistic or computer-generated fonts**
## Language
- Use the same language as the content provided below
- Match punctuation style to the content language (Chinese: "",。!)
---
## Style: Notion
**Color Palette**:
- Primary: Black (#1A1A1A), dark gray (#4A4A4A)
- Background: Pure white (#FFFFFF), off-white (#FAFAFA)
- Accents: Pastel blue (#A8D4F0), pastel yellow (#F9E79F), pastel pink (#FADBD8)
**Visual Elements**:
- Simple line doodles, hand-drawn wobble effect
- Geometric shapes, stick figures
- Maximum whitespace, single-weight ink lines
- Clean, uncluttered compositions
**Typography**:
- Clean hand-drawn lettering
- Simple sans-serif labels
- Minimal decoration on text
---
## Layout: Dense
**Information Density**: High (5-8 key points)
**Whitespace**: 20-30% of canvas
**Structure**:
- Multiple sections, structured grid
- More text, compact but organized
- Title + multiple sections with headers + numerous points
**Visual Balance**:
- Organized grid structure
- Clear section boundaries
- Compact but readable spacing
---
## Content
**Position**: Content (Page 3 of 6)
**Core Message**: ChatGPT 使用技巧
**Text Content**:
- Title: 「ChatGPT」
- Subtitle: 最强 AI 助手
- Points:
- 写文案:给出框架,秒出初稿
- 改文章:润色、翻译、总结
- 编程:写代码、找 bug
- 学习:解释概念、出题练习
**Visual Concept**:
ChatGPT logo 居中,四周放射状展示功能点
深色科技背景,霓虹绿点缀
---
## Watermark
Include a subtle watermark "@myxhsaccount" positioned at bottom-right
with approximately 50% visibility. The watermark should
be legible but not distracting from the main content.
---
Please use nano banana pro to generate the infographic based on the specifications above.
```
## Prompt Checklist
Before generating, verify:
- [ ] Style section loaded from correct preset
- [ ] Palette override applied (if `--palette` specified or style has `default_palette`)
- [ ] Layout section matches outline specification
- [ ] Content accurately reflects outline entry
- [ ] Language matches source content
- [ ] Watermark included (if enabled in preferences)
- [ ] No conflicting instructions
+101 -19
View File
@@ -1,7 +1,7 @@
---
name: baoyu-imagine
description: AI image generation with OpenAI, Azure OpenAI, Google, OpenRouter, DashScope, MiniMax, Jimeng, Seedream and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images.
version: 1.56.4
description: AI image generation with OpenAI, Azure OpenAI, Google, OpenRouter, DashScope, Z.AI GLM-Image, MiniMax, Jimeng, Seedream and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images.
version: 1.57.0
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-imagine
@@ -13,7 +13,7 @@ metadata:
# Image Generation (AI SDK)
Official API-based image generation. Supports OpenAI, Azure OpenAI, Google, OpenRouter, DashScope (阿里通义万象), MiniMax, Jimeng (即梦), Seedream (豆包) and Replicate providers.
Official API-based image generation. Supports OpenAI, Azure OpenAI, Google, OpenRouter, DashScope (阿里通义万象), Z.AI GLM-Image, MiniMax, Jimeng (即梦), Seedream (豆包) and Replicate providers.
## Script Directory
@@ -57,7 +57,7 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-imagine/EXTEND.md") { "user" }
Legacy compatibility: if `.baoyu-skills/baoyu-image-gen/EXTEND.md` exists and the new path does not, runtime renames it to `baoyu-imagine`. If both files exist, runtime leaves them unchanged and uses the new path.
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio | Default image size | Default models | Batch worker cap | Provider-specific batch limits
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio | Default image size | OpenAI image API dialect | Default models | Batch worker cap | Provider-specific batch limits
Schema: `references/config/preferences-schema.md`
@@ -76,7 +76,7 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --quality 2k
# From prompt files
${BUN_X} {baseDir}/scripts/main.ts --promptfiles system.md content.md --image out.png
# With reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate, MiniMax, or Seedream 4.0/4.5/5.0)
# With reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate supported families, MiniMax, or Seedream 4.0/4.5/5.0)
${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
# With reference images (explicit provider/model)
@@ -103,6 +103,12 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "为咖啡品牌设计一张 21:9
# DashScope legacy Qwen fixed-size model
${BUN_X} {baseDir}/scripts/main.ts --prompt "一张电影感海报" --image out.png --provider dashscope --model qwen-image-max --size 1664x928
# Z.AI GLM-image
${BUN_X} {baseDir}/scripts/main.ts --prompt "一张带清晰中文标题的科技海报" --image out.png --provider zai
# Z.AI GLM-image with explicit custom size
${BUN_X} {baseDir}/scripts/main.ts --prompt "A science illustration with labels" --image out.png --provider zai --model glm-image --size 1472x1088
# MiniMax
${BUN_X} {baseDir}/scripts/main.ts --prompt "A fashion editorial portrait by a bright studio window" --image out.jpg --provider minimax
@@ -112,11 +118,14 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A girl stands by the library window
# MiniMax with custom size (documented for image-01)
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cinematic poster" --image out.jpg --provider minimax --model image-01 --size 1536x1024
# Replicate (google/nano-banana-pro)
# Replicate (default: google/nano-banana-2)
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
# Replicate with specific model
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
# Replicate Seedream 4.5
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cinematic portrait" --image out.png --provider replicate --model bytedance/seedream-4.5 --ar 3:2
# Replicate Wan 2.7 Image Pro
${BUN_X} {baseDir}/scripts/main.ts --prompt "A concept frame" --image out.png --provider replicate --model wan-video/wan-2.7-image-pro --size 2048x1152
# Batch mode with saved prompt files
${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json
@@ -136,7 +145,7 @@ ${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json --jobs 4 --json
"promptFiles": ["prompts/hero.md"],
"image": "out/hero.png",
"provider": "replicate",
"model": "google/nano-banana-pro",
"model": "google/nano-banana-2",
"ar": "16:9",
"quality": "2k"
},
@@ -161,14 +170,15 @@ Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch fi
| `--image <path>` | Output image path (required in single-image mode) |
| `--batchfile <path>` | JSON batch file for multi-image generation |
| `--jobs <count>` | Worker count for batch mode (default: auto, max from config, built-in default 10) |
| `--provider google\|openai\|azure\|openrouter\|dashscope\|minimax\|jimeng\|seedream\|replicate` | Force provider (default: auto-detect) |
| `--model <id>`, `-m` | Model ID (Google: `gemini-3-pro-image-preview`; OpenAI: `gpt-image-1.5`; Azure: deployment name such as `gpt-image-1.5` or `image-prod`; OpenRouter: `google/gemini-3.1-flash-image-preview`; DashScope: `qwen-image-2.0-pro`; MiniMax: `image-01`) |
| `--provider google\|openai\|azure\|openrouter\|dashscope\|zai\|minimax\|jimeng\|seedream\|replicate` | Force provider (default: auto-detect) |
| `--model <id>`, `-m` | Model ID (Google: `gemini-3-pro-image-preview`; OpenAI: `gpt-image-1.5`; Azure: deployment name such as `gpt-image-1.5` or `image-prod`; OpenRouter: `google/gemini-3.1-flash-image-preview`; DashScope: `qwen-image-2.0-pro`; Z.AI: `glm-image`; MiniMax: `image-01`) |
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size <WxH>` | Size (e.g., `1024x1024`) |
| `--quality normal\|2k` | Quality preset (default: `2k`) |
| `--imageSize 1K\|2K\|4K` | Image size for Google/OpenRouter (default: from quality) |
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, Azure OpenAI edits (PNG/JPG only), OpenRouter multimodal models, Replicate, MiniMax subject-reference, and Seedream 5.0/4.5/4.0. Not supported by Jimeng, Seedream 3.0, or removed SeedEdit 3.0 |
| `--n <count>` | Number of images |
| `--imageApiDialect openai-native\|ratio-metadata` | OpenAI-compatible image API dialect. Use `ratio-metadata` when the endpoint is OpenAI-compatible but expects aspect-ratio `size` plus `metadata.resolution` instead of pixel `size` |
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, Azure OpenAI edits (PNG/JPG only), OpenRouter multimodal models, Replicate supported families, MiniMax subject-reference, and Seedream 5.0/4.5/4.0. Not supported by Jimeng, Seedream 3.0, or removed SeedEdit 3.0 |
| `--n <count>` | Number of images. Replicate currently supports only `--n 1` because this path saves exactly one output image |
| `--json` | JSON output |
## Environment Variables
@@ -180,6 +190,8 @@ Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch fi
| `OPENROUTER_API_KEY` | OpenRouter API key |
| `GOOGLE_API_KEY` | Google API key |
| `DASHSCOPE_API_KEY` | DashScope API key (阿里云) |
| `ZAI_API_KEY` | Z.AI API key |
| `BIGMODEL_API_KEY` | Backward-compatible alias for Z.AI API key |
| `MINIMAX_API_KEY` | MiniMax API key |
| `REPLICATE_API_TOKEN` | Replicate API token |
| `JIMENG_ACCESS_KEY_ID` | Jimeng (即梦) Volcengine access key |
@@ -191,11 +203,14 @@ Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch fi
| `OPENROUTER_IMAGE_MODEL` | OpenRouter model override (default: `google/gemini-3.1-flash-image-preview`) |
| `GOOGLE_IMAGE_MODEL` | Google model override |
| `DASHSCOPE_IMAGE_MODEL` | DashScope model override (default: `qwen-image-2.0-pro`) |
| `ZAI_IMAGE_MODEL` | Z.AI model override (default: `glm-image`) |
| `BIGMODEL_IMAGE_MODEL` | Backward-compatible alias for Z.AI model override |
| `MINIMAX_IMAGE_MODEL` | MiniMax model override (default: `image-01`) |
| `REPLICATE_IMAGE_MODEL` | Replicate model override (default: google/nano-banana-pro) |
| `REPLICATE_IMAGE_MODEL` | Replicate model override (default: google/nano-banana-2) |
| `JIMENG_IMAGE_MODEL` | Jimeng model override (default: jimeng_t2i_v40) |
| `SEEDREAM_IMAGE_MODEL` | Seedream model override (default: doubao-seedream-5-0-260128) |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint |
| `OPENAI_IMAGE_API_DIALECT` | OpenAI-compatible image API dialect override (`openai-native` or `ratio-metadata`) |
| `AZURE_OPENAI_BASE_URL` | Azure resource endpoint or deployment endpoint |
| `AZURE_API_VERSION` | Azure image API version (default: `2025-04-01-preview`) |
| `OPENROUTER_BASE_URL` | Custom OpenRouter endpoint (default: `https://openrouter.ai/api/v1`) |
@@ -203,6 +218,8 @@ Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch fi
| `OPENROUTER_TITLE` | Optional app name for OpenRouter attribution |
| `GOOGLE_BASE_URL` | Custom Google endpoint |
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
| `ZAI_BASE_URL` | Custom Z.AI endpoint (default: `https://api.z.ai/api/paas/v4`) |
| `BIGMODEL_BASE_URL` | Backward-compatible alias for Z.AI endpoint |
| `MINIMAX_BASE_URL` | Custom MiniMax endpoint (default: `https://api.minimax.io`) |
| `REPLICATE_BASE_URL` | Custom Replicate endpoint |
| `JIMENG_BASE_URL` | Custom Jimeng endpoint (default: `https://visual.volcengineapi.com`) |
@@ -227,6 +244,22 @@ For Azure, `--model` / `default_model.azure` should be the Azure deployment name
**EXTEND.md overrides env vars**. If both EXTEND.md `default_model.google: "gemini-3-pro-image-preview"` and env var `GOOGLE_IMAGE_MODEL=gemini-3.1-flash-image-preview` exist, EXTEND.md wins.
### OpenAI-Compatible Gateway Dialects
`provider=openai` means the auth and routing entrypoint is OpenAI-compatible. It does **not** guarantee that the upstream image API uses OpenAI native image-request semantics.
Use `default_image_api_dialect` in `EXTEND.md`, `OPENAI_IMAGE_API_DIALECT`, or `--imageApiDialect` when the endpoint expects a different wire format:
- `openai-native`: Sends pixel `size` such as `1536x1024` and native OpenAI quality fields when supported
- `ratio-metadata`: Sends aspect-ratio `size` such as `16:9` and maps quality/size intent into `metadata.resolution` (`1K|2K|4K`) plus `metadata.orientation`
Recommended use:
- OpenAI native Images API or strict clones: keep `openai-native`
- OpenAI-compatible gateways in front of Gemini or similar models: try `ratio-metadata`
Current limitation: `ratio-metadata` only applies to text-to-image generation. Reference-image edit flows still require `openai-native` or another provider with first-class edit support.
**Agent MUST display model info** before each generation:
- Show: `Using [provider] / [model]`
- Show switch hint: `Switch model: --model <id> | EXTEND.md default_model.[provider] | env <PROVIDER>_IMAGE_MODEL`
@@ -277,6 +310,32 @@ Official references:
- [Text-to-image guide](https://help.aliyun.com/zh/model-studio/text-to-image)
- [Qwen-Image Edit API](https://help.aliyun.com/zh/model-studio/qwen-image-edit-api)
### Z.AI Models
Use `--model glm-image` or set `default_model.zai` / `ZAI_IMAGE_MODEL` when the user wants GLM-image output.
Official Z.AI image model options currently documented in the sync image API:
- `glm-image` (recommended default)
- Text-to-image only in `baoyu-imagine`
- Native `quality` options are `hd` and `standard`; this skill maps `2k -> hd` and `normal -> standard`
- Recommended sizes: `1280x1280`, `1568x1056`, `1056x1568`, `1472x1088`, `1088x1472`, `1728x960`, `960x1728`
- Custom `--size` requires width and height between `1024` and `2048`, divisible by `32`, with total pixels <= `2^22`
- `cogview-4-250304`
- Legacy Z.AI image model family exposed by the same endpoint
- Custom `--size` requires width and height between `512` and `2048`, divisible by `16`, with total pixels <= `2^21`
Notes:
- The official sync API returns a temporary image URL; `baoyu-imagine` downloads that URL and writes the image locally
- `--ref` is not supported for Z.AI in this skill yet
- The sync API currently returns a single image, so `--n > 1` is rejected
Official references:
- [GLM-Image Guide](https://docs.z.ai/guides/image/glm-image)
- [Generate Image API](https://docs.z.ai/api-reference/image/generate-image)
### MiniMax Models
Use `--model image-01` or set `default_model.minimax` / `MINIMAX_IMAGE_MODEL` when the user wants MiniMax image generation.
@@ -322,10 +381,33 @@ Notes:
### Replicate Models
Supported model formats:
Replicate support in `baoyu-imagine` is intentionally scoped to the model families that the tool can validate locally and save without dropping outputs:
- `owner/name` (recommended for official models), e.g. `google/nano-banana-pro`
- `owner/name:version` (community models by version), e.g. `stability-ai/sdxl:<version>`
- `google/nano-banana*` (default: `google/nano-banana-2`)
- Supports prompt-only and reference-image generation
- Uses Replicate `aspect_ratio`, `resolution`, and `output_format`
- `--size <WxH>` is accepted only as a shorthand for a documented aspect ratio plus `1K` / `2K`
- `bytedance/seedream-4.5`
- Supports prompt-only and reference-image generation
- Uses Replicate `size`, `aspect_ratio`, and `image_input`
- Local validation blocks unsupported `1K` requests before the API call
- `bytedance/seedream-5-lite`
- Supports prompt-only and reference-image generation
- Uses Replicate `size`, `aspect_ratio`, and `image_input`
- Local validation currently accepts `2K` / `3K` only
- `wan-video/wan-2.7-image`
- Supports prompt-only and reference-image generation
- Uses Replicate `size` and `images`
- Max output size is 2K
- `wan-video/wan-2.7-image-pro`
- Supports prompt-only and reference-image generation
- Uses Replicate `size` and `images`
- 4K is allowed only for text-to-image; local validation blocks `4K + --ref`
Guardrails:
- Replicate currently supports only single-output save semantics in this tool. Keep `--n 1`.
- If a Replicate model is outside the compatibility list above, `baoyu-imagine` only treats it as prompt-only and rejects advanced local options instead of guessing a nano-banana-style schema.
Examples:
@@ -342,7 +424,7 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider r
1. `--ref` provided + no `--provider` → auto-select Google first, then OpenAI, then Azure, then OpenRouter, then Replicate, then Seedream, then MiniMax (MiniMax subject reference is more specialized toward character/portrait consistency)
2. `--provider` specified → use it (if `--ref`, must be `google`, `openai`, `azure`, `openrouter`, `replicate`, `seedream`, or `minimax`)
3. Only one API key available → use that provider
4. Multiple available → default to Google
4. Multiple available → default to Google, then OpenAI, Azure, OpenRouter, DashScope, Z.AI, MiniMax, Replicate, Jimeng, Seedream
## Quality Presets
@@ -360,7 +442,7 @@ Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
- Google multimodal: uses `imageConfig.aspectRatio`
- OpenAI: maps to closest supported size
- OpenRouter: sends `imageGenerationOptions.aspect_ratio`; if only `--size <WxH>` is given, aspect ratio is inferred automatically
- Replicate: passes `aspect_ratio` to model; when `--ref` is provided without `--ar`, defaults to `match_input_image`
- Replicate: behavior is model-family-specific. `google/nano-banana*` uses `aspect_ratio`; `bytedance/seedream-*` uses documented Replicate aspect ratios; Wan 2.7 maps `--ar` to a concrete `size`
- MiniMax: sends official `aspect_ratio` values directly; if `--size <WxH>` is given without `--ar`, `width` / `height` are sent for `image-01`
## Generation Mode
@@ -53,10 +53,12 @@ options:
description: "Router for Gemini/FLUX/OpenAI-compatible image models"
- label: "DashScope"
description: "Alibaba Cloud - Qwen-Image, strong Chinese/English text rendering"
- label: "Z.AI"
description: "GLM-image, strong poster and text-heavy image generation"
- label: "MiniMax"
description: "MiniMax image generation with subject-reference character workflows"
- label: "Replicate"
description: "Community models - nano-banana-pro, flexible model selection"
description: "Curated Replicate image families - nano-banana-2, Seedream, and Wan image models"
```
### Question 2: Default Google Model
@@ -119,6 +121,20 @@ options:
description: "Faster variant, use aspect ratio instead of custom size"
```
### Question 2e: Default Z.AI Model
Only show if user selected Z.AI.
```yaml
header: "Z.AI Model"
question: "Default Z.AI image generation model?"
options:
- label: "glm-image (Recommended)"
description: "Best default for posters, diagrams, and text-heavy images"
- label: "cogview-4-250304"
description: "Legacy Z.AI image model on the same endpoint"
```
### Question 3: Default Quality
```yaml
@@ -159,17 +175,21 @@ default_provider: [selected provider or null]
default_quality: [selected quality]
default_aspect_ratio: null
default_image_size: null
default_image_api_dialect: null
default_model:
google: [selected google model or null]
openai: null
azure: [selected azure deployment or null]
openrouter: [selected openrouter model or null]
dashscope: null
zai: [selected Z.AI model or null]
minimax: [selected minimax model or null]
replicate: null
---
```
If the user selects `OpenAI` but says their endpoint is only OpenAI-compatible and fronts another image model family, save `default_image_api_dialect: ratio-metadata` when they explicitly confirm the gateway expects aspect-ratio `size` plus metadata-based resolution. Otherwise leave it `null` / `openai-native`.
## Flow 2: EXTEND.md Exists, Model Null
When EXTEND.md exists but `default_model.[current_provider]` is null, ask ONLY the model question for the current provider.
@@ -257,16 +277,38 @@ Notes for DashScope setup:
- `qwen-image-max` / `qwen-image-plus` / `qwen-image` only support five fixed sizes: `1664*928`, `1472*1104`, `1328*1328`, `1104*1472`, `928*1664`.
- In `baoyu-imagine`, `quality` is a compatibility preset. It is not a native DashScope parameter.
### Z.AI Model Selection
```yaml
header: "Z.AI Model"
question: "Choose a default Z.AI image generation model?"
options:
- label: "glm-image (Recommended)"
description: "Current flagship image model with better text rendering and poster layouts"
- label: "cogview-4-250304"
description: "Legacy model on the sync image endpoint"
```
Notes for Z.AI setup:
- Prefer `glm-image` for posters, diagrams, and Chinese/English text-heavy layouts.
- In `baoyu-imagine`, Z.AI currently exposes text-to-image only; reference images are not wired for this provider.
- The sync Z.AI image API returns a downloadable image URL, which the runtime saves locally after download.
### Replicate Model Selection
```yaml
header: "Replicate Model"
question: "Choose a default Replicate image generation model?"
options:
- label: "google/nano-banana-pro (Recommended)"
description: "Google's fast image model on Replicate"
- label: "google/nano-banana"
description: "Google's base image model on Replicate"
- label: "google/nano-banana-2 (Recommended)"
description: "Current default for general Replicate image generation in baoyu-imagine"
- label: "bytedance/seedream-4.5"
description: "Replicate Seedream 4.5 with validated local size/ref guardrails"
- label: "bytedance/seedream-5-lite"
description: "Replicate Seedream 5 Lite with validated local size/ref guardrails"
- label: "wan-video/wan-2.7-image-pro"
description: "Replicate Wan 2.7 Image Pro with 4K text-to-image support"
```
### MiniMax Model Selection
@@ -302,6 +344,7 @@ default_model:
azure: [value or null]
openrouter: [value or null]
dashscope: [value or null]
zai: [value or null]
minimax: [value or null]
replicate: [value or null]
```
@@ -11,7 +11,7 @@ description: EXTEND.md YAML schema for baoyu-imagine user preferences
---
version: 1
default_provider: null # google|openai|azure|openrouter|dashscope|minimax|replicate|null (null = auto-detect)
default_provider: null # google|openai|azure|openrouter|dashscope|zai|minimax|replicate|null (null = auto-detect)
default_quality: null # normal|2k|null (null = use default: 2k)
@@ -19,14 +19,17 @@ default_aspect_ratio: null # "16:9"|"1:1"|"4:3"|"3:4"|"2.35:1"|null
default_image_size: null # 1K|2K|4K|null (Google/OpenRouter, overrides quality)
default_image_api_dialect: null # openai-native|ratio-metadata|null (OpenAI-compatible gateways; null = use env/default)
default_model:
google: null # e.g., "gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview"
openai: null # e.g., "gpt-image-1.5", "gpt-image-1"
azure: null # Azure deployment name, e.g., "gpt-image-1.5" or "image-prod"
openrouter: null # e.g., "google/gemini-3.1-flash-image-preview"
dashscope: null # e.g., "qwen-image-2.0-pro"
zai: null # e.g., "glm-image"
minimax: null # e.g., "image-01"
replicate: null # e.g., "google/nano-banana-pro"
replicate: null # e.g., "google/nano-banana-2"
batch:
max_workers: 10
@@ -49,6 +52,9 @@ batch:
dashscope:
concurrency: 3
start_interval_ms: 1100
zai:
concurrency: 3
start_interval_ms: 1100
minimax:
concurrency: 3
start_interval_ms: 1100
@@ -64,11 +70,13 @@ batch:
| `default_quality` | string\|null | null | Default quality (null = 2k) |
| `default_aspect_ratio` | string\|null | null | Default aspect ratio |
| `default_image_size` | string\|null | null | Google/OpenRouter image size (overrides quality) |
| `default_image_api_dialect` | string\|null | null | OpenAI-compatible image dialect (`openai-native` or `ratio-metadata`) |
| `default_model.google` | string\|null | null | Google default model |
| `default_model.openai` | string\|null | null | OpenAI default model |
| `default_model.azure` | string\|null | null | Azure default deployment name |
| `default_model.openrouter` | string\|null | null | OpenRouter default model |
| `default_model.dashscope` | string\|null | null | DashScope default model |
| `default_model.zai` | string\|null | null | Z.AI default model |
| `default_model.minimax` | string\|null | null | MiniMax default model |
| `default_model.replicate` | string\|null | null | Replicate default model |
| `batch.max_workers` | int\|null | 10 | Batch worker cap |
@@ -83,6 +91,7 @@ batch:
version: 1
default_provider: google
default_quality: 2k
default_image_api_dialect: null
---
```
@@ -94,14 +103,16 @@ default_provider: google
default_quality: 2k
default_aspect_ratio: "16:9"
default_image_size: 2K
default_image_api_dialect: null
default_model:
google: "gemini-3-pro-image-preview"
openai: "gpt-image-1.5"
azure: "gpt-image-1.5"
openrouter: "google/gemini-3.1-flash-image-preview"
dashscope: "qwen-image-2.0-pro"
zai: "glm-image"
minimax: "image-01"
replicate: "google/nano-banana-pro"
replicate: "google/nano-banana-2"
batch:
max_workers: 10
provider_limits:
@@ -111,6 +122,9 @@ batch:
azure:
concurrency: 3
start_interval_ms: 1100
zai:
concurrency: 3
start_interval_ms: 1100
openrouter:
concurrency: 3
start_interval_ms: 1100
+95 -2
View File
@@ -17,6 +17,7 @@ import {
mergeConfig,
normalizeOutputImagePath,
parseArgs,
parseOpenAIImageApiDialect,
parseSimpleYaml,
} from "./main.ts";
@@ -28,9 +29,12 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
provider: null,
model: null,
aspectRatio: null,
aspectRatioSource: null,
size: null,
quality: null,
imageSize: null,
imageSizeSource: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
@@ -78,11 +82,13 @@ test("parseArgs parses the main baoyu-imagine CLI flags", () => {
"--image",
"out/hero",
"--provider",
"openai",
"zai",
"--quality",
"2k",
"--imageSize",
"4k",
"--imageApiDialect",
"ratio-metadata",
"--ref",
"ref/one.png",
"ref/two.jpg",
@@ -95,9 +101,12 @@ test("parseArgs parses the main baoyu-imagine CLI flags", () => {
assert.deepEqual(args.promptFiles, ["prompts/system.md", "prompts/content.md"]);
assert.equal(args.imagePath, "out/hero");
assert.equal(args.provider, "openai");
assert.equal(args.provider, "zai");
assert.equal(args.quality, "2k");
assert.equal(args.aspectRatioSource, null);
assert.equal(args.imageSize, "4K");
assert.equal(args.imageSizeSource, "cli");
assert.equal(args.imageApiDialect, "ratio-metadata");
assert.deepEqual(args.referenceImages, ["ref/one.png", "ref/two.jpg"]);
assert.equal(args.n, 3);
assert.equal(args.jobs, 5);
@@ -121,9 +130,11 @@ default_provider: openrouter
default_quality: normal
default_aspect_ratio: '16:9'
default_image_size: 2K
default_image_api_dialect: ratio-metadata
default_model:
google: gemini-3-pro-image-preview
openai: gpt-image-1.5
zai: glm-image
azure: image-prod
minimax: image-01
batch:
@@ -134,6 +145,9 @@ batch:
start_interval_ms: 900
openai:
concurrency: 4
zai:
concurrency: 2
start_interval_ms: 1000
minimax:
concurrency: 2
start_interval_ms: 1400
@@ -149,8 +163,10 @@ batch:
assert.equal(config.default_quality, "normal");
assert.equal(config.default_aspect_ratio, "16:9");
assert.equal(config.default_image_size, "2K");
assert.equal(config.default_image_api_dialect, "ratio-metadata");
assert.equal(config.default_model?.google, "gemini-3-pro-image-preview");
assert.equal(config.default_model?.openai, "gpt-image-1.5");
assert.equal(config.default_model?.zai, "glm-image");
assert.equal(config.default_model?.azure, "image-prod");
assert.equal(config.default_model?.minimax, "image-01");
assert.equal(config.batch?.max_workers, 8);
@@ -161,6 +177,10 @@ batch:
assert.deepEqual(config.batch?.provider_limits?.openai, {
concurrency: 4,
});
assert.deepEqual(config.batch?.provider_limits?.zai, {
concurrency: 2,
start_interval_ms: 1000,
});
assert.deepEqual(config.batch?.provider_limits?.minimax, {
concurrency: 2,
start_interval_ms: 1400,
@@ -239,13 +259,48 @@ test("mergeConfig only fills values missing from CLI args", () => {
default_quality: "2k",
default_aspect_ratio: "3:2",
default_image_size: "2K",
default_image_api_dialect: "ratio-metadata",
} satisfies Partial<ExtendConfig>,
);
assert.equal(merged.provider, "openai");
assert.equal(merged.quality, "2k");
assert.equal(merged.aspectRatio, "3:2");
assert.equal(merged.aspectRatioSource, "config");
assert.equal(merged.imageSize, "4K");
assert.equal(merged.imageSizeSource, "cli");
assert.equal(merged.imageApiDialect, "ratio-metadata");
});
test("mergeConfig tags inherited imageSize defaults so providers can ignore incompatible config", () => {
const merged = mergeConfig(
makeArgs(),
{
default_image_size: "2K",
} satisfies Partial<ExtendConfig>,
);
assert.equal(merged.imageSize, "2K");
assert.equal(merged.imageSizeSource, "config");
});
test("mergeConfig falls back to OPENAI_IMAGE_API_DIALECT when CLI and EXTEND are unset", (t) => {
useEnv(t, {
OPENAI_IMAGE_API_DIALECT: "ratio-metadata",
});
const merged = mergeConfig(makeArgs(), {});
assert.equal(merged.imageApiDialect, "ratio-metadata");
});
test("parseOpenAIImageApiDialect validates supported values", () => {
assert.equal(parseOpenAIImageApiDialect("openai-native"), "openai-native");
assert.equal(parseOpenAIImageApiDialect("ratio-metadata"), "ratio-metadata");
assert.equal(parseOpenAIImageApiDialect(null), null);
assert.throws(
() => parseOpenAIImageApiDialect("gateway-magic"),
/Invalid OpenAI image API dialect/,
);
});
test("detectProvider rejects non-ref-capable providers and prefers Google first when multiple keys exist", (t) => {
@@ -316,6 +371,27 @@ test("detectProvider selects Azure when only Azure credentials are configured",
);
});
test("detectProvider selects Z.AI when credentials are present or the model id matches", (t) => {
useEnv(t, {
GOOGLE_API_KEY: null,
OPENAI_API_KEY: null,
AZURE_OPENAI_API_KEY: null,
AZURE_OPENAI_BASE_URL: null,
OPENROUTER_API_KEY: null,
DASHSCOPE_API_KEY: null,
ZAI_API_KEY: "zai-key",
BIGMODEL_API_KEY: null,
MINIMAX_API_KEY: null,
REPLICATE_API_TOKEN: null,
JIMENG_ACCESS_KEY_ID: null,
JIMENG_SECRET_ACCESS_KEY: null,
ARK_API_KEY: null,
});
assert.equal(detectProvider(makeArgs()), "zai");
assert.equal(detectProvider(makeArgs({ model: "glm-image" })), "zai");
});
test("detectProvider infers Seedream from model id and allows Seedream reference-image workflows", (t) => {
useEnv(t, {
GOOGLE_API_KEY: null,
@@ -375,6 +451,7 @@ test("batch worker and provider-rate-limit configuration prefer env over EXTEND
BAOYU_IMAGE_GEN_MAX_WORKERS: "12",
BAOYU_IMAGE_GEN_GOOGLE_CONCURRENCY: "5",
BAOYU_IMAGE_GEN_GOOGLE_START_INTERVAL_MS: "450",
BAOYU_IMAGE_GEN_ZAI_CONCURRENCY: "4",
});
const extendConfig: Partial<ExtendConfig> = {
@@ -385,6 +462,10 @@ test("batch worker and provider-rate-limit configuration prefer env over EXTEND
concurrency: 2,
start_interval_ms: 900,
},
zai: {
concurrency: 1,
start_interval_ms: 1200,
},
minimax: {
concurrency: 1,
start_interval_ms: 1500,
@@ -398,6 +479,10 @@ test("batch worker and provider-rate-limit configuration prefer env over EXTEND
concurrency: 5,
startIntervalMs: 450,
});
assert.deepEqual(getConfiguredProviderRateLimits(extendConfig).zai, {
concurrency: 4,
startIntervalMs: 1200,
});
assert.deepEqual(getConfiguredProviderRateLimits(extendConfig).minimax, {
concurrency: 1,
startIntervalMs: 1500,
@@ -435,6 +520,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
makeArgs({
provider: "replicate",
quality: "2k",
imageApiDialect: "ratio-metadata",
json: true,
}),
loaded.tasks[0]!,
@@ -451,6 +537,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
assert.equal(taskArgs.provider, "replicate");
assert.equal(taskArgs.aspectRatio, "16:9");
assert.equal(taskArgs.quality, "2k");
assert.equal(taskArgs.imageApiDialect, "ratio-metadata");
assert.equal(taskArgs.json, true);
});
@@ -464,5 +551,11 @@ test("path normalization, worker count, and retry classification follow expected
assert.equal(getWorkerCount(5, 0, 4), 1);
assert.equal(isRetryableGenerationError(new Error("API error (401): denied")), false);
assert.equal(
isRetryableGenerationError(
new Error("Replicate returned 2 outputs, but baoyu-imagine currently supports saving exactly one image per request."),
),
false,
);
assert.equal(isRetryableGenerationError(new Error("socket hang up")), true);
});
+81 -10
View File
@@ -8,6 +8,7 @@ import type {
BatchTaskInput,
CliArgs,
ExtendConfig,
OpenAIImageApiDialect,
Provider,
} from "./types";
@@ -58,6 +59,7 @@ const DEFAULT_PROVIDER_RATE_LIMITS: Record<Provider, ProviderRateLimit> = {
openai: { concurrency: 3, startIntervalMs: 1100 },
openrouter: { concurrency: 3, startIntervalMs: 1100 },
dashscope: { concurrency: 3, startIntervalMs: 1100 },
zai: { concurrency: 3, startIntervalMs: 1100 },
minimax: { concurrency: 3, startIntervalMs: 1100 },
jimeng: { concurrency: 3, startIntervalMs: 1100 },
seedream: { concurrency: 3, startIntervalMs: 1100 },
@@ -76,14 +78,15 @@ Options:
--image <path> Output image path (required in single-image mode)
--batchfile <path> JSON batch file for multi-image generation
--jobs <count> Worker count for batch mode (default: auto, max from config, built-in default 10)
--provider google|openai|openrouter|dashscope|minimax|replicate|jimeng|seedream|azure Force provider (auto-detect by default)
--provider google|openai|openrouter|dashscope|zai|minimax|replicate|jimeng|seedream|azure Force provider (auto-detect by default)
-m, --model <id> Model ID
--ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3)
--size <WxH> Size (e.g., 1024x1024)
--quality normal|2k Quality preset (default: 2k)
--imageSize 1K|2K|4K Image size for Google/OpenRouter (default: from quality)
--ref <files...> Reference images (Google, OpenAI, Azure, OpenRouter, Replicate, MiniMax, or Seedream 4.0/4.5/5.0)
--n <count> Number of images for the current task (default: 1)
--imageApiDialect <id> OpenAI-compatible image dialect: openai-native|ratio-metadata
--ref <files...> Reference images (Google, OpenAI, Azure, OpenRouter, Replicate supported families, MiniMax, or Seedream 4.0/4.5/5.0)
--n <count> Number of images for the current task (default: 1; Replicate currently requires 1)
--json JSON output
-h, --help Show help
@@ -96,7 +99,7 @@ Batch file format:
"promptFiles": ["prompts/hero.md"],
"image": "out/hero.png",
"provider": "replicate",
"model": "google/nano-banana-pro",
"model": "google/nano-banana-2",
"ar": "16:9"
}
]
@@ -106,6 +109,7 @@ Behavior:
- Batch mode automatically runs in parallel when pending tasks >= 2
- Each image retries automatically up to 3 attempts
- Batch summary reports success count, failure count, and per-image errors
- Replicate currently supports single-image save semantics only; --n must stay at 1
Environment variables:
OPENAI_API_KEY OpenAI API key
@@ -113,6 +117,8 @@ Environment variables:
GOOGLE_API_KEY Google API key
GEMINI_API_KEY Gemini API key (alias for GOOGLE_API_KEY)
DASHSCOPE_API_KEY DashScope API key
ZAI_API_KEY Z.AI API key
BIGMODEL_API_KEY Backward-compatible alias for Z.AI API key
MINIMAX_API_KEY MiniMax API key
REPLICATE_API_TOKEN Replicate API token
JIMENG_ACCESS_KEY_ID Jimeng Access Key ID
@@ -122,17 +128,22 @@ Environment variables:
OPENROUTER_IMAGE_MODEL Default OpenRouter model (google/gemini-3.1-flash-image-preview)
GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview)
DASHSCOPE_IMAGE_MODEL Default DashScope model (qwen-image-2.0-pro)
ZAI_IMAGE_MODEL Default Z.AI model (glm-image)
BIGMODEL_IMAGE_MODEL Backward-compatible alias for Z.AI model (glm-image)
MINIMAX_IMAGE_MODEL Default MiniMax model (image-01)
REPLICATE_IMAGE_MODEL Default Replicate model (google/nano-banana-pro)
REPLICATE_IMAGE_MODEL Default Replicate model (google/nano-banana-2)
JIMENG_IMAGE_MODEL Default Jimeng model (jimeng_t2i_v40)
SEEDREAM_IMAGE_MODEL Default Seedream model (doubao-seedream-5-0-260128)
OPENAI_BASE_URL Custom OpenAI endpoint
OPENAI_IMAGE_API_DIALECT OpenAI-compatible image dialect (openai-native|ratio-metadata)
OPENAI_IMAGE_USE_CHAT Use /chat/completions instead of /images/generations (true|false)
OPENROUTER_BASE_URL Custom OpenRouter endpoint
OPENROUTER_HTTP_REFERER Optional app URL for OpenRouter attribution
OPENROUTER_TITLE Optional app name for OpenRouter attribution
GOOGLE_BASE_URL Custom Google endpoint
DASHSCOPE_BASE_URL Custom DashScope endpoint
ZAI_BASE_URL Custom Z.AI endpoint
BIGMODEL_BASE_URL Backward-compatible alias for Z.AI endpoint
MINIMAX_BASE_URL Custom MiniMax endpoint
REPLICATE_BASE_URL Custom Replicate endpoint
JIMENG_BASE_URL Custom Jimeng endpoint
@@ -157,9 +168,12 @@ export function parseArgs(argv: string[]): CliArgs {
provider: null,
model: null,
aspectRatio: null,
aspectRatioSource: null,
size: null,
quality: null,
imageSize: null,
imageSizeSource: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
@@ -239,6 +253,7 @@ export function parseArgs(argv: string[]): CliArgs {
v !== "openai" &&
v !== "openrouter" &&
v !== "dashscope" &&
v !== "zai" &&
v !== "minimax" &&
v !== "replicate" &&
v !== "jimeng" &&
@@ -262,6 +277,7 @@ export function parseArgs(argv: string[]): CliArgs {
const v = argv[++i];
if (!v) throw new Error("Missing value for --ar");
out.aspectRatio = v;
out.aspectRatioSource = "cli";
continue;
}
@@ -283,6 +299,16 @@ export function parseArgs(argv: string[]): CliArgs {
const v = argv[++i]?.toUpperCase();
if (v !== "1K" && v !== "2K" && v !== "4K") throw new Error(`Invalid imageSize: ${v}`);
out.imageSize = v;
out.imageSizeSource = "cli";
continue;
}
if (a === "--imageApiDialect") {
const v = argv[++i];
if (v !== "openai-native" && v !== "ratio-metadata") {
throw new Error(`Invalid imageApiDialect: ${v}`);
}
out.imageApiDialect = v;
continue;
}
@@ -389,12 +415,16 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
config.default_aspect_ratio = cleaned === "null" ? null : cleaned;
} else if (key === "default_image_size") {
config.default_image_size = value === "null" ? null : value as "1K" | "2K" | "4K";
} else if (key === "default_image_api_dialect") {
config.default_image_api_dialect =
value === "null" ? null : parseOpenAIImageApiDialect(value);
} else if (key === "default_model") {
config.default_model = {
google: null,
openai: null,
openrouter: null,
dashscope: null,
zai: null,
minimax: null,
replicate: null,
jimeng: null,
@@ -423,6 +453,7 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
key === "openai" ||
key === "openrouter" ||
key === "dashscope" ||
key === "zai" ||
key === "minimax" ||
key === "replicate" ||
key === "jimeng" ||
@@ -441,6 +472,7 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
key === "openai" ||
key === "openrouter" ||
key === "dashscope" ||
key === "zai" ||
key === "minimax" ||
key === "replicate" ||
key === "jimeng" ||
@@ -471,6 +503,15 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
return config;
}
export function parseOpenAIImageApiDialect(
value: string | undefined | null
): OpenAIImageApiDialect | null {
if (!value) return null;
const normalized = value.replace(/['"]/g, "").trim();
if (normalized === "openai-native" || normalized === "ratio-metadata") return normalized;
throw new Error(`Invalid OpenAI image API dialect: ${value}`);
}
type ExtendConfigPathPair = {
current: string;
legacy: string;
@@ -530,12 +571,25 @@ export async function loadExtendConfig(
}
export function mergeConfig(args: CliArgs, extend: Partial<ExtendConfig>): CliArgs {
const aspectRatio = args.aspectRatio ?? extend.default_aspect_ratio ?? null;
const imageSize = args.imageSize ?? extend.default_image_size ?? null;
const imageApiDialect =
args.imageApiDialect ??
extend.default_image_api_dialect ??
parseOpenAIImageApiDialect(process.env.OPENAI_IMAGE_API_DIALECT);
return {
...args,
provider: args.provider ?? extend.default_provider ?? null,
quality: args.quality ?? extend.default_quality ?? null,
aspectRatio: args.aspectRatio ?? extend.default_aspect_ratio ?? null,
imageSize: args.imageSize ?? extend.default_image_size ?? null,
aspectRatio,
aspectRatioSource:
args.aspectRatioSource ??
(args.aspectRatio !== null ? "cli" : (aspectRatio !== null ? "config" : null)),
imageSize,
imageSizeSource:
args.imageSizeSource ??
(args.imageSize !== null ? "cli" : (imageSize !== null ? "config" : null)),
imageApiDialect,
};
}
@@ -571,13 +625,14 @@ export function getConfiguredProviderRateLimits(
openai: { ...DEFAULT_PROVIDER_RATE_LIMITS.openai },
openrouter: { ...DEFAULT_PROVIDER_RATE_LIMITS.openrouter },
dashscope: { ...DEFAULT_PROVIDER_RATE_LIMITS.dashscope },
zai: { ...DEFAULT_PROVIDER_RATE_LIMITS.zai },
minimax: { ...DEFAULT_PROVIDER_RATE_LIMITS.minimax },
jimeng: { ...DEFAULT_PROVIDER_RATE_LIMITS.jimeng },
seedream: { ...DEFAULT_PROVIDER_RATE_LIMITS.seedream },
azure: { ...DEFAULT_PROVIDER_RATE_LIMITS.azure },
};
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "minimax", "jimeng", "seedream", "azure"] as Provider[]) {
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure"] as Provider[]) {
const envPrefix = `BAOYU_IMAGE_GEN_${provider.toUpperCase()}`;
const extendLimit = extendConfig.batch?.provider_limits?.[provider];
configured[provider] = {
@@ -629,6 +684,7 @@ function inferProviderFromModel(model: string | null): Provider | null {
const normalized = model.trim();
if (normalized.includes("seedream") || normalized.includes("seededit")) return "seedream";
if (normalized === "image-01" || normalized === "image-01-live") return "minimax";
if (normalized === "glm-image" || normalized === "cogview-4-250304") return "zai";
return null;
}
@@ -656,6 +712,7 @@ export function detectProvider(args: CliArgs): Provider {
const hasOpenai = !!process.env.OPENAI_API_KEY;
const hasOpenrouter = !!process.env.OPENROUTER_API_KEY;
const hasDashscope = !!process.env.DASHSCOPE_API_KEY;
const hasZai = !!(process.env.ZAI_API_KEY || process.env.BIGMODEL_API_KEY);
const hasMinimax = !!process.env.MINIMAX_API_KEY;
const hasReplicate = !!process.env.REPLICATE_API_TOKEN;
const hasJimeng = !!(process.env.JIMENG_ACCESS_KEY_ID && process.env.JIMENG_SECRET_ACCESS_KEY);
@@ -676,6 +733,13 @@ export function detectProvider(args: CliArgs): Provider {
return "minimax";
}
if (modelProvider === "zai") {
if (!hasZai) {
throw new Error("Model looks like a Z.AI image model, but ZAI_API_KEY is not set.");
}
return "zai";
}
if (args.referenceImages.length > 0) {
if (hasGoogle) return "google";
if (hasOpenai) return "openai";
@@ -695,6 +759,7 @@ export function detectProvider(args: CliArgs): Provider {
hasAzure && "azure",
hasOpenrouter && "openrouter",
hasDashscope && "dashscope",
hasZai && "zai",
hasMinimax && "minimax",
hasReplicate && "replicate",
hasJimeng && "jimeng",
@@ -705,7 +770,7 @@ export function detectProvider(args: CliArgs): Provider {
if (available.length > 1) return available[0]!;
throw new Error(
"No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, AZURE_OPENAI_API_KEY+AZURE_OPENAI_BASE_URL, OPENROUTER_API_KEY, DASHSCOPE_API_KEY, MINIMAX_API_KEY, REPLICATE_API_TOKEN, JIMENG keys, or ARK_API_KEY.\n" +
"No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, AZURE_OPENAI_API_KEY+AZURE_OPENAI_BASE_URL, OPENROUTER_API_KEY, DASHSCOPE_API_KEY, ZAI_API_KEY, MINIMAX_API_KEY, REPLICATE_API_TOKEN, JIMENG keys, or ARK_API_KEY.\n" +
"Create ~/.baoyu-skills/.env or <cwd>/.baoyu-skills/.env with your keys."
);
}
@@ -737,6 +802,7 @@ export function isRetryableGenerationError(error: unknown): boolean {
"API error (403)",
"API error (404)",
"temporarily disabled",
"supports saving exactly one image",
];
return !nonRetryableMarkers.some((marker) => msg.includes(marker));
}
@@ -744,6 +810,7 @@ export function isRetryableGenerationError(error: unknown): boolean {
async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
if (provider === "google") return (await import("./providers/google")) as ProviderModule;
if (provider === "dashscope") return (await import("./providers/dashscope")) as ProviderModule;
if (provider === "zai") return (await import("./providers/zai")) as ProviderModule;
if (provider === "minimax") return (await import("./providers/minimax")) as ProviderModule;
if (provider === "replicate") return (await import("./providers/replicate")) as ProviderModule;
if (provider === "openrouter") return (await import("./providers/openrouter")) as ProviderModule;
@@ -775,6 +842,7 @@ function getModelForProvider(
return extendConfig.default_model.openrouter;
}
if (provider === "dashscope" && extendConfig.default_model.dashscope) return extendConfig.default_model.dashscope;
if (provider === "zai" && extendConfig.default_model.zai) return extendConfig.default_model.zai;
if (provider === "minimax" && extendConfig.default_model.minimax) return extendConfig.default_model.minimax;
if (provider === "replicate" && extendConfig.default_model.replicate) return extendConfig.default_model.replicate;
if (provider === "jimeng" && extendConfig.default_model.jimeng) return extendConfig.default_model.jimeng;
@@ -848,9 +916,12 @@ export function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput, batchDir
provider: task.provider ?? baseArgs.provider ?? null,
model: task.model ?? baseArgs.model ?? null,
aspectRatio: task.ar ?? baseArgs.aspectRatio ?? null,
aspectRatioSource: task.ar != null ? "task" : (baseArgs.aspectRatioSource ?? null),
size: task.size ?? baseArgs.size ?? null,
quality: task.quality ?? baseArgs.quality ?? null,
imageSize: task.imageSize ?? baseArgs.imageSize ?? null,
imageSizeSource: task.imageSize != null ? "task" : (baseArgs.imageSizeSource ?? null),
imageApiDialect: task.imageApiDialect ?? baseArgs.imageApiDialect ?? null,
referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchPath(batchDir, filePath)) : [],
n: task.n ?? baseArgs.n,
batchFile: null,
@@ -999,7 +1070,7 @@ async function runBatchTasks(
const acquireProvider = createProviderGate(providerRateLimits);
const workerCount = getWorkerCount(tasks.length, jobs, maxWorkers);
console.error(`Batch mode: ${tasks.length} tasks, ${workerCount} workers, parallel mode enabled.`);
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "jimeng", "seedream", "azure"] as Provider[]) {
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure"] as Provider[]) {
const limit = providerRateLimits[provider];
console.error(`- ${provider}: concurrency=${limit.concurrency}, startIntervalMs=${limit.startIntervalMs}`);
}
@@ -48,6 +48,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
size: null,
quality: null,
imageSize: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
@@ -50,6 +50,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
size: null,
quality: null,
imageSize: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
@@ -15,6 +15,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
size: null,
quality: null,
imageSize: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
@@ -50,6 +50,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
size: null,
quality: null,
imageSize: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
@@ -2,9 +2,16 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
buildOpenAIGenerationsBody,
extractImageFromResponse,
getOpenAIAspectRatio,
getOpenAIImageApiDialect,
getOpenAIResolution,
getMimeType,
getOpenAISize,
getOrientationFromAspectRatio,
inferAspectRatioFromSize,
inferResolutionFromSize,
parseAspectRatio,
} from "./openai.ts";
@@ -18,6 +25,69 @@ test("OpenAI aspect-ratio parsing and size selection match model families", () =
assert.equal(getOpenAISize("dall-e-2", "16:9", "2k"), "1024x1024");
assert.equal(getOpenAISize("gpt-image-1.5", "16:9", "2k"), "1536x1024");
assert.equal(getOpenAISize("gpt-image-1.5", "4:3", "2k"), "1024x1024");
assert.equal(inferAspectRatioFromSize("1536x1024"), "3:2");
assert.equal(inferResolutionFromSize("1536x1024"), "2K");
assert.equal(getOpenAIAspectRatio({ aspectRatio: null, size: "2048x1152" }), "16:9");
assert.equal(getOpenAIResolution({ imageSize: null, size: "2048x1152", quality: "normal" }), "2K");
assert.equal(getOrientationFromAspectRatio("16:9"), "landscape");
assert.equal(getOrientationFromAspectRatio("9:16"), "portrait");
assert.equal(getOrientationFromAspectRatio("1:1"), null);
assert.equal(getOpenAIImageApiDialect({ imageApiDialect: null }), "openai-native");
});
test("OpenAI generations body switches between native and ratio-metadata dialects", () => {
assert.deepEqual(
buildOpenAIGenerationsBody("Draw a skyline", "gpt-image-1.5", {
aspectRatio: "16:9",
size: null,
quality: "2k",
imageSize: null,
imageApiDialect: null,
}),
{
model: "gpt-image-1.5",
prompt: "Draw a skyline",
size: "1536x1024",
},
);
assert.deepEqual(
buildOpenAIGenerationsBody("Draw a skyline", "gemini-3-pro-image-preview", {
aspectRatio: "16:9",
size: null,
quality: "2k",
imageSize: null,
imageApiDialect: "ratio-metadata",
}),
{
model: "gemini-3-pro-image-preview",
prompt: "Draw a skyline",
size: "16:9",
metadata: {
resolution: "2K",
orientation: "landscape",
},
},
);
assert.deepEqual(
buildOpenAIGenerationsBody("Draw a portrait", "gemini-3-pro-image-preview", {
aspectRatio: null,
size: "1152x2048",
quality: "normal",
imageSize: null,
imageApiDialect: "ratio-metadata",
}),
{
model: "gemini-3-pro-image-preview",
prompt: "Draw a portrait",
size: "9:16",
metadata: {
resolution: "2K",
orientation: "portrait",
},
},
);
});
test("OpenAI mime-type detection covers supported reference image extensions", () => {
+124 -13
View File
@@ -1,6 +1,6 @@
import path from "node:path";
import { readFile } from "node:fs/promises";
import type { CliArgs } from "../types";
import type { CliArgs, OpenAIImageApiDialect } from "../types";
export function getDefaultModel(): string {
return process.env.OPENAI_IMAGE_MODEL || "gpt-image-1.5";
@@ -23,6 +23,8 @@ type SizeMapping = {
portrait: string;
};
type OpenAIGenerationsBody = Record<string, unknown>;
export function getOpenAISize(
model: string,
ar: string | null,
@@ -60,6 +62,114 @@ export function getOpenAISize(
return sizes.square;
}
function parsePixelSize(value: string): { width: number; height: number } | null {
const match = value.match(/^(\d+)\s*[xX]\s*(\d+)$/);
if (!match) return null;
const width = parseInt(match[1]!, 10);
const height = parseInt(match[2]!, 10);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return null;
}
return { width, height };
}
function gcd(a: number, b: number): number {
let x = Math.abs(a);
let y = Math.abs(b);
while (y !== 0) {
const next = x % y;
x = y;
y = next;
}
return x || 1;
}
export function getOpenAIImageApiDialect(args: Pick<CliArgs, "imageApiDialect">): OpenAIImageApiDialect {
return args.imageApiDialect ?? "openai-native";
}
export function inferAspectRatioFromSize(size: string | null): string | null {
if (!size) return null;
const parsed = parsePixelSize(size);
if (!parsed) return null;
const divisor = gcd(parsed.width, parsed.height);
return `${parsed.width / divisor}:${parsed.height / divisor}`;
}
export function inferResolutionFromSize(size: string | null): "1K" | "2K" | "4K" | null {
if (!size) return null;
const parsed = parsePixelSize(size);
if (!parsed) return null;
const longestEdge = Math.max(parsed.width, parsed.height);
if (longestEdge <= 1024) return "1K";
if (longestEdge <= 2048) return "2K";
return "4K";
}
export function getOpenAIAspectRatio(args: Pick<CliArgs, "aspectRatio" | "size">): string {
return args.aspectRatio ?? inferAspectRatioFromSize(args.size) ?? "1:1";
}
export function getOpenAIResolution(
args: Pick<CliArgs, "imageSize" | "size" | "quality">
): "1K" | "2K" | "4K" {
if (args.imageSize === "1K" || args.imageSize === "2K" || args.imageSize === "4K") {
return args.imageSize;
}
const inferred = inferResolutionFromSize(args.size);
if (inferred) return inferred;
return args.quality === "normal" ? "1K" : "2K";
}
export function getOrientationFromAspectRatio(ar: string): "landscape" | "portrait" | null {
const parsed = parseAspectRatio(ar);
if (!parsed) return null;
const ratio = parsed.width / parsed.height;
if (Math.abs(ratio - 1) < 0.1) return null;
return ratio > 1 ? "landscape" : "portrait";
}
export function buildOpenAIGenerationsBody(
prompt: string,
model: string,
args: Pick<CliArgs, "aspectRatio" | "size" | "quality" | "imageSize" | "imageApiDialect">
): OpenAIGenerationsBody {
if (getOpenAIImageApiDialect(args) === "ratio-metadata") {
const aspectRatio = getOpenAIAspectRatio(args);
const metadata: Record<string, string> = {
resolution: getOpenAIResolution(args),
};
const orientation = getOrientationFromAspectRatio(aspectRatio);
if (orientation) metadata.orientation = orientation;
return {
model,
prompt,
size: aspectRatio,
metadata,
};
}
const body: OpenAIGenerationsBody = {
model,
prompt,
size: args.size || getOpenAISize(model, args.aspectRatio, args.quality),
};
if (model.includes("dall-e-3")) {
body.quality = args.quality === "2k" ? "hd" : "standard";
}
return body;
}
export async function generateImage(
prompt: string,
model: string,
@@ -78,18 +188,28 @@ export async function generateImage(
return generateWithChatCompletions(baseURL, apiKey, prompt, model);
}
const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);
const imageApiDialect = getOpenAIImageApiDialect(args);
if (args.referenceImages.length > 0) {
if (imageApiDialect !== "openai-native") {
throw new Error(
"Reference images are not supported with the ratio-metadata OpenAI dialect yet. Use openai-native, Google, Azure, OpenRouter, MiniMax, Seedream, or Replicate for image-edit workflows."
);
}
if (model.includes("dall-e-2") || model.includes("dall-e-3")) {
throw new Error(
"Reference images with OpenAI in this skill require GPT Image models. Use --model gpt-image-1.5 (or another gpt-image model)."
);
}
const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);
return generateWithOpenAIEdits(baseURL, apiKey, prompt, model, size, args.referenceImages, args.quality);
}
return generateWithOpenAIGenerations(baseURL, apiKey, prompt, model, size, args.quality);
return generateWithOpenAIGenerations(
baseURL,
apiKey,
buildOpenAIGenerationsBody(prompt, model, args)
);
}
async function generateWithChatCompletions(
@@ -129,17 +249,8 @@ async function generateWithChatCompletions(
async function generateWithOpenAIGenerations(
baseURL: string,
apiKey: string,
prompt: string,
model: string,
size: string,
quality: CliArgs["quality"]
body: OpenAIGenerationsBody
): Promise<Uint8Array> {
const body: Record<string, any> = { model, prompt, size };
if (model.includes("dall-e-3")) {
body.quality = quality === "2k" ? "hd" : "standard";
}
const res = await fetch(`${baseURL}/images/generations`, {
method: "POST",
headers: {
@@ -28,6 +28,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
size: null,
quality: null,
imageSize: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
@@ -5,7 +5,10 @@ import type { CliArgs } from "../types.ts";
import {
buildInput,
extractOutputUrl,
getDefaultModel,
getModelFamily,
parseModelId,
validateArgs,
} from "./replicate.ts";
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
@@ -16,9 +19,12 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
provider: null,
model: null,
aspectRatio: null,
aspectRatioSource: null,
size: null,
quality: null,
imageSize: null,
imageSizeSource: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
@@ -29,10 +35,24 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
};
}
test("Replicate model parsing accepts official formats and rejects malformed ones", () => {
assert.deepEqual(parseModelId("google/nano-banana-pro"), {
test("Replicate default model now points at nano-banana-2", () => {
const previous = process.env.REPLICATE_IMAGE_MODEL;
delete process.env.REPLICATE_IMAGE_MODEL;
try {
assert.equal(getDefaultModel(), "google/nano-banana-2");
} finally {
if (previous == null) {
delete process.env.REPLICATE_IMAGE_MODEL;
} else {
process.env.REPLICATE_IMAGE_MODEL = previous;
}
}
});
test("Replicate model parsing and family detection accept supported official ids", () => {
assert.deepEqual(parseModelId("google/nano-banana-2"), {
owner: "google",
name: "nano-banana-pro",
name: "nano-banana-2",
version: null,
});
assert.deepEqual(parseModelId("owner/model:abc123"), {
@@ -41,46 +61,224 @@ test("Replicate model parsing accepts official formats and rejects malformed one
version: "abc123",
});
assert.equal(getModelFamily("google/nano-banana-pro"), "nano-banana");
assert.equal(getModelFamily("bytedance/seedream-4.5"), "seedream45");
assert.equal(getModelFamily("bytedance/seedream-5-lite"), "seedream5lite");
assert.equal(getModelFamily("wan-video/wan-2.7-image"), "wan27image");
assert.equal(getModelFamily("wan-video/wan-2.7-image-pro"), "wan27imagepro");
assert.equal(getModelFamily("stability-ai/sdxl"), "unknown");
assert.throws(
() => parseModelId("just-a-model-name"),
/Invalid Replicate model format/,
);
});
test("Replicate input builder maps aspect ratio, image count, quality, and refs", () => {
test("Replicate nano-banana input builder maps refs, aspect ratio, and quality presets", () => {
assert.deepEqual(
buildInput(
"google/nano-banana-2",
"A robot painter",
makeArgs({
aspectRatio: "16:9",
quality: "2k",
n: 3,
}),
["data:image/png;base64,AAAA"],
),
{
prompt: "A robot painter",
aspect_ratio: "16:9",
number_of_images: 3,
resolution: "2K",
output_format: "png",
aspect_ratio: "16:9",
image_input: ["data:image/png;base64,AAAA"],
},
);
assert.deepEqual(
buildInput("A robot painter", makeArgs({ quality: "normal" }), ["ref"]),
buildInput(
"google/nano-banana-2",
"A robot painter",
makeArgs({ size: "1024x1024", quality: "normal" }),
[],
),
{
prompt: "A robot painter",
aspect_ratio: "match_input_image",
resolution: "1K",
output_format: "png",
image_input: ["ref"],
aspect_ratio: "1:1",
},
);
});
test("Replicate output extraction supports string, array, and object URLs", () => {
test("Replicate Seedream and Wan inputs use family-specific request fields", () => {
assert.deepEqual(
buildInput(
"bytedance/seedream-4.5",
"A cinematic portrait",
makeArgs({ quality: "2k", referenceImages: ["local.png"] }),
["data:image/png;base64,AAAA"],
),
{
prompt: "A cinematic portrait",
size: "4K",
image_input: ["data:image/png;base64,AAAA"],
aspect_ratio: "match_input_image",
},
);
assert.deepEqual(
buildInput(
"bytedance/seedream-4.5",
"A cinematic portrait",
makeArgs({ size: "1536x1024" }),
[],
),
{
prompt: "A cinematic portrait",
size: "custom",
width: 1536,
height: 1024,
},
);
assert.deepEqual(
buildInput(
"bytedance/seedream-5-lite",
"A poster",
makeArgs({ aspectRatio: "21:9", quality: "2k" }),
[],
),
{
prompt: "A poster",
size: "3K",
aspect_ratio: "21:9",
},
);
assert.deepEqual(
buildInput(
"wan-video/wan-2.7-image",
"A storyboard frame",
makeArgs({ aspectRatio: "16:9", quality: "2k" }),
[],
),
{
prompt: "A storyboard frame",
size: "2048*1152",
},
);
assert.deepEqual(
buildInput(
"wan-video/wan-2.7-image-pro",
"Blend these references",
makeArgs({ size: "2K", referenceImages: ["a.png", "b.png"] }),
["ref-a", "ref-b"],
),
{
prompt: "Blend these references",
size: "2K",
images: ["ref-a", "ref-b"],
},
);
});
test("Replicate validateArgs blocks misleading multi-output and unsupported family options locally", () => {
assert.throws(
() =>
validateArgs(
"google/nano-banana-2",
makeArgs({ n: 2 }),
),
/exactly one output image/,
);
assert.throws(
() =>
validateArgs(
"bytedance/seedream-4.5",
makeArgs({ size: "1K" }),
),
/2K, 4K, or an explicit WxH size/,
);
assert.throws(
() =>
validateArgs(
"bytedance/seedream-5-lite",
makeArgs({ size: "4K" }),
),
/supports 2K or 3K output/,
);
assert.throws(
() =>
validateArgs(
"wan-video/wan-2.7-image",
makeArgs({ referenceImages: new Array(10).fill("ref.png") }),
),
/at most 9 reference images/,
);
assert.throws(
() =>
validateArgs(
"wan-video/wan-2.7-image-pro",
makeArgs({ referenceImages: ["ref.png"], size: "4K" }),
),
/only supports 4K text-to-image/,
);
assert.throws(
() =>
validateArgs(
"stability-ai/sdxl",
makeArgs({ aspectRatio: "16:9" }),
),
/compatibility list/,
);
assert.doesNotThrow(() =>
validateArgs(
"google/nano-banana-2",
makeArgs({ imageSize: "2K", imageSizeSource: "config" }),
),
);
assert.throws(
() =>
validateArgs(
"google/nano-banana-2",
makeArgs({ imageSize: "2K", imageSizeSource: "cli" }),
),
/do not use --imageSize/,
);
assert.doesNotThrow(() =>
validateArgs(
"stability-ai/sdxl",
makeArgs({ aspectRatio: "16:9", aspectRatioSource: "config" }),
),
);
assert.throws(
() =>
validateArgs(
"stability-ai/sdxl",
makeArgs({ aspectRatio: "16:9", aspectRatioSource: "cli" }),
),
/compatibility list/,
);
assert.doesNotThrow(() =>
validateArgs(
"stability-ai/sdxl",
makeArgs(),
),
);
});
test("Replicate output extraction supports single outputs and rejects silent multi-image drops", () => {
assert.equal(
extractOutputUrl({ output: "https://example.com/a.png" } as never),
"https://example.com/a.png",
@@ -94,6 +292,17 @@ test("Replicate output extraction supports string, array, and object URLs", () =
"https://example.com/c.png",
);
assert.throws(
() =>
extractOutputUrl({
output: [
"https://example.com/one.png",
"https://example.com/two.png",
],
} as never),
/supports saving exactly one image/,
);
assert.throws(
() => extractOutputUrl({ output: { invalid: true } } as never),
/Unexpected Replicate output format/,
@@ -2,10 +2,37 @@ import path from "node:path";
import { readFile } from "node:fs/promises";
import type { CliArgs } from "../types";
const DEFAULT_MODEL = "google/nano-banana-pro";
const DEFAULT_MODEL = "google/nano-banana-2";
const SYNC_WAIT_SECONDS = 60;
const POLL_INTERVAL_MS = 2000;
const MAX_POLL_MS = 300_000;
const DOCUMENTED_REPLICATE_ASPECT_RATIOS = new Set([
"1:1",
"2:3",
"3:2",
"3:4",
"4:3",
"5:4",
"4:5",
"9:16",
"16:9",
"21:9",
]);
export type ReplicateModelFamily =
| "nano-banana"
| "seedream45"
| "seedream5lite"
| "wan27image"
| "wan27imagepro"
| "unknown";
type PixelSize = {
width: number;
height: number;
};
type Seedream45Size = "2K" | "4K" | { width: number; height: number };
export function getDefaultModel(): string {
return process.env.REPLICATE_IMAGE_MODEL || DEFAULT_MODEL;
@@ -20,6 +47,40 @@ function getBaseUrl(): string {
return base.replace(/\/+$/g, "");
}
function normalizeModelId(model: string): string {
return model.trim().toLowerCase().split(":")[0]!;
}
export function getModelFamily(model: string): ReplicateModelFamily {
const normalized = normalizeModelId(model);
if (
normalized === "google/nano-banana" ||
normalized === "google/nano-banana-pro" ||
normalized === "google/nano-banana-2"
) {
return "nano-banana";
}
if (normalized === "bytedance/seedream-4.5") {
return "seedream45";
}
if (normalized === "bytedance/seedream-5-lite") {
return "seedream5lite";
}
if (normalized === "wan-video/wan-2.7-image") {
return "wan27image";
}
if (normalized === "wan-video/wan-2.7-image-pro") {
return "wan27imagepro";
}
return "unknown";
}
export function parseModelId(model: string): { owner: string; name: string; version: string | null } {
const [ownerName, version] = model.split(":");
const parts = ownerName!.split("/");
@@ -31,27 +92,219 @@ export function parseModelId(model: string): { owner: string; name: string; vers
return { owner: parts[0], name: parts[1], version: version || null };
}
export function buildInput(prompt: string, args: CliArgs, referenceImages: string[]): Record<string, unknown> {
const input: Record<string, unknown> = { prompt };
function parsePixelSize(value: string): PixelSize | null {
const match = value.trim().match(/^(\d+)\s*[xX*]\s*(\d+)$/);
if (!match) return null;
const width = parseInt(match[1]!, 10);
const height = parseInt(match[2]!, 10);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return null;
}
return { width, height };
}
function parseAspectRatio(value: string): PixelSize | null {
const match = value.trim().match(/^(\d+)\s*:\s*(\d+)$/);
if (!match) return null;
const width = parseInt(match[1]!, 10);
const height = parseInt(match[2]!, 10);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return null;
}
return { width, height };
}
function gcd(a: number, b: number): number {
let x = Math.abs(a);
let y = Math.abs(b);
while (y !== 0) {
const next = x % y;
x = y;
y = next;
}
return x || 1;
}
function inferAspectRatioFromSize(size: string): string | null {
const parsed = parsePixelSize(size);
if (!parsed) return null;
const divisor = gcd(parsed.width, parsed.height);
const normalized = `${parsed.width / divisor}:${parsed.height / divisor}`;
if (!DOCUMENTED_REPLICATE_ASPECT_RATIOS.has(normalized)) {
return null;
}
return normalized;
}
function getQualityPreset(args: CliArgs): "normal" | "2k" {
return args.quality === "normal" ? "normal" : "2k";
}
function validateDocumentedAspectRatio(model: string, aspectRatio: string): void {
if (aspectRatio === "match_input_image") {
return;
}
if (DOCUMENTED_REPLICATE_ASPECT_RATIOS.has(aspectRatio)) {
return;
}
throw new Error(
`Replicate model ${model} does not support aspect ratio ${aspectRatio}. Supported values: ${Array.from(DOCUMENTED_REPLICATE_ASPECT_RATIOS).join(", ")}`
);
}
function getRequestedAspectRatio(model: string, args: CliArgs): string | null {
if (args.aspectRatio) {
validateDocumentedAspectRatio(model, args.aspectRatio);
return args.aspectRatio;
}
if (!args.size) return null;
const inferred = inferAspectRatioFromSize(args.size);
if (!inferred) {
throw new Error(
`Replicate model ${model} cannot derive a supported aspect ratio from --size ${args.size}. Use one of: ${Array.from(DOCUMENTED_REPLICATE_ASPECT_RATIOS).join(", ")}`
);
}
return inferred;
}
function getNanoBananaResolution(args: CliArgs): "1K" | "2K" {
if (args.size) {
const parsed = parsePixelSize(args.size);
if (!parsed) {
throw new Error("Replicate nano-banana --size must be in WxH format, for example 1536x1024.");
}
const longestEdge = Math.max(parsed.width, parsed.height);
if (longestEdge <= 1024) return "1K";
if (longestEdge <= 2048) return "2K";
throw new Error("Replicate nano-banana only supports sizes that map to 1K or 2K output.");
}
return getQualityPreset(args) === "normal" ? "1K" : "2K";
}
function resolveSeedream45Size(args: CliArgs): Seedream45Size {
if (args.size) {
const upper = args.size.trim().toUpperCase();
if (upper === "2K" || upper === "4K") {
return upper;
}
const parsed = parsePixelSize(args.size);
if (!parsed) {
throw new Error("Replicate Seedream 4.5 --size must be 2K, 4K, or an explicit WxH size.");
}
if (parsed.width < 1024 || parsed.width > 4096 || parsed.height < 1024 || parsed.height > 4096) {
throw new Error("Replicate Seedream 4.5 custom --size must keep width and height between 1024 and 4096.");
}
return parsed;
}
return getQualityPreset(args) === "normal" ? "2K" : "4K";
}
function resolveSeedream5LiteSize(args: CliArgs): "2K" | "3K" {
if (args.size) {
const upper = args.size.trim().toUpperCase();
if (upper === "2K" || upper === "3K") {
return upper;
}
throw new Error("Replicate Seedream 5 Lite currently supports 2K or 3K output in this tool.");
}
return getQualityPreset(args) === "normal" ? "2K" : "3K";
}
function formatCustomWanSize(size: PixelSize): string {
return `${size.width}*${size.height}`;
}
function resolveWanSizeFromAspectRatio(
aspectRatio: string,
maxDimension: number,
): string {
const parsedRatio = parseAspectRatio(aspectRatio);
if (!parsedRatio) {
throw new Error(`Replicate Wan aspect ratio must be in W:H format, got ${aspectRatio}.`);
}
const scale = Math.min(maxDimension / parsedRatio.width, maxDimension / parsedRatio.height);
const width = Math.max(1, Math.floor(parsedRatio.width * scale));
const height = Math.max(1, Math.floor(parsedRatio.height * scale));
return formatCustomWanSize({ width, height });
}
function resolveWanSize(family: "wan27image" | "wan27imagepro", args: CliArgs): "1K" | "2K" | "4K" | string {
const referenceMode = args.referenceImages.length > 0;
const maxDimension = family === "wan27imagepro" && !referenceMode ? 4096 : 2048;
if (args.size) {
const upper = args.size.trim().toUpperCase();
if (upper === "1K" || upper === "2K" || upper === "4K") {
if (upper === "4K" && family !== "wan27imagepro") {
throw new Error("Replicate Wan 2.7 Image only supports 1K, 2K, or custom sizes up to 2048px.");
}
if (upper === "4K" && referenceMode) {
throw new Error("Replicate Wan 2.7 Image Pro only supports 4K text-to-image. Remove --ref or lower the size.");
}
return upper;
}
const parsed = parsePixelSize(args.size);
if (!parsed) {
throw new Error("Replicate Wan --size must be 1K, 2K, 4K, or an explicit WxH size.");
}
if (parsed.width > maxDimension || parsed.height > maxDimension) {
throw new Error(
`Replicate ${family === "wan27imagepro" ? "Wan 2.7 Image Pro" : "Wan 2.7 Image"} custom --size must keep width and height at or below ${maxDimension}px in the current mode.`
);
}
return formatCustomWanSize(parsed);
}
if (args.aspectRatio) {
input.aspect_ratio = args.aspectRatio;
return resolveWanSizeFromAspectRatio(
args.aspectRatio,
getQualityPreset(args) === "normal" ? 1024 : 2048,
);
}
return getQualityPreset(args) === "normal" ? "1K" : "2K";
}
function buildNanoBananaInput(
prompt: string,
model: string,
args: CliArgs,
referenceImages: string[],
): Record<string, unknown> {
const input: Record<string, unknown> = {
prompt,
resolution: getNanoBananaResolution(args),
output_format: "png",
};
const aspectRatio = getRequestedAspectRatio(model, args);
if (aspectRatio) {
input.aspect_ratio = aspectRatio;
} else if (referenceImages.length > 0) {
input.aspect_ratio = "match_input_image";
}
if (args.n > 1) {
input.number_of_images = args.n;
}
if (args.quality === "normal") {
input.resolution = "1K";
} else if (args.quality === "2k") {
input.resolution = "2K";
}
input.output_format = "png";
if (referenceImages.length > 0) {
input.image_input = referenceImages;
}
@@ -59,6 +312,158 @@ export function buildInput(prompt: string, args: CliArgs, referenceImages: strin
return input;
}
function buildSeedreamInput(
family: "seedream45" | "seedream5lite",
prompt: string,
model: string,
args: CliArgs,
referenceImages: string[],
): Record<string, unknown> {
const size = family === "seedream45" ? resolveSeedream45Size(args) : resolveSeedream5LiteSize(args);
const input: Record<string, unknown> = {
prompt,
};
if (family === "seedream45" && typeof size === "object") {
input.size = "custom";
input.width = size.width;
input.height = size.height;
} else {
input.size = size;
}
if (referenceImages.length > 0) {
input.image_input = referenceImages;
}
if (args.aspectRatio) {
validateDocumentedAspectRatio(model, args.aspectRatio);
input.aspect_ratio = args.aspectRatio;
} else if (referenceImages.length > 0 && family === "seedream45") {
input.aspect_ratio = "match_input_image";
}
return input;
}
function buildWanInput(
family: "wan27image" | "wan27imagepro",
prompt: string,
args: CliArgs,
referenceImages: string[],
): Record<string, unknown> {
const input: Record<string, unknown> = {
prompt,
size: resolveWanSize(family, args),
};
if (referenceImages.length > 0) {
input.images = referenceImages;
}
return input;
}
export function validateArgs(model: string, args: CliArgs): void {
parseModelId(model);
if (args.n !== 1) {
throw new Error("Replicate integration currently supports exactly one output image per request. Remove --n or use --n 1.");
}
if (args.imageSize && args.imageSizeSource !== "config") {
throw new Error("Replicate models in baoyu-imagine do not use --imageSize. Use --quality, --ar, or --size instead.");
}
const family = getModelFamily(model);
if (family === "nano-banana") {
if (args.referenceImages.length > 14) {
throw new Error("Replicate nano-banana supports at most 14 reference images.");
}
if (args.aspectRatio) {
validateDocumentedAspectRatio(model, args.aspectRatio);
}
if (args.size) {
getRequestedAspectRatio(model, args);
getNanoBananaResolution(args);
}
return;
}
if (family === "seedream45") {
if (args.referenceImages.length > 14) {
throw new Error("Replicate Seedream 4.5 supports at most 14 reference images.");
}
if (args.aspectRatio) {
validateDocumentedAspectRatio(model, args.aspectRatio);
}
resolveSeedream45Size(args);
return;
}
if (family === "seedream5lite") {
if (args.referenceImages.length > 14) {
throw new Error("Replicate Seedream 5 Lite supports at most 14 reference images.");
}
if (args.aspectRatio) {
validateDocumentedAspectRatio(model, args.aspectRatio);
}
resolveSeedream5LiteSize(args);
return;
}
if (family === "wan27image" || family === "wan27imagepro") {
if (args.referenceImages.length > 9) {
throw new Error("Replicate Wan 2.7 image models support at most 9 reference images.");
}
if (args.aspectRatio) {
const parsed = parseAspectRatio(args.aspectRatio);
if (!parsed) {
throw new Error(`Replicate Wan aspect ratio must be in W:H format, got ${args.aspectRatio}.`);
}
}
resolveWanSize(family, args);
return;
}
const hasExplicitAspectRatio = !!args.aspectRatio && args.aspectRatioSource !== "config";
if (args.referenceImages.length > 0 || hasExplicitAspectRatio || args.size) {
throw new Error(
`Replicate model ${model} is not in the baoyu-imagine compatibility list. Supported families: google/nano-banana*, bytedance/seedream-4.5, bytedance/seedream-5-lite, wan-video/wan-2.7-image, wan-video/wan-2.7-image-pro.`
);
}
}
export function getDefaultOutputExtension(model: string): ".png" {
const _family = getModelFamily(model);
return ".png";
}
export function buildInput(
model: string,
prompt: string,
args: CliArgs,
referenceImages: string[],
): Record<string, unknown> {
const family = getModelFamily(model);
if (family === "nano-banana") {
return buildNanoBananaInput(prompt, model, args, referenceImages);
}
if (family === "seedream45" || family === "seedream5lite") {
return buildSeedreamInput(family, prompt, model, args, referenceImages);
}
if (family === "wan27image" || family === "wan27imagepro") {
return buildWanInput(family, prompt, args, referenceImages);
}
return { prompt };
}
async function readImageAsDataUrl(p: string): Promise<string> {
const buf = await readFile(p);
const ext = path.extname(p).toLowerCase();
@@ -150,6 +555,11 @@ export function extractOutputUrl(prediction: PredictionResponse): string {
if (typeof output === "string") return output;
if (Array.isArray(output)) {
if (output.length !== 1) {
throw new Error(
`Replicate returned ${output.length} outputs, but baoyu-imagine currently supports saving exactly one image per request.`
);
}
const first = output[0];
if (typeof first === "string") return first;
}
@@ -178,13 +588,14 @@ export async function generateImage(
if (!apiToken) throw new Error("REPLICATE_API_TOKEN is required. Get one at https://replicate.com/account/api-tokens");
const parsedModel = parseModelId(model);
validateArgs(model, args);
const refDataUrls: string[] = [];
for (const refPath of args.referenceImages) {
refDataUrls.push(await readImageAsDataUrl(refPath));
}
const input = buildInput(prompt, args, refDataUrls);
const input = buildInput(model, prompt, args, refDataUrls);
console.log(`Generating image with Replicate (${model})...`);
@@ -25,6 +25,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
size: null,
quality: null,
imageSize: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
@@ -0,0 +1,181 @@
import assert from "node:assert/strict";
import test, { type TestContext } from "node:test";
import type { CliArgs } from "../types.ts";
import {
buildRequestBody,
buildZaiUrl,
extractImageFromResponse,
getDefaultModel,
getModelFamily,
parseAspectRatio,
parseSize,
resolveSizeForModel,
validateArgs,
} from "./zai.ts";
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
return {
prompt: null,
promptFiles: [],
imagePath: null,
provider: null,
model: null,
aspectRatio: null,
size: null,
quality: null,
imageSize: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
jobs: null,
json: false,
help: false,
...overrides,
};
}
function useEnv(
t: TestContext,
values: Record<string, string | null>,
): void {
const previous = new Map<string, string | undefined>();
for (const [key, value] of Object.entries(values)) {
previous.set(key, process.env[key]);
if (value == null) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
t.after(() => {
for (const [key, value] of previous.entries()) {
if (value == null) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
}
test("Z.AI default model prefers env override and otherwise uses glm-image", (t) => {
useEnv(t, {
ZAI_IMAGE_MODEL: null,
BIGMODEL_IMAGE_MODEL: null,
});
assert.equal(getDefaultModel(), "glm-image");
process.env.BIGMODEL_IMAGE_MODEL = "cogview-4-250304";
assert.equal(getDefaultModel(), "cogview-4-250304");
});
test("Z.AI URL builder normalizes host, v4 base, and full endpoint inputs", (t) => {
useEnv(t, { ZAI_BASE_URL: "https://api.z.ai" });
assert.equal(buildZaiUrl(), "https://api.z.ai/api/paas/v4/images/generations");
process.env.ZAI_BASE_URL = "https://proxy.example.com/api/paas/v4/";
assert.equal(buildZaiUrl(), "https://proxy.example.com/api/paas/v4/images/generations");
process.env.ZAI_BASE_URL = "https://proxy.example.com/custom/images/generations";
assert.equal(buildZaiUrl(), "https://proxy.example.com/custom/images/generations");
});
test("Z.AI model family and parsing helpers recognize documented formats", () => {
assert.equal(getModelFamily("glm-image"), "glm");
assert.equal(getModelFamily("cogview-4-250304"), "legacy");
assert.deepEqual(parseAspectRatio("16:9"), { width: 16, height: 9 });
assert.equal(parseAspectRatio("wide"), null);
assert.deepEqual(parseSize("1280x1280"), { width: 1280, height: 1280 });
assert.deepEqual(parseSize("1472*1088"), { width: 1472, height: 1088 });
assert.equal(parseSize("big"), null);
});
test("Z.AI size resolution follows documented recommended ratios and validates custom sizes", () => {
assert.equal(
resolveSizeForModel("glm-image", makeArgs({ aspectRatio: "16:9", quality: "2k" })),
"1728x960",
);
assert.equal(
resolveSizeForModel("cogview-4-250304", makeArgs({ aspectRatio: "4:3", quality: "normal" })),
"1152x864",
);
assert.equal(
resolveSizeForModel("glm-image", makeArgs({ size: "1568x1056", quality: "2k" })),
"1568x1056",
);
const uncommon = resolveSizeForModel(
"glm-image",
makeArgs({ aspectRatio: "5:2", quality: "normal" }),
);
const parsed = parseSize(uncommon);
assert.ok(parsed);
assert.ok(parsed.width % 32 === 0);
assert.ok(parsed.height % 32 === 0);
assert.ok(parsed.width * parsed.height <= 2 ** 22);
assert.throws(
() => resolveSizeForModel("glm-image", makeArgs({ size: "1000x1000", quality: "2k" })),
/between 1024 and 2048/,
);
assert.throws(
() => resolveSizeForModel("glm-image", makeArgs({ size: "1280x1260", quality: "2k" })),
/divisible by 32/,
);
assert.throws(
() => resolveSizeForModel("cogview-4-250304", makeArgs({ size: "2048x2048", quality: "2k" })),
/must not exceed 2\^21 total pixels/,
);
});
test("Z.AI validation rejects unsupported refs and multi-image requests", () => {
assert.throws(
() => validateArgs("glm-image", makeArgs({ referenceImages: ["ref.png"] })),
/text-to-image only/,
);
assert.throws(
() => validateArgs("glm-image", makeArgs({ n: 2 })),
/single image per request/,
);
});
test("Z.AI request body maps skill quality and resolved size into provider fields", () => {
const body = buildRequestBody(
"A cinematic science poster",
"glm-image",
makeArgs({ aspectRatio: "4:3", quality: "normal" }),
);
assert.deepEqual(body, {
model: "glm-image",
prompt: "A cinematic science poster",
quality: "standard",
size: "1472x1088",
});
});
test("Z.AI response extraction downloads the returned image URL", async (t) => {
const originalFetch = globalThis.fetch;
t.after(() => {
globalThis.fetch = originalFetch;
});
globalThis.fetch = async () =>
new Response(Uint8Array.from([1, 2, 3]), {
status: 200,
headers: { "Content-Type": "image/png" },
});
const image = await extractImageFromResponse({
data: [{ url: "https://cdn.example.com/glm-image.png" }],
});
assert.deepEqual([...image], [1, 2, 3]);
await assert.rejects(
() => extractImageFromResponse({ data: [{}] }),
/No image URL/,
);
});
@@ -0,0 +1,306 @@
import type { CliArgs, Quality } from "../types";
type ZaiModelFamily = "glm" | "legacy";
type ZaiRequestBody = {
model: string;
prompt: string;
quality: "hd" | "standard";
size: string;
};
type ZaiResponse = {
data?: Array<{ url?: string }>;
};
const DEFAULT_MODEL = "glm-image";
const GLM_MAX_PIXELS = 2 ** 22;
const LEGACY_MAX_PIXELS = 2 ** 21;
const GLM_SIZE_STEP = 32;
const LEGACY_SIZE_STEP = 16;
const GLM_RECOMMENDED_SIZES: Record<string, string> = {
"1:1": "1280x1280",
"3:2": "1568x1056",
"2:3": "1056x1568",
"4:3": "1472x1088",
"3:4": "1088x1472",
"16:9": "1728x960",
"9:16": "960x1728",
};
const LEGACY_RECOMMENDED_SIZES: Record<string, string> = {
"1:1": "1024x1024",
"9:16": "768x1344",
"3:4": "864x1152",
"16:9": "1344x768",
"4:3": "1152x864",
"2:1": "1440x720",
"1:2": "720x1440",
};
export function getDefaultModel(): string {
return process.env.ZAI_IMAGE_MODEL || process.env.BIGMODEL_IMAGE_MODEL || DEFAULT_MODEL;
}
function getApiKey(): string | null {
return process.env.ZAI_API_KEY || process.env.BIGMODEL_API_KEY || null;
}
export function buildZaiUrl(): string {
const base = (process.env.ZAI_BASE_URL || process.env.BIGMODEL_BASE_URL || "https://api.z.ai/api/paas/v4")
.replace(/\/+$/g, "");
if (base.endsWith("/images/generations")) return base;
if (base.endsWith("/api/paas/v4")) return `${base}/images/generations`;
if (base.endsWith("/v4")) return `${base}/images/generations`;
return `${base}/api/paas/v4/images/generations`;
}
export function getModelFamily(model: string): ZaiModelFamily {
return model.trim().toLowerCase() === "glm-image" ? "glm" : "legacy";
}
export function parseAspectRatio(ar: string): { width: number; height: number } | null {
const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
if (!match) return null;
const width = Number(match[1]);
const height = Number(match[2]);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return null;
}
return { width, height };
}
export function parseSize(size: string): { width: number; height: number } | null {
const match = size.trim().match(/^(\d+)\s*[xX*]\s*(\d+)$/);
if (!match) return null;
const width = parseInt(match[1]!, 10);
const height = parseInt(match[2]!, 10);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return null;
}
return { width, height };
}
function formatSize(width: number, height: number): string {
return `${width}x${height}`;
}
function roundToStep(value: number, step: number): number {
return Math.max(step, Math.round(value / step) * step);
}
function getRatioValue(ar: string): number | null {
const parsed = parseAspectRatio(ar);
if (!parsed) return null;
return parsed.width / parsed.height;
}
function findClosestRatioKey(ar: string, candidates: string[]): string | null {
const targetRatio = getRatioValue(ar);
if (targetRatio == null) return null;
let bestKey: string | null = null;
let bestDiff = Infinity;
for (const candidate of candidates) {
const candidateRatio = getRatioValue(candidate);
if (candidateRatio == null) continue;
const diff = Math.abs(candidateRatio - targetRatio);
if (diff < bestDiff) {
bestDiff = diff;
bestKey = candidate;
}
}
return bestDiff <= 0.05 ? bestKey : null;
}
function getTargetPixels(quality: Quality): number {
return quality === "normal" ? 1024 * 1024 : 1536 * 1536;
}
function fitToPixelBudget(
width: number,
height: number,
targetPixels: number,
maxPixels: number,
step: number,
): { width: number; height: number } {
let nextWidth = width;
let nextHeight = height;
const pixels = nextWidth * nextHeight;
if (pixels > maxPixels) {
const scale = Math.sqrt(maxPixels / pixels);
nextWidth *= scale;
nextHeight *= scale;
} else {
const scale = Math.sqrt(targetPixels / pixels);
nextWidth *= scale;
nextHeight *= scale;
}
let roundedWidth = roundToStep(nextWidth, step);
let roundedHeight = roundToStep(nextHeight, step);
let roundedPixels = roundedWidth * roundedHeight;
while (roundedPixels > maxPixels && (roundedWidth > step || roundedHeight > step)) {
if (roundedWidth >= roundedHeight && roundedWidth > step) {
roundedWidth -= step;
} else if (roundedHeight > step) {
roundedHeight -= step;
} else {
break;
}
roundedPixels = roundedWidth * roundedHeight;
}
return { width: roundedWidth, height: roundedHeight };
}
function validateCustomSize(
size: string,
family: ZaiModelFamily,
): string {
const parsed = parseSize(size);
if (!parsed) {
throw new Error("Z.AI --size must be in WxH format, for example 1280x1280.");
}
const widthStep = family === "glm" ? GLM_SIZE_STEP : LEGACY_SIZE_STEP;
const minEdge = family === "glm" ? 1024 : 512;
const maxPixels = family === "glm" ? GLM_MAX_PIXELS : LEGACY_MAX_PIXELS;
if (parsed.width < minEdge || parsed.width > 2048 || parsed.height < minEdge || parsed.height > 2048) {
throw new Error(
family === "glm"
? "GLM-image custom size requires width and height between 1024 and 2048."
: "Z.AI legacy image models require width and height between 512 and 2048."
);
}
if (parsed.width % widthStep !== 0 || parsed.height % widthStep !== 0) {
throw new Error(
family === "glm"
? "GLM-image custom size requires width and height divisible by 32."
: "Z.AI legacy image models require width and height divisible by 16."
);
}
if (parsed.width * parsed.height > maxPixels) {
throw new Error(
family === "glm"
? "GLM-image custom size must not exceed 2^22 total pixels."
: "Z.AI legacy image size must not exceed 2^21 total pixels."
);
}
return formatSize(parsed.width, parsed.height);
}
export function resolveSizeForModel(
model: string,
args: Pick<CliArgs, "size" | "aspectRatio" | "quality">,
): string {
const family = getModelFamily(model);
const quality = args.quality === "normal" ? "normal" : "2k";
if (args.size) {
return validateCustomSize(args.size, family);
}
const recommended = family === "glm" ? GLM_RECOMMENDED_SIZES : LEGACY_RECOMMENDED_SIZES;
const defaultSize = family === "glm" ? "1280x1280" : "1024x1024";
if (!args.aspectRatio) return defaultSize;
const recommendedRatio = findClosestRatioKey(args.aspectRatio, Object.keys(recommended));
if (recommendedRatio) {
return recommended[recommendedRatio]!;
}
const parsedRatio = parseAspectRatio(args.aspectRatio);
if (!parsedRatio) return defaultSize;
const targetPixels = getTargetPixels(quality);
const maxPixels = family === "glm" ? GLM_MAX_PIXELS : LEGACY_MAX_PIXELS;
const step = family === "glm" ? GLM_SIZE_STEP : LEGACY_SIZE_STEP;
const fit = fitToPixelBudget(
parsedRatio.width,
parsedRatio.height,
targetPixels,
maxPixels,
step,
);
return formatSize(fit.width, fit.height);
}
function getZaiQuality(quality: CliArgs["quality"]): "hd" | "standard" {
return quality === "normal" ? "standard" : "hd";
}
export function validateArgs(_model: string, args: CliArgs): void {
if (args.referenceImages.length > 0) {
throw new Error("Z.AI GLM-image currently supports text-to-image only in baoyu-imagine. Remove --ref or choose another provider.");
}
if (args.n > 1) {
throw new Error("Z.AI image generation currently returns a single image per request in baoyu-imagine.");
}
}
export function buildRequestBody(
prompt: string,
model: string,
args: CliArgs,
): ZaiRequestBody {
validateArgs(model, args);
return {
model,
prompt,
quality: getZaiQuality(args.quality),
size: resolveSizeForModel(model, args),
};
}
export async function extractImageFromResponse(result: ZaiResponse): Promise<Uint8Array> {
const url = result.data?.[0]?.url;
if (!url) {
throw new Error("No image URL in Z.AI response");
}
const imageResponse = await fetch(url);
if (!imageResponse.ok) {
throw new Error(`Failed to download image from Z.AI: ${imageResponse.status}`);
}
return new Uint8Array(await imageResponse.arrayBuffer());
}
export async function generateImage(
prompt: string,
model: string,
args: CliArgs,
): Promise<Uint8Array> {
const apiKey = getApiKey();
if (!apiKey) {
throw new Error("ZAI_API_KEY is required. Get one from https://docs.z.ai/.");
}
const response = await fetch(buildZaiUrl(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(buildRequestBody(prompt, model, args)),
});
if (!response.ok) {
const err = await response.text();
throw new Error(`Z.AI API error (${response.status}): ${err}`);
}
const result = (await response.json()) as ZaiResponse;
return extractImageFromResponse(result);
}
+8
View File
@@ -3,12 +3,14 @@ export type Provider =
| "openai"
| "openrouter"
| "dashscope"
| "zai"
| "minimax"
| "replicate"
| "jimeng"
| "seedream"
| "azure";
export type Quality = "normal" | "2k";
export type OpenAIImageApiDialect = "openai-native" | "ratio-metadata";
export type CliArgs = {
prompt: string | null;
@@ -17,9 +19,12 @@ export type CliArgs = {
provider: Provider | null;
model: string | null;
aspectRatio: string | null;
aspectRatioSource?: "cli" | "task" | "config" | null;
size: string | null;
quality: Quality | null;
imageSize: string | null;
imageSizeSource?: "cli" | "task" | "config" | null;
imageApiDialect: OpenAIImageApiDialect | null;
referenceImages: string[];
n: number;
batchFile: string | null;
@@ -39,6 +44,7 @@ export type BatchTaskInput = {
size?: string | null;
quality?: Quality | null;
imageSize?: "1K" | "2K" | "4K" | null;
imageApiDialect?: OpenAIImageApiDialect | null;
ref?: string[];
n?: number;
};
@@ -56,11 +62,13 @@ export type ExtendConfig = {
default_quality: Quality | null;
default_aspect_ratio: string | null;
default_image_size: "1K" | "2K" | "4K" | null;
default_image_api_dialect: OpenAIImageApiDialect | null;
default_model: {
google: string | null;
openai: string | null;
openrouter: string | null;
dashscope: string | null;
zai: string | null;
minimax: string | null;
replicate: string | null;
jimeng: string | null;
+6 -3
View File
@@ -1,6 +1,6 @@
---
name: baoyu-infographic
description: Generates professional infographics with 21 layout types and 20 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "信息图", "visual summary", "可视化", or "高密度信息大图".
description: Generates professional infographics with 21 layout types and 21 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "信息图", "visual summary", "可视化", or "高密度信息大图".
version: 1.56.1
metadata:
openclaw:
@@ -26,7 +26,7 @@ Two dimensions: **layout** (information structure) × **style** (visual aestheti
| Option | Values |
|--------|--------|
| `--layout` | 21 options (see Layout Gallery), default: bento-grid |
| `--style` | 20 options (see Style Gallery), default: craft-handmade |
| `--style` | 21 options (see Style Gallery), default: craft-handmade |
| `--aspect` | Named: landscape (16:9), portrait (9:16), square (1:1). Custom: any W:H ratio (e.g., 3:4, 4:3, 2.35:1) |
| `--lang` | en, zh, ja, etc. |
@@ -82,6 +82,7 @@ Full definitions: `references/layouts/<layout>.md`
| `pop-laboratory` | Blueprint grid, coordinate markers, lab precision |
| `morandi-journal` | Hand-drawn doodle, warm Morandi tones |
| `retro-pop-grid` | 1970s retro pop art, Swiss grid, thick outlines |
| `hand-drawn-edu` | Macaron pastels, hand-drawn wobble, stick figures |
Full definitions: `references/styles/<style>.md`
@@ -104,6 +105,8 @@ Full definitions: `references/styles/<style>.md`
| Product Guide | `dense-modules` + `morandi-journal` |
| Technical Guide | `dense-modules` + `pop-laboratory` |
| Trendy Guide | `dense-modules` + `retro-pop-grid` |
| Educational Diagram | `hub-spoke` + `hand-drawn-edu` |
| Process Tutorial | `linear-progression` + `hand-drawn-edu` |
Default: `bento-grid` + `craft-handmade`
@@ -259,7 +262,7 @@ Report: topic, layout, style, aspect, language, output path, files created.
- `references/structured-content-template.md` - Content format
- `references/base-prompt.md` - Prompt template
- `references/layouts/<layout>.md` - 21 layout definitions
- `references/styles/<style>.md` - 20 style definitions
- `references/styles/<style>.md` - 21 style definitions
## Extension Support
@@ -0,0 +1,63 @@
# hand-drawn-edu
Hand-drawn educational infographic with macaron pastel color blocks on warm cream paper texture.
## Color Palette
- Background: Warm cream (#F5F0E8) with subtle paper grain texture
- Primary text: Deep charcoal (#2D2D2D) for headlines, outlines
- Macaron Blue: #A8D8EA for cool-toned information zones
- Macaron Mint: #B5E5CF for growth/positive zones
- Macaron Lavender: #D5C6E0 for abstract/concept zones
- Macaron Peach: #FFD5C2 for warm-toned zones
- Accent: Coral Red (#E8655A) for key data, warnings, emphasis
- Muted annotations: Warm gray (#6B6B6B) for secondary labels
## Visual Elements
- Macaron pastel rounded cards as distinct information zones
- Hand-drawn wavy connection lines and arrows with small text labels
- Simple stick-figure characters and cartoon icons to humanize concepts
- Doodle decorations: small stars, underlines, spirals, sparkles
- Color fills don't completely fill outlines — preserve casual hand-drawn feel
- Dashed borders for secondary or contained zones
- Small icon doodles (clipboard, lock, checkmark, lightbulb) to reinforce concepts
- Bold centered quote or takeaway at the bottom
- Slight hand-drawn wobble on all lines and shapes
## Variants
| Variant | Focus | Visual Emphasis |
|---------|-------|-----------------|
| **Sketch-notes** | Concept mapping | More stick figures, thought bubbles, connecting arrows |
| **Pastel cards** | Structured info | Cleaner macaron blocks, less doodle, more white space |
## Typography
- Main title: Bold hand-drawn lettering with organic strokes, large confident letterforms with slight wobble
- Section headers: Hand-lettered text on or inside macaron color blocks
- Body text: Clear handwritten print style, legible but not mechanical
- Annotations: Warm gray (#6B6B6B), smaller, neat handwritten labels
- Keywords: Bold emphasis within body text
## Style Enforcement
- All lines must have slight hand-drawn wobble — no perfect geometry
- Each information zone uses a distinct macaron color block
- Maintain consistent wobble quality across all shapes and lines
- Include at least one simple cartoon character or stick figure
- Generous white space between zones — each zone should breathe
- Maximum 4 macaron colors per infographic
## Avoid
- Perfect geometric shapes or straight lines
- Photorealistic elements or stock illustration style
- Pure white backgrounds
- Flat vector icons or digital-precision graphics
- Overcrowded layouts — let zones breathe
- Corporate or clinical aesthetic
## Best For
Educational diagrams, process explainers, concept maps, knowledge summaries, tutorial walkthroughs, onboarding visuals
@@ -46,6 +46,45 @@ export function stripWrappingQuotes(value: string): string {
return value.trim();
}
const HTML_ENTITIES: Record<string, string> = {
amp: "&",
apos: "'",
gt: ">",
lt: "<",
nbsp: " ",
quot: '"',
};
function decodeHtmlCodePoint(codePoint: number, fallback: string): string {
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {
return fallback;
}
return String.fromCodePoint(codePoint);
}
function decodeHtmlEntities(value: string): string {
return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (entity, body: string) => {
const normalized = body.toLowerCase();
if (normalized.startsWith("#x")) {
return decodeHtmlCodePoint(Number.parseInt(normalized.slice(2), 16), entity);
}
if (normalized.startsWith("#")) {
return decodeHtmlCodePoint(Number.parseInt(normalized.slice(1), 10), entity);
}
return HTML_ENTITIES[normalized] ?? entity;
});
}
export function cleanSummaryText(value: string): string {
return decodeHtmlEntities(stripWrappingQuotes(value))
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
.replace(/<br\s*\/?>/gi, " ")
.replace(/<\/?[a-z][a-z0-9:-]*(?:\s+[^>]*)?>/gi, " ")
.replace(/\s+/g, " ")
.trim();
}
export function toFrontmatterString(value: unknown): string | undefined {
if (typeof value === "string") {
return stripWrappingQuotes(value);
@@ -94,10 +133,11 @@ export function extractSummaryFromBody(body: string, maxLen: number): string {
.replace(/\*(.+?)\*/g, "$1")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/`([^`]+)`/g, "$1");
const summaryText = cleanSummaryText(cleanText);
if (cleanText.length > 20) {
if (cleanText.length <= maxLen) return cleanText;
return `${cleanText.slice(0, maxLen - 3)}...`;
if (summaryText.length > 20) {
if (summaryText.length <= maxLen) return summaryText;
return `${summaryText.slice(0, maxLen - 3)}...`;
}
}
@@ -45,19 +45,24 @@ export function loadCodeThemeCss(themeName: string): string {
}
export function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string, codeThemeCss?: string): string {
const escapeHtmlAttribute = (value: string) => value
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const lines = [
"<!doctype html>",
"<html>",
"<head>",
' <meta charset="utf-8" />',
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
` <title>${meta.title}</title>`,
` <title>${escapeHtmlAttribute(meta.title)}</title>`,
];
if (meta.author) {
lines.push(` <meta name="author" content="${meta.author}" />`);
lines.push(` <meta name="author" content="${escapeHtmlAttribute(meta.author)}" />`);
}
if (meta.description) {
lines.push(` <meta name="description" content="${meta.description}" />`);
lines.push(` <meta name="description" content="${escapeHtmlAttribute(meta.description)}" />`);
}
lines.push(` <style>${css}</style>`);
if (codeThemeCss) {
@@ -4,6 +4,7 @@ import path from "node:path";
import process from "node:process";
import {
cleanSummaryText,
extractSummaryFromBody,
extractTitleFromMarkdown,
parseFrontmatter,
@@ -47,8 +48,9 @@ export async function convertMarkdown(
}
const author = stripWrappingQuotes(frontmatter.author ?? "");
let summary = stripWrappingQuotes(frontmatter.description ?? "")
const frontmatterSummary = stripWrappingQuotes(frontmatter.description ?? "")
|| stripWrappingQuotes(frontmatter.summary ?? "");
let summary = cleanSummaryText(frontmatterSummary);
if (!summary) {
summary = extractSummaryFromBody(body, 120);
}
@@ -46,6 +46,45 @@ export function stripWrappingQuotes(value: string): string {
return value.trim();
}
const HTML_ENTITIES: Record<string, string> = {
amp: "&",
apos: "'",
gt: ">",
lt: "<",
nbsp: " ",
quot: '"',
};
function decodeHtmlCodePoint(codePoint: number, fallback: string): string {
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {
return fallback;
}
return String.fromCodePoint(codePoint);
}
function decodeHtmlEntities(value: string): string {
return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (entity, body: string) => {
const normalized = body.toLowerCase();
if (normalized.startsWith("#x")) {
return decodeHtmlCodePoint(Number.parseInt(normalized.slice(2), 16), entity);
}
if (normalized.startsWith("#")) {
return decodeHtmlCodePoint(Number.parseInt(normalized.slice(1), 10), entity);
}
return HTML_ENTITIES[normalized] ?? entity;
});
}
export function cleanSummaryText(value: string): string {
return decodeHtmlEntities(stripWrappingQuotes(value))
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
.replace(/<br\s*\/?>/gi, " ")
.replace(/<\/?[a-z][a-z0-9:-]*(?:\s+[^>]*)?>/gi, " ")
.replace(/\s+/g, " ")
.trim();
}
export function toFrontmatterString(value: unknown): string | undefined {
if (typeof value === "string") {
return stripWrappingQuotes(value);
@@ -94,10 +133,11 @@ export function extractSummaryFromBody(body: string, maxLen: number): string {
.replace(/\*(.+?)\*/g, "$1")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/`([^`]+)`/g, "$1");
const summaryText = cleanSummaryText(cleanText);
if (cleanText.length > 20) {
if (cleanText.length <= maxLen) return cleanText;
return `${cleanText.slice(0, maxLen - 3)}...`;
if (summaryText.length > 20) {
if (summaryText.length <= maxLen) return summaryText;
return `${summaryText.slice(0, maxLen - 3)}...`;
}
}
@@ -45,19 +45,24 @@ export function loadCodeThemeCss(themeName: string): string {
}
export function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string, codeThemeCss?: string): string {
const escapeHtmlAttribute = (value: string) => value
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const lines = [
"<!doctype html>",
"<html>",
"<head>",
' <meta charset="utf-8" />',
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
` <title>${meta.title}</title>`,
` <title>${escapeHtmlAttribute(meta.title)}</title>`,
];
if (meta.author) {
lines.push(` <meta name="author" content="${meta.author}" />`);
lines.push(` <meta name="author" content="${escapeHtmlAttribute(meta.author)}" />`);
}
if (meta.description) {
lines.push(` <meta name="description" content="${meta.description}" />`);
lines.push(` <meta name="description" content="${escapeHtmlAttribute(meta.description)}" />`);
}
lines.push(` <style>${css}</style>`);
if (codeThemeCss) {
@@ -194,6 +194,54 @@ async function pasteFromClipboardInEditor(session: ChromeSession): Promise<void>
await sleep(1000);
}
async function prepareEditorPasteTarget(
session: ChromeSession,
context: string,
options: { clickEditor?: boolean } = {},
): Promise<void> {
await session.cdp.send('Target.activateTarget', { targetId: session.targetId }).catch(() => {});
await sleep(100);
if (options.clickEditor) {
await clickElement(session, '.ProseMirror');
await sleep(200);
}
const ready = await evaluate<boolean>(session, `
(function() {
const editor = document.querySelector('.ProseMirror');
if (!editor) return false;
const active = document.activeElement;
const selection = window.getSelection();
const selectionInEditor = !!selection && selection.rangeCount > 0 && !!selection.anchorNode && editor.contains(selection.anchorNode);
const focusInEditor = !!active && (active === editor || editor.contains(active));
if (selectionInEditor || focusInEditor) return true;
if (${JSON.stringify(Boolean(options.clickEditor))}) {
editor.focus();
const nextActive = document.activeElement;
return nextActive === editor || editor.contains(nextActive);
}
return false;
})()
`);
if (ready) return;
const activeElement = await evaluate<string>(session, `
(function() {
const el = document.activeElement;
if (!el) return '(none)';
const id = el.id ? '#' + el.id : '';
const className = typeof el.className === 'string' && el.className ? '.' + el.className.split(/\\s+/).join('.') : '';
return el.tagName.toLowerCase() + id + className;
})()
`);
throw new Error(`Body editor is not focused before ${context}; active element: ${activeElement}`);
}
async function parseMarkdownWithPlaceholders(
markdownPath: string,
theme?: string,
@@ -567,6 +615,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
console.log(`[wechat] Copying HTML content from: ${effectiveHtmlFile}`);
await copyHtmlFromBrowser(cdp, effectiveHtmlFile, contentImages);
await sleep(500);
await prepareEditorPasteTarget(session, 'body content paste', { clickEditor: true });
console.log('[wechat] Pasting into editor...');
await pasteFromClipboardInEditor(session);
await sleep(3000);
@@ -608,6 +657,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
await sleep(200);
console.log('[wechat] Pasting image...');
await prepareEditorPasteTarget(session, 'inline image paste');
await pasteFromClipboardInEditor(session);
await sleep(3000);
await removeExtraEmptyLineAfterImage(session);
@@ -620,6 +670,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
console.log(`[wechat] Pasting image: ${img}`);
await copyImageToClipboard(img);
await sleep(500);
await prepareEditorPasteTarget(session, 'leading image paste');
await pasteInEditor(session);
await sleep(2000);
await removeExtraEmptyLineAfterImage(session);
@@ -627,6 +678,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
}
console.log('[wechat] Typing content...');
await prepareEditorPasteTarget(session, 'content typing');
await typeText(session, content);
await sleep(1000);
@@ -46,6 +46,45 @@ export function stripWrappingQuotes(value: string): string {
return value.trim();
}
const HTML_ENTITIES: Record<string, string> = {
amp: "&",
apos: "'",
gt: ">",
lt: "<",
nbsp: " ",
quot: '"',
};
function decodeHtmlCodePoint(codePoint: number, fallback: string): string {
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {
return fallback;
}
return String.fromCodePoint(codePoint);
}
function decodeHtmlEntities(value: string): string {
return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (entity, body: string) => {
const normalized = body.toLowerCase();
if (normalized.startsWith("#x")) {
return decodeHtmlCodePoint(Number.parseInt(normalized.slice(2), 16), entity);
}
if (normalized.startsWith("#")) {
return decodeHtmlCodePoint(Number.parseInt(normalized.slice(1), 10), entity);
}
return HTML_ENTITIES[normalized] ?? entity;
});
}
export function cleanSummaryText(value: string): string {
return decodeHtmlEntities(stripWrappingQuotes(value))
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
.replace(/<br\s*\/?>/gi, " ")
.replace(/<\/?[a-z][a-z0-9:-]*(?:\s+[^>]*)?>/gi, " ")
.replace(/\s+/g, " ")
.trim();
}
export function toFrontmatterString(value: unknown): string | undefined {
if (typeof value === "string") {
return stripWrappingQuotes(value);
@@ -94,10 +133,11 @@ export function extractSummaryFromBody(body: string, maxLen: number): string {
.replace(/\*(.+?)\*/g, "$1")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/`([^`]+)`/g, "$1");
const summaryText = cleanSummaryText(cleanText);
if (cleanText.length > 20) {
if (cleanText.length <= maxLen) return cleanText;
return `${cleanText.slice(0, maxLen - 3)}...`;
if (summaryText.length > 20) {
if (summaryText.length <= maxLen) return summaryText;
return `${summaryText.slice(0, maxLen - 3)}...`;
}
}

Some files were not shown because too many files have changed in this diff Show More