mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 22:09:48 +08:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9977ff520c | |||
| 03bd68d3a4 | |||
| 39792f4360 | |||
| 25b8a7f73d | |||
| e13092ba2a | |||
| acbcf19ba2 | |||
| dcd0f81433 | |||
| c938396efc | |||
| 1ba68c9a9c | |||
| b3f5c0a8aa | |||
| 9c45ede0c7 | |||
| 30d2ac98ce | |||
| bfdd64bd4e | |||
| 434d4857da | |||
| 990fea4f7b | |||
| 517ff566a1 | |||
| 4c9af7d92f | |||
| 46c4859d48 | |||
| f3f886217b | |||
| 4d465d55d0 | |||
| 11d80eeaa9 | |||
| 58ba4579ef | |||
| 67a45a57a0 | |||
| 0b8ac256f4 | |||
| eaa0f1aa11 | |||
| ec5f4ffcc9 | |||
| d206e1674d | |||
| b62ad26098 | |||
| 31b2929d1c | |||
| 7c2253dd3e | |||
| 8be98c5afe | |||
| fe1a854bed | |||
| eeba585315 | |||
| 1ca99ae5ae | |||
| c5df9d01bf | |||
| ee1fc3c823 | |||
| b7725a4ba8 | |||
| a596c653d3 | |||
| 3017bfe423 | |||
| ae1d619ab2 | |||
| 4821508c34 | |||
| f1042c8a6c | |||
| fa0fe441f5 |
@@ -6,14 +6,14 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.94.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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 Mermaid–SVG 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
@@ -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
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,6 +272,43 @@ Generate professional infographics with 20 layout types and 17 visual styles. An
|
||||
|  |  | |
|
||||
| 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 11 color palettes with 7 rendering styles for 77 unique combinations.
|
||||
@@ -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
|
||||
|
||||
|
||||
+81
-12
@@ -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,6 +272,43 @@ clawhub install baoyu-markdown-to-html
|
||||
|  |  | |
|
||||
| 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
|
||||
|
||||
为文章生成封面图,支持五维定制系统:类型 × 配色 × 渲染 × 文字 × 氛围。11 种配色方案与 7 种渲染风格组合,提供 77 种独特效果。
|
||||
@@ -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 使用完整模型 ID;MiniMax 使用 `image-01` / `image-01-live` |
|
||||
| `--provider` | `google`、`openai`、`azure`、`openrouter`、`dashscope`、`zai`、`minimax`、`jimeng`、`seedream` 或 `replicate` |
|
||||
| `--model`, `-m` | 模型 ID 或部署名。Azure 使用部署名;OpenRouter 使用完整模型 ID;Z.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) — 图表技能设计体系的灵感来源
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
Generated
+196
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 text!'</strong>"), "Good text!'");
|
||||
});
|
||||
|
||||
@@ -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 <draft><\/title>/);
|
||||
assert.match(html, /meta name="author" content="Bao"yu"/);
|
||||
assert.match(html, /meta name="description" content="<p style="color: red">Summary & notes<\/p>"/);
|
||||
});
|
||||
|
||||
test("normalizeCssText and normalizeInlineCss replace variables and strip declarations", () => {
|
||||
const rawCss = `
|
||||
:root { --md-primary-color: #000; --md-font-size: 12px; --foreground: 0 0% 5%; }
|
||||
|
||||
@@ -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, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,6 +21,10 @@ Soft macaron pastel color blocks on warm cream
|
||||
- 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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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&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
|
||||
@@ -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();
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
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, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
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)}...`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
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) {
|
||||
|
||||
@@ -71,6 +71,7 @@ Transform content into professional slide deck images.
|
||||
| `corporate` | clean + professional + geometric + balanced | Investor decks, proposals |
|
||||
| `minimal` | clean + neutral + geometric + minimal | Executive briefings |
|
||||
| `sketch-notes` | organic + warm + handwritten + balanced | Educational, tutorials |
|
||||
| `hand-drawn-edu` | organic + macaron + handwritten + balanced | Educational diagrams, process explainers |
|
||||
| `watercolor` | organic + warm + humanist + minimal | Lifestyle, wellness |
|
||||
| `dark-atmospheric` | clean + dark + editorial + balanced | Entertainment, gaming |
|
||||
| `notion` | clean + neutral + geometric + dense | Product demos, SaaS |
|
||||
@@ -88,7 +89,7 @@ Transform content into professional slide deck images.
|
||||
| Dimension | Options | Description |
|
||||
|-----------|---------|-------------|
|
||||
| **Texture** | clean, grid, organic, pixel, paper | Visual texture and background treatment |
|
||||
| **Mood** | professional, warm, cool, vibrant, dark, neutral | Color temperature and palette style |
|
||||
| **Mood** | professional, warm, cool, vibrant, dark, neutral, macaron | Color temperature and palette style |
|
||||
| **Typography** | geometric, humanist, handwritten, editorial, technical | Headline and body text styling |
|
||||
| **Density** | minimal, balanced, dense | Information density per slide |
|
||||
|
||||
@@ -99,6 +100,7 @@ Full specs: `references/dimensions/*.md`
|
||||
| Content Signals | Preset |
|
||||
|-----------------|--------|
|
||||
| tutorial, learn, education, guide, beginner | `sketch-notes` |
|
||||
| hand-drawn, infographic, diagram, process, onboarding | `hand-drawn-edu` |
|
||||
| classroom, teaching, school, chalkboard | `chalkboard` |
|
||||
| architecture, system, data, analysis, technical | `blueprint` |
|
||||
| creative, children, kids, cute | `vector-illustration` |
|
||||
@@ -393,6 +395,8 @@ options:
|
||||
description: "Blues, grays, analytical"
|
||||
- label: "vibrant"
|
||||
description: "High saturation, bold"
|
||||
- label: "macaron"
|
||||
description: "Pastel blocks (blue, mint, lavender, peach) on cream"
|
||||
```
|
||||
(Note: "dark", "neutral" available via Other)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ Color temperature and palette style.
|
||||
|--------|-------------------|---------------|----------|
|
||||
| `professional` | Cool-neutral | Navy, gold, structured grays | Business, investor, corporate |
|
||||
| `warm` | Warm | Earth tones, oranges, natural colors | Education, friendly, approachable |
|
||||
| `macaron` | Warm | Pastel blocks (blue, mint, lavender, peach) on cream | Educational diagrams, knowledge sharing |
|
||||
| `cool` | Cool | Blues, grays, cyan, teal | Technical, data, analytical |
|
||||
| `vibrant` | Varied | High saturation, bold colors | Marketing, creative, attention-grabbing |
|
||||
| `dark` | Dark | Deep backgrounds with bright accents | Entertainment, gaming, atmospheric |
|
||||
@@ -36,6 +37,19 @@ Accent 2: #E9C46A (Mustard Yellow)
|
||||
Accent 3: #87A96B (Sage Green)
|
||||
```
|
||||
|
||||
### macaron
|
||||
|
||||
```
|
||||
Background: #F5F0E8 (Warm Cream)
|
||||
Primary Text: #2D2D2D (Deep Charcoal)
|
||||
Muted Text: #6B6B6B (Warm Gray)
|
||||
Macaron Blue: #A8D8EA (Sky Blue)
|
||||
Macaron Mint: #B5E5CF (Mint Green)
|
||||
Macaron Lavender: #D5C6E0 (Lavender)
|
||||
Macaron Peach: #FFD5C2 (Peach)
|
||||
Accent: #E8655A (Coral Red)
|
||||
```
|
||||
|
||||
### cool
|
||||
|
||||
```
|
||||
@@ -95,6 +109,13 @@ Accent 2: #A1A1AA (Light Gray)
|
||||
- Soft transitions between colors
|
||||
- Welcoming atmosphere
|
||||
|
||||
### macaron
|
||||
|
||||
- Soft pastel color blocks as distinct information zones
|
||||
- Each zone uses one macaron color (blue, mint, lavender, peach)
|
||||
- Coral Red accent used sparingly for emphasis
|
||||
- Warm cream background maintains friendly, approachable feel
|
||||
|
||||
### cool
|
||||
|
||||
- Blue-dominant palette
|
||||
@@ -129,6 +150,7 @@ Accent 2: #A1A1AA (Light Gray)
|
||||
|------|-----------------|------------|
|
||||
| professional | clean texture, geometric typography | organic texture, handwritten |
|
||||
| warm | organic texture, humanist typography | pixel texture, minimal density |
|
||||
| macaron | organic texture, handwritten typography | pixel texture, dark mood |
|
||||
| cool | grid texture, technical typography | paper texture, handwritten |
|
||||
| vibrant | pixel/organic texture, editorial typography | neutral mood overlaps |
|
||||
| dark | clean/pixel texture, technical typography | paper texture |
|
||||
|
||||
@@ -11,6 +11,7 @@ Maps 16 preset styles to their dimension combinations.
|
||||
| corporate | clean | professional | geometric | balanced |
|
||||
| minimal | clean | neutral | geometric | minimal |
|
||||
| sketch-notes | organic | warm | handwritten | balanced |
|
||||
| hand-drawn-edu | organic | macaron | handwritten | balanced |
|
||||
| watercolor | organic | warm | humanist | minimal |
|
||||
| dark-atmospheric | clean | dark | editorial | balanced |
|
||||
| notion | clean | neutral | geometric | dense |
|
||||
@@ -50,6 +51,11 @@ Maps 16 preset styles to their dimension combinations.
|
||||
- **Feel**: Friendly learning, approachable education
|
||||
- **Auto-select**: tutorial, learn, education, guide, beginner
|
||||
|
||||
### hand-drawn-edu
|
||||
- **Dimensions**: organic + macaron + handwritten + balanced
|
||||
- **Feel**: Hand-drawn educational infographic, macaron pastel zones
|
||||
- **Auto-select**: hand-drawn, infographic, diagram, process, onboarding
|
||||
|
||||
### watercolor
|
||||
- **Dimensions**: organic + warm + humanist + minimal
|
||||
- **Feel**: Artistic, natural, lifestyle
|
||||
@@ -114,7 +120,7 @@ When user selects "Custom dimensions", combine any:
|
||||
- **Typography** (5): geometric, humanist, handwritten, editorial, technical
|
||||
- **Density** (3): minimal, balanced, dense
|
||||
|
||||
Total possible combinations: 5 × 6 × 5 × 3 = **450 unique styles**
|
||||
Total possible combinations: 5 × 7 × 5 × 3 = **525 unique styles**
|
||||
|
||||
## Recommended Combinations (Beyond Presets)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user