mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea8ad82786 | |||
| 6fffe252c0 | |||
| 86a84739e8 | |||
| 7f80100b3e | |||
| 61960961ba | |||
| cdd9ee042d | |||
| c742bfa1af | |||
| 0faea4ecaa | |||
| 7c9ec259a1 | |||
| 24a17709a8 | |||
| a3849af0cf | |||
| cc95e6fe05 | |||
| e7d9ed7917 | |||
| 876c6332f6 | |||
| 502d2448b2 | |||
| b9e48d9483 | |||
| b0554f8d0c | |||
| 12f3d95639 | |||
| 809fb29ca9 | |||
| be20bdf0be | |||
| 7c36b2a6a5 | |||
| 7979cb9dde | |||
| e563bbae7b | |||
| c63d1eec69 | |||
| 78d6d23359 | |||
| ec6781a231 | |||
| edf9030675 | |||
| 10e1c426a0 | |||
| dbab83ff82 | |||
| 478e66a93a | |||
| 3a5866eb4b | |||
| c0162bb3af | |||
| e9342a16f8 | |||
| 5cb86d8e51 | |||
| 66ed08f010 | |||
| b179166d28 | |||
| 4805526649 | |||
| 89159bf86e | |||
| b45b5d2c6e | |||
| 62866426c5 |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "1.25.4"
|
"version": "1.30.1"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-1
@@ -156,4 +156,6 @@ comic/
|
|||||||
.idea
|
.idea
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
|
|
||||||
|
.claude/skills/baoyu-skill-evolution
|
||||||
|
|||||||
@@ -2,6 +2,77 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.30.1 - 2026-02-06
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-image-gen`: add OpenAI GPT Image edits support for reference images (`--ref`); auto-select Google or OpenAI when ref provided.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-image-gen`: change ref-related warnings to explicit errors with fix hints; add reference image validation.
|
||||||
|
- `baoyu-cover-image`: enhance reference image analysis with deep extraction template; require MUST INCORPORATE section for concrete visual elements.
|
||||||
|
|
||||||
|
## 1.30.0 - 2026-02-06
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-cover-image`: add font dimension with 4 typography styles (clean, handwritten, serif, display); includes auto-selection rules, compatibility matrix, and `warm-flat` style preset.
|
||||||
|
|
||||||
|
## 1.29.0 - 2026-02-06
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-image-gen`: add EXTEND.md configuration support, including schema documentation and runtime preference loading in scripts (by @kingdomad).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: fix duplicated title and ordered-list numbering in WeChat article publishing (by @NantesCheval).
|
||||||
|
- `baoyu-url-to-markdown`: replace regex-only conversion with multi-strategy content extraction and Turndown conversion; improve noise filtering for Substack-style pages.
|
||||||
|
|
||||||
|
## 1.28.4 - 2026-02-03
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-markdown-to-html`: add author and description meta tags to generated HTML from YAML frontmatter; strip quotes from frontmatter values (supports both English and Chinese quotation marks).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: remove extra empty lines after image paste; fix summary field timing to fill after content paste (prevents being overwritten).
|
||||||
|
|
||||||
|
## 1.28.3 - 2026-02-03
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: fix placeholder matching issue where `WECHATIMGPH_1` incorrectly matched `WECHATIMGPH_10`.
|
||||||
|
|
||||||
|
## 1.28.2 - 2026-02-03
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-x`: reuse existing Chrome instance when available; fix placeholder matching issue where `XIMGPH_1` incorrectly matched `XIMGPH_10`; improve image sorting by placeholder index; use `execCommand` for more reliable placeholder deletion.
|
||||||
|
|
||||||
|
## 1.28.1 - 2026-02-02
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
- `baoyu-article-illustrator`: simplify main SKILL.md by extracting detailed procedures to `workflow.md`; add Core Styles tier (vector, minimal-flat, sci-fi, hand-drawn, editorial, scene) for quick selection; add `vector-illustration` as recommended default style; add Illustration Purpose (information/visualization/imagination) for better type/style recommendations; add default composition requirements, character rendering guidelines, and text styling rules to prompt construction.
|
||||||
|
|
||||||
|
## 1.28.0 - 2026-02-01
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-cover-image`: add reference image support (`--ref` parameter) with direct/style/palette usage types; add visual elements library with icon vocabulary by topic.
|
||||||
|
- `baoyu-article-illustrator`: add reference image support with direct/style/palette usage types.
|
||||||
|
- `baoyu-post-to-wechat`: add `newspic` article type for image-text posts.
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
- `baoyu-cover-image`, `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-xhs-images`: enforce first-time setup as blocking operation before any other workflow steps.
|
||||||
|
- `baoyu-cover-image`: remove character limits from titles, use original source titles.
|
||||||
|
|
||||||
|
## 1.26.1 - 2026-01-29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-cover-image`, `baoyu-infographic`, `baoyu-slide-deck`, `baoyu-xhs-images`: add backup rules for existing files—automatically renames source, prompt, and image files with timestamp suffix before overwriting.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-xhs-images`: remove `notebook` style (10 styles remaining).
|
||||||
|
|
||||||
|
## 1.26.0 - 2026-01-29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-xhs-images`: add `notebook` style (hand-drawn infographic with watercolor rendering and Morandi palette) and `study-notes` style (realistic handwritten photo aesthetic).
|
||||||
|
- `baoyu-xhs-images`: add `mindmap` (center radial) and `quadrant` (four-section grid) layouts.
|
||||||
|
|
||||||
## 1.25.4 - 2026-01-29
|
## 1.25.4 - 2026-01-29
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|||||||
@@ -2,6 +2,77 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.30.1 - 2026-02-06
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-image-gen`:新增 OpenAI GPT Image edits 支持参考图片(`--ref`);提供 ref 时自动选择 Google 或 OpenAI。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-image-gen`:将 ref 相关警告改为明确错误提示;新增参考图片验证。
|
||||||
|
- `baoyu-cover-image`:增强参考图片分析,使用深度提取模板;要求 MUST INCORPORATE 章节以包含具体可复现的视觉元素。
|
||||||
|
|
||||||
|
## 1.30.0 - 2026-02-06
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-cover-image`:新增字体维度,支持 4 种字体风格(clean、handwritten、serif、display);包含自动选择规则、兼容性矩阵和 `warm-flat` 风格预设。
|
||||||
|
|
||||||
|
## 1.29.0 - 2026-02-06
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-image-gen`:新增 EXTEND.md 配置支持,补充配置 schema 文档并在脚本运行时读取偏好设置 (by @kingdomad)。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:修复公众号文章发布时标题和有序列表编号重复问题 (by @NantesCheval)。
|
||||||
|
- `baoyu-url-to-markdown`:将正则转换升级为多策略正文抽取 + Turndown 转换,提升 Substack 类页面的噪声过滤能力。
|
||||||
|
|
||||||
|
## 1.28.4 - 2026-02-03
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-markdown-to-html`:从 YAML frontmatter 生成 author 和 description meta 标签;自动去除 frontmatter 值两端的引号(支持中英文引号)。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:移除图片粘贴后产生的多余空行;修复摘要填充时机,改为内容粘贴后填写(避免被覆盖)。
|
||||||
|
|
||||||
|
## 1.28.3 - 2026-02-03
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:修复占位符匹配问题(`WECHATIMGPH_1` 错误匹配 `WECHATIMGPH_10`)。
|
||||||
|
|
||||||
|
## 1.28.2 - 2026-02-03
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-x`:复用已有 Chrome 实例;修复占位符匹配问题(`XIMGPH_1` 错误匹配 `XIMGPH_10`);改进图片按占位符序号排序;使用 `execCommand` 提高占位符删除可靠性。
|
||||||
|
|
||||||
|
## 1.28.1 - 2026-02-02
|
||||||
|
|
||||||
|
### 重构
|
||||||
|
- `baoyu-article-illustrator`:简化主 SKILL.md,将详细步骤提取到 `workflow.md`;新增 Core Styles 快速选择层(vector、minimal-flat、sci-fi、hand-drawn、editorial、scene);新增 `vector-illustration` 作为推荐默认风格;新增插图目的(information/visualization/imagination)以优化类型/风格推荐;在提示词构建中新增默认构图要求、人物渲染指南和文本样式规则。
|
||||||
|
|
||||||
|
## 1.28.0 - 2026-02-01
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-cover-image`:新增参考图片支持(`--ref` 参数),支持 direct/style/palette 三种用法;新增视觉元素库,按主题分类图标词汇。
|
||||||
|
- `baoyu-article-illustrator`:新增参考图片支持,支持 direct/style/palette 三种用法。
|
||||||
|
- `baoyu-post-to-wechat`:新增 `newspic` 图文消息类型支持。
|
||||||
|
|
||||||
|
### 重构
|
||||||
|
- `baoyu-cover-image`、`baoyu-article-illustrator`、`baoyu-comic`、`baoyu-xhs-images`:强化首次设置为阻塞操作,必须在其他工作流步骤之前完成。
|
||||||
|
- `baoyu-cover-image`:移除标题字符数限制,使用原始来源标题。
|
||||||
|
|
||||||
|
## 1.26.1 - 2026-01-29
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-article-illustrator`、`baoyu-comic`、`baoyu-cover-image`、`baoyu-infographic`、`baoyu-slide-deck`、`baoyu-xhs-images`:新增文件备份规则,覆盖前自动将现有源文件、提示词和图片重命名为带时间戳后缀的备份文件。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-xhs-images`:移除 `notebook` 风格(保留 10 种风格)。
|
||||||
|
|
||||||
|
## 1.26.0 - 2026-01-29
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-xhs-images`:新增 `notebook` 风格(水彩渲染手绘信息图 + 莫兰迪配色)和 `study-notes` 风格(真实手写照片美学)。
|
||||||
|
- `baoyu-xhs-images`:新增 `mindmap`(中心发散式)和 `quadrant`(四象限)布局。
|
||||||
|
|
||||||
## 1.25.4 - 2026-01-29
|
## 1.25.4 - 2026-01-29
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ Analyze articles, identify illustration positions, generate images with Type ×
|
|||||||
|
|
||||||
Type × Style can be freely combined. Example: `--type infographic --style blueprint`
|
Type × Style can be freely combined. Example: `--type infographic --style blueprint`
|
||||||
|
|
||||||
|
## Illustration Purpose
|
||||||
|
|
||||||
|
Auto-detected during content analysis. Influences type/style recommendations.
|
||||||
|
|
||||||
|
| Purpose | Description | Best Types |
|
||||||
|
|---------|-------------|------------|
|
||||||
|
| **information** | Help understand abstract concepts | infographic, flowchart, comparison |
|
||||||
|
| **visualization** | Turn abstract ideas into concrete visuals | framework, comparison, infographic |
|
||||||
|
| **imagination** | Create atmosphere, spark imagination | scene, timeline |
|
||||||
|
|
||||||
## Type Gallery
|
## Type Gallery
|
||||||
|
|
||||||
| Type | Best For |
|
| Type | Best For |
|
||||||
@@ -27,92 +37,37 @@ Type × Style can be freely combined. Example: `--type infographic --style bluep
|
|||||||
| `framework` | Methodologies, models, architecture |
|
| `framework` | Methodologies, models, architecture |
|
||||||
| `timeline` | History, progress, evolution |
|
| `timeline` | History, progress, evolution |
|
||||||
|
|
||||||
## Style Gallery
|
## Styles
|
||||||
|
|
||||||
| Style | Best For |
|
See [references/styles.md](references/styles.md) for:
|
||||||
|-------|----------|
|
- **Core Styles**: Simplified tier for quick selection (vector, minimal-flat, sci-fi, hand-drawn, editorial, scene)
|
||||||
| `notion` (Default) | Knowledge sharing, SaaS, productivity |
|
- **Style Gallery**: Full 20+ style options with descriptions
|
||||||
| `elegant` | Business, thought leadership |
|
- **Auto Selection**: Content signals → Type/Style recommendations
|
||||||
| `warm` | Personal growth, lifestyle, education |
|
- **Compatibility Matrix**: Type × Style combinations
|
||||||
| `minimal` | Philosophy, core concepts |
|
|
||||||
| `blueprint` | Architecture, system design |
|
|
||||||
| `watercolor` | Lifestyle, travel, creative |
|
|
||||||
| `editorial` | Tech explainers, journalism |
|
|
||||||
| `scientific` | Academic, technical research |
|
|
||||||
|
|
||||||
Full styles: [references/styles.md](references/styles.md)
|
|
||||||
|
|
||||||
## Auto Selection
|
|
||||||
|
|
||||||
| Content Signals | Type | Style |
|
|
||||||
|-----------------|------|-------|
|
|
||||||
| API, metrics, data, numbers | infographic | blueprint, notion |
|
|
||||||
| Story, emotion, journey | scene | warm, watercolor |
|
|
||||||
| How-to, steps, workflow | flowchart | notion, minimal |
|
|
||||||
| vs, pros/cons, before/after | comparison | notion, elegant |
|
|
||||||
| Framework, model, architecture | framework | blueprint, notion |
|
|
||||||
| History, timeline, progress | timeline | elegant, warm |
|
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
Copy this checklist and track progress:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Progress:
|
Progress:
|
||||||
- [ ] Step 1: Pre-check
|
- [ ] Step 1: Pre-check
|
||||||
|
- [ ] 1.5 Load preferences (EXTEND.md) ⛔ BLOCKING
|
||||||
|
- [ ] 1.0 Reference images ⚠️ (if provided)
|
||||||
|
- [ ] 1.2-1.4 Config questions (1 AskUserQuestion, max 4 Qs)
|
||||||
- [ ] Step 2: Setup & Analyze
|
- [ ] Step 2: Setup & Analyze
|
||||||
- [ ] Step 3: Confirm Settings ⚠️ REQUIRED
|
- [ ] Step 3: Confirm Settings (1 AskUserQuestion, max 4 Qs)
|
||||||
|
- [ ] Q1: Type ⚠️
|
||||||
|
- [ ] Q2: Density ⚠️ MUST ASK
|
||||||
|
- [ ] Q3: Style ⚠️
|
||||||
- [ ] Step 4: Generate Outline
|
- [ ] Step 4: Generate Outline
|
||||||
- [ ] Step 5: Generate Images
|
- [ ] Step 5: Generate Images
|
||||||
- [ ] Step 6: Finalize
|
- [ ] Step 6: Finalize
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 1: Pre-check
|
### Step 1: Pre-check
|
||||||
|
|
||||||
**1.1 Determine Input Type**
|
**1.5 Load Preferences (EXTEND.md) ⛔ BLOCKING**
|
||||||
|
|
||||||
| Input | Output Directory | Next |
|
**CRITICAL**: If EXTEND.md not found, MUST complete first-time setup before ANY other steps.
|
||||||
|-------|------------------|------|
|
|
||||||
| File path | Ask user (1.2) | → 1.2 |
|
|
||||||
| Pasted content | `illustrations/{topic-slug}/` | → 1.4 |
|
|
||||||
|
|
||||||
**1.2 Determine Output Directory** (file path input only)
|
|
||||||
|
|
||||||
Check `default_output_dir` in preferences:
|
|
||||||
|
|
||||||
| Preference Value | Action |
|
|
||||||
|------------------|--------|
|
|
||||||
| `same-dir` | Use `{article-dir}/`, display "Output: {path}" |
|
|
||||||
| `imgs-subdir` | Use `{article-dir}/imgs/`, display "Output: {path}" |
|
|
||||||
| `illustrations-subdir` | Use `{article-dir}/illustrations/`, display "Output: {path}" |
|
|
||||||
| `independent` | Use `illustrations/{topic-slug}/`, display "Output: {path}" |
|
|
||||||
| Not configured | **MUST** ask with AskUserQuestion ↓ |
|
|
||||||
|
|
||||||
**AskUserQuestion** (when no preference):
|
|
||||||
- `{article-dir}/` - Same directory as article
|
|
||||||
- `{article-dir}/imgs/` - Images subdirectory
|
|
||||||
- `{article-dir}/illustrations/` - Illustrations subdirectory (Recommended)
|
|
||||||
- `illustrations/{topic-slug}/` - Independent directory
|
|
||||||
- Save as default - Remember this choice for future runs
|
|
||||||
|
|
||||||
**1.3 Check Existing Images**
|
|
||||||
|
|
||||||
Scan target directory for `.png/.jpg/.webp` files.
|
|
||||||
|
|
||||||
If images exist → AskUserQuestion: How to handle?
|
|
||||||
- `supplement` - Keep existing, generate only new positions
|
|
||||||
- `overwrite` - Overwrite same-name files
|
|
||||||
- `regenerate` - Clear all and regenerate
|
|
||||||
|
|
||||||
**1.4 Confirm Article Update Method** (file path input only)
|
|
||||||
|
|
||||||
AskUserQuestion: How to update article?
|
|
||||||
- `update` - Modify original file directly
|
|
||||||
- `copy` - Create `{name}-illustrated.md` copy
|
|
||||||
|
|
||||||
**1.5 Load Preferences (EXTEND.md)**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
|
||||||
@@ -121,166 +76,86 @@ test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
|
|||||||
|
|
||||||
| Result | Action |
|
| Result | Action |
|
||||||
|--------|--------|
|
|--------|--------|
|
||||||
| Found | Read, parse, display summary |
|
| Found | Read, parse, display summary → Continue |
|
||||||
| Not found | Ask with AskUserQuestion (see references/config/first-time-setup.md) |
|
| Not found | ⛔ Run first-time setup ([references/config/first-time-setup.md](references/config/first-time-setup.md)) |
|
||||||
|
|
||||||
**Supports**: Watermark | Preferred type/style | Custom styles | Language | Output directory
|
**Supports**: Watermark | Preferred type/style | Custom styles | Language | Output directory
|
||||||
|
|
||||||
|
**1.0-1.4**: Handle reference images, determine input type, ask config questions.
|
||||||
|
|
||||||
|
Full procedures: [references/workflow.md](references/workflow.md#step-1-pre-check)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 2: Setup & Analyze
|
### Step 2: Setup & Analyze
|
||||||
|
|
||||||
**2.1 Analyze Content**
|
|
||||||
|
|
||||||
| Analysis | Description |
|
| Analysis | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| Content type | Technical / Tutorial / Methodology / Narrative |
|
| Content type | Technical / Tutorial / Methodology / Narrative |
|
||||||
|
| Illustration purpose | information / visualization / imagination |
|
||||||
| Core arguments | 2-5 main points to visualize |
|
| Core arguments | 2-5 main points to visualize |
|
||||||
| Visual opportunities | Positions where illustrations add value |
|
| Visual opportunities | Positions where illustrations add value |
|
||||||
| Recommended type | Based on content signals |
|
|
||||||
| Recommended density | Based on length and complexity |
|
|
||||||
|
|
||||||
**2.2 Extract Core Arguments**
|
**CRITICAL**: If article uses metaphors, do NOT illustrate literally. Visualize the **underlying concept**.
|
||||||
|
|
||||||
- Main thesis
|
Full procedures: [references/workflow.md](references/workflow.md#step-2-setup--analyze)
|
||||||
- Key concepts reader needs
|
|
||||||
- Comparisons/contrasts
|
|
||||||
- Framework/model proposed
|
|
||||||
|
|
||||||
**CRITICAL**: If article uses metaphors (e.g., "电锯切西瓜"), do NOT illustrate literally. Visualize the **underlying concept**.
|
|
||||||
|
|
||||||
**2.3 Identify Positions**
|
|
||||||
|
|
||||||
**Illustrate**:
|
|
||||||
- Core arguments (REQUIRED)
|
|
||||||
- Abstract concepts
|
|
||||||
- Data comparisons
|
|
||||||
- Processes, workflows
|
|
||||||
|
|
||||||
**Do NOT Illustrate**:
|
|
||||||
- Metaphors literally
|
|
||||||
- Decorative scenes
|
|
||||||
- Generic illustrations
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 3: Confirm Settings ⚠️
|
### Step 3: Confirm Settings ⚠️
|
||||||
|
|
||||||
**Do NOT skip.** Use AskUserQuestion with 3-4 questions in ONE call.
|
**Do NOT skip.** Use ONE AskUserQuestion call with max 4 questions. **Q1, Q2, Q3 are ALL REQUIRED.**
|
||||||
|
|
||||||
**Q1: Illustration Type**
|
| Question | Options |
|
||||||
- [Recommended based on analysis] (Recommended)
|
|----------|---------|
|
||||||
- infographic / scene / flowchart / comparison / framework / timeline / mixed
|
| **Q1: Type** ⚠️ | [Recommended], infographic, scene, flowchart, comparison, framework, timeline, mixed |
|
||||||
|
| **Q2: Density** ⚠️ | minimal (1-2), balanced (3-5), per-section (Recommended), rich (6+) |
|
||||||
|
| **Q3: Style** ⚠️ | [Recommended], minimal-flat, sci-fi, hand-drawn, editorial, scene, Other |
|
||||||
|
| **Q4: Language** | When article language ≠ EXTEND.md setting |
|
||||||
|
|
||||||
**Q2: Density**
|
Full procedures: [references/workflow.md](references/workflow.md#step-3-confirm-settings-)
|
||||||
- minimal (1-2) - Core concepts only
|
|
||||||
- balanced (3-5) (Recommended) - Major sections
|
|
||||||
- rich (6+) - Comprehensive support
|
|
||||||
|
|
||||||
**Q3: Style** (ALWAYS ask, even with preferred_style in EXTEND.md)
|
|
||||||
|
|
||||||
If EXTEND.md has `preferred_style`:
|
|
||||||
- [Custom style name + brief description] (Recommended)
|
|
||||||
- [Top compatible built-in style 1]
|
|
||||||
- [Top compatible built-in style 2]
|
|
||||||
- [Top compatible built-in style 3]
|
|
||||||
|
|
||||||
If no `preferred_style`:
|
|
||||||
- [Best compatible from matrix] (Recommended)
|
|
||||||
- [Other ✓✓ style 1]
|
|
||||||
- [Other ✓✓ style 2]
|
|
||||||
- [Other ✓ style]
|
|
||||||
|
|
||||||
Style selection based on Type × Style compatibility matrix (references/styles.md).
|
|
||||||
Full specs: `references/styles/<style>.md`
|
|
||||||
|
|
||||||
**Q4** (only if source ≠ user language):
|
|
||||||
- Language: Source / User language
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 4: Generate Outline
|
### Step 4: Generate Outline
|
||||||
|
|
||||||
Save as `outline.md`:
|
Save as `outline.md` with frontmatter (type, density, style, image_count, references) and illustration entries:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
|
||||||
type: infographic
|
|
||||||
density: balanced
|
|
||||||
style: blueprint
|
|
||||||
image_count: 4
|
|
||||||
---
|
|
||||||
|
|
||||||
## Illustration 1
|
## Illustration 1
|
||||||
|
|
||||||
**Position**: [section] / [paragraph]
|
**Position**: [section] / [paragraph]
|
||||||
**Purpose**: [why this helps]
|
**Purpose**: [why this helps]
|
||||||
**Visual Content**: [what to show]
|
**Visual Content**: [what to show]
|
||||||
**Type Application**: [how type applies]
|
|
||||||
**Filename**: 01-infographic-concept-name.png
|
**Filename**: 01-infographic-concept-name.png
|
||||||
|
|
||||||
## Illustration 2
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Requirements**:
|
Full template: [references/workflow.md](references/workflow.md#step-4-generate-outline)
|
||||||
- Each position justified by content needs
|
|
||||||
- Type applied consistently
|
|
||||||
- Style reflected in descriptions
|
|
||||||
- Count matches density
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 5: Generate Images
|
### Step 5: Generate Images
|
||||||
|
|
||||||
**5.1 Create Prompts**
|
1. **Create Prompts**: Follow [references/prompt-construction.md](references/prompt-construction.md)
|
||||||
|
2. **Select Generation Skill**: Check available skills
|
||||||
|
3. **Process References**: Handle `direct`/`style`/`palette` usage
|
||||||
|
4. **Apply Watermark**: If enabled in EXTEND.md
|
||||||
|
5. **Generate**: Sequential, retry once on failure
|
||||||
|
|
||||||
Follow [references/prompt-construction.md](references/prompt-construction.md). Save to `prompts/illustration-{slug}.md`.
|
Full procedures: [references/workflow.md](references/workflow.md#step-5-generate-images)
|
||||||
|
|
||||||
**5.2 Select Generation Skill**
|
|
||||||
|
|
||||||
Check available skills. If multiple, ask user.
|
|
||||||
|
|
||||||
**5.3 Apply Watermark** (if enabled)
|
|
||||||
|
|
||||||
Add: `Include a subtle watermark "[content]" at [position].`
|
|
||||||
|
|
||||||
**5.4 Generate**
|
|
||||||
|
|
||||||
1. Generate sequentially
|
|
||||||
2. After each: "Generated X/N"
|
|
||||||
3. On failure: retry once, then log and continue
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 6: Finalize
|
### Step 6: Finalize
|
||||||
|
|
||||||
**6.1 Update Article**
|
**Update Article**: Insert `` after corresponding paragraphs.
|
||||||
|
|
||||||
Insert after corresponding paragraph:
|
|
||||||
```markdown
|
|
||||||

|
|
||||||
```
|
|
||||||
|
|
||||||
Alt text: concise description in article's language.
|
|
||||||
|
|
||||||
**6.2 Output Summary**
|
|
||||||
|
|
||||||
|
**Output Summary**:
|
||||||
```
|
```
|
||||||
Article Illustration Complete!
|
Article Illustration Complete!
|
||||||
|
Article: [path] | Type: [type] | Density: [level] | Style: [style]
|
||||||
Article: [path]
|
|
||||||
Type: [type] | Density: [level] | Style: [style]
|
|
||||||
Location: [directory]
|
|
||||||
Images: X/N generated
|
Images: X/N generated
|
||||||
|
|
||||||
Positions:
|
Positions:
|
||||||
- 01-xxx.png → After "[Section]"
|
- 01-xxx.png → After "[Section]"
|
||||||
- 02-yyy.png → After "[Section]"
|
|
||||||
|
|
||||||
[If failures]
|
|
||||||
Failed:
|
|
||||||
- NN-zzz.png: [reason]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -290,6 +165,8 @@ Failed:
|
|||||||
```
|
```
|
||||||
illustrations/{topic-slug}/
|
illustrations/{topic-slug}/
|
||||||
├── source-{slug}.{ext}
|
├── source-{slug}.{ext}
|
||||||
|
├── references/ # Only if references provided
|
||||||
|
│ └── NN-ref-{slug}.png
|
||||||
├── outline.md
|
├── outline.md
|
||||||
├── prompts/
|
├── prompts/
|
||||||
│ └── illustration-{slug}.md
|
│ └── illustration-{slug}.md
|
||||||
@@ -303,26 +180,18 @@ illustrations/{topic-slug}/
|
|||||||
|
|
||||||
| Action | Steps |
|
| Action | Steps |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| **Edit** | **Update prompt file FIRST** → Regenerate → Update reference |
|
| **Edit** | Update prompt file FIRST → Regenerate → Update reference |
|
||||||
| **Add** | Identify position → Create prompt → Generate → Update outline → Insert |
|
| **Add** | Identify position → Create prompt → Generate → Update outline → Insert |
|
||||||
| **Delete** | Delete files → Remove reference → Update outline |
|
| **Delete** | Delete files → Remove reference → Update outline |
|
||||||
|
|
||||||
**IMPORTANT**: When updating illustrations, ALWAYS update the prompt file (`prompts/illustration-{slug}.md`) FIRST before regenerating. This ensures:
|
|
||||||
1. Changes are documented and reproducible
|
|
||||||
2. The prompt reflects the new requirements
|
|
||||||
3. Future regeneration uses the correct prompt
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
| File | Content |
|
| File | Content |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
|
| [references/workflow.md](references/workflow.md) | Detailed workflow procedures |
|
||||||
| [references/usage.md](references/usage.md) | Command syntax and options |
|
| [references/usage.md](references/usage.md) | Command syntax and options |
|
||||||
| [references/styles.md](references/styles.md) | Style gallery & compatibility |
|
| [references/styles.md](references/styles.md) | Style gallery & compatibility |
|
||||||
| [references/prompt-construction.md](references/prompt-construction.md) | Prompt templates |
|
| [references/prompt-construction.md](references/prompt-construction.md) | Prompt templates |
|
||||||
| `references/styles/<style>.md` | Full style specifications |
|
| `references/styles/<style>.md` | Full style specifications |
|
||||||
| `references/config/preferences-schema.md` | EXTEND.md schema |
|
| `references/config/preferences-schema.md` | EXTEND.md schema |
|
||||||
| `references/config/first-time-setup.md` | First-time setup flow |
|
| `references/config/first-time-setup.md` | First-time setup flow |
|
||||||
|
|
||||||
## Extension Support
|
|
||||||
|
|
||||||
Custom configurations via EXTEND.md. See **Step 1.5** for paths and supported options.
|
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ description: First-time setup flow for baoyu-article-illustrator preferences
|
|||||||
|
|
||||||
When no EXTEND.md is found, guide user through preference setup.
|
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 reference images
|
||||||
|
- Ask about content/article
|
||||||
|
- Ask about type or style preferences
|
||||||
|
- Proceed to content analysis
|
||||||
|
|
||||||
|
ONLY ask the questions in this setup flow, save EXTEND.md, then continue.
|
||||||
|
|
||||||
## Setup Flow
|
## Setup Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,102 @@
|
|||||||
# Prompt Construction
|
# Prompt Construction
|
||||||
|
|
||||||
|
## Prompt File Format
|
||||||
|
|
||||||
|
Each prompt file uses YAML frontmatter + content:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
illustration_id: 01
|
||||||
|
type: infographic
|
||||||
|
style: blueprint
|
||||||
|
references: # ⚠️ ONLY if files EXIST in references/ directory
|
||||||
|
- ref_id: 01
|
||||||
|
filename: 01-ref-diagram.png
|
||||||
|
usage: direct # direct | style | palette
|
||||||
|
---
|
||||||
|
|
||||||
|
[Type-specific template content below...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ CRITICAL - When to include `references` field**:
|
||||||
|
|
||||||
|
| Situation | Action |
|
||||||
|
|-----------|--------|
|
||||||
|
| Reference file saved to `references/` | Include in frontmatter ✓ |
|
||||||
|
| Style extracted verbally (no file) | DO NOT include in frontmatter, append to prompt body instead |
|
||||||
|
| File path in frontmatter but file doesn't exist | ERROR - remove references field |
|
||||||
|
|
||||||
|
**Reference Usage Types** (only when file exists):
|
||||||
|
|
||||||
|
| Usage | Description | Generation Action |
|
||||||
|
|-------|-------------|-------------------|
|
||||||
|
| `direct` | Primary visual reference | Pass to `--ref` parameter |
|
||||||
|
| `style` | Style characteristics only | Describe style in prompt text |
|
||||||
|
| `palette` | Color palette extraction | Include colors in prompt |
|
||||||
|
|
||||||
|
**If no reference file but style/palette extracted verbally**, append directly to prompt body:
|
||||||
|
```
|
||||||
|
COLORS (from reference):
|
||||||
|
- Primary: #E8756D coral
|
||||||
|
- Secondary: #7ECFC0 mint
|
||||||
|
...
|
||||||
|
|
||||||
|
STYLE (from reference):
|
||||||
|
- Clean lines, minimal shadows
|
||||||
|
- Gradient backgrounds
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Default Composition Requirements
|
||||||
|
|
||||||
|
**Apply to ALL prompts by default**:
|
||||||
|
|
||||||
|
| Requirement | Description |
|
||||||
|
|-------------|-------------|
|
||||||
|
| **Clean composition** | Simple layouts, no visual clutter |
|
||||||
|
| **White space** | Generous margins, breathing room around elements |
|
||||||
|
| **No complex backgrounds** | Solid colors or subtle gradients only, avoid busy textures |
|
||||||
|
| **Centered or content-appropriate** | Main visual elements centered or positioned by content needs |
|
||||||
|
| **Matching graphics** | Use graphic elements that align with content theme |
|
||||||
|
| **Highlight core info** | White space draws attention to key information |
|
||||||
|
|
||||||
|
**Add to ALL prompts**:
|
||||||
|
> Clean composition with generous white space. Simple or no background. Main elements centered or positioned by content needs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Character Rendering
|
||||||
|
|
||||||
|
When depicting people:
|
||||||
|
|
||||||
|
| Guideline | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| **Style** | Simplified cartoon silhouettes or symbolic expressions |
|
||||||
|
| **Avoid** | Realistic human portrayals, detailed faces |
|
||||||
|
| **Diversity** | Varied body types when showing multiple people |
|
||||||
|
| **Emotion** | Express through posture and simple gestures |
|
||||||
|
|
||||||
|
**Add to ALL prompts with human figures**:
|
||||||
|
> Human figures: simplified stylized silhouettes or symbolic representations, not photorealistic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Text in Illustrations
|
||||||
|
|
||||||
|
| Element | Guideline |
|
||||||
|
|---------|-----------|
|
||||||
|
| **Size** | Large, prominent, immediately readable |
|
||||||
|
| **Style** | Handwritten fonts preferred for warmth |
|
||||||
|
| **Content** | Concise keywords and core concepts only |
|
||||||
|
| **Language** | Match article language |
|
||||||
|
|
||||||
|
**Add to prompts with text**:
|
||||||
|
> Text should be large and prominent with handwritten-style fonts. Keep minimal, focus on keywords.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Principles
|
## Principles
|
||||||
|
|
||||||
Good prompts must include:
|
Good prompts must include:
|
||||||
@@ -31,6 +128,13 @@ STYLE: [style characteristics]
|
|||||||
ASPECT: 16:9
|
ASPECT: 16:9
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Infographic + vector-illustration**:
|
||||||
|
```
|
||||||
|
Flat vector illustration infographic. Clean black outlines on all elements.
|
||||||
|
COLORS: Cream background (#F5F0E6), Coral Red (#E07A5F), Mint Green (#81B29A), Mustard Yellow (#F2CC8F)
|
||||||
|
ELEMENTS: Geometric simplified icons, no gradients, playful decorative elements (dots, stars)
|
||||||
|
```
|
||||||
|
|
||||||
### Scene
|
### Scene
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -61,6 +165,13 @@ STYLE: [style characteristics]
|
|||||||
ASPECT: 16:9
|
ASPECT: 16:9
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Flowchart + vector-illustration**:
|
||||||
|
```
|
||||||
|
Flat vector flowchart with bold arrows and geometric step containers.
|
||||||
|
COLORS: Cream background (#F5F0E6), steps in Coral/Mint/Mustard, black outlines
|
||||||
|
ELEMENTS: Rounded rectangles, thick arrows, simple icons per step
|
||||||
|
```
|
||||||
|
|
||||||
### Comparison
|
### Comparison
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -79,6 +190,13 @@ STYLE: [style characteristics]
|
|||||||
ASPECT: 16:9
|
ASPECT: 16:9
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Comparison + vector-illustration**:
|
||||||
|
```
|
||||||
|
Flat vector comparison with split layout. Clear visual separation.
|
||||||
|
COLORS: Left side Coral (#E07A5F), Right side Mint (#81B29A), cream background
|
||||||
|
ELEMENTS: Bold icons, black outlines, centered divider line
|
||||||
|
```
|
||||||
|
|
||||||
### Framework
|
### Framework
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -95,6 +213,13 @@ STYLE: [style characteristics]
|
|||||||
ASPECT: 16:9
|
ASPECT: 16:9
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Framework + vector-illustration**:
|
||||||
|
```
|
||||||
|
Flat vector framework diagram with geometric nodes and bold connectors.
|
||||||
|
COLORS: Cream background (#F5F0E6), nodes in Coral/Mint/Mustard/Blue, black outlines
|
||||||
|
ELEMENTS: Rounded rectangles or circles for nodes, thick connecting lines
|
||||||
|
```
|
||||||
|
|
||||||
### Timeline
|
### Timeline
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
# Style Reference
|
# Style Reference
|
||||||
|
|
||||||
|
## Core Styles
|
||||||
|
|
||||||
|
Simplified style tier for quick selection:
|
||||||
|
|
||||||
|
| Core Style | Maps To | Best For |
|
||||||
|
|------------|---------|----------|
|
||||||
|
| `vector` | vector-illustration | Knowledge articles, tutorials, tech content |
|
||||||
|
| `minimal-flat` | notion | General, knowledge sharing, SaaS |
|
||||||
|
| `sci-fi` | blueprint | AI, frontier tech, system design |
|
||||||
|
| `hand-drawn` | sketch/warm | Relaxed, reflective, casual content |
|
||||||
|
| `editorial` | editorial | Processes, data, journalism |
|
||||||
|
| `scene` | warm/watercolor | Narratives, emotional, lifestyle |
|
||||||
|
|
||||||
|
Use Core Styles for most cases. See full Style Gallery below for granular control.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Style Gallery
|
## Style Gallery
|
||||||
|
|
||||||
| Style | Description | Best For |
|
| Style | Description | Best For |
|
||||||
|-------|-------------|----------|
|
|-------|-------------|----------|
|
||||||
| `notion` (Default) | Minimalist hand-drawn line art | Knowledge sharing, SaaS, productivity |
|
| `vector-illustration` | Clean flat vector art with bold shapes | Knowledge articles, tutorials, tech content |
|
||||||
|
| `notion` | Minimalist hand-drawn line art | Knowledge sharing, SaaS, productivity |
|
||||||
| `elegant` | Refined, sophisticated | Business, thought leadership |
|
| `elegant` | Refined, sophisticated | Business, thought leadership |
|
||||||
| `warm` | Friendly, approachable | Personal growth, lifestyle, education |
|
| `warm` | Friendly, approachable | Personal growth, lifestyle, education |
|
||||||
| `minimal` | Ultra-clean, zen-like | Philosophy, minimalism, core concepts |
|
| `minimal` | Ultra-clean, zen-like | Philosophy, minimalism, core concepts |
|
||||||
@@ -12,19 +30,31 @@
|
|||||||
| `watercolor` | Soft artistic with natural warmth | Lifestyle, travel, creative |
|
| `watercolor` | Soft artistic with natural warmth | Lifestyle, travel, creative |
|
||||||
| `editorial` | Magazine-style infographic | Tech explainers, journalism |
|
| `editorial` | Magazine-style infographic | Tech explainers, journalism |
|
||||||
| `scientific` | Academic precise diagrams | Biology, chemistry, technical research |
|
| `scientific` | Academic precise diagrams | Biology, chemistry, technical research |
|
||||||
|
| `chalkboard` | Classroom chalk drawing style | Education, teaching, explanations |
|
||||||
|
| `fantasy-animation` | Ghibli/Disney-inspired hand-drawn | Storybook, magical, emotional |
|
||||||
|
| `flat` | Modern bold geometric shapes | Modern digital, contemporary |
|
||||||
|
| `flat-doodle` | Cute flat with bold outlines | Cute, friendly, approachable |
|
||||||
|
| `intuition-machine` | Technical briefing with aged paper | Technical briefings, academic |
|
||||||
|
| `nature` | Organic earthy illustration | Environmental, wellness |
|
||||||
|
| `pixel-art` | Retro 8-bit gaming aesthetic | Gaming, retro tech |
|
||||||
|
| `playful` | Whimsical pastel doodles | Fun, casual, educational |
|
||||||
|
| `retro` | 80s/90s neon geometric | 80s/90s nostalgic, bold |
|
||||||
|
| `sketch` | Raw pencil notebook style | Brainstorming, creative exploration |
|
||||||
|
| `sketch-notes` | Soft hand-drawn warm notes | Educational, warm notes |
|
||||||
|
| `vintage` | Aged parchment historical | Historical, heritage |
|
||||||
|
|
||||||
Full specifications: `references/styles/<style>.md`
|
Full specifications: `references/styles/<style>.md`
|
||||||
|
|
||||||
## Type × Style Compatibility Matrix
|
## Type × Style Compatibility Matrix
|
||||||
|
|
||||||
| | notion | warm | minimal | blueprint | watercolor | elegant | editorial | scientific |
|
| | vector-illustration | notion | warm | minimal | blueprint | watercolor | elegant | editorial | scientific |
|
||||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| infographic | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ |
|
| infographic | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ |
|
||||||
| scene | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ |
|
| scene | ✓ | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ |
|
||||||
| flowchart | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ |
|
| flowchart | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ |
|
||||||
| comparison | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
| comparison | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
||||||
| framework | ✓✓ | ✓ | ✓✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓✓ |
|
| framework | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓✓ |
|
||||||
| timeline | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ |
|
| timeline | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ |
|
||||||
|
|
||||||
✓✓ = highly recommended | ✓ = compatible | ✗ = not recommended
|
✓✓ = highly recommended | ✓ = compatible | ✗ = not recommended
|
||||||
|
|
||||||
@@ -32,30 +62,57 @@ Full specifications: `references/styles/<style>.md`
|
|||||||
|
|
||||||
| Type | Primary Style | Secondary Styles |
|
| Type | Primary Style | Secondary Styles |
|
||||||
|------|---------------|------------------|
|
|------|---------------|------------------|
|
||||||
| infographic | blueprint | notion, editorial, scientific |
|
| infographic | vector-illustration | notion, blueprint, editorial |
|
||||||
| scene | warm | watercolor, elegant |
|
| scene | warm | watercolor, elegant |
|
||||||
| flowchart | notion | blueprint, editorial |
|
| flowchart | vector-illustration | notion, blueprint |
|
||||||
| comparison | notion | elegant, editorial |
|
| comparison | vector-illustration | notion, elegant |
|
||||||
| framework | blueprint | notion, scientific |
|
| framework | blueprint | vector-illustration, notion |
|
||||||
| timeline | elegant | warm, editorial |
|
| timeline | elegant | warm, editorial |
|
||||||
|
|
||||||
## Auto Selection by Content Signals
|
## Auto Selection by Content Signals
|
||||||
|
|
||||||
| Content Signals | Recommended Type | Recommended Style |
|
| Content Signals | Recommended Type | Recommended Style |
|
||||||
|-----------------|------------------|-------------------|
|
|-----------------|------------------|-------------------|
|
||||||
| API, metrics, data, comparison, numbers | infographic | blueprint, notion |
|
| API, metrics, data, comparison, numbers | infographic | blueprint, vector-illustration |
|
||||||
|
| Knowledge, concept, tutorial, learning, guide | infographic | vector-illustration, notion |
|
||||||
|
| Tech, AI, programming, development, code | infographic | vector-illustration, blueprint |
|
||||||
|
| 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 |
|
||||||
| Story, emotion, journey, experience, personal | scene | warm, watercolor |
|
| Story, emotion, journey, experience, personal | scene | warm, watercolor |
|
||||||
| How-to, steps, workflow, process, tutorial | flowchart | notion, minimal |
|
|
||||||
| vs, pros/cons, before/after, alternatives | comparison | notion, elegant |
|
|
||||||
| Framework, model, architecture, principles | framework | blueprint, notion |
|
|
||||||
| History, timeline, progress, evolution | timeline | elegant, warm |
|
| History, timeline, progress, evolution | timeline | elegant, warm |
|
||||||
| Knowledge, concept, productivity, SaaS, tool | infographic | notion |
|
| Productivity, SaaS, tool, app, software | infographic | notion, vector-illustration |
|
||||||
| Business, professional, strategy, corporate | framework | elegant |
|
| Business, professional, strategy, corporate | framework | elegant |
|
||||||
| Biology, chemistry, medical, scientific | infographic | scientific |
|
| Biology, chemistry, medical, scientific | infographic | scientific |
|
||||||
| Explainer, journalism, magazine, investigation | infographic | editorial |
|
| Explainer, journalism, magazine, investigation | infographic | editorial |
|
||||||
|
|
||||||
## Style Characteristics by Type
|
## Style Characteristics by Type
|
||||||
|
|
||||||
|
### infographic + vector-illustration
|
||||||
|
- Clean flat vector shapes, bold geometric forms
|
||||||
|
- Vibrant but harmonious color palette
|
||||||
|
- Clear visual hierarchy with icons and labels
|
||||||
|
- Modern, professional, highly readable
|
||||||
|
- Perfect for knowledge articles and tutorials
|
||||||
|
|
||||||
|
### flowchart + vector-illustration
|
||||||
|
- Bold arrows and connectors
|
||||||
|
- Distinct step containers with icons
|
||||||
|
- Clean progression flow
|
||||||
|
- High contrast for readability
|
||||||
|
|
||||||
|
### comparison + vector-illustration
|
||||||
|
- Split layout with clear visual separation
|
||||||
|
- Bold iconography for each side
|
||||||
|
- Color-coded distinctions
|
||||||
|
- Easy at-a-glance comparison
|
||||||
|
|
||||||
|
### framework + vector-illustration
|
||||||
|
- Geometric node representations
|
||||||
|
- Clear hierarchical structure
|
||||||
|
- Bold connecting lines
|
||||||
|
- Modern system diagram aesthetic
|
||||||
|
|
||||||
### infographic + blueprint
|
### infographic + blueprint
|
||||||
- Technical precision, schematic lines
|
- Technical precision, schematic lines
|
||||||
- Grid-based layout, clear zones
|
- Grid-based layout, clear zones
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
# Detailed Workflow Procedures
|
||||||
|
|
||||||
|
## Step 1: Pre-check
|
||||||
|
|
||||||
|
### 1.0 Detect & Save Reference Images ⚠️ REQUIRED if images provided
|
||||||
|
|
||||||
|
Check if user provided reference images. Handle based on input type:
|
||||||
|
|
||||||
|
| Input Type | Action |
|
||||||
|
|------------|--------|
|
||||||
|
| Image file path provided | Copy to `references/` subdirectory → can use `--ref` |
|
||||||
|
| Image in conversation (no path) | **ASK user for file path** with AskUserQuestion |
|
||||||
|
| User can't provide path | Extract style/palette verbally → append to prompts (NO frontmatter references) |
|
||||||
|
|
||||||
|
**CRITICAL**: Only add `references` to prompt frontmatter if files are ACTUALLY SAVED to `references/` directory.
|
||||||
|
|
||||||
|
**If user provides file path**:
|
||||||
|
1. Copy to `references/NN-ref-{slug}.png`
|
||||||
|
2. Create description: `references/NN-ref-{slug}.md`
|
||||||
|
3. Verify files exist before proceeding
|
||||||
|
|
||||||
|
**If user can't provide path** (extracted verbally):
|
||||||
|
1. Analyze image visually, extract: colors, style, composition
|
||||||
|
2. Create `references/extracted-style.md` with extracted info
|
||||||
|
3. DO NOT add `references` to prompt frontmatter
|
||||||
|
4. Instead, append extracted style/colors directly to prompt text
|
||||||
|
|
||||||
|
**Description File Format** (only when file saved):
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
ref_id: NN
|
||||||
|
filename: NN-ref-{slug}.png
|
||||||
|
---
|
||||||
|
[User's description or auto-generated description]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verification** (only for saved files):
|
||||||
|
```
|
||||||
|
Reference Images Saved:
|
||||||
|
- 01-ref-{slug}.png ✓ (can use --ref)
|
||||||
|
- 02-ref-{slug}.png ✓ (can use --ref)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Or for extracted style**:
|
||||||
|
```
|
||||||
|
Reference Style Extracted (no file):
|
||||||
|
- Colors: #E8756D coral, #7ECFC0 mint...
|
||||||
|
- Style: minimal flat vector, clean lines...
|
||||||
|
→ Will append to prompt text (not --ref)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.1 Determine Input Type
|
||||||
|
|
||||||
|
| Input | Output Directory | Next |
|
||||||
|
|-------|------------------|------|
|
||||||
|
| File path | Ask user (1.2) | → 1.2 |
|
||||||
|
| Pasted content | `illustrations/{topic-slug}/` | → 1.4 |
|
||||||
|
|
||||||
|
**Backup rule for pasted content**: If `source.md` exists in target directory, rename to `source-backup-YYYYMMDD-HHMMSS.md` before saving.
|
||||||
|
|
||||||
|
### 1.2-1.4 Configuration (file path input only)
|
||||||
|
|
||||||
|
Check preferences and existing state, then ask ALL needed questions in ONE AskUserQuestion call (max 4 questions).
|
||||||
|
|
||||||
|
**Questions to include** (skip if preference exists or not applicable):
|
||||||
|
|
||||||
|
| Question | When to Ask | Options |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| Output directory | No `default_output_dir` in EXTEND.md | `{article-dir}/`, `{article-dir}/imgs/` (Recommended), `{article-dir}/illustrations/`, `illustrations/{topic-slug}/` |
|
||||||
|
| Existing images | Target dir has `.png/.jpg/.webp` files | `supplement`, `overwrite`, `regenerate` |
|
||||||
|
| Article update | Always (file path input) | `update`, `copy` |
|
||||||
|
|
||||||
|
**Preference Values** (if configured, skip asking):
|
||||||
|
|
||||||
|
| `default_output_dir` | Path |
|
||||||
|
|----------------------|------|
|
||||||
|
| `same-dir` | `{article-dir}/` |
|
||||||
|
| `imgs-subdir` | `{article-dir}/imgs/` |
|
||||||
|
| `illustrations-subdir` | `{article-dir}/illustrations/` |
|
||||||
|
| `independent` | `illustrations/{topic-slug}/` |
|
||||||
|
|
||||||
|
### 1.5 Load Preferences (EXTEND.md) ⛔ BLOCKING
|
||||||
|
|
||||||
|
**CRITICAL**: If EXTEND.md not found, MUST complete first-time setup before ANY other questions or steps. Do NOT proceed to reference images, do NOT ask about content, do NOT ask about type/style — ONLY complete the preferences setup first.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
|
||||||
|
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Result | Action |
|
||||||
|
|--------|--------|
|
||||||
|
| Found | Read, parse, display summary → Continue |
|
||||||
|
| Not found | ⛔ **BLOCKING**: Run first-time setup ONLY ([config/first-time-setup.md](config/first-time-setup.md)) → Complete and save EXTEND.md → Then continue |
|
||||||
|
|
||||||
|
**Supports**: Watermark | Preferred type/style | Custom styles | Language | Output directory
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Setup & Analyze
|
||||||
|
|
||||||
|
### 2.1 Analyze Content
|
||||||
|
|
||||||
|
| Analysis | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Content type | Technical / Tutorial / Methodology / Narrative |
|
||||||
|
| Illustration purpose | information / visualization / imagination |
|
||||||
|
| Core arguments | 2-5 main points to visualize |
|
||||||
|
| Visual opportunities | Positions where illustrations add value |
|
||||||
|
| Recommended type | Based on content signals and purpose |
|
||||||
|
| Recommended density | Based on length and complexity |
|
||||||
|
|
||||||
|
### 2.2 Extract Core Arguments
|
||||||
|
|
||||||
|
- Main thesis
|
||||||
|
- Key concepts reader needs
|
||||||
|
- Comparisons/contrasts
|
||||||
|
- Framework/model proposed
|
||||||
|
|
||||||
|
**CRITICAL**: If article uses metaphors (e.g., "电锯切西瓜"), do NOT illustrate literally. Visualize the **underlying concept**.
|
||||||
|
|
||||||
|
### 2.3 Identify Positions
|
||||||
|
|
||||||
|
**Illustrate**:
|
||||||
|
- Core arguments (REQUIRED)
|
||||||
|
- Abstract concepts
|
||||||
|
- Data comparisons
|
||||||
|
- Processes, workflows
|
||||||
|
|
||||||
|
**Do NOT Illustrate**:
|
||||||
|
- Metaphors literally
|
||||||
|
- Decorative scenes
|
||||||
|
- Generic illustrations
|
||||||
|
|
||||||
|
### 2.4 Analyze Reference Images (if provided in Step 1.0)
|
||||||
|
|
||||||
|
For each reference image:
|
||||||
|
|
||||||
|
| Analysis | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Visual characteristics | Style, colors, composition |
|
||||||
|
| Content/subject | What the reference depicts |
|
||||||
|
| Suitable positions | Which sections match this reference |
|
||||||
|
| Style match | Which illustration types/styles align |
|
||||||
|
| Usage recommendation | `direct` / `style` / `palette` |
|
||||||
|
|
||||||
|
| Usage | When to Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `direct` | Reference matches desired output closely |
|
||||||
|
| `style` | Extract visual style characteristics only |
|
||||||
|
| `palette` | Extract color scheme only |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Confirm Settings ⚠️
|
||||||
|
|
||||||
|
**Do NOT skip.** Use ONE AskUserQuestion call with max 4 questions. **Q1, Q2, Q3 are ALL REQUIRED.**
|
||||||
|
|
||||||
|
### Q1: Illustration Type ⚠️ REQUIRED
|
||||||
|
- [Recommended based on analysis] (Recommended)
|
||||||
|
- infographic / scene / flowchart / comparison / framework / timeline / mixed
|
||||||
|
|
||||||
|
### Q2: Density ⚠️ REQUIRED - DO NOT SKIP
|
||||||
|
- minimal (1-2) - Core concepts only
|
||||||
|
- balanced (3-5) - Major sections
|
||||||
|
- per-section - At least 1 per section/chapter (Recommended)
|
||||||
|
- rich (6+) - Comprehensive coverage
|
||||||
|
|
||||||
|
### Q3: Style ⚠️ REQUIRED (ALWAYS ask, even with preferred_style in EXTEND.md)
|
||||||
|
|
||||||
|
If EXTEND.md has `preferred_style`:
|
||||||
|
- [Custom style name + brief description] (Recommended)
|
||||||
|
- [Top compatible core style 1]
|
||||||
|
- [Top compatible core style 2]
|
||||||
|
- Other (see full Style Gallery)
|
||||||
|
|
||||||
|
If no `preferred_style` (present Core Styles first):
|
||||||
|
- [Best compatible core style] (Recommended)
|
||||||
|
- [Other compatible core style 1]
|
||||||
|
- [Other compatible core style 2]
|
||||||
|
- Other (see full Style Gallery)
|
||||||
|
|
||||||
|
**Core Styles** (simplified selection):
|
||||||
|
|
||||||
|
| Core Style | Best For |
|
||||||
|
|------------|----------|
|
||||||
|
| `minimal-flat` | General, knowledge sharing, SaaS |
|
||||||
|
| `sci-fi` | AI, frontier tech, system design |
|
||||||
|
| `hand-drawn` | Relaxed, reflective, casual |
|
||||||
|
| `editorial` | Processes, data, journalism |
|
||||||
|
| `scene` | Narratives, emotional, lifestyle |
|
||||||
|
|
||||||
|
Style selection based on Type × Style compatibility matrix (styles.md).
|
||||||
|
Full specs: `styles/<style>.md`
|
||||||
|
|
||||||
|
### Q4: Image Text Language ⚠️ REQUIRED when article language ≠ EXTEND.md `language`
|
||||||
|
|
||||||
|
Detect article language from content. If different from EXTEND.md `language` setting, MUST ask:
|
||||||
|
- Article language (match article content) (Recommended)
|
||||||
|
- EXTEND.md language (user's general preference)
|
||||||
|
|
||||||
|
**Skip only if**: Article language matches EXTEND.md `language`, or EXTEND.md has no `language` setting.
|
||||||
|
|
||||||
|
### Display Reference Usage (if references detected in Step 1.0)
|
||||||
|
|
||||||
|
When presenting outline preview to user, show reference assignments:
|
||||||
|
|
||||||
|
```
|
||||||
|
Reference Images:
|
||||||
|
| Ref | Filename | Recommended Usage |
|
||||||
|
|-----|----------|-------------------|
|
||||||
|
| 01 | 01-ref-diagram.png | direct → Illustration 1, 3 |
|
||||||
|
| 02 | 02-ref-chart.png | palette → Illustration 2 |
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Generate Outline
|
||||||
|
|
||||||
|
Save as `outline.md`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
type: infographic
|
||||||
|
density: balanced
|
||||||
|
style: blueprint
|
||||||
|
image_count: 4
|
||||||
|
references: # Only if references provided
|
||||||
|
- ref_id: 01
|
||||||
|
filename: 01-ref-diagram.png
|
||||||
|
description: "Technical diagram showing system architecture"
|
||||||
|
- ref_id: 02
|
||||||
|
filename: 02-ref-chart.png
|
||||||
|
description: "Color chart with brand palette"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Illustration 1
|
||||||
|
|
||||||
|
**Position**: [section] / [paragraph]
|
||||||
|
**Purpose**: [why this helps]
|
||||||
|
**Visual Content**: [what to show]
|
||||||
|
**Type Application**: [how type applies]
|
||||||
|
**References**: [01] # Optional: list ref_ids used
|
||||||
|
**Reference Usage**: direct # direct | style | palette
|
||||||
|
**Filename**: 01-infographic-concept-name.png
|
||||||
|
|
||||||
|
## Illustration 2
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
- Each position justified by content needs
|
||||||
|
- Type applied consistently
|
||||||
|
- Style reflected in descriptions
|
||||||
|
- Count matches density
|
||||||
|
- References assigned based on Step 2.4 analysis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Generate Images
|
||||||
|
|
||||||
|
### 5.1 Create Prompts
|
||||||
|
|
||||||
|
Follow [prompt-construction.md](prompt-construction.md). Save to `prompts/illustration-{slug}.md`.
|
||||||
|
- **Backup rule**: If prompt file exists, rename to `prompts/illustration-{slug}-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
|
**CRITICAL - References in Frontmatter**:
|
||||||
|
- Only add `references` field if files ACTUALLY EXIST in `references/` directory
|
||||||
|
- If style/palette was extracted verbally (no file), append info to prompt BODY instead
|
||||||
|
- Before writing frontmatter, verify: `test -f references/NN-ref-{slug}.png`
|
||||||
|
|
||||||
|
### 5.2 Select Generation Skill
|
||||||
|
|
||||||
|
Check available skills. If multiple, ask user.
|
||||||
|
|
||||||
|
### 5.3 Process References ⚠️ REQUIRED if references saved in Step 1.0
|
||||||
|
|
||||||
|
**DO NOT SKIP if user provided reference images.** For each illustration with references:
|
||||||
|
|
||||||
|
1. **VERIFY files exist first**:
|
||||||
|
```bash
|
||||||
|
test -f references/NN-ref-{slug}.png && echo "exists" || echo "MISSING"
|
||||||
|
```
|
||||||
|
- If file MISSING but in frontmatter → ERROR, fix frontmatter or remove references field
|
||||||
|
- If file exists → proceed with processing
|
||||||
|
|
||||||
|
2. Read prompt frontmatter for reference info
|
||||||
|
3. Process based on usage type:
|
||||||
|
|
||||||
|
| Usage | Action | Example |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| `direct` | Add reference path to `--ref` parameter | `--ref references/01-ref-brand.png` |
|
||||||
|
| `style` | Analyze reference, append style traits to prompt | "Style: clean lines, gradient backgrounds..." |
|
||||||
|
| `palette` | Extract colors from reference, append to prompt | "Colors: #E8756D coral, #7ECFC0 mint..." |
|
||||||
|
|
||||||
|
4. Check image generation skill capability:
|
||||||
|
|
||||||
|
| Skill Supports `--ref` | Action |
|
||||||
|
|------------------------|--------|
|
||||||
|
| Yes (e.g., baoyu-image-gen with Google) | Pass reference images via `--ref` |
|
||||||
|
| No | Convert to text description, append to prompt |
|
||||||
|
|
||||||
|
**Verification**: Before generating, confirm reference processing:
|
||||||
|
```
|
||||||
|
Reference Processing:
|
||||||
|
- Illustration 1: using 01-ref-brand.png (direct) ✓
|
||||||
|
- Illustration 2: extracted palette from 02-ref-style.png ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 Apply Watermark (if enabled)
|
||||||
|
|
||||||
|
Add: `Include a subtle watermark "[content]" at [position].`
|
||||||
|
|
||||||
|
### 5.5 Generate
|
||||||
|
|
||||||
|
1. For each illustration:
|
||||||
|
- **Backup rule**: If image file exists, rename to `NN-{type}-{slug}-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
- If references with `direct` usage: include `--ref` parameter
|
||||||
|
- Generate image
|
||||||
|
2. After each: "Generated X/N"
|
||||||
|
3. On failure: retry once, then log and continue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Finalize
|
||||||
|
|
||||||
|
### 6.1 Update Article
|
||||||
|
|
||||||
|
Insert after corresponding paragraph:
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
Alt text: concise description in article's language.
|
||||||
|
|
||||||
|
### 6.2 Output Summary
|
||||||
|
|
||||||
|
```
|
||||||
|
Article Illustration Complete!
|
||||||
|
|
||||||
|
Article: [path]
|
||||||
|
Type: [type] | Density: [level] | Style: [style]
|
||||||
|
Location: [directory]
|
||||||
|
Images: X/N generated
|
||||||
|
|
||||||
|
Positions:
|
||||||
|
- 01-xxx.png → After "[Section]"
|
||||||
|
- 02-yyy.png → After "[Section]"
|
||||||
|
|
||||||
|
[If failures]
|
||||||
|
Failed:
|
||||||
|
- NN-zzz.png: [reason]
|
||||||
|
```
|
||||||
@@ -153,7 +153,11 @@ Technical terms remain in English.
|
|||||||
|
|
||||||
```
|
```
|
||||||
Comic Progress:
|
Comic Progress:
|
||||||
- [ ] Step 1: Setup & Analyze (1.1 Preferences, 1.2 Analyze, 1.3 Check existing)
|
- [ ] Step 1: Setup & Analyze
|
||||||
|
- [ ] 1.1 Preferences (EXTEND.md) ⛔ BLOCKING
|
||||||
|
- [ ] Found → load preferences → continue
|
||||||
|
- [ ] Not found → run first-time setup → MUST complete before other steps
|
||||||
|
- [ ] 1.2 Analyze, 1.3 Check existing
|
||||||
- [ ] Step 2: Confirmation - Style & options ⚠️ REQUIRED
|
- [ ] Step 2: Confirmation - Style & options ⚠️ REQUIRED
|
||||||
- [ ] Step 3: Generate storyboard + characters
|
- [ ] Step 3: Generate storyboard + characters
|
||||||
- [ ] Step 4: Review outline (conditional)
|
- [ ] Step 4: Review outline (conditional)
|
||||||
@@ -169,14 +173,22 @@ Comic Progress:
|
|||||||
### Flow
|
### Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
Input → Preferences → Analyze → [Check Existing?] → [Confirm: Style + Reviews] → Storyboard → [Review?] → Prompts → [Review?] → Images → PDF → Complete
|
Input → [Preferences] ─┬─ Found → Continue
|
||||||
|
│
|
||||||
|
└─ Not found → First-Time Setup ⛔ BLOCKING
|
||||||
|
│
|
||||||
|
└─ Complete setup → Save EXTEND.md → Continue
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
Analyze → [Check Existing?] → [Confirm: Style + Reviews] → Storyboard → [Review?] → Prompts → [Review?] → Images → PDF → Complete
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step Summary
|
### Step Summary
|
||||||
|
|
||||||
| Step | Action | Key Output |
|
| Step | Action | Key Output |
|
||||||
|------|--------|------------|
|
|------|--------|------------|
|
||||||
| 1.1 | Load EXTEND.md preferences | Config loaded |
|
| 1.1 | Load EXTEND.md preferences ⛔ BLOCKING if not found | Config loaded |
|
||||||
| 1.2 | Analyze content | `analysis.md` |
|
| 1.2 | Analyze content | `analysis.md` |
|
||||||
| 1.3 | Check existing directory | Handle conflicts |
|
| 1.3 | Check existing directory | Handle conflicts |
|
||||||
| 2 | Confirm style, focus, audience, reviews | User preferences |
|
| 2 | Confirm style, focus, audience, reviews | User preferences |
|
||||||
@@ -194,6 +206,7 @@ Input → Preferences → Analyze → [Check Existing?] → [Confirm: Style + Re
|
|||||||
**Character reference is MANDATORY for visual consistency.**
|
**Character reference is MANDATORY for visual consistency.**
|
||||||
|
|
||||||
**7.1 Generate character sheet first**:
|
**7.1 Generate character sheet first**:
|
||||||
|
- **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
||||||
```bash
|
```bash
|
||||||
# Use Reference Sheet Prompt from characters/characters.md
|
# Use Reference Sheet Prompt from characters/characters.md
|
||||||
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
||||||
@@ -214,6 +227,10 @@ Compress to reduce token usage when used as reference image:
|
|||||||
| Supports `--ref` | Pass `characters/characters.png` with EVERY page |
|
| Supports `--ref` | Pass `characters/characters.png` with EVERY page |
|
||||||
| No `--ref` support | Prepend character descriptions to EVERY prompt file |
|
| No `--ref` support | Prepend character descriptions to EVERY prompt file |
|
||||||
|
|
||||||
|
**Backup rules for page generation**:
|
||||||
|
- If prompt file exists: rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
- If image file exists: rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example: ALWAYS include --ref for consistency
|
# Example: ALWAYS include --ref for consistency
|
||||||
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
||||||
@@ -224,13 +241,20 @@ npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
|||||||
|
|
||||||
**Full workflow details**: [references/workflow.md](references/workflow.md)
|
**Full workflow details**: [references/workflow.md](references/workflow.md)
|
||||||
|
|
||||||
### EXTEND.md Paths
|
### EXTEND.md Paths ⛔ BLOCKING
|
||||||
|
|
||||||
|
**CRITICAL**: 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 art style, do NOT ask about tone — ONLY complete the preferences setup first.
|
||||||
|
|
||||||
| Path | Location |
|
| Path | Location |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `.baoyu-skills/baoyu-comic/EXTEND.md` | Project directory |
|
| `.baoyu-skills/baoyu-comic/EXTEND.md` | Project directory |
|
||||||
| `$HOME/.baoyu-skills/baoyu-comic/EXTEND.md` | User home |
|
| `$HOME/.baoyu-skills/baoyu-comic/EXTEND.md` | User home |
|
||||||
|
|
||||||
|
| Result | Action |
|
||||||
|
|--------|--------|
|
||||||
|
| Found | Read, parse, display summary → Continue |
|
||||||
|
| Not found | ⛔ **BLOCKING**: Run first-time setup ONLY ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → Complete and save EXTEND.md → Then continue |
|
||||||
|
|
||||||
**EXTEND.md Supports**: Watermark | Preferred art/tone/layout | Custom style definitions | Character presets | Language preference
|
**EXTEND.md Supports**: Watermark | Preferred art/tone/layout | Custom style definitions | Character presets | Language preference
|
||||||
|
|
||||||
Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md)
|
Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md)
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ description: First-time setup flow for baoyu-comic preferences
|
|||||||
|
|
||||||
When no EXTEND.md is found, guide user through preference setup.
|
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/source material
|
||||||
|
- Ask about art style or tone
|
||||||
|
- Ask about layout preferences
|
||||||
|
- Proceed to content analysis
|
||||||
|
|
||||||
|
ONLY ask the questions in this setup flow, save EXTEND.md, then continue.
|
||||||
|
|
||||||
## Setup Flow
|
## Setup Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ Read source content, save it if needed, and perform deep analysis.
|
|||||||
1. **Save source content** (if not already a file):
|
1. **Save source content** (if not already a file):
|
||||||
- If user provides a file path: use as-is
|
- If user provides a file path: use as-is
|
||||||
- If user pastes content: save to `source.md` in target directory
|
- 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
|
2. Read source content
|
||||||
3. **Deep analysis** following `analysis-framework.md`:
|
3. **Deep analysis** following `analysis-framework.md`:
|
||||||
- Target audience identification
|
- Target audience identification
|
||||||
@@ -329,6 +330,7 @@ Create image generation prompts for all pages.
|
|||||||
1. Create prompt following art style + tone guidelines
|
1. Create prompt following art style + tone guidelines
|
||||||
2. Include character visual descriptions for consistency
|
2. Include character visual descriptions for consistency
|
||||||
3. Save to `prompts/NN-{cover|page}-[slug].md`
|
3. Save to `prompts/NN-{cover|page}-[slug].md`
|
||||||
|
- **Backup rule**: If prompt file exists, rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
**Prompt File Format**:
|
**Prompt File Format**:
|
||||||
```markdown
|
```markdown
|
||||||
@@ -405,8 +407,9 @@ With confirmed prompts from Step 5/6:
|
|||||||
### 7.1 Generate Character Reference Sheet (first)
|
### 7.1 Generate Character Reference Sheet (first)
|
||||||
|
|
||||||
1. Use Reference Sheet Prompt from `characters/characters.md`
|
1. Use Reference Sheet Prompt from `characters/characters.md`
|
||||||
2. Generate → `characters/characters.png`
|
2. **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
||||||
3. This ensures visual consistency for all subsequent pages
|
3. Generate → `characters/characters.png`
|
||||||
|
4. This ensures visual consistency for all subsequent pages
|
||||||
|
|
||||||
### 7.2 Generate Comic Pages
|
### 7.2 Generate Comic Pages
|
||||||
|
|
||||||
@@ -453,9 +456,10 @@ When skill does NOT support reference images, create combined prompt files:
|
|||||||
|
|
||||||
**For each page (cover + pages)**:
|
**For each page (cover + pages)**:
|
||||||
1. Read prompt from `prompts/NN-{cover|page}-[slug].md`
|
1. Read prompt from `prompts/NN-{cover|page}-[slug].md`
|
||||||
2. Generate image using Strategy A or B (based on skill capability)
|
2. **Backup rule**: If image file exists, rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||||
3. Save to `NN-{cover|page}-[slug].png`
|
3. Generate image using Strategy A or B (based on skill capability)
|
||||||
4. Report progress after each generation: "Generated X/N: [page title]"
|
4. Save to `NN-{cover|page}-[slug].png`
|
||||||
|
5. Report progress after each generation: "Generated X/N: [page title]"
|
||||||
|
|
||||||
**Session Management**:
|
**Session Management**:
|
||||||
If image generation skill supports `--sessionId`:
|
If image generation skill supports `--sessionId`:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-cover-image
|
name: baoyu-cover-image
|
||||||
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 9 color palettes and 6 rendering styles. Supports cinematic (2.35:1), widescreen (16:9), and square (1:1) aspects. Use when user asks to "generate cover image", "create article cover", "make cover", or mentions "封面图".
|
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 9 color palettes and 6 rendering styles. Supports cinematic (2.35:1), widescreen (16:9), and square (1:1) aspects. Use when user asks to "generate cover image", "create article cover", or "make cover".
|
||||||
---
|
---
|
||||||
|
|
||||||
# Cover Image Generator
|
# Cover Image Generator
|
||||||
@@ -34,6 +34,10 @@ Generate elegant cover images for articles with 5-dimensional customization.
|
|||||||
# Direct input with options
|
# Direct input with options
|
||||||
/baoyu-cover-image --palette mono --rendering digital --aspect 1:1 --quick
|
/baoyu-cover-image --palette mono --rendering digital --aspect 1:1 --quick
|
||||||
[paste content]
|
[paste content]
|
||||||
|
|
||||||
|
# With reference images
|
||||||
|
/baoyu-cover-image article.md --ref style-ref.png
|
||||||
|
/baoyu-cover-image article.md --ref ref1.png ref2.png --quick
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
@@ -46,10 +50,12 @@ Generate elegant cover images for articles with 5-dimensional customization.
|
|||||||
| `--style <name>` | Preset shorthand (expands to palette + rendering, see [Style Presets](references/style-presets.md)) |
|
| `--style <name>` | Preset shorthand (expands to palette + rendering, see [Style Presets](references/style-presets.md)) |
|
||||||
| `--text <level>` | Text density: none, title-only, title-subtitle, text-rich |
|
| `--text <level>` | Text density: none, title-only, title-subtitle, text-rich |
|
||||||
| `--mood <level>` | Emotional intensity: subtle, balanced, bold |
|
| `--mood <level>` | Emotional intensity: subtle, balanced, bold |
|
||||||
|
| `--font <name>` | Font style: clean, handwritten, serif, display |
|
||||||
| `--aspect <ratio>` | 16:9 (default), 2.35:1, 4:3, 3:2, 1:1, 3:4 |
|
| `--aspect <ratio>` | 16:9 (default), 2.35:1, 4:3, 3:2, 1:1, 3:4 |
|
||||||
| `--lang <code>` | Title language (en, zh, ja, etc.) |
|
| `--lang <code>` | Title language (en, zh, ja, etc.) |
|
||||||
| `--no-title` | Alias for `--text none` |
|
| `--no-title` | Alias for `--text none` |
|
||||||
| `--quick` | Skip confirmation, use auto-selection for missing dimensions |
|
| `--quick` | Skip confirmation, use auto-selection for missing dimensions |
|
||||||
|
| `--ref <files...>` | Reference images for style/composition guidance |
|
||||||
|
|
||||||
## Five Dimensions
|
## Five Dimensions
|
||||||
|
|
||||||
@@ -60,6 +66,7 @@ Generate elegant cover images for articles with 5-dimensional customization.
|
|||||||
| **Rendering** | Line quality, texture, depth, element style | flat-vector, hand-drawn, painterly, digital, pixel, chalk | auto |
|
| **Rendering** | Line quality, texture, depth, element style | flat-vector, hand-drawn, painterly, digital, pixel, chalk | auto |
|
||||||
| **Text** | Text density, information hierarchy | none, title-only, title-subtitle, text-rich | title-only |
|
| **Text** | Text density, information hierarchy | none, title-only, title-subtitle, text-rich | title-only |
|
||||||
| **Mood** | Emotional intensity, visual weight | subtle, balanced, bold | balanced |
|
| **Mood** | Emotional intensity, visual weight | subtle, balanced, bold | balanced |
|
||||||
|
| **Font** | Typography style, character feel | clean, handwritten, serif, display | clean |
|
||||||
|
|
||||||
Dimensions can be freely combined. Auto-selection rules: [references/auto-selection.md](references/auto-selection.md)
|
Dimensions can be freely combined. Auto-selection rules: [references/auto-selection.md](references/auto-selection.md)
|
||||||
|
|
||||||
@@ -110,8 +117,8 @@ Rendering definitions: [references/renderings/](references/renderings/)
|
|||||||
| Text Level | Title | Subtitle | Tags | Use Case |
|
| Text Level | Title | Subtitle | Tags | Use Case |
|
||||||
|------------|:-----:|:--------:|:----:|----------|
|
|------------|:-----:|:--------:|:----:|----------|
|
||||||
| `none` | - | - | - | Pure visual, no text |
|
| `none` | - | - | - | Pure visual, no text |
|
||||||
| `title-only` | ✓ (≤8 字) | - | - | Simple headline (default) |
|
| `title-only` | ✓ | - | - | Simple headline (default) |
|
||||||
| `title-subtitle` | ✓ | ✓ (≤15 字) | - | Title + supporting context |
|
| `title-subtitle` | ✓ | ✓ | - | Title + supporting context |
|
||||||
| `text-rich` | ✓ | ✓ | ✓ (2-4) | Information-dense |
|
| `text-rich` | ✓ | ✓ | ✓ (2-4) | Information-dense |
|
||||||
|
|
||||||
| Mood | Contrast | Saturation | Weight | Use Case |
|
| Mood | Contrast | Saturation | Weight | Use Case |
|
||||||
@@ -122,6 +129,17 @@ Rendering definitions: [references/renderings/](references/renderings/)
|
|||||||
|
|
||||||
Full guides: [references/dimensions/text.md](references/dimensions/text.md) | [references/dimensions/mood.md](references/dimensions/mood.md)
|
Full guides: [references/dimensions/text.md](references/dimensions/text.md) | [references/dimensions/mood.md](references/dimensions/mood.md)
|
||||||
|
|
||||||
|
## Font Gallery
|
||||||
|
|
||||||
|
| Font | Description | Best For |
|
||||||
|
|------|-------------|----------|
|
||||||
|
| `clean` | Modern geometric sans-serif | Tech, professional, minimal |
|
||||||
|
| `handwritten` | Warm hand-lettered style | Personal, friendly, lifestyle |
|
||||||
|
| `serif` | Classic elegant typography | Editorial, academic, luxury |
|
||||||
|
| `display` | Bold decorative headlines | Announcements, entertainment |
|
||||||
|
|
||||||
|
Full guide: [references/dimensions/font.md](references/dimensions/font.md)
|
||||||
|
|
||||||
## Style Presets & Compatibility
|
## Style Presets & Compatibility
|
||||||
|
|
||||||
- **Style Presets**: `--style X` expands to palette + rendering. See [references/style-presets.md](references/style-presets.md)
|
- **Style Presets**: `--style X` expands to palette + rendering. See [references/style-presets.md](references/style-presets.md)
|
||||||
@@ -142,6 +160,12 @@ Output directory depends on `default_output_dir` preference:
|
|||||||
```
|
```
|
||||||
<output-dir>/
|
<output-dir>/
|
||||||
├── source-{slug}.{ext} # Source files (text, images, etc.)
|
├── source-{slug}.{ext} # Source files (text, images, etc.)
|
||||||
|
├── refs/ # Reference images (if provided)
|
||||||
|
│ ├── ref-01-{slug}.{ext}
|
||||||
|
│ ├── ref-01-{slug}.md # Description file (optional)
|
||||||
|
│ ├── ref-02-{slug}.{ext}
|
||||||
|
│ ├── ref-02-{slug}.md # Description file (optional)
|
||||||
|
│ └── extracted-style.md # Verbally extracted style (if no file path)
|
||||||
├── prompts/cover.md # Generation prompt
|
├── prompts/cover.md # Generation prompt
|
||||||
└── cover.png # Output image
|
└── cover.png # Output image
|
||||||
```
|
```
|
||||||
@@ -156,25 +180,45 @@ Output directory depends on `default_output_dir` preference:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Cover Image Progress:
|
Cover Image Progress:
|
||||||
- [ ] Step 0: Check preferences (EXTEND.md) ⚠️ REQUIRED if not found
|
- [ ] Step 0: Check preferences (EXTEND.md) ⛔ BLOCKING
|
||||||
- [ ] Step 1: Analyze content + determine output directory ⚠️ MUST ask if not configured
|
- [ ] Found → load preferences → continue
|
||||||
- [ ] Step 2: Confirm options (5 dimensions) ⚠️ REQUIRED unless --quick or all specified
|
- [ ] Not found → run first-time setup → MUST complete before Step 1
|
||||||
|
- [ ] Step 1: Analyze content + determine output directory
|
||||||
|
- [ ] 1.1 Reference images ⚠️ (if provided)
|
||||||
|
- [ ] File path given → saved to refs/ ✓
|
||||||
|
- [ ] No path → asked user OR extracted verbally
|
||||||
|
- [ ] 1.2 Output directory determined
|
||||||
|
- [ ] Step 2: Confirm options (6 dimensions) ⚠️ REQUIRED unless --quick or all specified
|
||||||
- [ ] Step 3: Create prompt
|
- [ ] Step 3: Create prompt
|
||||||
|
- [ ] References in prompt ONLY if files exist in refs/
|
||||||
|
- [ ] Extracted style/palette appended to prompt body (if no file)
|
||||||
- [ ] Step 4: Generate image
|
- [ ] Step 4: Generate image
|
||||||
|
- [ ] 4.1 References verified before generation
|
||||||
|
- [ ] 4.2 Pass refs via --ref if skill supports AND files exist
|
||||||
- [ ] Step 5: Completion report
|
- [ ] Step 5: Completion report
|
||||||
```
|
```
|
||||||
|
|
||||||
### Flow
|
### Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
Input → [Step 0: Preferences/Setup] → Analyze → [Output Dir ⚠️] → [Confirm: 5 Dimensions] → Prompt → Generate → Complete
|
Input → [Step 0: Preferences] ─┬─ Found → Continue
|
||||||
↓
|
│
|
||||||
(skip if --quick or all specified)
|
└─ Not found → First-Time Setup ⛔ BLOCKING
|
||||||
|
│
|
||||||
|
└─ Complete setup → Save EXTEND.md → Continue
|
||||||
|
│
|
||||||
|
┌───────────────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
Analyze + Save Refs → [Output Dir ⚠️] → [Confirm: 6 Dimensions] → Prompt → Generate → Complete
|
||||||
|
↓
|
||||||
|
(skip if --quick or all specified)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 0: Load Preferences (EXTEND.md) ⚠️
|
### Step 0: Load Preferences (EXTEND.md) ⛔ BLOCKING
|
||||||
|
|
||||||
**Purpose**: Load user preferences or run first-time setup. **Do NOT skip setup if EXTEND.md not found.**
|
**Purpose**: Load user preferences or run first-time setup.
|
||||||
|
|
||||||
|
**CRITICAL**: 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 reference images, do NOT ask about dimensions — ONLY complete the preferences setup first.
|
||||||
|
|
||||||
Use Bash to check EXTEND.md existence (priority order):
|
Use Bash to check EXTEND.md existence (priority order):
|
||||||
|
|
||||||
@@ -189,7 +233,7 @@ test -f "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md" && echo "user"
|
|||||||
| Result | Action |
|
| Result | Action |
|
||||||
|--------|--------|
|
|--------|--------|
|
||||||
| Found | Read, parse, display preferences summary → Continue to Step 1 |
|
| Found | Read, parse, display preferences summary → Continue to Step 1 |
|
||||||
| Not found | ⚠️ MUST run first-time setup ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → Then continue to Step 1 |
|
| Not found | ⛔ **BLOCKING**: Run first-time setup ONLY ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → Complete and save EXTEND.md → Then continue to Step 1 |
|
||||||
|
|
||||||
**Preferences Summary** (when found):
|
**Preferences Summary** (when found):
|
||||||
|
|
||||||
@@ -202,38 +246,208 @@ Preferences loaded from [project/user]:
|
|||||||
• Quick mode: [enabled/disabled] | Language: [value or "auto"]
|
• Quick mode: [enabled/disabled] | Language: [value or "auto"]
|
||||||
```
|
```
|
||||||
|
|
||||||
**EXTEND.md Supports**: Watermark | Preferred type | Preferred palette | Preferred rendering | Preferred text | Preferred mood | Default aspect ratio | Default output directory | Quick mode | Custom palette definitions | Language preference
|
**EXTEND.md Supports**: Watermark | Preferred type | Preferred palette | Preferred rendering | Preferred text | Preferred mood | Preferred font | Default aspect ratio | Default output directory | Quick mode | Custom palette definitions | Language preference
|
||||||
|
|
||||||
Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md)
|
Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md)
|
||||||
|
|
||||||
### Step 1: Analyze Content
|
### Step 1: Analyze Content
|
||||||
|
|
||||||
1. **Save source content** (if pasted, save to `source.md` in target directory; if file path, use as-is)
|
**1.0 Detect & Save Reference Images** ⚠️ REQUIRED if images provided
|
||||||
2. **Content analysis**: Extract topic, core message, tone, keywords; identify visual metaphors; detect content type
|
|
||||||
3. **Language detection**: Detect source language, note user's input language, compare with EXTEND.md preference
|
Check if user provided reference images. Handle based on input type:
|
||||||
4. **Determine output directory** per File Structure rules. If no `default_output_dir` preference + file path input, include in Step 2 Q4
|
|
||||||
|
| Input Type | Action |
|
||||||
|
|------------|--------|
|
||||||
|
| Image file path provided | Copy to `refs/` subdirectory → can use `--ref` |
|
||||||
|
| Image in conversation (no path) | **ASK user for file path** with AskUserQuestion |
|
||||||
|
| User can't provide path | Extract style/palette verbally → append to prompt (NO frontmatter references) |
|
||||||
|
|
||||||
|
**CRITICAL**: Only add `references` to prompt frontmatter if files are ACTUALLY SAVED to `refs/` directory.
|
||||||
|
|
||||||
|
**If user provides file path**:
|
||||||
|
1. Copy to `refs/ref-NN-{slug}.{ext}` (NN = 01, 02, ...)
|
||||||
|
2. Create description: `refs/ref-NN-{slug}.md`
|
||||||
|
3. Verify files exist before proceeding
|
||||||
|
|
||||||
|
**If user can't provide path** (extracted verbally):
|
||||||
|
1. Analyze image visually, extract: colors, style, composition
|
||||||
|
2. Create `refs/extracted-style.md` with extracted info
|
||||||
|
3. DO NOT add `references` to prompt frontmatter
|
||||||
|
4. Instead, append extracted style/colors directly to prompt text
|
||||||
|
|
||||||
|
**Description File Format** (only when file saved):
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
ref_id: NN
|
||||||
|
filename: ref-NN-{slug}.{ext}
|
||||||
|
usage: direct | style | palette
|
||||||
|
---
|
||||||
|
[User's description or auto-generated description]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Usage | When to Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `direct` | Reference matches desired output closely |
|
||||||
|
| `style` | Extract visual style characteristics only |
|
||||||
|
| `palette` | Extract color scheme only |
|
||||||
|
|
||||||
|
**Verification** (only for saved files):
|
||||||
|
```
|
||||||
|
Reference Images Saved:
|
||||||
|
- ref-01-{slug}.png ✓ (can use --ref)
|
||||||
|
- ref-02-{slug}.png ✓ (can use --ref)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Or for extracted style**:
|
||||||
|
```
|
||||||
|
Reference Style Extracted (no file):
|
||||||
|
- Colors: #E8756D coral, #7ECFC0 mint...
|
||||||
|
- Style: minimal flat vector, clean lines...
|
||||||
|
→ Will append to prompt text (not --ref)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**1.1 Save Source Content**
|
||||||
|
- If pasted, save to `source.md` in target directory; if file path, use as-is
|
||||||
|
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
|
**1.2 Content Analysis**
|
||||||
|
- Extract topic, core message, tone, keywords
|
||||||
|
- Identify visual metaphors
|
||||||
|
- Detect content type
|
||||||
|
|
||||||
|
**1.3 Reference Image Deep Analysis** ⚠️ CRITICAL (if provided in Step 1.0)
|
||||||
|
|
||||||
|
References are high-priority inputs. Perform deep visual analysis to extract **specific, concrete, reproducible** elements — not vague summaries.
|
||||||
|
|
||||||
|
For each reference image, extract ALL of the following:
|
||||||
|
|
||||||
|
| Analysis | Description | Example (good vs bad) |
|
||||||
|
|----------|-------------|----------------------|
|
||||||
|
| **Brand elements** | Logos, wordmarks, specific typography treatments | Good: "Logo uses vertical parallel lines for 'm'" / Bad: "Has a logo" |
|
||||||
|
| **Signature patterns** | Unique decorative motifs, textures, geometric patterns | Good: "Woven intersecting curves forming diamond grid" / Bad: "Has patterns" |
|
||||||
|
| **Color palette** | Exact hex values or close approximations for key colors | Good: "#2D4A3E dark teal, #F5F0E0 cream" / Bad: "Dark and light colors" |
|
||||||
|
| **Layout structure** | Specific spatial arrangement, banner placement, zone splits | Good: "Bottom 30% dark banner with branding" / Bad: "Has a banner" |
|
||||||
|
| **Typography** | Font style, weight, spacing, case, positioning | Good: "Uppercase, wide letter-spacing, handwritten ep number" / Bad: "Has text" |
|
||||||
|
| **Content/subject** | What the reference depicts | Factual description |
|
||||||
|
| **Usage recommendation** | `direct` / `style` / `palette` | Based on analysis |
|
||||||
|
|
||||||
|
**Output format**: List each element as a bullet point that can be directly copy-pasted into a prompt as a mandatory instruction.
|
||||||
|
|
||||||
|
**1.4 Language Detection**
|
||||||
|
- Detect source language
|
||||||
|
- Note user's input language
|
||||||
|
- Compare with EXTEND.md preference
|
||||||
|
|
||||||
|
**1.5 Determine Output Directory**
|
||||||
|
- Per File Structure rules
|
||||||
|
- If no `default_output_dir` preference + file path input, include in Step 2 Q4
|
||||||
|
|
||||||
### Step 2: Confirm Options ⚠️
|
### Step 2: Confirm Options ⚠️
|
||||||
|
|
||||||
Validate all 5 dimensions + aspect ratio. Full confirmation flow: [references/workflow/confirm-options.md](references/workflow/confirm-options.md)
|
Validate all 6 dimensions + aspect ratio. Full confirmation flow: [references/workflow/confirm-options.md](references/workflow/confirm-options.md)
|
||||||
|
|
||||||
**Skip Conditions**:
|
**Skip Conditions**:
|
||||||
|
|
||||||
| Condition | Skipped | Still Asked |
|
| Condition | Skipped | Still Asked |
|
||||||
|-----------|---------|-------------|
|
|-----------|---------|-------------|
|
||||||
| `--quick` or `quick_mode: true` | 5 dimensions | Aspect ratio (unless `--aspect`) |
|
| `--quick` or `quick_mode: true` | 6 dimensions | Aspect ratio (unless `--aspect`) |
|
||||||
| All 5 + `--aspect` specified | All | None |
|
| All 6 + `--aspect` specified | All | None |
|
||||||
|
|
||||||
### Step 3: Create Prompt
|
### Step 3: Create Prompt
|
||||||
|
|
||||||
|
**Backup rule**: If `prompts/cover.md` exists, rename to `prompts/cover-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
Save to `prompts/cover.md`. Full template: [references/workflow/prompt-template.md](references/workflow/prompt-template.md)
|
Save to `prompts/cover.md`. Full template: [references/workflow/prompt-template.md](references/workflow/prompt-template.md)
|
||||||
|
|
||||||
|
**CRITICAL - References in YAML Frontmatter**:
|
||||||
|
|
||||||
|
When reference files are saved to `refs/`, **MUST add `references` field in frontmatter**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
type: cover
|
||||||
|
palette: warm
|
||||||
|
rendering: flat-vector
|
||||||
|
references:
|
||||||
|
- ref_id: 01
|
||||||
|
filename: refs/ref-01-podcast-thumbnail.jpg
|
||||||
|
usage: style
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
| Rule | Action |
|
||||||
|
|------|--------|
|
||||||
|
| Files saved to `refs/` | Add to frontmatter `references` list |
|
||||||
|
| Style extracted verbally (no file) | Omit `references` field, describe in body |
|
||||||
|
| Before writing | Verify: `test -f refs/ref-NN-{slug}.{ext}` |
|
||||||
|
|
||||||
|
**Reference Embedding**:
|
||||||
|
|
||||||
|
| Situation | Frontmatter | Body |
|
||||||
|
|-----------|-------------|------|
|
||||||
|
| Reference file saved to `refs/` | Add to `references` ✓ | **Detailed mandatory style instructions** (see below) |
|
||||||
|
| Style extracted verbally (no file) | Omit `references` | Full style description |
|
||||||
|
| File in frontmatter but doesn't exist | ERROR - fix or remove | — |
|
||||||
|
|
||||||
|
⚠️ **"Brief style note" is NOT sufficient.** When references are provided, the prompt body MUST contain a dedicated `# Reference Style — MUST INCORPORATE` section with:
|
||||||
|
1. Per-reference breakdown of **specific visual elements** extracted in Step 1.3
|
||||||
|
2. Each element prefixed with "MUST" or "REQUIRED"
|
||||||
|
3. An **integration approach** describing exactly how reference elements should be composed into the cover layout (e.g., "Use a split layout: illustration area 65% + dark branded banner 35%")
|
||||||
|
|
||||||
### Step 4: Generate Image
|
### Step 4: Generate Image
|
||||||
|
|
||||||
1. Backup existing `cover.png` → `cover-backup-YYYYMMDD-HHMMSS.png` (if regenerating)
|
**4.1 Backup existing** `cover.png` → `cover-backup-YYYYMMDD-HHMMSS.png` (if regenerating)
|
||||||
2. Check available image generation skills; if multiple, ask user preference
|
|
||||||
3. Call selected skill with prompt file path, output path (`cover.png`), aspect ratio
|
**4.2 Check available image generation skills**; if multiple, ask user preference
|
||||||
4. On failure: auto-retry once before reporting error
|
|
||||||
|
**4.3 Process References** ⚠️ REQUIRED if references in frontmatter
|
||||||
|
|
||||||
|
**Read `references` from prompt frontmatter** and process each entry:
|
||||||
|
|
||||||
|
1. **Parse frontmatter** to get references list:
|
||||||
|
```yaml
|
||||||
|
references:
|
||||||
|
- ref_id: 01
|
||||||
|
filename: refs/ref-01-podcast-thumbnail.jpg
|
||||||
|
usage: style
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **VERIFY each file exists**:
|
||||||
|
```bash
|
||||||
|
test -f refs/ref-NN-{slug}.{ext} && echo "exists" || echo "MISSING"
|
||||||
|
```
|
||||||
|
- If file MISSING → ERROR, fix prompt or remove from references
|
||||||
|
- If file exists → proceed with processing
|
||||||
|
|
||||||
|
3. Process based on `usage` type:
|
||||||
|
|
||||||
|
| Usage | Action | Example |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| `direct` | Add reference path to `--ref` parameter | `--ref refs/ref-01-brand.png` |
|
||||||
|
| `style` | Analyze reference, append style traits to prompt | "Style: clean lines, gradient backgrounds..." |
|
||||||
|
| `palette` | Extract colors from reference, append to prompt | "Colors: #E8756D coral, #7ECFC0 mint..." |
|
||||||
|
|
||||||
|
3. Check image generation skill capability:
|
||||||
|
|
||||||
|
| Skill Supports `--ref` | Action |
|
||||||
|
|------------------------|--------|
|
||||||
|
| Yes (e.g., baoyu-image-gen with Google/OpenAI) | Pass reference images via `--ref` |
|
||||||
|
| No | If usage is `direct`, switch to a ref-capable backend (baoyu-image-gen + Google multimodal or OpenAI GPT Image edits). If unavailable, convert to text description and remove direct refs |
|
||||||
|
|
||||||
|
**Verification**: Before generating, confirm reference processing:
|
||||||
|
```
|
||||||
|
Reference Processing:
|
||||||
|
- ref-01-brand.png: using as direct reference ✓
|
||||||
|
- ref-02-style.png: extracted palette ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
**4.4 Generate**
|
||||||
|
|
||||||
|
1. Call selected skill with prompt file path, output path (`cover.png`), aspect ratio
|
||||||
|
2. If references with `direct` usage: use ref-capable backend and include `--ref` (recommend `baoyu-image-gen --provider google --model gemini-3-pro-image-preview` or `--provider openai --model gpt-image-1.5`)
|
||||||
|
3. On failure: auto-retry once before reporting error
|
||||||
|
|
||||||
### Step 5: Completion Report
|
### Step 5: Completion Report
|
||||||
|
|
||||||
@@ -242,13 +456,17 @@ Cover Generated!
|
|||||||
|
|
||||||
Topic: [topic]
|
Topic: [topic]
|
||||||
Type: [type] | Palette: [palette] | Rendering: [rendering]
|
Type: [type] | Palette: [palette] | Rendering: [rendering]
|
||||||
Text: [text] | Mood: [mood] | Aspect: [ratio]
|
Text: [text] | Mood: [mood] | Font: [font] | Aspect: [ratio]
|
||||||
Title: [title text or "visual only"]
|
Title: [title text or "visual only"]
|
||||||
Language: [lang] | Watermark: [enabled/disabled]
|
Language: [lang] | Watermark: [enabled/disabled]
|
||||||
|
References: [N images (direct/style/palette) or "extracted style" or "none"]
|
||||||
Location: [directory path]
|
Location: [directory path]
|
||||||
|
|
||||||
Files:
|
Files:
|
||||||
✓ source-{slug}.{ext}
|
✓ source-{slug}.{ext}
|
||||||
|
[✓ refs/ref-01-{slug}.{ext} ... (if references saved)]
|
||||||
|
[✓ refs/ref-01-{slug}.md ... (description files)]
|
||||||
|
[✓ refs/extracted-style.md (if style extracted verbally)]
|
||||||
✓ prompts/cover.md
|
✓ prompts/cover.md
|
||||||
✓ cover.png
|
✓ cover.png
|
||||||
[✓ cover-backup-{timestamp}.png (if regenerated)]
|
[✓ cover-backup-{timestamp}.png (if regenerated)]
|
||||||
@@ -269,22 +487,45 @@ All modifications automatically backup existing `cover.png` before regenerating.
|
|||||||
|
|
||||||
- Cover must be readable at small preview sizes
|
- Cover must be readable at small preview sizes
|
||||||
- Visual metaphors > literal representations
|
- Visual metaphors > literal representations
|
||||||
- Title: max 8 chars, readable, impactful
|
- Title: readable, impactful
|
||||||
- Two confirmation points: Step 0 (first-time setup) + Step 2 (options) - skip Step 2 with `--quick`
|
- Two confirmation points: Step 0 (first-time setup) + Step 2 (6 dimensions) - skip Step 2 with `--quick`
|
||||||
- Use confirmed language for title text
|
- Use confirmed language for title text
|
||||||
- Maintain watermark consistency if enabled
|
- Maintain watermark consistency if enabled
|
||||||
- Check compatibility matrices when selecting combinations
|
- Check compatibility matrices when selecting combinations
|
||||||
- `--no-title` is alias for `--text none`
|
- `--no-title` is alias for `--text none`
|
||||||
- `--style` presets are backward-compatible; explicit `--palette`/`--rendering` override preset values
|
- `--style` presets are backward-compatible; explicit `--palette`/`--rendering` override preset values
|
||||||
|
|
||||||
|
### Reference Image Priority ⚠️
|
||||||
|
|
||||||
|
When user provides reference images, they are **HIGH PRIORITY** and MUST strongly influence the generated cover:
|
||||||
|
|
||||||
|
- **References override defaults**: If reference images conflict with preferred palette/rendering, the reference visual identity takes precedence in the prompt
|
||||||
|
- **Concrete > abstract**: Extract specific, reproducible visual elements (exact patterns, logo treatments, color hex values, layout structures) — not vague descriptions like "clean style"
|
||||||
|
- **Mandatory language**: Use "MUST", "REQUIRED", "CRITICAL" in the prompt for reference elements — generic "follow the style" is insufficient for image generation models
|
||||||
|
- **Visible in output**: After generation, verify that reference elements are actually present in the cover. If not, strengthen the prompt and regenerate
|
||||||
|
|
||||||
|
### Composition Principles
|
||||||
|
|
||||||
|
- **Generous whitespace**: 40-60% breathing room; avoid cluttered layouts
|
||||||
|
- **Visual anchor**: Main element centered or offset left (reserve right for title)
|
||||||
|
- **Character handling**: Simplified silhouettes or icon-style figures; NO realistic humans
|
||||||
|
- **Icon vocabulary**: Use simple, recognizable symbols (see [references/visual-elements.md](references/visual-elements.md))
|
||||||
|
|
||||||
|
### Title Handling
|
||||||
|
|
||||||
|
- **Source**: Use the exact title provided by user, or extract from source content
|
||||||
|
- **Do NOT invent titles**: Stay faithful to the original
|
||||||
|
- If no title in source and user doesn't provide one, ask user to specify
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
**Dimensions**: [text.md](references/dimensions/text.md) | [mood.md](references/dimensions/mood.md)
|
**Dimensions**: [text.md](references/dimensions/text.md) | [mood.md](references/dimensions/mood.md) | [font.md](references/dimensions/font.md)
|
||||||
**Palettes**: [references/palettes/](references/palettes/)
|
**Palettes**: [references/palettes/](references/palettes/)
|
||||||
**Renderings**: [references/renderings/](references/renderings/)
|
**Renderings**: [references/renderings/](references/renderings/)
|
||||||
**Auto-Selection**: [references/auto-selection.md](references/auto-selection.md)
|
**Auto-Selection**: [references/auto-selection.md](references/auto-selection.md)
|
||||||
**Style Presets**: [references/style-presets.md](references/style-presets.md)
|
**Style Presets**: [references/style-presets.md](references/style-presets.md)
|
||||||
**Compatibility**: [references/compatibility.md](references/compatibility.md)
|
**Compatibility**: [references/compatibility.md](references/compatibility.md)
|
||||||
**Types**: [references/types.md](references/types.md)
|
**Types**: [references/types.md](references/types.md)
|
||||||
|
**Visual Elements**: [references/visual-elements.md](references/visual-elements.md)
|
||||||
**Workflow**: [confirm-options.md](references/workflow/confirm-options.md) | [prompt-template.md](references/workflow/prompt-template.md)
|
**Workflow**: [confirm-options.md](references/workflow/confirm-options.md) | [prompt-template.md](references/workflow/prompt-template.md)
|
||||||
**Config**: [preferences-schema.md](references/config/preferences-schema.md) | [first-time-setup.md](references/config/first-time-setup.md) | [watermark-guide.md](references/config/watermark-guide.md)
|
**Config**: [preferences-schema.md](references/config/preferences-schema.md) | [first-time-setup.md](references/config/first-time-setup.md) | [watermark-guide.md](references/config/watermark-guide.md)
|
||||||
|
|||||||
@@ -58,3 +58,14 @@ Default: `title-only`
|
|||||||
| Launch, announcement, promotion, event, gaming, entertainment | `bold` |
|
| Launch, announcement, promotion, event, gaming, entertainment | `bold` |
|
||||||
|
|
||||||
Default: `balanced`
|
Default: `balanced`
|
||||||
|
|
||||||
|
## Auto Font Selection
|
||||||
|
|
||||||
|
| Signals | Font |
|
||||||
|
|---------|------|
|
||||||
|
| Personal, lifestyle, human, warm, friendly, story | `handwritten` |
|
||||||
|
| Technical, professional, clean, modern, minimal, data | `clean` |
|
||||||
|
| Editorial, academic, luxury, classic, literary | `serif` |
|
||||||
|
| Announcement, entertainment, promotion, bold, event, gaming | `display` |
|
||||||
|
|
||||||
|
Default: `clean`
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ Apply the specified rendering's characteristics:
|
|||||||
|
|
||||||
### Text (Density Level)
|
### Text (Density Level)
|
||||||
- `none`: No text elements, full visual area
|
- `none`: No text elements, full visual area
|
||||||
- `title-only`: Single headline (≤8 characters), 85% visual area
|
- `title-only`: Single headline, 85% visual area
|
||||||
- `title-subtitle`: Title + context (≤15 chars), 75% visual area
|
- `title-subtitle`: Title + context, 75% visual area
|
||||||
- `text-rich`: Title + subtitle + 2-4 keyword tags, 60% visual area
|
- `text-rich`: Title + subtitle + 2-4 keyword tags, 60% visual area
|
||||||
|
|
||||||
### Mood (Emotional Intensity)
|
### Mood (Emotional Intensity)
|
||||||
@@ -50,17 +50,44 @@ Apply the specified rendering's characteristics:
|
|||||||
|
|
||||||
## Text Style (When Title Included)
|
## Text Style (When Title Included)
|
||||||
|
|
||||||
- Title text: Large, eye-catching, max 8 characters
|
- **Title source**: Use the exact title provided by user, or extract from source content. Do NOT invent or modify titles.
|
||||||
- Subtitle: Secondary, max 15 characters (if title-subtitle or text-rich)
|
- Title text: Large, eye-catching, faithful to source
|
||||||
|
- Subtitle: Secondary element (if title-subtitle or text-rich)
|
||||||
- Tags: 2-4 keyword badges (if text-rich)
|
- Tags: 2-4 keyword badges (if text-rich)
|
||||||
- Font style harmonizes with rendering style
|
- Font style harmonizes with rendering style
|
||||||
- **Engagement hooks**: Use numbers ("3个关键"), questions ("Why?"), contrasts ("A vs B"), pain points ("别再X了"), or suspense ("隐藏的X") for compelling titles
|
|
||||||
|
|
||||||
## Composition Guidance
|
## Composition Guidance
|
||||||
|
|
||||||
**Icon Vocabulary**: Represent concepts with simple, recognizable icons rather than detailed illustrations. Use the rendering style to determine icon complexity (flat-vector = geometric, hand-drawn = sketchy, etc.)
|
### Layout Principles
|
||||||
|
|
||||||
**Character Handling**: When people are needed, use simplified silhouettes or abstract representations. NO realistic faces, detailed anatomy, or photographic representations.
|
- **Generous whitespace**: Maintain 40-60% breathing room; avoid cluttered compositions
|
||||||
|
- **Visual anchor placement**: Main element centered or offset left (reserve right side for title if included)
|
||||||
|
- **Information hierarchy**: One dominant focal point, 1-2 supporting elements, decorative accents
|
||||||
|
- **Clean backgrounds**: Solid colors or subtle gradients; no complex textures or patterns
|
||||||
|
|
||||||
|
### Icon & Symbol Vocabulary
|
||||||
|
|
||||||
|
Represent concepts with simple, recognizable icons rather than detailed illustrations:
|
||||||
|
|
||||||
|
| Category | Examples |
|
||||||
|
|----------|----------|
|
||||||
|
| Tech | Code window, gear, circuit, cloud, lock, API brackets |
|
||||||
|
| Ideas | Lightbulb, rocket, target, puzzle, key, magnifier |
|
||||||
|
| Communication | Speech bubble, chat dots, megaphone, mail |
|
||||||
|
| Growth | Plant/sprout, tree, arrow, chart, mountain |
|
||||||
|
| Tools | Wrench, pencil, brush, checklist, clock |
|
||||||
|
|
||||||
|
Use the rendering style to determine icon complexity (flat-vector = geometric, hand-drawn = sketchy, etc.)
|
||||||
|
|
||||||
|
Full library: [references/visual-elements.md](visual-elements.md)
|
||||||
|
|
||||||
|
### Character Handling
|
||||||
|
|
||||||
|
When people are needed:
|
||||||
|
- Use simplified silhouettes or abstract stick figures
|
||||||
|
- Symbolic representations (head + shoulders outline)
|
||||||
|
- NO realistic faces, detailed anatomy, or photographic representations
|
||||||
|
- Cartoon/icon style consistent with rendering choice
|
||||||
|
|
||||||
## Mood Application
|
## Mood Application
|
||||||
|
|
||||||
@@ -77,6 +104,16 @@ Apply mood adjustments to the base palette:
|
|||||||
- Use the same language as the content provided below for any text elements
|
- Use the same language as the content provided below for any text elements
|
||||||
- Match punctuation style to the content language
|
- Match punctuation style to the content language
|
||||||
|
|
||||||
|
## Reference Images
|
||||||
|
|
||||||
|
When reference images are provided:
|
||||||
|
|
||||||
|
- **Style extraction**: Identify rendering technique, line quality, texture, and visual vocabulary
|
||||||
|
- **Composition learning**: Note layout patterns, whitespace usage, element placement
|
||||||
|
- **Mood matching**: Capture the emotional tone and visual weight
|
||||||
|
- **Adaptation**: Apply extracted characteristics while respecting the specified Type, Palette, and Rendering dimensions
|
||||||
|
- **Priority**: If reference style conflicts with specified dimensions, dimensions take precedence for structural choices; reference influences decorative details
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Please generate the cover image based on the content provided below:
|
Please generate the cover image based on the content provided below:
|
||||||
|
|||||||
@@ -48,3 +48,12 @@
|
|||||||
| metaphor | ✓✓ | ✓✓ | ✓ |
|
| metaphor | ✓✓ | ✓✓ | ✓ |
|
||||||
| scene | ✓✓ | ✓✓ | ✓ |
|
| scene | ✓✓ | ✓✓ | ✓ |
|
||||||
| minimal | ✓✓ | ✓✓ | ✗ |
|
| minimal | ✓✓ | ✓✓ | ✗ |
|
||||||
|
|
||||||
|
## Font × Rendering
|
||||||
|
|
||||||
|
| | flat-vector | hand-drawn | painterly | digital | pixel | chalk |
|
||||||
|
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
|
| clean | ✓✓ | ✗ | ✗ | ✓✓ | ✓ | ✗ |
|
||||||
|
| handwritten | ✓ | ✓✓ | ✓✓ | ✓ | ✗ | ✓✓ |
|
||||||
|
| serif | ✓ | ✗ | ✓ | ✓✓ | ✗ | ✗ |
|
||||||
|
| display | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ description: First-time setup flow for baoyu-cover-image preferences
|
|||||||
|
|
||||||
When no EXTEND.md is found, guide user through preference setup.
|
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 reference images
|
||||||
|
- Ask about content/article
|
||||||
|
- Ask about dimensions (type, palette, rendering)
|
||||||
|
- Proceed to content analysis
|
||||||
|
|
||||||
|
ONLY ask the questions in this setup flow, save EXTEND.md, then continue.
|
||||||
|
|
||||||
## Setup Flow
|
## Setup Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ custom_palettes:
|
|||||||
| Value | Description |
|
| Value | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `none` | Pure visual, no text elements |
|
| `none` | Pure visual, no text elements |
|
||||||
| `title-only` | Single headline (≤8 characters) |
|
| `title-only` | Single headline |
|
||||||
| `title-subtitle` | Title + subtitle (≤15 characters) |
|
| `title-subtitle` | Title + subtitle |
|
||||||
| `text-rich` | Title + subtitle + keyword tags (2-4) |
|
| `text-rich` | Title + subtitle + keyword tags (2-4) |
|
||||||
|
|
||||||
## Mood Options
|
## Mood Options
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
name: font-dimension
|
||||||
|
description: Typography style dimension for cover images
|
||||||
|
---
|
||||||
|
|
||||||
|
# Font Dimension
|
||||||
|
|
||||||
|
Controls typography style and character feel.
|
||||||
|
|
||||||
|
## Values
|
||||||
|
|
||||||
|
| Font | Visual Style | Line Quality | Character |
|
||||||
|
|------|--------------|--------------|-----------|
|
||||||
|
| `clean` | Geometric sans-serif | Sharp, uniform | Modern, precise, neutral |
|
||||||
|
| `handwritten` | Hand-lettered, brush | Organic, varied | Warm, personal, friendly |
|
||||||
|
| `serif` | Classic serifs, elegant | Refined, structured | Editorial, authoritative |
|
||||||
|
| `display` | Bold, decorative | Heavy, expressive | Attention-grabbing, playful |
|
||||||
|
|
||||||
|
## Detail
|
||||||
|
|
||||||
|
### clean
|
||||||
|
|
||||||
|
Modern, universal typography with neutral character.
|
||||||
|
|
||||||
|
**Characteristics**:
|
||||||
|
- Geometric sans-serif letterforms
|
||||||
|
- Sharp, uniform line weight
|
||||||
|
- Clean edges, no flourishes
|
||||||
|
- High readability at all sizes
|
||||||
|
- Minimal personality, maximum clarity
|
||||||
|
|
||||||
|
**Use Cases**:
|
||||||
|
- Technical documentation
|
||||||
|
- Professional/corporate content
|
||||||
|
- Minimal design approaches
|
||||||
|
- Data-driven articles
|
||||||
|
- Modern brand aesthetics
|
||||||
|
|
||||||
|
**Prompt Hints**:
|
||||||
|
- Use clean geometric sans-serif typography
|
||||||
|
- Modern, minimal letterforms
|
||||||
|
- Sharp edges, uniform stroke weight
|
||||||
|
- High contrast against background
|
||||||
|
|
||||||
|
### handwritten
|
||||||
|
|
||||||
|
Warm, organic typography with personal character.
|
||||||
|
|
||||||
|
**Characteristics**:
|
||||||
|
- Hand-lettered or brush style
|
||||||
|
- Organic, varied line weight
|
||||||
|
- Natural imperfections
|
||||||
|
- Approachable, human feel
|
||||||
|
- Casual yet intentional
|
||||||
|
|
||||||
|
**Use Cases**:
|
||||||
|
- Personal stories
|
||||||
|
- Lifestyle content
|
||||||
|
- Wellness and self-improvement
|
||||||
|
- Creative tutorials
|
||||||
|
- Friendly brand voices
|
||||||
|
|
||||||
|
**Prompt Hints**:
|
||||||
|
- Use warm hand-lettered typography with organic brush strokes
|
||||||
|
- Friendly, personal feel
|
||||||
|
- Natural variation in stroke weight
|
||||||
|
- Approachable, human character
|
||||||
|
|
||||||
|
### serif
|
||||||
|
|
||||||
|
Classic, elegant typography with editorial authority.
|
||||||
|
|
||||||
|
**Characteristics**:
|
||||||
|
- Traditional serif letterforms
|
||||||
|
- Refined, structured strokes
|
||||||
|
- Elegant proportions
|
||||||
|
- Timeless sophistication
|
||||||
|
- Formal, trustworthy feel
|
||||||
|
|
||||||
|
**Use Cases**:
|
||||||
|
- Editorial content
|
||||||
|
- Academic articles
|
||||||
|
- Luxury brand content
|
||||||
|
- Historical topics
|
||||||
|
- Literary pieces
|
||||||
|
|
||||||
|
**Prompt Hints**:
|
||||||
|
- Use elegant serif typography with refined letterforms
|
||||||
|
- Classic, editorial character
|
||||||
|
- Structured, proportional spacing
|
||||||
|
- Authoritative, sophisticated feel
|
||||||
|
|
||||||
|
### display
|
||||||
|
|
||||||
|
Bold, decorative typography for maximum impact.
|
||||||
|
|
||||||
|
**Characteristics**:
|
||||||
|
- Heavy, expressive letterforms
|
||||||
|
- Decorative elements
|
||||||
|
- Strong visual presence
|
||||||
|
- Playful or dramatic character
|
||||||
|
- Designed for headlines
|
||||||
|
|
||||||
|
**Use Cases**:
|
||||||
|
- Announcements
|
||||||
|
- Entertainment content
|
||||||
|
- Promotional materials
|
||||||
|
- Event marketing
|
||||||
|
- Gaming topics
|
||||||
|
|
||||||
|
**Prompt Hints**:
|
||||||
|
- Use bold decorative display typography
|
||||||
|
- Heavy, expressive headlines
|
||||||
|
- Strong visual impact
|
||||||
|
- Attention-grabbing character
|
||||||
|
|
||||||
|
## Default
|
||||||
|
|
||||||
|
`clean` — Universal, pairs well with most rendering styles.
|
||||||
|
|
||||||
|
## Rendering Compatibility
|
||||||
|
|
||||||
|
| Font × Rendering | flat-vector | hand-drawn | painterly | digital | pixel | chalk |
|
||||||
|
|------------------|:-----------:|:----------:|:---------:|:-------:|:-----:|:-----:|
|
||||||
|
| clean | ✓✓ | ✗ | ✗ | ✓✓ | ✓ | ✗ |
|
||||||
|
| handwritten | ✓ | ✓✓ | ✓✓ | ✓ | ✗ | ✓✓ |
|
||||||
|
| serif | ✓ | ✗ | ✓ | ✓✓ | ✗ | ✗ |
|
||||||
|
| display | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
||||||
|
|
||||||
|
✓✓ = highly recommended | ✓ = compatible | ✗ = not recommended
|
||||||
|
|
||||||
|
## Type Compatibility
|
||||||
|
|
||||||
|
| Font × Type | hero | conceptual | typography | metaphor | scene | minimal |
|
||||||
|
|-------------|:----:|:----------:|:----------:|:--------:|:-----:|:-------:|
|
||||||
|
| clean | ✓ | ✓✓ | ✓✓ | ✓ | ✗ | ✓✓ |
|
||||||
|
| handwritten | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
||||||
|
| serif | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ |
|
||||||
|
| display | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
## Palette Interaction
|
||||||
|
|
||||||
|
Font style adapts to palette characteristics:
|
||||||
|
|
||||||
|
| Palette Category | clean | handwritten | serif | display |
|
||||||
|
|------------------|-------|-------------|-------|---------|
|
||||||
|
| Warm (warm, earth, pastel) | Softer weight | Natural fit | Warm tones | Playful energy |
|
||||||
|
| Cool (cool, mono, elegant) | Perfect match | Contrast | Classic pairing | Bold statement |
|
||||||
|
| Dark (dark, vivid) | High contrast | Glow effects | Dramatic | Maximum impact |
|
||||||
|
| Vintage (retro) | Modern contrast | Nostalgic fit | Period-appropriate | Retro headlines |
|
||||||
|
|
||||||
|
## Auto Selection
|
||||||
|
|
||||||
|
When `--font` is omitted, select based on signals:
|
||||||
|
|
||||||
|
| Signals | Font |
|
||||||
|
|---------|------|
|
||||||
|
| Personal, lifestyle, human, warm, friendly, story | `handwritten` |
|
||||||
|
| Technical, professional, clean, modern, minimal, data | `clean` |
|
||||||
|
| Editorial, academic, luxury, classic, literary | `serif` |
|
||||||
|
| Announcement, entertainment, promotion, bold, event, gaming | `display` |
|
||||||
|
|
||||||
|
Default: `clean`
|
||||||
@@ -12,8 +12,8 @@ Controls text density and information hierarchy on cover images.
|
|||||||
| Value | Title | Subtitle | Tags | Visual Area |
|
| Value | Title | Subtitle | Tags | Visual Area |
|
||||||
|-------|:-----:|:--------:|:----:|:-----------:|
|
|-------|:-----:|:--------:|:----:|:-----------:|
|
||||||
| `none` | - | - | - | 100% |
|
| `none` | - | - | - | 100% |
|
||||||
| `title-only` | ✓ (≤8字) | - | - | 85% |
|
| `title-only` | ✓ | - | - | 85% |
|
||||||
| `title-subtitle` | ✓ | ✓ (≤15字) | - | 75% |
|
| `title-subtitle` | ✓ | ✓ | - | 75% |
|
||||||
| `text-rich` | ✓ | ✓ | ✓ (2-4) | 60% |
|
| `text-rich` | ✓ | ✓ | ✓ (2-4) | 60% |
|
||||||
|
|
||||||
## Detail
|
## Detail
|
||||||
@@ -43,24 +43,14 @@ Single headline, maximum impact.
|
|||||||
- Strong brand recognition
|
- Strong brand recognition
|
||||||
|
|
||||||
**Composition**:
|
**Composition**:
|
||||||
- Title: ≤8 characters, prominent
|
- Title: prominent placement
|
||||||
- Reserved zone: top or bottom 15%
|
- Reserved zone: top or bottom 15%
|
||||||
- Visual supports title message
|
- Visual supports title message
|
||||||
|
|
||||||
**Title Guidelines**:
|
**Title Guidelines**:
|
||||||
- Punchy, action-oriented
|
- Use exact title from source content or user-provided title
|
||||||
|
- Do NOT invent or modify titles
|
||||||
- Match content language
|
- Match content language
|
||||||
- Use engagement hooks (see below)
|
|
||||||
|
|
||||||
**Engagement Hooks**:
|
|
||||||
|
|
||||||
| Hook Type | Examples (EN) | Examples (ZH) |
|
|
||||||
|-----------|---------------|---------------|
|
|
||||||
| Numbers | "3 Traps", "5 Keys" | "3个陷阱", "5个关键" |
|
|
||||||
| Questions | "Why X?", "How?" | "为什么X?", "怎么做?" |
|
|
||||||
| Contrasts | "A vs B", "Old→New" | "A还是B", "旧→新" |
|
|
||||||
| Pain Points | "Stop X", "Never Do" | "别再X了", "千万别" |
|
|
||||||
| Suspense | "Hidden X", "Secret" | "隐藏的X", "秘密" |
|
|
||||||
|
|
||||||
### title-subtitle
|
### title-subtitle
|
||||||
|
|
||||||
@@ -72,14 +62,14 @@ Title with supporting context.
|
|||||||
- Content with dual messages
|
- Content with dual messages
|
||||||
|
|
||||||
**Composition**:
|
**Composition**:
|
||||||
- Title: ≤8 characters, primary
|
- Title: primary element
|
||||||
- Subtitle: ≤15 characters, secondary
|
- Subtitle: secondary element
|
||||||
- Reserved zone: 25%
|
- Reserved zone: 25%
|
||||||
- Clear hierarchy between title/subtitle
|
- Clear hierarchy between title/subtitle
|
||||||
|
|
||||||
**Title Guidelines**:
|
**Title Guidelines**:
|
||||||
- Use engagement hooks: numbers, questions, contrasts, pain points, suspense
|
- Use exact title from source content or user-provided title
|
||||||
- Punchy, action-oriented
|
- Do NOT invent or modify titles
|
||||||
|
|
||||||
**Subtitle Guidelines**:
|
**Subtitle Guidelines**:
|
||||||
- Clarify or contextualize title
|
- Clarify or contextualize title
|
||||||
@@ -104,8 +94,8 @@ Information-dense cover with multiple text elements.
|
|||||||
- Clear visual hierarchy
|
- Clear visual hierarchy
|
||||||
|
|
||||||
**Title Guidelines**:
|
**Title Guidelines**:
|
||||||
- Use engagement hooks: numbers, questions, contrasts, pain points, suspense
|
- Use exact title from source content or user-provided title
|
||||||
- Punchy, action-oriented
|
- Do NOT invent or modify titles
|
||||||
|
|
||||||
**Tag Guidelines**:
|
**Tag Guidelines**:
|
||||||
- 2-4 tags maximum
|
- 2-4 tags maximum
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
| `vector-illustration` | `retro` | `flat-vector` |
|
| `vector-illustration` | `retro` | `flat-vector` |
|
||||||
| `vintage` | `retro` | `hand-drawn` |
|
| `vintage` | `retro` | `hand-drawn` |
|
||||||
| `warm` | `warm` | `hand-drawn` |
|
| `warm` | `warm` | `hand-drawn` |
|
||||||
|
| `warm-flat` | `warm` | `flat-vector` |
|
||||||
| `watercolor` | `earth` | `painterly` |
|
| `watercolor` | `earth` | `painterly` |
|
||||||
|
|
||||||
## Override Examples
|
## Override Examples
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Visual Elements Library
|
||||||
|
|
||||||
|
Icon and symbol vocabulary organized by topic. Use these as building blocks for cover compositions.
|
||||||
|
|
||||||
|
## Tech & Development
|
||||||
|
|
||||||
|
| Element | Use For |
|
||||||
|
|---------|---------|
|
||||||
|
| Code window / Terminal | Programming, development |
|
||||||
|
| Gear / Cog | Engineering, settings, process |
|
||||||
|
| Circuit board / Chip | Hardware, AI, computing |
|
||||||
|
| Binary / Data stream | Data, algorithms |
|
||||||
|
| API brackets `</>` | Web development, APIs |
|
||||||
|
| Cloud | Cloud computing, SaaS |
|
||||||
|
| Lock / Shield | Security, privacy |
|
||||||
|
| Network nodes | Distributed systems, connections |
|
||||||
|
|
||||||
|
## Ideas & Innovation
|
||||||
|
|
||||||
|
| Element | Use For |
|
||||||
|
|---------|---------|
|
||||||
|
| Lightbulb | Ideas, insights, innovation |
|
||||||
|
| Rocket | Launch, growth, startups |
|
||||||
|
| Target / Bullseye | Goals, precision, focus |
|
||||||
|
| Puzzle piece | Problem solving, integration |
|
||||||
|
| Key | Solutions, access, unlocking |
|
||||||
|
| Magnifying glass | Analysis, search, discovery |
|
||||||
|
| Chart / Graph | Data, trends, growth |
|
||||||
|
| Arrow / Path | Direction, journey, progress |
|
||||||
|
|
||||||
|
## Communication & Collaboration
|
||||||
|
|
||||||
|
| Element | Use For |
|
||||||
|
|---------|---------|
|
||||||
|
| Speech bubble | Communication, dialogue |
|
||||||
|
| Chat dots `...` | Conversation, messaging |
|
||||||
|
| Handshake | Partnership, agreement |
|
||||||
|
| Team / Figures | Collaboration, community |
|
||||||
|
| Mail / Envelope | Notifications, outreach |
|
||||||
|
| Megaphone | Announcements, marketing |
|
||||||
|
| Network / Web | Social, connections |
|
||||||
|
|
||||||
|
## Nature & Growth
|
||||||
|
|
||||||
|
| Element | Use For |
|
||||||
|
|---------|---------|
|
||||||
|
| Plant / Sprout | Growth, organic, sustainability |
|
||||||
|
| Tree | Established, structure, branching |
|
||||||
|
| Leaf | Eco, natural, fresh |
|
||||||
|
| Sun / Rays | Energy, positivity, new beginnings |
|
||||||
|
| Mountain | Challenge, achievement, scale |
|
||||||
|
| Wave | Flow, change, rhythm |
|
||||||
|
| Seed → Plant | Transformation, potential |
|
||||||
|
|
||||||
|
## Tools & Actions
|
||||||
|
|
||||||
|
| Element | Use For |
|
||||||
|
|---------|---------|
|
||||||
|
| Wrench / Hammer | Building, fixing, tools |
|
||||||
|
| Pencil / Pen | Writing, creation, editing |
|
||||||
|
| Brush | Design, creativity, art |
|
||||||
|
| Scissors | Cutting, editing, trimming |
|
||||||
|
| Clock / Timer | Time, scheduling, deadlines |
|
||||||
|
| Calendar | Planning, events, milestones |
|
||||||
|
| Checklist / Checkbox | Tasks, completion, validation |
|
||||||
|
|
||||||
|
## Abstract Concepts
|
||||||
|
|
||||||
|
| Element | Use For |
|
||||||
|
|---------|---------|
|
||||||
|
| Infinity ∞ | Continuous, endless, loops |
|
||||||
|
| Yin-yang | Balance, duality, harmony |
|
||||||
|
| Spiral | Evolution, recursion, cycles |
|
||||||
|
| Stack / Layers | Depth, hierarchy, structure |
|
||||||
|
| Bridge | Connection, transition, spanning |
|
||||||
|
| Door / Portal | Opportunity, entry, access |
|
||||||
|
| Mirror / Reflection | Self-improvement, analysis |
|
||||||
|
|
||||||
|
## Combination Patterns
|
||||||
|
|
||||||
|
Create visual metaphors by combining elements:
|
||||||
|
|
||||||
|
| Combination | Represents |
|
||||||
|
|-------------|------------|
|
||||||
|
| Lightbulb + Gear | Innovative engineering |
|
||||||
|
| Plant + Code | Organic tech growth |
|
||||||
|
| Rocket + Target | Precise acceleration |
|
||||||
|
| Key + Lock | Security solutions |
|
||||||
|
| Bridge + People | Team connections |
|
||||||
|
| Magnifier + Data | Analytics, insights |
|
||||||
|
|
||||||
|
## Rendering-Specific Treatment
|
||||||
|
|
||||||
|
| Rendering | Element Style |
|
||||||
|
|-----------|---------------|
|
||||||
|
| `flat-vector` | Geometric, simple shapes, uniform fills |
|
||||||
|
| `hand-drawn` | Sketchy, organic, doodle-like |
|
||||||
|
| `painterly` | Soft edges, brush strokes |
|
||||||
|
| `digital` | Precise, gradient hints, polished |
|
||||||
|
| `pixel` | 8-bit chunky, grid-aligned |
|
||||||
|
| `chalk` | Dusty, textured, board style |
|
||||||
@@ -2,22 +2,22 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Validate all 5 dimensions + aspect ratio.
|
Validate all 6 dimensions + aspect ratio.
|
||||||
|
|
||||||
## Skip Conditions
|
## Skip Conditions
|
||||||
|
|
||||||
| Condition | Skipped Questions | Still Asked |
|
| Condition | Skipped Questions | Still Asked |
|
||||||
|-----------|-------------------|-------------|
|
|-----------|-------------------|-------------|
|
||||||
| `--quick` flag | Type, Palette, Rendering, Text, Mood | **Aspect Ratio** (unless `--aspect` specified) |
|
| `--quick` flag | Type, Palette, Rendering, Text, Mood, Font | **Aspect Ratio** (unless `--aspect` specified) |
|
||||||
| All 5 dimensions + `--aspect` specified | All | None |
|
| All 6 dimensions + `--aspect` specified | All | None |
|
||||||
| `quick_mode: true` in EXTEND.md | Type, Palette, Rendering, Text, Mood | **Aspect Ratio** (unless `--aspect` specified) |
|
| `quick_mode: true` in EXTEND.md | Type, Palette, Rendering, Text, Mood, Font | **Aspect Ratio** (unless `--aspect` specified) |
|
||||||
| Otherwise | None | All 6 questions |
|
| Otherwise | None | All 7 questions |
|
||||||
|
|
||||||
**Important**: Aspect ratio is ALWAYS asked unless explicitly specified via `--aspect` CLI flag. User presets in EXTEND.md are shown as recommended option, not auto-selected.
|
**Important**: Aspect ratio is ALWAYS asked unless explicitly specified via `--aspect` CLI flag. User presets in EXTEND.md are shown as recommended option, not auto-selected.
|
||||||
|
|
||||||
## Quick Mode Output
|
## Quick Mode Output
|
||||||
|
|
||||||
When skipping 5 dimensions:
|
When skipping 6 dimensions:
|
||||||
|
|
||||||
```
|
```
|
||||||
Quick Mode: Auto-selected dimensions
|
Quick Mode: Auto-selected dimensions
|
||||||
@@ -26,8 +26,9 @@ Quick Mode: Auto-selected dimensions
|
|||||||
• Rendering: [rendering] ([reason])
|
• Rendering: [rendering] ([reason])
|
||||||
• Text: [text] ([reason])
|
• Text: [text] ([reason])
|
||||||
• Mood: [mood] ([reason])
|
• Mood: [mood] ([reason])
|
||||||
|
• Font: [font] ([reason])
|
||||||
|
|
||||||
[Then ask Question 6: Aspect Ratio]
|
[Then ask Question 7: Aspect Ratio]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Confirmation Flow
|
## Confirmation Flow
|
||||||
@@ -91,7 +92,26 @@ options:
|
|||||||
description: "Polished, precise, subtle gradients"
|
description: "Polished, precise, subtle gradients"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Q4: Other Settings (skip if all remaining dimensions already specified)
|
### Q4: Font (skip if `--font`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
header: "Font"
|
||||||
|
question: "Which font style?"
|
||||||
|
multiSelect: false
|
||||||
|
options:
|
||||||
|
- label: "[auto-recommended font] (Recommended)"
|
||||||
|
description: "[reason based on content signals]"
|
||||||
|
- label: "clean"
|
||||||
|
description: "Modern geometric sans-serif - tech, professional"
|
||||||
|
- label: "handwritten"
|
||||||
|
description: "Warm hand-lettered - personal, friendly"
|
||||||
|
- label: "serif"
|
||||||
|
description: "Classic elegant - editorial, luxury"
|
||||||
|
- label: "display"
|
||||||
|
description: "Bold decorative - announcements, entertainment"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q5: Other Settings (skip if all remaining dimensions already specified)
|
||||||
|
|
||||||
Combine remaining settings into one question. Include: Output Dir (if no preference + file path input), Text, Mood, Aspect. Show auto-selected values as recommended option. User can accept all or type adjustments via "Other".
|
Combine remaining settings into one question. Include: Output Dir (if no preference + file path input), Text, Mood, Aspect. Show auto-selected values as recommended option. User can accept all or type adjustments via "Other".
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,19 @@
|
|||||||
Save to `prompts/cover.md`:
|
Save to `prompts/cover.md`:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
---
|
||||||
|
type: cover
|
||||||
|
palette: [confirmed palette]
|
||||||
|
rendering: [confirmed rendering]
|
||||||
|
references:
|
||||||
|
- ref_id: 01
|
||||||
|
filename: refs/ref-01-{slug}.{ext}
|
||||||
|
usage: direct | style | palette
|
||||||
|
- ref_id: 02
|
||||||
|
filename: refs/ref-02-{slug}.{ext}
|
||||||
|
usage: direct | style | palette
|
||||||
|
---
|
||||||
|
|
||||||
# Content Context
|
# Content Context
|
||||||
Article title: [full original title from source]
|
Article title: [full original title from source]
|
||||||
Content summary: [2-3 sentence summary of key points and themes]
|
Content summary: [2-3 sentence summary of key points and themes]
|
||||||
@@ -13,6 +26,7 @@ Cover theme: [2-3 words visual interpretation]
|
|||||||
Type: [confirmed type]
|
Type: [confirmed type]
|
||||||
Palette: [confirmed palette]
|
Palette: [confirmed palette]
|
||||||
Rendering: [confirmed rendering]
|
Rendering: [confirmed rendering]
|
||||||
|
Font: [confirmed font]
|
||||||
Text level: [confirmed text level]
|
Text level: [confirmed text level]
|
||||||
Mood: [confirmed mood]
|
Mood: [confirmed mood]
|
||||||
Aspect ratio: [confirmed ratio]
|
Aspect ratio: [confirmed ratio]
|
||||||
@@ -21,9 +35,9 @@ Language: [confirmed language]
|
|||||||
# Text Elements
|
# Text Elements
|
||||||
[Based on text level:]
|
[Based on text level:]
|
||||||
- none: "No text elements"
|
- none: "No text elements"
|
||||||
- title-only: "Title: [max 8 chars headline]"
|
- title-only: "Title: [exact title from source or user]"
|
||||||
- title-subtitle: "Title: [headline] / Subtitle: [max 15 chars context]"
|
- title-subtitle: "Title: [title] / Subtitle: [context]"
|
||||||
- text-rich: "Title: [headline] / Subtitle: [context] / Tags: [2-4 keywords]"
|
- text-rich: "Title: [title] / Subtitle: [context] / Tags: [2-4 keywords]"
|
||||||
|
|
||||||
# Mood Application
|
# Mood Application
|
||||||
[Based on mood level:]
|
[Based on mood level:]
|
||||||
@@ -31,6 +45,13 @@ Language: [confirmed language]
|
|||||||
- balanced: "Use medium contrast, normal saturation, balanced visual weight"
|
- balanced: "Use medium contrast, normal saturation, balanced visual weight"
|
||||||
- bold: "Use high contrast, vivid saturated colors, heavy visual weight, dynamic energy"
|
- bold: "Use high contrast, vivid saturated colors, heavy visual weight, dynamic energy"
|
||||||
|
|
||||||
|
# Font Application
|
||||||
|
[Based on font style:]
|
||||||
|
- clean: "Use clean geometric sans-serif typography. Modern, minimal letterforms."
|
||||||
|
- handwritten: "Use warm hand-lettered typography with organic brush strokes. Friendly, personal feel."
|
||||||
|
- serif: "Use elegant serif typography with refined letterforms. Classic, editorial character."
|
||||||
|
- display: "Use bold decorative display typography. Heavy, expressive headlines."
|
||||||
|
|
||||||
# Composition
|
# Composition
|
||||||
Type composition:
|
Type composition:
|
||||||
- [Type-specific layout and structure]
|
- [Type-specific layout and structure]
|
||||||
@@ -46,14 +67,37 @@ Type notes: [key characteristics from type definition]
|
|||||||
Palette notes: [key characteristics from palette definition]
|
Palette notes: [key characteristics from palette definition]
|
||||||
|
|
||||||
[Watermark section if enabled]
|
[Watermark section if enabled]
|
||||||
|
|
||||||
|
[Reference images section if provided — REQUIRED, see below]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Reference-Driven Design ⚠️ HIGH PRIORITY
|
||||||
|
|
||||||
|
When reference images are provided, they are the **primary visual input** and MUST strongly influence the output. The cover should look like it belongs to the same visual family as the references.
|
||||||
|
|
||||||
|
**Passing `--ref` alone is NOT enough.** Image generation models often ignore reference images unless the prompt text explicitly describes what to reproduce. Always combine `--ref` with detailed textual instructions.
|
||||||
|
|
||||||
## Content-Driven Design
|
## Content-Driven Design
|
||||||
|
|
||||||
- Article title and summary inform the visual metaphor choice
|
- Article title and summary inform the visual metaphor choice
|
||||||
- Keywords guide decorative elements and symbols
|
- Keywords guide decorative elements and symbols
|
||||||
- The skill controls visual style; the content drives meaning
|
- The skill controls visual style; the content drives meaning
|
||||||
|
|
||||||
|
## Visual Element Selection
|
||||||
|
|
||||||
|
Match content themes to icon vocabulary:
|
||||||
|
|
||||||
|
| Content Theme | Suggested Elements |
|
||||||
|
|---------------|-------------------|
|
||||||
|
| Programming/Dev | Code window, terminal, API brackets, gear |
|
||||||
|
| AI/ML | Brain, neural network, robot, circuit |
|
||||||
|
| Growth/Business | Chart, rocket, plant, mountain, arrow |
|
||||||
|
| Security | Lock, shield, key, fingerprint |
|
||||||
|
| Communication | Speech bubble, megaphone, mail, handshake |
|
||||||
|
| Tools/Methods | Wrench, checklist, pencil, puzzle |
|
||||||
|
|
||||||
|
Full library: [../visual-elements.md](../visual-elements.md)
|
||||||
|
|
||||||
## Type-Specific Composition
|
## Type-Specific Composition
|
||||||
|
|
||||||
| Type | Composition Guidelines |
|
| Type | Composition Guidelines |
|
||||||
@@ -68,8 +112,8 @@ Palette notes: [key characteristics from palette definition]
|
|||||||
## Title Guidelines
|
## Title Guidelines
|
||||||
|
|
||||||
When text level includes title:
|
When text level includes title:
|
||||||
- Max 8 characters, punchy headline
|
- **Source**: Use the exact title provided by user, or extract from source content
|
||||||
- Use engagement hooks: numbers ("3个陷阱"), questions ("Why X?"), contrasts ("A vs B"), pain points ("别再X了")
|
- **Do NOT invent titles**: Stay faithful to the original
|
||||||
- Match confirmed language
|
- Match confirmed language
|
||||||
|
|
||||||
## Watermark Application
|
## Watermark Application
|
||||||
@@ -82,3 +126,122 @@ The watermark should be legible but not distracting from the main content.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Reference: `config/watermark-guide.md`
|
Reference: `config/watermark-guide.md`
|
||||||
|
|
||||||
|
## Reference Image Handling
|
||||||
|
|
||||||
|
When user provides reference images (`--ref` or pasted images):
|
||||||
|
|
||||||
|
### ⚠️ CRITICAL - Frontmatter References
|
||||||
|
|
||||||
|
**MUST add `references` field in YAML frontmatter** when reference files are saved to `refs/`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
type: cover
|
||||||
|
palette: warm
|
||||||
|
rendering: flat-vector
|
||||||
|
references:
|
||||||
|
- ref_id: 01
|
||||||
|
filename: refs/ref-01-podcast-thumbnail.jpg
|
||||||
|
usage: style
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `ref_id` | Sequential number (01, 02, ...) |
|
||||||
|
| `filename` | Relative path from prompt file's parent directory |
|
||||||
|
| `usage` | `direct` / `style` / `palette` |
|
||||||
|
|
||||||
|
**Omit `references` field entirely** if no reference files saved (style extracted verbally only).
|
||||||
|
|
||||||
|
### When to Include References in Frontmatter
|
||||||
|
|
||||||
|
| Situation | Frontmatter Action | Generation Action |
|
||||||
|
|-----------|-------------------|-------------------|
|
||||||
|
| Reference file saved to `refs/` | Add to `references` list ✓ | Pass via `--ref` parameter |
|
||||||
|
| Style extracted verbally (no file) | Omit `references` field | Describe in prompt body only |
|
||||||
|
| File path in frontmatter but doesn't exist | ERROR - fix or remove | Generation will fail |
|
||||||
|
|
||||||
|
**Before writing prompt with references, verify**: `test -f refs/ref-NN-{slug}.{ext}`
|
||||||
|
|
||||||
|
### Reference Usage Types
|
||||||
|
|
||||||
|
| Usage | When to Use | Generation Action |
|
||||||
|
|-------|-------------|-------------------|
|
||||||
|
| `direct` | Reference matches desired output closely | Pass to `--ref` parameter |
|
||||||
|
| `style` | Extract visual style characteristics only | Describe style in prompt text |
|
||||||
|
| `palette` | Extract color palette only | Include colors in prompt |
|
||||||
|
|
||||||
|
### Step 1: Analyze References
|
||||||
|
|
||||||
|
For each reference image, extract:
|
||||||
|
- **Style**: Rendering technique, line quality, texture
|
||||||
|
- **Composition**: Layout, visual hierarchy, focal points
|
||||||
|
- **Color mood**: Palette characteristics (without specific colors)
|
||||||
|
- **Elements**: Key visual elements and symbols used
|
||||||
|
|
||||||
|
### Step 2: Embed in Prompt ⚠️ CRITICAL
|
||||||
|
|
||||||
|
**Passing `--ref` alone is NOT enough.** Image generation models frequently ignore reference images unless the prompt text explicitly and forcefully describes what to reproduce. You MUST always write detailed textual instructions regardless of whether `--ref` is used.
|
||||||
|
|
||||||
|
**If file saved (with or without `--ref` support)**:
|
||||||
|
- Pass ref images via `--ref` parameter if skill supports it
|
||||||
|
- **ALWAYS** add a detailed mandatory section in the prompt body:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Reference Style — MUST INCORPORATE
|
||||||
|
|
||||||
|
CRITICAL: The generated cover MUST visually reference the provided images. The cover must feel like it belongs to the same visual family.
|
||||||
|
|
||||||
|
## From Ref 1 ([filename]) — REQUIRED elements:
|
||||||
|
- [Brand element]: [Specific description of logo/wordmark treatment, e.g., "The logo uses vertical parallel lines (|||) for the letter 'm'. Reproduce this exact treatment."]
|
||||||
|
- [Signature pattern]: [Specific description, e.g., "Woven intersecting curves forming a diamond/lozenge grid pattern. This MUST appear prominently as a banner, border, or background section."]
|
||||||
|
- [Colors]: [Exact hex values, e.g., "Dark teal #2D4A3E background, cream #F5F0E0 text"]
|
||||||
|
- [Typography]: [Specific treatment, e.g., "Uppercase text with wide letter-spacing"]
|
||||||
|
- [Layout element]: [Specific spatial element, e.g., "Bottom banner strip in dark color"]
|
||||||
|
|
||||||
|
## From Ref 2 ([filename]) — REQUIRED elements:
|
||||||
|
[Same detailed breakdown]
|
||||||
|
|
||||||
|
## Integration approach:
|
||||||
|
[Specific layout instruction describing how reference elements combine with the cover content, e.g., "Use a SPLIT LAYOUT: main illustration area (warm cream background) occupies ~65% of the image, while a dark teal BANNER STRIP (with the woven line pattern from Ref 2) runs along the bottom ~35%, containing branding elements from Ref 1."]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key rules**:
|
||||||
|
- Each visual element gets its own bullet with "MUST" or "REQUIRED"
|
||||||
|
- Descriptions must be **specific enough to reproduce** — not vague ("clean style")
|
||||||
|
- The integration approach must describe **exact spatial arrangement**
|
||||||
|
- After generation, verify reference elements are visible; if not, strengthen and regenerate
|
||||||
|
|
||||||
|
**If style/palette extracted verbally (NO file saved)**:
|
||||||
|
- DO NOT add references metadata to prompt
|
||||||
|
- Append extracted info directly to prompt body using the same MUST INCORPORATE format above:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Reference Style — MUST INCORPORATE (extracted from visual analysis)
|
||||||
|
|
||||||
|
CRITICAL: Apply these specific visual elements extracted from the reference images.
|
||||||
|
|
||||||
|
## REQUIRED elements:
|
||||||
|
- [Same detailed bullet format as above]
|
||||||
|
|
||||||
|
## Integration approach:
|
||||||
|
[Same spatial layout instruction]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reference Analysis Template
|
||||||
|
|
||||||
|
Use this format when analyzing reference images. Extract **specific, concrete, reproducible** details — not vague summaries.
|
||||||
|
|
||||||
|
| Aspect | Analysis Points | Good Example | Bad Example |
|
||||||
|
|--------|-----------------|--------------|-------------|
|
||||||
|
| **Brand elements** | Logos, wordmarks, distinctive typography | "Logo 'm' formed by 3 vertical lines" | "Has a logo" |
|
||||||
|
| **Signature patterns** | Unique motifs, textures, geometric patterns | "Woven curves forming diamond grid" | "Has patterns" |
|
||||||
|
| **Colors** | Exact hex values or close approximations | "#2D4A3E dark teal, #F5F0E0 cream" | "Dark and light" |
|
||||||
|
| **Layout** | Spatial zones, banner placement, proportions | "Bottom 30% is dark banner with branding" | "Has a banner" |
|
||||||
|
| **Typography** | Font style, weight, case, spacing, position | "Uppercase, wide letter-spacing, right-aligned" | "Has text" |
|
||||||
|
| **Rendering** | Line quality, texture, depth treatment | "Topographic contour lines as background texture" | "Clean style" |
|
||||||
|
| **Elements** | Icon vocabulary, decorative motifs | "Geometric intersecting line ornaments at corners" | "Has decorations" |
|
||||||
|
|
||||||
|
**Output**: Each extracted element should be written as a **copy-pasteable prompt instruction** prefixed with "MUST" or "REQUIRED".
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ Scripts in `scripts/` subdirectory. Replace `${SKILL_DIR}` with this SKILL.md's
|
|||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| `scripts/main.ts` | Main entry point with CLI options |
|
| `scripts/main.ts` | Main entry point with CLI options (uses remark-cjk-friendly for CJK emphasis) |
|
||||||
| `scripts/cjk-emphasis.ts` | Fix CJK emphasis/bold punctuation issues |
|
|
||||||
| `scripts/quotes.ts` | Replace ASCII quotes with fullwidth quotes |
|
| `scripts/quotes.ts` | Replace ASCII quotes with fullwidth quotes |
|
||||||
| `scripts/autocorrect.ts` | Add CJK/English spacing via autocorrect |
|
| `scripts/autocorrect.ts` | Add CJK/English spacing via autocorrect |
|
||||||
|
|
||||||
|
|||||||
@@ -1,314 +0,0 @@
|
|||||||
const CJK_PUNCT_COMMON = "。.,、?!:;";
|
|
||||||
const CJK_OPENING_PUNCT = "(〔〖〘〚「『〈《【\u201C\u2018";
|
|
||||||
const CJK_CLOSING_PUNCT = ")〕〗〙〛」』〉》】\u201D\u2019" + CJK_PUNCT_COMMON;
|
|
||||||
const CJK_SCRIPTS =
|
|
||||||
"\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}";
|
|
||||||
|
|
||||||
export const CJK_CLOSING_PUNCT_RE = new RegExp(`[${CJK_CLOSING_PUNCT}]`);
|
|
||||||
export const CJK_OPENING_PUNCT_RE = new RegExp(`^[${CJK_OPENING_PUNCT}]`);
|
|
||||||
export const CJK_CHAR_RE = new RegExp(`[${CJK_SCRIPTS}]`, "u");
|
|
||||||
|
|
||||||
const PUNCT_OR_SYMBOL_RE = /[\p{P}\p{S}]/u;
|
|
||||||
const WORD_CHAR_RE = /[\p{L}\p{N}]/u;
|
|
||||||
|
|
||||||
const CJK_PUNCT_PAIRS: Record<string, string> = {
|
|
||||||
"“": "”",
|
|
||||||
"‘": "’",
|
|
||||||
"(": ")",
|
|
||||||
"〔": "〕",
|
|
||||||
"〖": "〗",
|
|
||||||
"〘": "〙",
|
|
||||||
"〚": "〛",
|
|
||||||
"「": "」",
|
|
||||||
"『": "』",
|
|
||||||
"〈": "〉",
|
|
||||||
"《": "》",
|
|
||||||
"【": "】",
|
|
||||||
};
|
|
||||||
|
|
||||||
function findInlineCodeRanges(text: string): Array<[number, number]> {
|
|
||||||
const ranges: Array<[number, number]> = [];
|
|
||||||
let i = 0;
|
|
||||||
while (i < text.length) {
|
|
||||||
if (text[i] !== "`") {
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let run = 1;
|
|
||||||
while (i + run < text.length && text[i + run] === "`") {
|
|
||||||
run += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = i;
|
|
||||||
let j = i + run;
|
|
||||||
let found = false;
|
|
||||||
while (j < text.length) {
|
|
||||||
if (text[j] !== "`") {
|
|
||||||
j += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let closeRun = 1;
|
|
||||||
while (j + closeRun < text.length && text[j + closeRun] === "`") {
|
|
||||||
closeRun += 1;
|
|
||||||
}
|
|
||||||
if (closeRun === run) {
|
|
||||||
ranges.push([start, j + closeRun - 1]);
|
|
||||||
i = j + closeRun;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
j += closeRun;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
i = start + run;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ranges;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEscaped(text: string, pos: number): boolean {
|
|
||||||
let count = 0;
|
|
||||||
for (let i = pos - 1; i >= 0 && text[i] === "\\"; i -= 1) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
return count % 2 === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWhitespaceChar(ch: string | undefined): boolean {
|
|
||||||
return !ch || /\s/u.test(ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPunctuationOrSymbol(ch: string | undefined): boolean {
|
|
||||||
return !!ch && PUNCT_OR_SYMBOL_RE.test(ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMatchingCjkPunct(open: string, close: string): boolean {
|
|
||||||
return CJK_PUNCT_PAIRS[open] === close;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapNonCodeSegments(
|
|
||||||
block: string,
|
|
||||||
mapper: (segment: string) => string
|
|
||||||
): string {
|
|
||||||
const codeRanges = findInlineCodeRanges(block);
|
|
||||||
if (codeRanges.length === 0) {
|
|
||||||
return mapper(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = "";
|
|
||||||
let lastIndex = 0;
|
|
||||||
for (const [start, end] of codeRanges) {
|
|
||||||
if (start > lastIndex) {
|
|
||||||
result += mapper(block.slice(lastIndex, start));
|
|
||||||
}
|
|
||||||
result += block.slice(start, end + 1);
|
|
||||||
lastIndex = end + 1;
|
|
||||||
}
|
|
||||||
if (lastIndex < block.length) {
|
|
||||||
result += mapper(block.slice(lastIndex));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveCjkPunctuationOutsideEmphasis(block: string): string {
|
|
||||||
return mapNonCodeSegments(block, (segment) => {
|
|
||||||
const delimiterPositions: number[] = [];
|
|
||||||
let cursor = 0;
|
|
||||||
while (cursor < segment.length - 1) {
|
|
||||||
if (
|
|
||||||
segment[cursor] === "*" &&
|
|
||||||
segment[cursor + 1] === "*" &&
|
|
||||||
segment[cursor - 1] !== "*" &&
|
|
||||||
segment[cursor + 2] !== "*" &&
|
|
||||||
!isEscaped(segment, cursor)
|
|
||||||
) {
|
|
||||||
delimiterPositions.push(cursor);
|
|
||||||
cursor += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cursor += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delimiterPositions.length < 2) return segment;
|
|
||||||
|
|
||||||
const stack: number[] = [];
|
|
||||||
const pairs: Array<{ open: number; close: number }> = [];
|
|
||||||
for (const pos of delimiterPositions) {
|
|
||||||
if (stack.length === 0) {
|
|
||||||
stack.push(pos);
|
|
||||||
} else {
|
|
||||||
const open = stack.pop() as number;
|
|
||||||
pairs.push({ open, close: pos });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const skip = new Set<number>();
|
|
||||||
const insertBefore = new Map<number, string>();
|
|
||||||
|
|
||||||
for (const pair of pairs) {
|
|
||||||
const openPunctPos = pair.open + 2;
|
|
||||||
const closePunctPos = pair.close - 1;
|
|
||||||
if (openPunctPos >= closePunctPos) continue;
|
|
||||||
|
|
||||||
const openPunct = segment[openPunctPos];
|
|
||||||
const closePunct = segment[closePunctPos];
|
|
||||||
if (!openPunct || !closePunct) continue;
|
|
||||||
if (!CJK_OPENING_PUNCT_RE.test(openPunct)) continue;
|
|
||||||
if (!CJK_CLOSING_PUNCT_RE.test(closePunct)) continue;
|
|
||||||
if (!isMatchingCjkPunct(openPunct, closePunct)) continue;
|
|
||||||
if (openPunctPos + 1 >= closePunctPos) continue;
|
|
||||||
|
|
||||||
const inner = segment.slice(openPunctPos + 1, closePunctPos);
|
|
||||||
if (inner.length === 0) continue;
|
|
||||||
|
|
||||||
skip.add(openPunctPos);
|
|
||||||
skip.add(closePunctPos);
|
|
||||||
|
|
||||||
insertBefore.set(pair.open, (insertBefore.get(pair.open) ?? "") + openPunct);
|
|
||||||
const afterClose = pair.close + 2;
|
|
||||||
insertBefore.set(
|
|
||||||
afterClose,
|
|
||||||
(insertBefore.get(afterClose) ?? "") + closePunct
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skip.size === 0) return segment;
|
|
||||||
|
|
||||||
let result = "";
|
|
||||||
for (let idx = 0; idx < segment.length; idx += 1) {
|
|
||||||
const insert = insertBefore.get(idx);
|
|
||||||
if (insert) {
|
|
||||||
result += insert;
|
|
||||||
}
|
|
||||||
if (skip.has(idx)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
result += segment[idx];
|
|
||||||
}
|
|
||||||
const tailInsert = insertBefore.get(segment.length);
|
|
||||||
if (tailInsert) {
|
|
||||||
result += tailInsert;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixCjkEmphasisSpacingInBlock(block: string): string {
|
|
||||||
const normalized = moveCjkPunctuationOutsideEmphasis(block);
|
|
||||||
const codeRanges = findInlineCodeRanges(normalized);
|
|
||||||
let rangeIndex = 0;
|
|
||||||
|
|
||||||
const delimiters: Array<{
|
|
||||||
pos: number;
|
|
||||||
canOpen: boolean;
|
|
||||||
canClose: boolean;
|
|
||||||
}> = [];
|
|
||||||
let cursor = 0;
|
|
||||||
while (cursor < normalized.length - 1) {
|
|
||||||
if (rangeIndex < codeRanges.length && cursor >= codeRanges[rangeIndex][0]) {
|
|
||||||
if (cursor <= codeRanges[rangeIndex][1]) {
|
|
||||||
cursor = codeRanges[rangeIndex][1] + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
rangeIndex += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
normalized[cursor] === "*" &&
|
|
||||||
normalized[cursor + 1] === "*" &&
|
|
||||||
normalized[cursor - 1] !== "*" &&
|
|
||||||
normalized[cursor + 2] !== "*" &&
|
|
||||||
!isEscaped(normalized, cursor)
|
|
||||||
) {
|
|
||||||
const before = normalized[cursor - 1];
|
|
||||||
const after = normalized[cursor + 2];
|
|
||||||
const beforeIsSpace = isWhitespaceChar(before);
|
|
||||||
const afterIsSpace = isWhitespaceChar(after);
|
|
||||||
const beforeIsPunct = isPunctuationOrSymbol(before);
|
|
||||||
const afterIsPunct = isPunctuationOrSymbol(after);
|
|
||||||
const leftFlanking =
|
|
||||||
!afterIsSpace && (!afterIsPunct || beforeIsSpace || beforeIsPunct);
|
|
||||||
const rightFlanking =
|
|
||||||
!beforeIsSpace && (!beforeIsPunct || afterIsSpace || afterIsPunct);
|
|
||||||
const cjkPunctBefore = !!before && CJK_CLOSING_PUNCT_RE.test(before);
|
|
||||||
const wordAfter = !!after && WORD_CHAR_RE.test(after);
|
|
||||||
|
|
||||||
delimiters.push({
|
|
||||||
pos: cursor,
|
|
||||||
canOpen: leftFlanking,
|
|
||||||
canClose: rightFlanking || (cjkPunctBefore && wordAfter),
|
|
||||||
});
|
|
||||||
cursor += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stack: Array<{ pos: number }> = [];
|
|
||||||
const pairs: Array<{ open: number; close: number }> = [];
|
|
||||||
for (const delimiter of delimiters) {
|
|
||||||
if (delimiter.canClose) {
|
|
||||||
let openerIndex = -1;
|
|
||||||
for (let j = stack.length - 1; j >= 0; j -= 1) {
|
|
||||||
openerIndex = j;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (openerIndex !== -1) {
|
|
||||||
const opener = stack.splice(openerIndex, 1)[0];
|
|
||||||
pairs.push({ open: opener.pos, close: delimiter.pos });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (delimiter.canOpen) {
|
|
||||||
stack.push({ pos: delimiter.pos });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pairs.length === 0) return normalized;
|
|
||||||
|
|
||||||
const insertPositions = new Set<number>();
|
|
||||||
for (const pair of pairs) {
|
|
||||||
const insideLast = normalized[pair.close - 1];
|
|
||||||
const afterClose = normalized[pair.close + 2];
|
|
||||||
if (!afterClose) continue;
|
|
||||||
if (
|
|
||||||
CJK_CLOSING_PUNCT_RE.test(insideLast) &&
|
|
||||||
WORD_CHAR_RE.test(afterClose)
|
|
||||||
) {
|
|
||||||
insertPositions.add(pair.close + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insertPositions.size === 0) return normalized;
|
|
||||||
|
|
||||||
let result = "";
|
|
||||||
for (let idx = 0; idx < normalized.length; idx += 1) {
|
|
||||||
if (insertPositions.has(idx)) {
|
|
||||||
result += " ";
|
|
||||||
}
|
|
||||||
result += normalized[idx];
|
|
||||||
}
|
|
||||||
if (insertPositions.has(normalized.length)) {
|
|
||||||
result += " ";
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fixCjkEmphasisSpacing(content: string): string {
|
|
||||||
const parts = content.split(/(^```[\s\S]*?^```|^~~~[\s\S]*?^~~~)/m);
|
|
||||||
return parts
|
|
||||||
.map((part, i) => {
|
|
||||||
if (i % 2 === 1) return part;
|
|
||||||
const blocks = part.split(/(\n\s*\n+)/);
|
|
||||||
return blocks
|
|
||||||
.map((block, index) => {
|
|
||||||
if (index % 2 === 1) return block;
|
|
||||||
return fixCjkEmphasisSpacingInBlock(block);
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
import { readFileSync, writeFileSync } from "fs";
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
import { unified } from "unified";
|
import { unified } from "unified";
|
||||||
import remarkParse from "remark-parse";
|
import remarkParse from "remark-parse";
|
||||||
|
import remarkCjkFriendly from "remark-cjk-friendly";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import remarkFrontmatter from "remark-frontmatter";
|
import remarkFrontmatter from "remark-frontmatter";
|
||||||
import remarkStringify from "remark-stringify";
|
import remarkStringify from "remark-stringify";
|
||||||
import { visit } from "unist-util-visit";
|
import { visit } from "unist-util-visit";
|
||||||
import YAML from "yaml";
|
import YAML from "yaml";
|
||||||
import {
|
|
||||||
fixCjkEmphasisSpacing,
|
|
||||||
CJK_CLOSING_PUNCT_RE,
|
|
||||||
CJK_OPENING_PUNCT_RE,
|
|
||||||
CJK_CHAR_RE,
|
|
||||||
} from "./cjk-emphasis";
|
|
||||||
import { replaceQuotes } from "./quotes";
|
import { replaceQuotes } from "./quotes";
|
||||||
import { applyAutocorrect } from "./autocorrect";
|
import { applyAutocorrect } from "./autocorrect";
|
||||||
|
|
||||||
@@ -36,6 +31,12 @@ const DEFAULT_OPTIONS: Required<FormatOptions> = {
|
|||||||
emphasis: true,
|
emphasis: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function decodeHtmlEntities(text: string): string {
|
||||||
|
return text.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) =>
|
||||||
|
String.fromCodePoint(parseInt(hex, 16))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function formatFrontmatter(value: string): string | null {
|
function formatFrontmatter(value: string): string | null {
|
||||||
try {
|
try {
|
||||||
const doc = YAML.parseDocument(value);
|
const doc = YAML.parseDocument(value);
|
||||||
@@ -49,12 +50,9 @@ function formatMarkdownContent(
|
|||||||
content: string,
|
content: string,
|
||||||
options: Required<FormatOptions>
|
options: Required<FormatOptions>
|
||||||
): string {
|
): string {
|
||||||
if (options.emphasis) {
|
|
||||||
content = fixCjkEmphasisSpacing(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
const processor = unified()
|
const processor = unified()
|
||||||
.use(remarkParse)
|
.use(remarkParse)
|
||||||
|
.use(options.emphasis ? remarkCjkFriendly : [])
|
||||||
.use(remarkGfm)
|
.use(remarkGfm)
|
||||||
.use(remarkFrontmatter, ["yaml"])
|
.use(remarkFrontmatter, ["yaml"])
|
||||||
.use(remarkStringify, {
|
.use(remarkStringify, {
|
||||||
@@ -63,7 +61,7 @@ function formatMarkdownContent(
|
|||||||
|
|
||||||
const tree = processor.parse(content);
|
const tree = processor.parse(content);
|
||||||
|
|
||||||
visit(tree, (node, _index, parent) => {
|
visit(tree, (node) => {
|
||||||
if (node.type === "text" && options.quotes) {
|
if (node.type === "text" && options.quotes) {
|
||||||
const textNode = node as { value: string };
|
const textNode = node as { value: string };
|
||||||
textNode.value = replaceQuotes(textNode.value);
|
textNode.value = replaceQuotes(textNode.value);
|
||||||
@@ -77,54 +75,11 @@ function formatMarkdownContent(
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
options.emphasis &&
|
|
||||||
(node.type === "strong" ||
|
|
||||||
node.type === "emphasis" ||
|
|
||||||
node.type === "delete") &&
|
|
||||||
parent
|
|
||||||
) {
|
|
||||||
const siblings = (parent as { children: typeof node[] }).children;
|
|
||||||
const idx = siblings.indexOf(node);
|
|
||||||
const children = (node as { children: typeof node[] }).children;
|
|
||||||
if (!children || children.length === 0) return;
|
|
||||||
|
|
||||||
const lastChild = children[children.length - 1];
|
|
||||||
if (lastChild.type === "text") {
|
|
||||||
const lastText = (lastChild as { value: string }).value;
|
|
||||||
if (
|
|
||||||
CJK_CLOSING_PUNCT_RE.test(lastText.slice(-1)) &&
|
|
||||||
idx + 1 < siblings.length
|
|
||||||
) {
|
|
||||||
const nextSib = siblings[idx + 1];
|
|
||||||
if (nextSib.type === "text") {
|
|
||||||
const nextText = (nextSib as { value: string }).value;
|
|
||||||
if (CJK_CHAR_RE.test(nextText.charAt(0))) {
|
|
||||||
(nextSib as { value: string }).value = " " + nextText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstChild = children[0];
|
|
||||||
if (firstChild.type === "text") {
|
|
||||||
const firstText = (firstChild as { value: string }).value;
|
|
||||||
if (CJK_OPENING_PUNCT_RE.test(firstText) && idx > 0) {
|
|
||||||
const prevSib = siblings[idx - 1];
|
|
||||||
if (prevSib.type === "text") {
|
|
||||||
const prevText = (prevSib as { value: string }).value;
|
|
||||||
if (CJK_CHAR_RE.test(prevText.charAt(prevText.length - 1))) {
|
|
||||||
(prevSib as { value: string }).value = prevText + " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = processor.stringify(tree);
|
let result = processor.stringify(tree);
|
||||||
if (options.emphasis) {
|
if (options.emphasis) {
|
||||||
result = fixCjkEmphasisSpacing(result);
|
result = decodeHtmlEntities(result);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
+1125
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"remark-cjk-friendly": "^1.1.0",
|
||||||
"remark-frontmatter": "^5.0.0",
|
"remark-frontmatter": "^5.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ test -f "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md" && echo "user"
|
|||||||
│ Not found │ Use defaults │
|
│ Not found │ Use defaults │
|
||||||
└───────────┴───────────────────────────────────────────────────────────────────────────┘
|
└───────────┴───────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio
|
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio | Default image size | Default models
|
||||||
|
|
||||||
|
Schema: `references/config/preferences-schema.md`
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -58,9 +60,12 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --quali
|
|||||||
# From prompt files
|
# From prompt files
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
npx -y bun ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||||
|
|
||||||
# With reference images (Google multimodal only)
|
# With reference images (Google multimodal or OpenAI edits)
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
|
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
|
||||||
|
|
||||||
|
# With reference images (explicit provider/model)
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --provider google --model gemini-3-pro-image-preview --ref source.png
|
||||||
|
|
||||||
# Specific provider
|
# Specific provider
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider openai
|
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider openai
|
||||||
|
|
||||||
@@ -76,12 +81,12 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image ou
|
|||||||
| `--promptfiles <files...>` | Read prompt from files (concatenated) |
|
| `--promptfiles <files...>` | Read prompt from files (concatenated) |
|
||||||
| `--image <path>` | Output image path (required) |
|
| `--image <path>` | Output image path (required) |
|
||||||
| `--provider google\|openai\|dashscope` | Force provider (default: google) |
|
| `--provider google\|openai\|dashscope` | Force provider (default: google) |
|
||||||
| `--model <id>`, `-m` | Model ID |
|
| `--model <id>`, `-m` | Model ID (`--ref` with OpenAI requires GPT Image model, e.g. `gpt-image-1.5`) |
|
||||||
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
||||||
| `--size <WxH>` | Size (e.g., `1024x1024`) |
|
| `--size <WxH>` | Size (e.g., `1024x1024`) |
|
||||||
| `--quality normal\|2k` | Quality preset (default: 2k) |
|
| `--quality normal\|2k` | Quality preset (default: 2k) |
|
||||||
| `--imageSize 1K\|2K\|4K` | Image size for Google (default: from quality) |
|
| `--imageSize 1K\|2K\|4K` | Image size for Google (default: from quality) |
|
||||||
| `--ref <files...>` | Reference images (Google multimodal only) |
|
| `--ref <files...>` | Reference images. Supported by Google multimodal and OpenAI edits (GPT Image models). If provider omitted: Google first, then OpenAI |
|
||||||
| `--n <count>` | Number of images |
|
| `--n <count>` | Number of images |
|
||||||
| `--json` | JSON output |
|
| `--json` | JSON output |
|
||||||
|
|
||||||
@@ -99,13 +104,14 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image ou
|
|||||||
| `GOOGLE_BASE_URL` | Custom Google endpoint |
|
| `GOOGLE_BASE_URL` | Custom Google endpoint |
|
||||||
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
|
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
|
||||||
|
|
||||||
**Load Priority**: CLI args > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
|
**Load Priority**: CLI args > EXTEND.md > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
|
||||||
|
|
||||||
## Provider Selection
|
## Provider Selection
|
||||||
|
|
||||||
1. `--provider` specified → use it
|
1. `--ref` provided + no `--provider` → auto-select Google first, then OpenAI
|
||||||
2. Only one API key available → use that provider
|
2. `--provider` specified → use it (if `--ref`, must be `google` or `openai`)
|
||||||
3. Multiple available → default to Google
|
3. Only one API key available → use that provider
|
||||||
|
4. Multiple available → default to Google
|
||||||
|
|
||||||
## Quality Presets
|
## Quality Presets
|
||||||
|
|
||||||
@@ -155,7 +161,7 @@ Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
|
|||||||
- Missing API key → error with setup instructions
|
- Missing API key → error with setup instructions
|
||||||
- Generation failure → auto-retry once
|
- Generation failure → auto-retry once
|
||||||
- Invalid aspect ratio → warning, proceed with default
|
- Invalid aspect ratio → warning, proceed with default
|
||||||
- Reference images with non-multimodal model → warning, ignore refs
|
- Reference images with unsupported provider/model → error with fix hint (switch to Google multimodal or OpenAI GPT Image edits)
|
||||||
|
|
||||||
## Extension Support
|
## Extension Support
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: preferences-schema
|
||||||
|
description: EXTEND.md YAML schema for baoyu-image-gen user preferences
|
||||||
|
---
|
||||||
|
|
||||||
|
# Preferences Schema
|
||||||
|
|
||||||
|
## Full Schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
default_provider: null # google|openai|dashscope|null (null = auto-detect)
|
||||||
|
|
||||||
|
default_quality: null # normal|2k|null (null = use default: 2k)
|
||||||
|
|
||||||
|
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 only, overrides quality)
|
||||||
|
|
||||||
|
default_model:
|
||||||
|
google: null # e.g., "gemini-3-pro-image-preview"
|
||||||
|
openai: null # e.g., "gpt-image-1.5"
|
||||||
|
dashscope: null # e.g., "z-image-turbo"
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Reference
|
||||||
|
|
||||||
|
| Field | Type | Default | Description |
|
||||||
|
|-------|------|---------|-------------|
|
||||||
|
| `version` | int | 1 | Schema version |
|
||||||
|
| `default_provider` | string\|null | null | Default provider (null = auto-detect) |
|
||||||
|
| `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 image size (overrides quality) |
|
||||||
|
| `default_model.google` | string\|null | null | Google default model |
|
||||||
|
| `default_model.openai` | string\|null | null | OpenAI default model |
|
||||||
|
| `default_model.dashscope` | string\|null | null | DashScope default model |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Minimal**:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
version: 1
|
||||||
|
default_provider: google
|
||||||
|
default_quality: 2k
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full**:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
version: 1
|
||||||
|
default_provider: google
|
||||||
|
default_quality: 2k
|
||||||
|
default_aspect_ratio: "16:9"
|
||||||
|
default_image_size: 2K
|
||||||
|
default_model:
|
||||||
|
google: "gemini-3-pro-image-preview"
|
||||||
|
openai: "gpt-image-1.5"
|
||||||
|
dashscope: "z-image-turbo"
|
||||||
|
---
|
||||||
|
```
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
import type { CliArgs, Provider } from "./types";
|
import type { CliArgs, Provider, ExtendConfig } from "./types";
|
||||||
|
|
||||||
function printUsage(): void {
|
function printUsage(): void {
|
||||||
console.log(`Usage:
|
console.log(`Usage:
|
||||||
@@ -20,7 +20,7 @@ Options:
|
|||||||
--size <WxH> Size (e.g., 1024x1024)
|
--size <WxH> Size (e.g., 1024x1024)
|
||||||
--quality normal|2k Quality preset (default: 2k)
|
--quality normal|2k Quality preset (default: 2k)
|
||||||
--imageSize 1K|2K|4K Image size for Google (default: from quality)
|
--imageSize 1K|2K|4K Image size for Google (default: from quality)
|
||||||
--ref <files...> Reference images (Google multimodal only)
|
--ref <files...> Reference images (Google multimodal or OpenAI edits)
|
||||||
--n <count> Number of images (default: 1)
|
--n <count> Number of images (default: 1)
|
||||||
--json JSON output
|
--json JSON output
|
||||||
-h, --help Show help
|
-h, --help Show help
|
||||||
@@ -37,7 +37,7 @@ Environment variables:
|
|||||||
GOOGLE_BASE_URL Custom Google endpoint
|
GOOGLE_BASE_URL Custom Google endpoint
|
||||||
DASHSCOPE_BASE_URL Custom DashScope endpoint
|
DASHSCOPE_BASE_URL Custom DashScope endpoint
|
||||||
|
|
||||||
Env file load order: CLI args > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
|
Env file load order: CLI args > EXTEND.md > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(argv: string[]): CliArgs {
|
function parseArgs(argv: string[]): CliArgs {
|
||||||
@@ -49,7 +49,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|||||||
model: null,
|
model: null,
|
||||||
aspectRatio: null,
|
aspectRatio: null,
|
||||||
size: null,
|
size: null,
|
||||||
quality: "2k",
|
quality: null,
|
||||||
imageSize: null,
|
imageSize: null,
|
||||||
referenceImages: [],
|
referenceImages: [],
|
||||||
n: 1,
|
n: 1,
|
||||||
@@ -215,6 +215,87 @@ async function loadEnv(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractYamlFrontMatter(content: string): string | null {
|
||||||
|
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*$/m);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
|
||||||
|
const config: Partial<ExtendConfig> = {};
|
||||||
|
const lines = yaml.split("\n");
|
||||||
|
let currentKey: string | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||||
|
|
||||||
|
if (trimmed.includes(":") && !trimmed.startsWith("-")) {
|
||||||
|
const colonIdx = trimmed.indexOf(":");
|
||||||
|
const key = trimmed.slice(0, colonIdx).trim();
|
||||||
|
let value = trimmed.slice(colonIdx + 1).trim();
|
||||||
|
|
||||||
|
if (value === "null" || value === "") {
|
||||||
|
value = "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "version") {
|
||||||
|
config.version = value === "null" ? 1 : parseInt(value, 10);
|
||||||
|
} else if (key === "default_provider") {
|
||||||
|
config.default_provider = value === "null" ? null : (value as Provider);
|
||||||
|
} else if (key === "default_quality") {
|
||||||
|
config.default_quality = value === "null" ? null : (value as "normal" | "2k");
|
||||||
|
} else if (key === "default_aspect_ratio") {
|
||||||
|
const cleaned = value.replace(/['"]/g, "");
|
||||||
|
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_model") {
|
||||||
|
config.default_model = { google: null, openai: null, dashscope: null };
|
||||||
|
currentKey = "default_model";
|
||||||
|
} else if (currentKey === "default_model" && (key === "google" || key === "openai" || key === "dashscope")) {
|
||||||
|
const cleaned = value.replace(/['"]/g, "");
|
||||||
|
config.default_model![key] = cleaned === "null" ? null : cleaned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadExtendConfig(): Promise<Partial<ExtendConfig>> {
|
||||||
|
const home = homedir();
|
||||||
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
const paths = [
|
||||||
|
path.join(cwd, ".baoyu-skills", "baoyu-image-gen", "EXTEND.md"),
|
||||||
|
path.join(home, ".baoyu-skills", "baoyu-image-gen", "EXTEND.md"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of paths) {
|
||||||
|
try {
|
||||||
|
const content = await readFile(p, "utf8");
|
||||||
|
const yaml = extractYamlFrontMatter(content);
|
||||||
|
if (!yaml) continue;
|
||||||
|
|
||||||
|
return parseSimpleYaml(yaml);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeConfig(args: CliArgs, extend: Partial<ExtendConfig>): CliArgs {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function readPromptFromFiles(files: string[]): Promise<string> {
|
async function readPromptFromFiles(files: string[]): Promise<string> {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
@@ -242,12 +323,26 @@ function normalizeOutputImagePath(p: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectProvider(args: CliArgs): Provider {
|
function detectProvider(args: CliArgs): Provider {
|
||||||
|
if (args.referenceImages.length > 0 && args.provider && args.provider !== "google" && args.provider !== "openai") {
|
||||||
|
throw new Error(
|
||||||
|
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal) or --provider openai (GPT Image edits)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (args.provider) return args.provider;
|
if (args.provider) return args.provider;
|
||||||
|
|
||||||
const hasGoogle = !!(process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY);
|
const hasGoogle = !!(process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY);
|
||||||
const hasOpenai = !!process.env.OPENAI_API_KEY;
|
const hasOpenai = !!process.env.OPENAI_API_KEY;
|
||||||
const hasDashscope = !!process.env.DASHSCOPE_API_KEY;
|
const hasDashscope = !!process.env.DASHSCOPE_API_KEY;
|
||||||
|
|
||||||
|
if (args.referenceImages.length > 0) {
|
||||||
|
if (hasGoogle) return "google";
|
||||||
|
if (hasOpenai) return "openai";
|
||||||
|
throw new Error(
|
||||||
|
"Reference images require Google or OpenAI. Set GOOGLE_API_KEY/GEMINI_API_KEY or OPENAI_API_KEY, or remove --ref."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const available = [hasGoogle && "google", hasOpenai && "openai", hasDashscope && "dashscope"].filter(Boolean) as Provider[];
|
const available = [hasGoogle && "google", hasOpenai && "openai", hasDashscope && "dashscope"].filter(Boolean) as Provider[];
|
||||||
|
|
||||||
if (available.length === 1) return available[0]!;
|
if (available.length === 1) return available[0]!;
|
||||||
@@ -259,11 +354,34 @@ function detectProvider(args: CliArgs): Provider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function validateReferenceImages(referenceImages: string[]): Promise<void> {
|
||||||
|
for (const refPath of referenceImages) {
|
||||||
|
const fullPath = path.resolve(refPath);
|
||||||
|
try {
|
||||||
|
await access(fullPath);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Reference image not found: ${fullPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ProviderModule = {
|
type ProviderModule = {
|
||||||
getDefaultModel: () => string;
|
getDefaultModel: () => string;
|
||||||
generateImage: (prompt: string, model: string, args: CliArgs) => Promise<Uint8Array>;
|
generateImage: (prompt: string, model: string, args: CliArgs) => Promise<Uint8Array>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isRetryableGenerationError(error: unknown): boolean {
|
||||||
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
|
const nonRetryableMarkers = [
|
||||||
|
"Reference image",
|
||||||
|
"not supported",
|
||||||
|
"only supported",
|
||||||
|
"No API key found",
|
||||||
|
"is required",
|
||||||
|
];
|
||||||
|
return !nonRetryableMarkers.some((marker) => msg.includes(marker));
|
||||||
|
}
|
||||||
|
|
||||||
async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
|
async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
|
||||||
if (provider === "google") {
|
if (provider === "google") {
|
||||||
return (await import("./providers/google")) as ProviderModule;
|
return (await import("./providers/google")) as ProviderModule;
|
||||||
@@ -283,9 +401,13 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await loadEnv();
|
await loadEnv();
|
||||||
|
const extendConfig = await loadExtendConfig();
|
||||||
|
const mergedArgs = mergeConfig(args, extendConfig);
|
||||||
|
|
||||||
let prompt: string | null = args.prompt;
|
if (!mergedArgs.quality) mergedArgs.quality = "2k";
|
||||||
if (!prompt && args.promptFiles.length > 0) prompt = await readPromptFromFiles(args.promptFiles);
|
|
||||||
|
let prompt: string | null = mergedArgs.prompt;
|
||||||
|
if (!prompt && mergedArgs.promptFiles.length > 0) prompt = await readPromptFromFiles(mergedArgs.promptFiles);
|
||||||
if (!prompt) prompt = await readPromptFromStdin();
|
if (!prompt) prompt = await readPromptFromStdin();
|
||||||
|
|
||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
@@ -295,27 +417,39 @@ async function main(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.imagePath) {
|
if (!mergedArgs.imagePath) {
|
||||||
console.error("Error: --image is required");
|
console.error("Error: --image is required");
|
||||||
printUsage();
|
printUsage();
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = detectProvider(args);
|
if (mergedArgs.referenceImages.length > 0) {
|
||||||
|
await validateReferenceImages(mergedArgs.referenceImages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = detectProvider(mergedArgs);
|
||||||
const providerModule = await loadProviderModule(provider);
|
const providerModule = await loadProviderModule(provider);
|
||||||
const model = args.model || providerModule.getDefaultModel();
|
|
||||||
const outputPath = normalizeOutputImagePath(args.imagePath);
|
let model = mergedArgs.model;
|
||||||
|
if (!model && extendConfig.default_model) {
|
||||||
|
if (provider === "google") model = extendConfig.default_model.google ?? null;
|
||||||
|
if (provider === "openai") model = extendConfig.default_model.openai ?? null;
|
||||||
|
if (provider === "dashscope") model = extendConfig.default_model.dashscope ?? null;
|
||||||
|
}
|
||||||
|
model = model || providerModule.getDefaultModel();
|
||||||
|
|
||||||
|
const outputPath = normalizeOutputImagePath(mergedArgs.imagePath);
|
||||||
|
|
||||||
let imageData: Uint8Array;
|
let imageData: Uint8Array;
|
||||||
let retried = false;
|
let retried = false;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
imageData = await providerModule.generateImage(prompt, model, args);
|
imageData = await providerModule.generateImage(prompt, model, mergedArgs);
|
||||||
break;
|
break;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!retried) {
|
if (!retried && isRetryableGenerationError(e)) {
|
||||||
retried = true;
|
retried = true;
|
||||||
console.error("Generation failed, retrying...");
|
console.error("Generation failed, retrying...");
|
||||||
continue;
|
continue;
|
||||||
@@ -328,7 +462,7 @@ async function main(): Promise<void> {
|
|||||||
await mkdir(dir, { recursive: true });
|
await mkdir(dir, { recursive: true });
|
||||||
await writeFile(outputPath, imageData);
|
await writeFile(outputPath, imageData);
|
||||||
|
|
||||||
if (args.json) {
|
if (mergedArgs.json) {
|
||||||
console.log(
|
console.log(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ export async function generateImage(
|
|||||||
if (!apiKey) throw new Error("DASHSCOPE_API_KEY is required");
|
if (!apiKey) throw new Error("DASHSCOPE_API_KEY is required");
|
||||||
|
|
||||||
if (args.referenceImages.length > 0) {
|
if (args.referenceImages.length > 0) {
|
||||||
console.error("Warning: Reference images not yet supported with DashScope, ignoring.");
|
throw new Error(
|
||||||
|
"Reference images are not supported with DashScope provider in baoyu-image-gen. Use --provider google with a Gemini multimodal model."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = args.size ? normalizeSize(args.size) : getSizeFromAspectRatio(args.aspectRatio, args.quality);
|
const size = args.size ? normalizeSize(args.size) : getSizeFromAspectRatio(args.aspectRatio, args.quality);
|
||||||
|
|||||||
@@ -216,13 +216,17 @@ export async function generateImage(
|
|||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
if (isGoogleImagen(model)) {
|
if (isGoogleImagen(model)) {
|
||||||
if (args.referenceImages.length > 0) {
|
if (args.referenceImages.length > 0) {
|
||||||
console.error("Warning: Reference images not supported with Imagen models, ignoring.");
|
throw new Error(
|
||||||
|
"Reference images are not supported with Imagen models. Use gemini-3-pro-image-preview or gemini-3-flash-preview."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return generateWithImagen(prompt, model, args);
|
return generateWithImagen(prompt, model, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGoogleMultimodal(model) && args.referenceImages.length > 0) {
|
if (!isGoogleMultimodal(model) && args.referenceImages.length > 0) {
|
||||||
console.error("Warning: Reference images are only supported with Gemini multimodal models.");
|
throw new Error(
|
||||||
|
"Reference images are only supported with Gemini multimodal models. Use gemini-3-pro-image-preview or gemini-3-flash-preview."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return generateWithGemini(prompt, model, args);
|
return generateWithGemini(prompt, model, args);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
import type { CliArgs } from "../types";
|
import type { CliArgs } from "../types";
|
||||||
|
|
||||||
export function getDefaultModel(): string {
|
export function getDefaultModel(): string {
|
||||||
return process.env.OPENAI_IMAGE_MODEL || "gpt-image-1.5";
|
return process.env.OPENAI_IMAGE_MODEL || "gpt-image-1.5";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenAIImageResponse = { data: Array<{ url?: string; b64_json?: string }> };
|
||||||
|
|
||||||
function parseAspectRatio(ar: string): { width: number; height: number } | null {
|
function parseAspectRatio(ar: string): { width: number; height: number } | null {
|
||||||
const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
|
const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
@@ -66,20 +70,32 @@ export async function generateImage(
|
|||||||
|
|
||||||
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
|
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
|
||||||
|
|
||||||
if (args.referenceImages.length > 0) {
|
|
||||||
console.error("Warning: Reference images not supported with OpenAI, ignoring.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);
|
const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);
|
||||||
|
|
||||||
const body: Record<string, any> = {
|
if (args.referenceImages.length > 0) {
|
||||||
model,
|
if (model.includes("dall-e-2") || model.includes("dall-e-3")) {
|
||||||
prompt,
|
throw new Error(
|
||||||
size,
|
"Reference images with OpenAI in this skill require GPT Image models. Use --model gpt-image-1.5 (or another gpt-image model)."
|
||||||
};
|
);
|
||||||
|
}
|
||||||
|
return generateWithOpenAIEdits(baseURL, apiKey, prompt, model, size, args.referenceImages, args.quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateWithOpenAIGenerations(baseURL, apiKey, prompt, model, size, args.quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateWithOpenAIGenerations(
|
||||||
|
baseURL: string,
|
||||||
|
apiKey: string,
|
||||||
|
prompt: string,
|
||||||
|
model: string,
|
||||||
|
size: string,
|
||||||
|
quality: CliArgs["quality"]
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const body: Record<string, any> = { model, prompt, size };
|
||||||
|
|
||||||
if (model.includes("dall-e-3")) {
|
if (model.includes("dall-e-3")) {
|
||||||
body.quality = args.quality === "2k" ? "hd" : "standard";
|
body.quality = quality === "2k" ? "hd" : "standard";
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${baseURL}/images/generations`, {
|
const res = await fetch(`${baseURL}/images/generations`, {
|
||||||
@@ -96,7 +112,62 @@ export async function generateImage(
|
|||||||
throw new Error(`OpenAI API error: ${err}`);
|
throw new Error(`OpenAI API error: ${err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = (await res.json()) as { data: Array<{ url?: string; b64_json?: string }> };
|
const result = (await res.json()) as OpenAIImageResponse;
|
||||||
|
return extractImageFromResponse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateWithOpenAIEdits(
|
||||||
|
baseURL: string,
|
||||||
|
apiKey: string,
|
||||||
|
prompt: string,
|
||||||
|
model: string,
|
||||||
|
size: string,
|
||||||
|
referenceImages: string[],
|
||||||
|
quality: CliArgs["quality"]
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("model", model);
|
||||||
|
form.append("prompt", prompt);
|
||||||
|
form.append("size", size);
|
||||||
|
|
||||||
|
if (model.includes("gpt-image")) {
|
||||||
|
form.append("quality", quality === "2k" ? "high" : "medium");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const refPath of referenceImages) {
|
||||||
|
const bytes = await readFile(refPath);
|
||||||
|
const filename = path.basename(refPath);
|
||||||
|
const mimeType = getMimeType(filename);
|
||||||
|
const blob = new Blob([bytes], { type: mimeType });
|
||||||
|
form.append("image[]", blob, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${baseURL}/images/edits`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text();
|
||||||
|
throw new Error(`OpenAI edits API error: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = (await res.json()) as OpenAIImageResponse;
|
||||||
|
return extractImageFromResponse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMimeType(filename: string): string {
|
||||||
|
const ext = path.extname(filename).toLowerCase();
|
||||||
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
||||||
|
if (ext === ".webp") return "image/webp";
|
||||||
|
if (ext === ".gif") return "image/gif";
|
||||||
|
return "image/png";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractImageFromResponse(result: OpenAIImageResponse): Promise<Uint8Array> {
|
||||||
const img = result.data[0];
|
const img = result.data[0];
|
||||||
|
|
||||||
if (img?.b64_json) {
|
if (img?.b64_json) {
|
||||||
|
|||||||
@@ -9,10 +9,23 @@ export type CliArgs = {
|
|||||||
model: string | null;
|
model: string | null;
|
||||||
aspectRatio: string | null;
|
aspectRatio: string | null;
|
||||||
size: string | null;
|
size: string | null;
|
||||||
quality: Quality;
|
quality: Quality | null;
|
||||||
imageSize: string | null;
|
imageSize: string | null;
|
||||||
referenceImages: string[];
|
referenceImages: string[];
|
||||||
n: number;
|
n: number;
|
||||||
json: boolean;
|
json: boolean;
|
||||||
help: boolean;
|
help: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExtendConfig = {
|
||||||
|
version: number;
|
||||||
|
default_provider: Provider | null;
|
||||||
|
default_quality: Quality | null;
|
||||||
|
default_aspect_ratio: string | null;
|
||||||
|
default_image_size: "1K" | "2K" | "4K" | null;
|
||||||
|
default_model: {
|
||||||
|
google: string | null;
|
||||||
|
openai: string | null;
|
||||||
|
dashscope: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -153,10 +153,12 @@ Schema: `references/config/preferences-schema.md`
|
|||||||
**1.2 Analyze Content → `analysis.md`**
|
**1.2 Analyze Content → `analysis.md`**
|
||||||
|
|
||||||
1. Save source content (file path or paste → `source.md`)
|
1. Save source content (file path or paste → `source.md`)
|
||||||
|
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
|
||||||
2. Analyze: topic, data type, complexity, tone, audience
|
2. Analyze: topic, data type, complexity, tone, audience
|
||||||
3. Detect source language and user language
|
3. Detect source language and user language
|
||||||
4. Extract design instructions from user input
|
4. Extract design instructions from user input
|
||||||
5. Save analysis
|
5. Save analysis
|
||||||
|
- **Backup rule**: If `analysis.md` exists, rename to `analysis-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
See `references/analysis-framework.md` for detailed format.
|
See `references/analysis-framework.md` for detailed format.
|
||||||
|
|
||||||
@@ -182,13 +184,20 @@ Recommend 3-5 layout×style combinations based on:
|
|||||||
|
|
||||||
### Step 4: Confirm Options
|
### Step 4: Confirm Options
|
||||||
|
|
||||||
Present all options in single confirmation:
|
Use **single AskUserQuestion call** with multiple questions to confirm all options together:
|
||||||
1. **Combination** (always): 3+ options with rationale
|
|
||||||
2. **Aspect** (always): landscape/portrait/square
|
| Question | When | Options |
|
||||||
3. **Language** (only if source ≠ user language): which language for text
|
|----------|------|---------|
|
||||||
|
| **Combination** | Always | 3+ layout×style combos with rationale |
|
||||||
|
| **Aspect** | Always | landscape (16:9), portrait (9:16), square (1:1) |
|
||||||
|
| **Language** | Only if source ≠ user language | Language for text content |
|
||||||
|
|
||||||
|
**Important**: Do NOT split into separate AskUserQuestion calls. Combine all applicable questions into one call.
|
||||||
|
|
||||||
### Step 5: Generate Prompt → `prompts/infographic.md`
|
### Step 5: Generate Prompt → `prompts/infographic.md`
|
||||||
|
|
||||||
|
**Backup rule**: If `prompts/infographic.md` exists, rename to `prompts/infographic-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
Combine:
|
Combine:
|
||||||
1. Layout definition from `references/layouts/<layout>.md`
|
1. Layout definition from `references/layouts/<layout>.md`
|
||||||
2. Style definition from `references/styles/<style>.md`
|
2. Style definition from `references/styles/<style>.md`
|
||||||
@@ -199,8 +208,10 @@ Combine:
|
|||||||
### Step 6: Generate Image
|
### Step 6: Generate Image
|
||||||
|
|
||||||
1. Select available image generation skill (ask user if multiple)
|
1. Select available image generation skill (ask user if multiple)
|
||||||
2. Call with prompt file and output path
|
2. **Check for existing file**: Before generating, check if `infographic.png` exists
|
||||||
3. On failure, auto-retry once
|
- If exists: Rename to `infographic-backup-YYYYMMDD-HHMMSS.png`
|
||||||
|
3. Call with prompt file and output path
|
||||||
|
4. On failure, auto-retry once
|
||||||
|
|
||||||
### Step 7: Output Summary
|
### Step 7: Output Summary
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,18 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
|
|
||||||
const { frontmatter, body } = parseFrontmatter(content);
|
const { frontmatter, body } = parseFrontmatter(content);
|
||||||
|
|
||||||
let title = options?.title ?? frontmatter.title ?? '';
|
const stripQuotes = (s?: string): string => {
|
||||||
|
if (!s) return '';
|
||||||
|
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
if ((s.startsWith('\u201c') && s.endsWith('\u201d')) || (s.startsWith('\u2018') && s.endsWith('\u2019'))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
let title = options?.title ?? stripQuotes(frontmatter.title) ?? '';
|
||||||
if (!title) {
|
if (!title) {
|
||||||
const lines = body.split('\n');
|
const lines = body.split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
@@ -138,8 +149,8 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
|
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
|
||||||
const author = frontmatter.author || '';
|
const author = stripQuotes(frontmatter.author);
|
||||||
let summary = frontmatter.description || frontmatter.summary || '';
|
let summary = stripQuotes(frontmatter.description) || stripQuotes(frontmatter.summary);
|
||||||
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
const lines = body.split('\n');
|
const lines = body.split('\n');
|
||||||
|
|||||||
+1395
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"fflate": "^0.8.2",
|
||||||
|
"front-matter": "^4.0.2",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"juice": "^11.0.1",
|
||||||
|
"marked": "^15.0.6",
|
||||||
|
"reading-time": "^1.5.0",
|
||||||
|
"remark-cjk-friendly": "^1.1.0",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-stringify": "^11.0.0",
|
||||||
|
"unified": "^11.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@ import frontMatter from "front-matter";
|
|||||||
import hljs from "highlight.js/lib/core";
|
import hljs from "highlight.js/lib/core";
|
||||||
import { marked, type RendererObject, type Tokens } from "marked";
|
import { marked, type RendererObject, type Tokens } from "marked";
|
||||||
import readingTime, { type ReadTimeResults } from "reading-time";
|
import readingTime, { type ReadTimeResults } from "reading-time";
|
||||||
|
import { unified } from "unified";
|
||||||
|
import remarkParse from "remark-parse";
|
||||||
|
import remarkCjkFriendly from "remark-cjk-friendly";
|
||||||
|
import remarkStringify from "remark-stringify";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
markedAlert,
|
markedAlert,
|
||||||
@@ -552,12 +556,47 @@ interface CliOptions {
|
|||||||
keepTitle: boolean;
|
keepTitle: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function preprocessCjkEmphasis(markdown: string): string {
|
||||||
|
const processor = unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkCjkFriendly);
|
||||||
|
const tree = processor.parse(markdown);
|
||||||
|
const visit = (node: any, parent?: any, index?: number) => {
|
||||||
|
if (node.children) {
|
||||||
|
for (let i = 0; i < node.children.length; i++) {
|
||||||
|
visit(node.children[i], node, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.type === "strong" && parent && typeof index === "number") {
|
||||||
|
const text = extractText(node);
|
||||||
|
parent.children[index] = { type: "html", value: `<strong>${text}</strong>` };
|
||||||
|
}
|
||||||
|
if (node.type === "emphasis" && parent && typeof index === "number") {
|
||||||
|
const text = extractText(node);
|
||||||
|
parent.children[index] = { type: "html", value: `<em>${text}</em>` };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const extractText = (node: any): string => {
|
||||||
|
if (node.type === "text") return node.value;
|
||||||
|
if (node.children) return node.children.map(extractText).join("");
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
visit(tree);
|
||||||
|
const stringify = unified().use(remarkStringify);
|
||||||
|
let result = stringify.stringify(tree);
|
||||||
|
result = result.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) =>
|
||||||
|
String.fromCodePoint(parseInt(hex, 16))
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function renderMarkdown(raw: string, renderer: RendererAPI): {
|
function renderMarkdown(raw: string, renderer: RendererAPI): {
|
||||||
html: string;
|
html: string;
|
||||||
readingTime: ReadTimeResults;
|
readingTime: ReadTimeResults;
|
||||||
} {
|
} {
|
||||||
|
const preprocessed = preprocessCjkEmphasis(raw);
|
||||||
const { markdownContent, readingTime: readingTimeResult } =
|
const { markdownContent, readingTime: readingTimeResult } =
|
||||||
renderer.parseFrontMatterAndContent(raw);
|
renderer.parseFrontMatterAndContent(preprocessed);
|
||||||
|
|
||||||
const html = marked.parse(markdownContent) as string;
|
const html = marked.parse(markdownContent) as string;
|
||||||
|
|
||||||
@@ -669,14 +708,28 @@ function normalizeThemeCss(css: string): string {
|
|||||||
return stripOutputScope(css);
|
return stripOutputScope(css);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHtmlDocument(title: string, css: string, html: string): string {
|
interface HtmlDocumentMeta {
|
||||||
return [
|
title: string;
|
||||||
|
author?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string): string {
|
||||||
|
const lines = [
|
||||||
"<!doctype html>",
|
"<!doctype html>",
|
||||||
"<html>",
|
"<html>",
|
||||||
"<head>",
|
"<head>",
|
||||||
' <meta charset="utf-8" />',
|
' <meta charset="utf-8" />',
|
||||||
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
||||||
` <title>${title}</title>`,
|
` <title>${meta.title}</title>`,
|
||||||
|
];
|
||||||
|
if (meta.author) {
|
||||||
|
lines.push(` <meta name="author" content="${meta.author}" />`);
|
||||||
|
}
|
||||||
|
if (meta.description) {
|
||||||
|
lines.push(` <meta name="description" content="${meta.description}" />`);
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
` <style>${css}</style>`,
|
` <style>${css}</style>`,
|
||||||
"</head>",
|
"</head>",
|
||||||
"<body>",
|
"<body>",
|
||||||
@@ -684,8 +737,9 @@ function buildHtmlDocument(title: string, css: string, html: string): string {
|
|||||||
html,
|
html,
|
||||||
" </div>",
|
" </div>",
|
||||||
"</body>",
|
"</body>",
|
||||||
"</html>",
|
"</html>"
|
||||||
].join("\n");
|
);
|
||||||
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function inlineCss(html: string): Promise<string> {
|
async function inlineCss(html: string): Promise<string> {
|
||||||
@@ -775,6 +829,7 @@ async function main(): Promise<void> {
|
|||||||
const markdown = fs.readFileSync(inputPath, "utf-8");
|
const markdown = fs.readFileSync(inputPath, "utf-8");
|
||||||
|
|
||||||
const renderer = initRenderer({});
|
const renderer = initRenderer({});
|
||||||
|
const { yamlData } = renderer.parseFrontMatterAndContent(markdown);
|
||||||
const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown(
|
const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown(
|
||||||
markdown,
|
markdown,
|
||||||
renderer
|
renderer
|
||||||
@@ -784,8 +839,23 @@ async function main(): Promise<void> {
|
|||||||
content = removeFirstHeading(content);
|
content = removeFirstHeading(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = path.basename(outputPath, ".html");
|
const stripQuotes = (s?: string): string | undefined => {
|
||||||
const html = buildHtmlDocument(title, css, content);
|
if (!s) return s;
|
||||||
|
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
if ((s.startsWith('\u201c') && s.endsWith('\u201d')) || (s.startsWith('\u2018') && s.endsWith('\u2019'))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta: HtmlDocumentMeta = {
|
||||||
|
title: stripQuotes(yamlData.title) || path.basename(outputPath, ".html"),
|
||||||
|
author: stripQuotes(yamlData.author),
|
||||||
|
description: stripQuotes(yamlData.description) || stripQuotes(yamlData.summary),
|
||||||
|
};
|
||||||
|
const html = buildHtmlDocument(meta, css, content);
|
||||||
const inlinedHtml = normalizeInlineCss(await inlineCss(html));
|
const inlinedHtml = normalizeInlineCss(await inlineCss(html));
|
||||||
const finalHtml = modifyHtmlStructure(inlinedHtml);
|
const finalHtml = modifyHtmlStructure(inlinedHtml);
|
||||||
|
|
||||||
|
|||||||
@@ -120,22 +120,28 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
const { frontmatter, body } = parseFrontmatter(content);
|
const { frontmatter, body } = parseFrontmatter(content);
|
||||||
|
|
||||||
let title = options?.title ?? frontmatter.title ?? '';
|
let title = options?.title ?? frontmatter.title ?? '';
|
||||||
|
let bodyWithoutTitle = body;
|
||||||
if (!title) {
|
if (!title) {
|
||||||
const lines = body.split('\n');
|
const lines = body.split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
const headingMatch = trimmed.match(/^#{1,2}\s+(.+)$/);
|
const headingMatch = trimmed.match(/^#{1,2}\s+(.+)$/);
|
||||||
if (headingMatch) title = headingMatch[1]!;
|
if (headingMatch) {
|
||||||
|
title = headingMatch[1]!;
|
||||||
|
bodyWithoutTitle = body.replace(/^#{1,2}\s+.+\r?\n?/, '');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
bodyWithoutTitle = body.replace(/^#{1,2}\s+.+\r?\n?/, '');
|
||||||
}
|
}
|
||||||
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
|
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
|
||||||
const author = frontmatter.author || '';
|
const author = frontmatter.author || '';
|
||||||
let summary = frontmatter.description || frontmatter.summary || '';
|
let summary = frontmatter.description || frontmatter.summary || '';
|
||||||
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
const lines = body.split('\n');
|
const lines = bodyWithoutTitle.split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
@@ -161,7 +167,7 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
const images: Array<{ src: string; placeholder: string }> = [];
|
const images: Array<{ src: string; placeholder: string }> = [];
|
||||||
let imageCounter = 0;
|
let imageCounter = 0;
|
||||||
|
|
||||||
const modifiedBody = body.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
const modifiedBody = bodyWithoutTitle.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
||||||
const placeholder = `WECHATIMGPH_${++imageCounter}`;
|
const placeholder = `WECHATIMGPH_${++imageCounter}`;
|
||||||
images.push({ src, placeholder });
|
images.push({ src, placeholder });
|
||||||
return placeholder;
|
return placeholder;
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ export function initRenderer(opts: IOpts = {}): RendererAPI {
|
|||||||
|
|
||||||
listCounters[listCounters.length - 1] = idx + 1;
|
listCounters[listCounters.length - 1] = idx + 1;
|
||||||
|
|
||||||
const prefix = ordered ? `${idx}. ` : "• ";
|
const prefix = ordered ? "" : "• ";
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ interface PublishResponse {
|
|||||||
errmsg?: string;
|
errmsg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArticleType = "news" | "newspic";
|
||||||
|
|
||||||
|
interface ArticleOptions {
|
||||||
|
title: string;
|
||||||
|
author?: string;
|
||||||
|
digest?: string;
|
||||||
|
content: string;
|
||||||
|
thumbMediaId: string;
|
||||||
|
articleType: ArticleType;
|
||||||
|
imageMediaIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
const TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
|
const TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
|
||||||
const UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material";
|
const UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material";
|
||||||
const DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add";
|
const DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add";
|
||||||
@@ -176,16 +188,17 @@ async function uploadImagesInHtml(
|
|||||||
html: string,
|
html: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
baseDir: string
|
baseDir: string
|
||||||
): Promise<{ html: string; firstMediaId: string }> {
|
): Promise<{ html: string; firstMediaId: string; allMediaIds: string[] }> {
|
||||||
const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
|
const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
|
||||||
const matches = [...html.matchAll(imgRegex)];
|
const matches = [...html.matchAll(imgRegex)];
|
||||||
|
|
||||||
if (matches.length === 0) {
|
if (matches.length === 0) {
|
||||||
return { html, firstMediaId: "" };
|
return { html, firstMediaId: "", allMediaIds: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
let firstMediaId = "";
|
let firstMediaId = "";
|
||||||
let updatedHtml = html;
|
let updatedHtml = html;
|
||||||
|
const allMediaIds: string[] = [];
|
||||||
|
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const [fullTag, src] = match;
|
const [fullTag, src] = match;
|
||||||
@@ -208,6 +221,7 @@ async function uploadImagesInHtml(
|
|||||||
.replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)
|
.replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)
|
||||||
.replace(/\sdata-local-path=["'][^"']+["']/, "");
|
.replace(/\sdata-local-path=["'][^"']+["']/, "");
|
||||||
updatedHtml = updatedHtml.replace(fullTag, newTag);
|
updatedHtml = updatedHtml.replace(fullTag, newTag);
|
||||||
|
allMediaIds.push(resp.media_id);
|
||||||
if (!firstMediaId) {
|
if (!firstMediaId) {
|
||||||
firstMediaId = resp.media_id;
|
firstMediaId = resp.media_id;
|
||||||
}
|
}
|
||||||
@@ -216,28 +230,51 @@ async function uploadImagesInHtml(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { html: updatedHtml, firstMediaId };
|
return { html: updatedHtml, firstMediaId, allMediaIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function publishToDraft(
|
async function publishToDraft(
|
||||||
title: string,
|
options: ArticleOptions,
|
||||||
html: string,
|
|
||||||
thumbMediaId: string,
|
|
||||||
accessToken: string
|
accessToken: string
|
||||||
): Promise<PublishResponse> {
|
): Promise<PublishResponse> {
|
||||||
const url = `${DRAFT_URL}?access_token=${accessToken}`;
|
const url = `${DRAFT_URL}?access_token=${accessToken}`;
|
||||||
|
|
||||||
|
let article: Record<string, unknown>;
|
||||||
|
|
||||||
|
if (options.articleType === "newspic") {
|
||||||
|
if (!options.imageMediaIds || options.imageMediaIds.length === 0) {
|
||||||
|
throw new Error("newspic requires at least one image");
|
||||||
|
}
|
||||||
|
article = {
|
||||||
|
article_type: "newspic",
|
||||||
|
title: options.title,
|
||||||
|
content: options.content,
|
||||||
|
need_open_comment: 1,
|
||||||
|
only_fans_can_comment: 0,
|
||||||
|
image_info: {
|
||||||
|
image_list: options.imageMediaIds.map(id => ({ image_media_id: id })),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (options.author) article.author = options.author;
|
||||||
|
} else {
|
||||||
|
article = {
|
||||||
|
article_type: "news",
|
||||||
|
title: options.title,
|
||||||
|
content: options.content,
|
||||||
|
thumb_media_id: options.thumbMediaId,
|
||||||
|
need_open_comment: 1,
|
||||||
|
only_fans_can_comment: 0,
|
||||||
|
};
|
||||||
|
if (options.author) article.author = options.author;
|
||||||
|
if (options.digest) article.digest = options.digest;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ articles: [article] }),
|
||||||
articles: [{
|
|
||||||
title,
|
|
||||||
content: html,
|
|
||||||
thumb_media_id: thumbMediaId,
|
|
||||||
}],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json() as PublishResponse;
|
const data = await res.json() as PublishResponse;
|
||||||
@@ -315,13 +352,24 @@ Arguments:
|
|||||||
file Markdown (.md) or HTML (.html) file
|
file Markdown (.md) or HTML (.html) file
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
--type <type> Article type: news (文章, default) or newspic (图文)
|
||||||
--title <title> Override title
|
--title <title> Override title
|
||||||
--summary <text> Article summary (for future use)
|
--author <name> Author name (max 16 chars)
|
||||||
|
--summary <text> Article summary/digest (max 128 chars)
|
||||||
--theme <name> Theme name for markdown (default, grace, simple). Default: default
|
--theme <name> Theme name for markdown (default, grace, simple). Default: default
|
||||||
--cover <path> Cover image path (local or URL)
|
--cover <path> Cover image path (local or URL)
|
||||||
--dry-run Parse and render only, don't publish
|
--dry-run Parse and render only, don't publish
|
||||||
--help Show this help
|
--help Show this help
|
||||||
|
|
||||||
|
Frontmatter Fields (markdown):
|
||||||
|
title Article title
|
||||||
|
author Author name
|
||||||
|
digest/summary Article summary
|
||||||
|
featureImage/coverImage/cover/image Cover image path
|
||||||
|
|
||||||
|
Comments:
|
||||||
|
Comments are enabled by default, open to all users.
|
||||||
|
|
||||||
Environment Variables:
|
Environment Variables:
|
||||||
WECHAT_APP_ID WeChat App ID
|
WECHAT_APP_ID WeChat App ID
|
||||||
WECHAT_APP_SECRET WeChat App Secret
|
WECHAT_APP_SECRET WeChat App Secret
|
||||||
@@ -334,7 +382,9 @@ Config File Locations (in priority order):
|
|||||||
Example:
|
Example:
|
||||||
npx -y bun wechat-api.ts article.md
|
npx -y bun wechat-api.ts article.md
|
||||||
npx -y bun wechat-api.ts article.md --theme grace --cover cover.png
|
npx -y bun wechat-api.ts article.md --theme grace --cover cover.png
|
||||||
|
npx -y bun wechat-api.ts article.md --author "Author Name" --summary "Brief intro"
|
||||||
npx -y bun wechat-api.ts article.html --title "My Article"
|
npx -y bun wechat-api.ts article.html --title "My Article"
|
||||||
|
npx -y bun wechat-api.ts images/ --type newspic --title "Photo Album"
|
||||||
npx -y bun wechat-api.ts article.md --dry-run
|
npx -y bun wechat-api.ts article.md --dry-run
|
||||||
`);
|
`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
@@ -343,7 +393,9 @@ Example:
|
|||||||
interface CliArgs {
|
interface CliArgs {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
isHtml: boolean;
|
isHtml: boolean;
|
||||||
|
articleType: ArticleType;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
author?: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
cover?: string;
|
cover?: string;
|
||||||
@@ -358,14 +410,22 @@ function parseArgs(argv: string[]): CliArgs {
|
|||||||
const args: CliArgs = {
|
const args: CliArgs = {
|
||||||
filePath: "",
|
filePath: "",
|
||||||
isHtml: false,
|
isHtml: false,
|
||||||
|
articleType: "news",
|
||||||
theme: "default",
|
theme: "default",
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < argv.length; i++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
const arg = argv[i]!;
|
const arg = argv[i]!;
|
||||||
if (arg === "--title" && argv[i + 1]) {
|
if (arg === "--type" && argv[i + 1]) {
|
||||||
|
const t = argv[++i]!.toLowerCase();
|
||||||
|
if (t === "news" || t === "newspic") {
|
||||||
|
args.articleType = t;
|
||||||
|
}
|
||||||
|
} else if (arg === "--title" && argv[i + 1]) {
|
||||||
args.title = argv[++i];
|
args.title = argv[++i];
|
||||||
|
} else if (arg === "--author" && argv[i + 1]) {
|
||||||
|
args.author = argv[++i];
|
||||||
} else if (arg === "--summary" && argv[i + 1]) {
|
} else if (arg === "--summary" && argv[i + 1]) {
|
||||||
args.summary = argv[++i];
|
args.summary = argv[++i];
|
||||||
} else if (arg === "--theme" && argv[i + 1]) {
|
} else if (arg === "--theme" && argv[i + 1]) {
|
||||||
@@ -410,6 +470,8 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
const baseDir = path.dirname(filePath);
|
const baseDir = path.dirname(filePath);
|
||||||
let title = args.title || "";
|
let title = args.title || "";
|
||||||
|
let author = args.author || "";
|
||||||
|
let digest = args.summary || "";
|
||||||
let htmlPath: string;
|
let htmlPath: string;
|
||||||
let htmlContent: string;
|
let htmlContent: string;
|
||||||
let frontmatter: Record<string, string> = {};
|
let frontmatter: Record<string, string> = {};
|
||||||
@@ -422,9 +484,9 @@ async function main(): Promise<void> {
|
|||||||
const mdContent = fs.readFileSync(mdPath, "utf-8");
|
const mdContent = fs.readFileSync(mdPath, "utf-8");
|
||||||
const parsed = parseFrontmatter(mdContent);
|
const parsed = parseFrontmatter(mdContent);
|
||||||
frontmatter = parsed.frontmatter;
|
frontmatter = parsed.frontmatter;
|
||||||
if (!title && frontmatter.title) {
|
if (!title && frontmatter.title) title = frontmatter.title;
|
||||||
title = frontmatter.title;
|
if (!author) author = frontmatter.author || "";
|
||||||
}
|
if (!digest) digest = frontmatter.digest || frontmatter.summary || frontmatter.description || "";
|
||||||
}
|
}
|
||||||
if (!title) {
|
if (!title) {
|
||||||
title = extractHtmlTitle(fs.readFileSync(htmlPath, "utf-8"));
|
title = extractHtmlTitle(fs.readFileSync(htmlPath, "utf-8"));
|
||||||
@@ -441,6 +503,8 @@ async function main(): Promise<void> {
|
|||||||
const h1Match = body.match(/^#\s+(.+)$/m);
|
const h1Match = body.match(/^#\s+(.+)$/m);
|
||||||
if (h1Match) title = h1Match[1]!;
|
if (h1Match) title = h1Match[1]!;
|
||||||
}
|
}
|
||||||
|
if (!author) author = frontmatter.author || "";
|
||||||
|
if (!digest) digest = frontmatter.digest || frontmatter.summary || frontmatter.description || "";
|
||||||
|
|
||||||
console.error(`[wechat-api] Theme: ${args.theme}`);
|
console.error(`[wechat-api] Theme: ${args.theme}`);
|
||||||
htmlPath = renderMarkdownToHtml(filePath, args.theme);
|
htmlPath = renderMarkdownToHtml(filePath, args.theme);
|
||||||
@@ -454,10 +518,16 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.error(`[wechat-api] Title: ${title}`);
|
console.error(`[wechat-api] Title: ${title}`);
|
||||||
|
if (author) console.error(`[wechat-api] Author: ${author}`);
|
||||||
|
if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`);
|
||||||
|
console.error(`[wechat-api] Type: ${args.articleType}`);
|
||||||
|
|
||||||
if (args.dryRun) {
|
if (args.dryRun) {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
|
articleType: args.articleType,
|
||||||
title,
|
title,
|
||||||
|
author: author || undefined,
|
||||||
|
digest: digest || undefined,
|
||||||
htmlPath,
|
htmlPath,
|
||||||
contentLength: htmlContent.length,
|
contentLength: htmlContent.length,
|
||||||
}, null, 2));
|
}, null, 2));
|
||||||
@@ -469,7 +539,7 @@ async function main(): Promise<void> {
|
|||||||
const accessToken = await fetchAccessToken(config.appId, config.appSecret);
|
const accessToken = await fetchAccessToken(config.appId, config.appSecret);
|
||||||
|
|
||||||
console.error("[wechat-api] Uploading images...");
|
console.error("[wechat-api] Uploading images...");
|
||||||
const { html: processedHtml, firstMediaId } = await uploadImagesInHtml(
|
const { html: processedHtml, firstMediaId, allMediaIds } = await uploadImagesInHtml(
|
||||||
htmlContent,
|
htmlContent,
|
||||||
accessToken,
|
accessToken,
|
||||||
baseDir
|
baseDir
|
||||||
@@ -497,18 +567,32 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!thumbMediaId) {
|
if (args.articleType === "news" && !thumbMediaId) {
|
||||||
console.error("Error: No cover image. Provide via --cover, frontmatter.featureImage, or include an image in content.");
|
console.error("Error: No cover image. Provide via --cover, frontmatter.featureImage, or include an image in content.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.articleType === "newspic" && allMediaIds.length === 0) {
|
||||||
|
console.error("Error: newspic requires at least one image in content.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
console.error("[wechat-api] Publishing to draft...");
|
console.error("[wechat-api] Publishing to draft...");
|
||||||
const result = await publishToDraft(title, htmlContent, thumbMediaId, accessToken);
|
const result = await publishToDraft({
|
||||||
|
title,
|
||||||
|
author: author || undefined,
|
||||||
|
digest: digest || undefined,
|
||||||
|
content: htmlContent,
|
||||||
|
thumbMediaId,
|
||||||
|
articleType: args.articleType,
|
||||||
|
imageMediaIds: args.articleType === "newspic" ? allMediaIds : undefined,
|
||||||
|
}, accessToken);
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
media_id: result.media_id,
|
media_id: result.media_id,
|
||||||
title,
|
title,
|
||||||
|
articleType: args.articleType,
|
||||||
}, null, 2));
|
}, null, 2));
|
||||||
|
|
||||||
console.error(`[wechat-api] Published successfully! media_id: ${result.media_id}`);
|
console.error(`[wechat-api] Published successfully! media_id: ${result.media_id}`);
|
||||||
|
|||||||
@@ -273,22 +273,31 @@ async function selectAndReplacePlaceholder(session: ChromeSession, placeholder:
|
|||||||
const editor = document.querySelector('.ProseMirror');
|
const editor = document.querySelector('.ProseMirror');
|
||||||
if (!editor) return false;
|
if (!editor) return false;
|
||||||
|
|
||||||
|
const placeholder = ${JSON.stringify(placeholder)};
|
||||||
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null, false);
|
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null, false);
|
||||||
let node;
|
let node;
|
||||||
|
|
||||||
while ((node = walker.nextNode())) {
|
while ((node = walker.nextNode())) {
|
||||||
const text = node.textContent || '';
|
const text = node.textContent || '';
|
||||||
const idx = text.indexOf(${JSON.stringify(placeholder)});
|
let searchStart = 0;
|
||||||
if (idx !== -1) {
|
let idx;
|
||||||
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
// Search for exact match (not prefix of longer placeholder like XIMGPH_1 in XIMGPH_10)
|
||||||
|
while ((idx = text.indexOf(placeholder, searchStart)) !== -1) {
|
||||||
|
const afterIdx = idx + placeholder.length;
|
||||||
|
const charAfter = text[afterIdx];
|
||||||
|
// Exact match if next char is not a digit
|
||||||
|
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
||||||
|
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.setStart(node, idx);
|
range.setStart(node, idx);
|
||||||
range.setEnd(node, idx + ${placeholder.length});
|
range.setEnd(node, idx + placeholder.length);
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
sel.addRange(range);
|
sel.addRange(range);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
searchStart = afterIdx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -306,6 +315,71 @@ async function pressDeleteKey(session: ChromeSession): Promise<void> {
|
|||||||
await session.cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId: session.sessionId });
|
await session.cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId: session.sessionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<boolean> {
|
||||||
|
const removed = await evaluate<boolean>(session, `
|
||||||
|
(function() {
|
||||||
|
const editor = document.querySelector('.ProseMirror');
|
||||||
|
if (!editor) return false;
|
||||||
|
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (!sel || sel.rangeCount === 0) return false;
|
||||||
|
|
||||||
|
let node = sel.anchorNode;
|
||||||
|
if (!node) return false;
|
||||||
|
let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
||||||
|
if (!element || !editor.contains(element)) return false;
|
||||||
|
|
||||||
|
const isEmptyParagraph = (el) => {
|
||||||
|
if (!el || el.tagName !== 'P') return false;
|
||||||
|
const text = (el.textContent || '').trim();
|
||||||
|
if (text.length > 0) return false;
|
||||||
|
return el.querySelectorAll('img, figure, video, iframe').length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasImage = (el) => {
|
||||||
|
if (!el) return false;
|
||||||
|
return !!el.querySelector('img, figure img, picture img');
|
||||||
|
};
|
||||||
|
|
||||||
|
const placeCursorAfter = (el) => {
|
||||||
|
if (!el) return;
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStartAfter(el);
|
||||||
|
range.collapse(true);
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Case 1: caret is inside an empty paragraph right after an image block.
|
||||||
|
const emptyPara = element.closest('p');
|
||||||
|
if (emptyPara && editor.contains(emptyPara) && isEmptyParagraph(emptyPara)) {
|
||||||
|
const prev = emptyPara.previousElementSibling;
|
||||||
|
if (prev && hasImage(prev)) {
|
||||||
|
emptyPara.remove();
|
||||||
|
placeCursorAfter(prev);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: caret is on the image block itself; remove the next empty paragraph.
|
||||||
|
const imageBlock = element.closest('figure, p');
|
||||||
|
if (imageBlock && editor.contains(imageBlock) && hasImage(imageBlock)) {
|
||||||
|
const next = imageBlock.nextElementSibling;
|
||||||
|
if (next && isEmptyParagraph(next)) {
|
||||||
|
next.remove();
|
||||||
|
placeCursorAfter(imageBlock);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})()
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (removed) console.log('[wechat] Removed extra empty line after image.');
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
export async function postArticle(options: ArticleOptions): Promise<void> {
|
export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||||
const { title, content, htmlFile, markdownFile, theme, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
|
const { title, content, htmlFile, markdownFile, theme, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
|
||||||
let { contentImages = [] } = options;
|
let { contentImages = [] } = options;
|
||||||
@@ -445,11 +519,6 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
await evaluate(session, `document.querySelector('#author').value = ${JSON.stringify(effectiveAuthor)}; document.querySelector('#author').dispatchEvent(new Event('input', { bubbles: true }));`);
|
await evaluate(session, `document.querySelector('#author').value = ${JSON.stringify(effectiveAuthor)}; document.querySelector('#author').dispatchEvent(new Event('input', { bubbles: true }));`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveSummary) {
|
|
||||||
console.log(`[wechat] Filling summary: ${effectiveSummary}`);
|
|
||||||
await evaluate(session, `document.querySelector('#js_description').value = ${JSON.stringify(effectiveSummary)}; document.querySelector('#js_description').dispatchEvent(new Event('input', { bubbles: true }));`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
if (effectiveTitle) {
|
if (effectiveTitle) {
|
||||||
@@ -461,15 +530,6 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveSummary) {
|
|
||||||
const actualSummary = await evaluate<string>(session, `document.querySelector('#js_description')?.value || ''`);
|
|
||||||
if (actualSummary === effectiveSummary) {
|
|
||||||
console.log('[wechat] Summary verified OK.');
|
|
||||||
} else {
|
|
||||||
console.warn(`[wechat] Summary verification failed. Expected: "${effectiveSummary}", got: "${actualSummary}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[wechat] Clicking on editor...');
|
console.log('[wechat] Clicking on editor...');
|
||||||
await clickElement(session, '.ProseMirror');
|
await clickElement(session, '.ProseMirror');
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
@@ -525,6 +585,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
console.log('[wechat] Pasting image...');
|
console.log('[wechat] Pasting image...');
|
||||||
await pasteFromClipboardInEditor(session);
|
await pasteFromClipboardInEditor(session);
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
|
await removeExtraEmptyLineAfterImage(session);
|
||||||
}
|
}
|
||||||
console.log('[wechat] All images inserted.');
|
console.log('[wechat] All images inserted.');
|
||||||
}
|
}
|
||||||
@@ -536,6 +597,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
await sleep(500);
|
await sleep(500);
|
||||||
await pasteInEditor(session);
|
await pasteInEditor(session);
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
|
await removeExtraEmptyLineAfterImage(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,6 +620,30 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (effectiveSummary) {
|
||||||
|
console.log(`[wechat] Filling summary (after content paste): ${effectiveSummary}`);
|
||||||
|
await evaluate(session, `
|
||||||
|
(function() {
|
||||||
|
const el = document.querySelector('#js_description');
|
||||||
|
if (!el) return;
|
||||||
|
el.focus();
|
||||||
|
el.select();
|
||||||
|
el.value = ${JSON.stringify(effectiveSummary)};
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||||
|
})()
|
||||||
|
`);
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const actualSummary = await evaluate<string>(session, `document.querySelector('#js_description')?.value || ''`);
|
||||||
|
if (actualSummary === effectiveSummary) {
|
||||||
|
console.log('[wechat] Summary verified OK.');
|
||||||
|
} else {
|
||||||
|
console.warn(`[wechat] Summary verification failed. Expected: "${effectiveSummary}", got: "${actualSummary}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[wechat] Saving as draft...');
|
console.log('[wechat] Saving as draft...');
|
||||||
await evaluate(session, `document.querySelector('#js_submit button').click()`);
|
await evaluate(session, `document.querySelector('#js_submit button').click()`);
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
|
|||||||
@@ -61,6 +61,25 @@ interface ArticleOptions {
|
|||||||
chromePath?: string;
|
chromePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findExistingDebugPort(profileDir: string): Promise<number | null> {
|
||||||
|
const portFile = path.join(profileDir, 'DevToolsActivePort');
|
||||||
|
if (!fs.existsSync(portFile)) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(portFile, 'utf-8').trim();
|
||||||
|
if (!content) return null;
|
||||||
|
const [portLine] = content.split(/\r?\n/);
|
||||||
|
const port = Number(portLine);
|
||||||
|
if (!Number.isFinite(port) || port <= 0) return null;
|
||||||
|
|
||||||
|
// Verify the port is actually active.
|
||||||
|
await waitForChromeDebugPort(port, 1500, { includeLastError: true });
|
||||||
|
return port;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function publishArticle(options: ArticleOptions): Promise<void> {
|
export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||||
const { markdownPath, submit = false, profileDir = getDefaultProfileDir() } = options;
|
const { markdownPath, submit = false, profileDir = getDefaultProfileDir() } = options;
|
||||||
|
|
||||||
@@ -83,28 +102,33 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
if (!chromePath) throw new Error('Chrome not found');
|
if (!chromePath) throw new Error('Chrome not found');
|
||||||
|
|
||||||
await mkdir(profileDir, { recursive: true });
|
await mkdir(profileDir, { recursive: true });
|
||||||
const port = await getFreePort();
|
const existingPort = await findExistingDebugPort(profileDir);
|
||||||
|
const port = existingPort ?? await getFreePort();
|
||||||
|
|
||||||
console.log(`[x-article] Launching Chrome...`);
|
if (existingPort) {
|
||||||
const chrome = spawn(chromePath, [
|
console.log(`[x-article] Reusing existing Chrome instance on port ${port}`);
|
||||||
`--remote-debugging-port=${port}`,
|
} else {
|
||||||
`--user-data-dir=${profileDir}`,
|
console.log(`[x-article] Launching Chrome...`);
|
||||||
'--no-first-run',
|
spawn(chromePath, [
|
||||||
'--no-default-browser-check',
|
`--remote-debugging-port=${port}`,
|
||||||
'--disable-blink-features=AutomationControlled',
|
`--user-data-dir=${profileDir}`,
|
||||||
'--start-maximized',
|
'--no-first-run',
|
||||||
X_ARTICLES_URL,
|
'--no-default-browser-check',
|
||||||
], { stdio: 'ignore' });
|
'--disable-blink-features=AutomationControlled',
|
||||||
|
'--start-maximized',
|
||||||
|
X_ARTICLES_URL,
|
||||||
|
], { stdio: 'ignore' });
|
||||||
|
}
|
||||||
|
|
||||||
let cdp: CdpConnection | null = null;
|
let cdp: CdpConnection | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wsUrl = await waitForChromeDebugPort(port, 30_000);
|
const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true });
|
||||||
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 30_000 });
|
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 30_000 });
|
||||||
|
|
||||||
// Get page target
|
// Get page target
|
||||||
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
||||||
let pageTarget = targets.targetInfos.find((t) => t.type === 'page' && t.url.includes('x.com'));
|
let pageTarget = targets.targetInfos.find((t) => t.type === 'page' && t.url.startsWith(X_ARTICLES_URL));
|
||||||
|
|
||||||
if (!pageTarget) {
|
if (!pageTarget) {
|
||||||
const { targetId } = await cdp.send<{ targetId: string }>('Target.createTarget', { url: X_ARTICLES_URL });
|
const { targetId } = await cdp.send<{ targetId: string }>('Target.createTarget', { url: X_ARTICLES_URL });
|
||||||
@@ -409,15 +433,23 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
console.log('[x-article] Checking for placeholders in content...');
|
console.log('[x-article] Checking for placeholders in content...');
|
||||||
for (const img of parsed.contentImages) {
|
for (const img of parsed.contentImages) {
|
||||||
if (editorContent.result.value.includes(img.placeholder)) {
|
// Use regex for exact match (not followed by digit, e.g., XIMGPH_1 should not match XIMGPH_10)
|
||||||
|
const regex = new RegExp(img.placeholder + '(?!\\d)');
|
||||||
|
if (regex.test(editorContent.result.value)) {
|
||||||
console.log(`[x-article] Found: ${img.placeholder}`);
|
console.log(`[x-article] Found: ${img.placeholder}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[x-article] NOT found: ${img.placeholder}`);
|
console.log(`[x-article] NOT found: ${img.placeholder}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process images in sequential order (1, 2, 3, ...)
|
// Process images in XIMGPH order (1, 2, 3, ...) regardless of blockIndex
|
||||||
const sortedImages = [...parsed.contentImages].sort((a, b) => a.blockIndex - b.blockIndex);
|
const getPlaceholderIndex = (placeholder: string): number => {
|
||||||
|
const match = placeholder.match(/XIMGPH_(\d+)/);
|
||||||
|
return match ? Number(match[1]) : Number.POSITIVE_INFINITY;
|
||||||
|
};
|
||||||
|
const sortedImages = [...parsed.contentImages].sort(
|
||||||
|
(a, b) => getPlaceholderIndex(a.placeholder) - getPlaceholderIndex(b.placeholder),
|
||||||
|
);
|
||||||
|
|
||||||
for (let i = 0; i < sortedImages.length; i++) {
|
for (let i = 0; i < sortedImages.length; i++) {
|
||||||
const img = sortedImages[i]!;
|
const img = sortedImages[i]!;
|
||||||
@@ -440,22 +472,30 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
while ((node = walker.nextNode())) {
|
while ((node = walker.nextNode())) {
|
||||||
const text = node.textContent || '';
|
const text = node.textContent || '';
|
||||||
const idx = text.indexOf(placeholder);
|
let searchStart = 0;
|
||||||
if (idx !== -1) {
|
let idx;
|
||||||
// Found the placeholder - scroll to it first
|
// Search for exact match (not prefix of longer placeholder like XIMGPH_1 in XIMGPH_10)
|
||||||
const parentElement = node.parentElement;
|
while ((idx = text.indexOf(placeholder, searchStart)) !== -1) {
|
||||||
if (parentElement) {
|
const afterIdx = idx + placeholder.length;
|
||||||
parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
const charAfter = text[afterIdx];
|
||||||
}
|
// Exact match if next char is not a digit (XIMGPH_1 should not match XIMGPH_10)
|
||||||
|
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
||||||
|
// Found exact placeholder - scroll to it first
|
||||||
|
const parentElement = node.parentElement;
|
||||||
|
if (parentElement) {
|
||||||
|
parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
// Select it
|
// Select it
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.setStart(node, idx);
|
range.setStart(node, idx);
|
||||||
range.setEnd(node, idx + placeholder.length);
|
range.setEnd(node, idx + placeholder.length);
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
sel.addRange(range);
|
sel.addRange(range);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
searchStart = afterIdx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -505,31 +545,54 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
// Wait for clipboard to be fully ready
|
// Wait for clipboard to be fully ready
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
|
||||||
// Delete placeholder by pressing Backspace (more reliable than Enter for replacing selection)
|
// Delete placeholder using execCommand (more reliable than keyboard events for DraftJS)
|
||||||
console.log(`[x-article] Deleting placeholder...`);
|
console.log(`[x-article] Deleting placeholder...`);
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
const deleteResult = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
expression: `(() => {
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (!sel || sel.isCollapsed) return false;
|
||||||
|
// Try execCommand delete first
|
||||||
|
if (document.execCommand('delete', false)) return true;
|
||||||
|
// Fallback: replace selection with empty using insertText
|
||||||
|
document.execCommand('insertText', false, '');
|
||||||
|
return true;
|
||||||
|
})()`,
|
||||||
|
returnByValue: true,
|
||||||
|
}, { sessionId });
|
||||||
|
|
||||||
// Wait and verify placeholder is deleted
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
// Check that placeholder is no longer in editor
|
// Check that placeholder is no longer in editor (exact match, not substring)
|
||||||
const afterDelete = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
const afterDelete = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||||
expression: `(() => {
|
expression: `(() => {
|
||||||
const editor = document.querySelector('.DraftEditor-editorContainer [data-contents="true"]');
|
const editor = document.querySelector('.DraftEditor-editorContainer [data-contents="true"]');
|
||||||
if (!editor) return true;
|
if (!editor) return true;
|
||||||
return !editor.innerText.includes(${JSON.stringify(img.placeholder)});
|
const text = editor.innerText;
|
||||||
|
const placeholder = ${JSON.stringify(img.placeholder)};
|
||||||
|
// Use regex to find exact match (not followed by digit)
|
||||||
|
const regex = new RegExp(placeholder + '(?!\\\\d)');
|
||||||
|
return !regex.test(text);
|
||||||
})()`,
|
})()`,
|
||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
|
|
||||||
if (!afterDelete.result.value) {
|
if (!afterDelete.result.value) {
|
||||||
console.warn(`[x-article] Placeholder may not have been deleted, trying again...`);
|
console.warn(`[x-article] Placeholder may not have been deleted, trying dispatchEvent...`);
|
||||||
// Try selecting and deleting again
|
// Try selecting and deleting with InputEvent
|
||||||
await selectPlaceholder(1);
|
await selectPlaceholder(1);
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
await cdp.send('Runtime.evaluate', {
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
expression: `(() => {
|
||||||
|
const editor = document.querySelector('.DraftEditor-editorContainer [contenteditable="true"]');
|
||||||
|
if (!editor) return;
|
||||||
|
editor.focus();
|
||||||
|
// Dispatch beforeinput and input events for deletion
|
||||||
|
const beforeEvent = new InputEvent('beforeinput', { inputType: 'deleteContentBackward', bubbles: true, cancelable: true });
|
||||||
|
editor.dispatchEvent(beforeEvent);
|
||||||
|
const inputEvent = new InputEvent('input', { inputType: 'deleteContentBackward', bubbles: true });
|
||||||
|
editor.dispatchEvent(inputEvent);
|
||||||
|
})()`,
|
||||||
|
}, { sessionId });
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ Schema: `references/config/preferences-schema.md`
|
|||||||
**1.2 Analyze Content**
|
**1.2 Analyze Content**
|
||||||
|
|
||||||
1. Save source content (if pasted, save as `source.md`)
|
1. Save source content (if pasted, save as `source.md`)
|
||||||
|
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
|
||||||
2. Follow `references/analysis-framework.md` for content analysis
|
2. Follow `references/analysis-framework.md` for content analysis
|
||||||
3. Analyze content signals for style recommendations
|
3. Analyze content signals for style recommendations
|
||||||
4. Detect source language
|
4. Detect source language
|
||||||
@@ -483,6 +484,7 @@ options:
|
|||||||
- Add slide-specific content
|
- Add slide-specific content
|
||||||
- If `Layout:` specified, include layout guidance from `references/layouts.md`
|
- If `Layout:` specified, include layout guidance from `references/layouts.md`
|
||||||
3. Save to `prompts/` directory
|
3. Save to `prompts/` directory
|
||||||
|
- **Backup rule**: If prompt file exists, rename to `prompts/NN-slide-{slug}-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
**After generation**:
|
**After generation**:
|
||||||
- If `--prompts-only`, stop here and output prompt summary
|
- If `--prompts-only`, stop here and output prompt summary
|
||||||
@@ -539,7 +541,9 @@ options:
|
|||||||
**Standard flow**:
|
**Standard flow**:
|
||||||
1. Select available image generation skill
|
1. Select available image generation skill
|
||||||
2. Generate session ID: `slides-{topic-slug}-{timestamp}`
|
2. Generate session ID: `slides-{topic-slug}-{timestamp}`
|
||||||
3. Generate each slide sequentially with same session ID
|
3. For each slide:
|
||||||
|
- **Backup rule**: If image file exists, rename to `NN-slide-{slug}-backup-YYYYMMDD-HHMMSS.png`
|
||||||
|
- Generate image sequentially with same session ID
|
||||||
4. Report progress: "Generated X/N" (in user's language)
|
4. Report progress: "Generated X/N" (in user's language)
|
||||||
5. Auto-retry once on failure before reporting error
|
5. Auto-retry once on failure before reporting error
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-xhs-images
|
name: baoyu-xhs-images
|
||||||
description: Generates Xiaohongshu (Little Red Book) infographic series with 9 visual styles and 6 layouts. Breaks content into 1-10 cartoon-style images optimized for XHS engagement. Use when user mentions "小红书图片", "XHS images", "RedNote infographics", "小红书种草", or wants social media infographics for Chinese platforms.
|
description: Generates Xiaohongshu (Little Red Book) infographic series with 10 visual styles and 8 layouts. Breaks content into 1-10 cartoon-style images optimized for XHS engagement. Use when user mentions "小红书图片", "XHS images", "RedNote infographics", "小红书种草", or wants social media infographics for Chinese platforms.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Xiaohongshu Infographic Series Generator
|
# Xiaohongshu Infographic Series Generator
|
||||||
@@ -42,8 +42,8 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
|
|||||||
|
|
||||||
| Dimension | Controls | Options |
|
| Dimension | Controls | Options |
|
||||||
|-----------|----------|---------|
|
|-----------|----------|---------|
|
||||||
| **Style** | Visual aesthetics: colors, lines, decorations | cute, fresh, warm, bold, minimal, retro, pop, notion, chalkboard |
|
| **Style** | Visual aesthetics: colors, lines, decorations | cute, fresh, warm, bold, minimal, retro, pop, notion, chalkboard, study-notes |
|
||||||
| **Layout** | Information structure: density, arrangement | sparse, balanced, dense, list, comparison, flow |
|
| **Layout** | Information structure: density, arrangement | sparse, balanced, dense, list, comparison, flow, mindmap, quadrant |
|
||||||
|
|
||||||
Style × Layout can be freely combined. Example: `--style notion --layout dense` creates an intellectual-looking knowledge card with high information density.
|
Style × Layout can be freely combined. Example: `--style notion --layout dense` creates an intellectual-looking knowledge card with high information density.
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ Style × Layout can be freely combined. Example: `--style notion --layout dense`
|
|||||||
| `pop` | Vibrant, energetic, eye-catching |
|
| `pop` | Vibrant, energetic, eye-catching |
|
||||||
| `notion` | Minimalist hand-drawn line art, intellectual |
|
| `notion` | Minimalist hand-drawn line art, intellectual |
|
||||||
| `chalkboard` | Colorful chalk on black board, educational |
|
| `chalkboard` | Colorful chalk on black board, educational |
|
||||||
|
| `study-notes` | Realistic handwritten photo style, blue pen + red annotations + yellow highlighter |
|
||||||
|
|
||||||
Detailed style definitions: `references/presets/<style>.md`
|
Detailed style definitions: `references/presets/<style>.md`
|
||||||
|
|
||||||
@@ -73,6 +74,8 @@ Detailed style definitions: `references/presets/<style>.md`
|
|||||||
| `list` | Enumeration and ranking format (4-7 items) |
|
| `list` | Enumeration and ranking format (4-7 items) |
|
||||||
| `comparison` | Side-by-side contrast layout |
|
| `comparison` | Side-by-side contrast layout |
|
||||||
| `flow` | Process and timeline layout (3-6 steps) |
|
| `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`
|
Detailed layout definitions: `references/elements/canvas.md`
|
||||||
|
|
||||||
@@ -89,6 +92,7 @@ Detailed layout definitions: `references/elements/canvas.md`
|
|||||||
| Fun, exciting, wow, amazing | `pop` | sparse/list |
|
| Fun, exciting, wow, amazing | `pop` | sparse/list |
|
||||||
| Knowledge, concept, productivity, SaaS | `notion` | dense/list |
|
| Knowledge, concept, productivity, SaaS | `notion` | dense/list |
|
||||||
| Education, tutorial, learning, teaching, classroom | `chalkboard` | balanced/dense |
|
| Education, tutorial, learning, teaching, classroom | `chalkboard` | balanced/dense |
|
||||||
|
| Notes, handwritten, study guide, knowledge, realistic, photo | `study-notes` | dense/list/mindmap |
|
||||||
|
|
||||||
## Outline Strategies
|
## Outline Strategies
|
||||||
|
|
||||||
@@ -164,7 +168,9 @@ Copy and track progress:
|
|||||||
|
|
||||||
```
|
```
|
||||||
XHS Infographic Progress:
|
XHS Infographic Progress:
|
||||||
- [ ] Step 0: Check preferences (EXTEND.md) ⚠️ REQUIRED if not found
|
- [ ] Step 0: Check preferences (EXTEND.md) ⛔ BLOCKING
|
||||||
|
- [ ] Found → load preferences → continue
|
||||||
|
- [ ] Not found → run first-time setup → MUST complete before Step 1
|
||||||
- [ ] Step 1: Analyze content → analysis.md
|
- [ ] Step 1: Analyze content → analysis.md
|
||||||
- [ ] Step 2: Confirmation 1 - Content understanding ⚠️ REQUIRED
|
- [ ] Step 2: Confirmation 1 - Content understanding ⚠️ REQUIRED
|
||||||
- [ ] Step 3: Generate 3 outline + style variants
|
- [ ] Step 3: Generate 3 outline + style variants
|
||||||
@@ -176,12 +182,22 @@ XHS Infographic Progress:
|
|||||||
### Flow
|
### Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
Input → Analyze → [Confirm 1] → 3 Outlines → [Confirm 2: Outline + Style + Elements] → Generate → Complete
|
Input → [Step 0: Preferences] ─┬─ Found → Continue
|
||||||
|
│
|
||||||
|
└─ Not found → First-Time Setup ⛔ BLOCKING
|
||||||
|
│
|
||||||
|
└─ Complete setup → Save EXTEND.md → Continue
|
||||||
|
│
|
||||||
|
┌───────────────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
Analyze → [Confirm 1] → 3 Outlines → [Confirm 2: Outline + Style + Elements] → Generate → Complete
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 0: Load Preferences (EXTEND.md) ⚠️
|
### Step 0: Load Preferences (EXTEND.md) ⛔ BLOCKING
|
||||||
|
|
||||||
**Purpose**: Load user preferences or run first-time setup. **Do NOT skip setup if EXTEND.md not found.**
|
**Purpose**: Load user preferences or run first-time setup.
|
||||||
|
|
||||||
|
**CRITICAL**: 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.
|
||||||
|
|
||||||
Use Bash to check EXTEND.md existence (priority order):
|
Use Bash to check EXTEND.md existence (priority order):
|
||||||
|
|
||||||
@@ -201,13 +217,13 @@ test -f "$HOME/.baoyu-skills/baoyu-xhs-images/EXTEND.md" && echo "user"
|
|||||||
│ $HOME/.baoyu-skills/baoyu-xhs-images/EXTEND.md │ User home │
|
│ $HOME/.baoyu-skills/baoyu-xhs-images/EXTEND.md │ User home │
|
||||||
└────────────────────────────────────────────────────┴───────────────────┘
|
└────────────────────────────────────────────────────┴───────────────────┘
|
||||||
|
|
||||||
┌───────────┬───────────────────────────────────────────────────────────────────────────┐
|
┌───────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
│ Result │ Action │
|
│ Result │ Action │
|
||||||
├───────────┼───────────────────────────────────────────────────────────────────────────┤
|
├───────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ Found │ Read, parse, display summary → Continue to Step 1 │
|
│ Found │ Read, parse, display summary → Continue to Step 1 │
|
||||||
├───────────┼───────────────────────────────────────────────────────────────────────────┤
|
├───────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ Not found │ ⚠️ MUST run first-time setup (see below) → Then 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):
|
**First-Time Setup** (when EXTEND.md not found):
|
||||||
|
|
||||||
@@ -227,6 +243,7 @@ Read source content, save it if needed, and perform deep analysis.
|
|||||||
1. **Save source content** (if not already a file):
|
1. **Save source content** (if not already a file):
|
||||||
- If user provides a file path: use as-is
|
- If user provides a file path: use as-is
|
||||||
- If user pastes content: save to `source.md` in target directory
|
- 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
|
2. Read source content
|
||||||
3. **Deep analysis** following `references/workflows/analysis-framework.md`:
|
3. **Deep analysis** following `references/workflows/analysis-framework.md`:
|
||||||
- Content type classification (种草/干货/测评/教程/避坑...)
|
- Content type classification (种草/干货/测评/教程/避坑...)
|
||||||
@@ -353,7 +370,9 @@ With confirmed outline + style + layout:
|
|||||||
|
|
||||||
**For each image (cover + content + ending)**:
|
**For each image (cover + content + ending)**:
|
||||||
1. Save prompt to `prompts/NN-{type}-[slug].md` (in user's preferred language)
|
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 using confirmed style and layout
|
2. Generate image using confirmed style and layout
|
||||||
|
- **Backup rule**: If image file exists, rename to `NN-{type}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||||
3. Report progress after each generation
|
3. Report progress after each generation
|
||||||
|
|
||||||
**Watermark Application** (if enabled in preferences):
|
**Watermark Application** (if enabled in preferences):
|
||||||
@@ -417,17 +436,18 @@ Files:
|
|||||||
|
|
||||||
**Style × Layout Matrix** (✓✓ = highly recommended, ✓ = works well):
|
**Style × Layout Matrix** (✓✓ = highly recommended, ✓ = works well):
|
||||||
|
|
||||||
| | sparse | balanced | dense | list | comparison | flow |
|
| | sparse | balanced | dense | list | comparison | flow | mindmap | quadrant |
|
||||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| cute | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ |
|
| cute | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| fresh | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ |
|
| fresh | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓ | ✓ |
|
||||||
| warm | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ |
|
| warm | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ |
|
||||||
| bold | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
| bold | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ |
|
||||||
| minimal | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ |
|
| minimal | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| retro | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ |
|
| retro | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| pop | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
| pop | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ |
|
||||||
| notion | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
|
| notion | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
|
||||||
| chalkboard | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ |
|
| chalkboard | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
||||||
|
| study-notes | ✗ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ |
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ description: First-time setup flow for baoyu-xhs-images preferences
|
|||||||
|
|
||||||
When no EXTEND.md is found, guide user through preference setup.
|
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
|
## Setup Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ Avoid placing critical content in these areas:
|
|||||||
| list | Vertical enumeration | 4-7 | Rankings, checklists, step guides |
|
| list | Vertical enumeration | 4-7 | Rankings, checklists, step guides |
|
||||||
| comparison | Left vs Right | 2 sections | Before/after, pros/cons |
|
| comparison | Left vs Right | 2 sections | Before/after, pros/cons |
|
||||||
| flow | Connected nodes | 3-6 steps | Processes, timelines, workflows |
|
| 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
|
## Layout by Position
|
||||||
|
|
||||||
@@ -106,3 +108,15 @@ For multi-element compositions:
|
|||||||
- Directional flow (top→bottom or left→right)
|
- Directional flow (top→bottom or left→right)
|
||||||
- Connected nodes with arrows
|
- Connected nodes with arrows
|
||||||
- Clear progression indicators
|
- 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,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
|
||||||
Reference in New Issue
Block a user