Compare commits

...

17 Commits

Author SHA1 Message Date
Jim Liu 宝玉 b12ae7374d chore: release v1.22.0 2026-01-25 21:05:37 -06:00
Jim Liu 宝玉 1afa6f5a58 refactor(baoyu-post-to-x): simplify image placeholders from bracket format to XIMGPH 2026-01-25 21:05:08 -06:00
Jim Liu 宝玉 977598d5ae feat(baoyu-post-to-wechat): add theme selection, HTML preview, and simplify image placeholders 2026-01-25 21:05:04 -06:00
Jim Liu 宝玉 eb738aa61a feat(baoyu-cover-image): add default_output_dir preference with output directory selection 2026-01-25 21:04:59 -06:00
Jim Liu 宝玉 e9a030f917 feat(baoyu-article-illustrator): add imgs-subdir output option and improve style selection 2026-01-25 21:04:56 -06:00
Jim Liu 宝玉 e6a9b217e6 chore: release v1.21.4 2026-01-25 16:10:08 -06:00
Jim Liu 宝玉 ec1743592c fix(baoyu-post-to-wechat): fix regressions from Windows compatibility PR
- Fix broken md-to-wechat-fixed.ts/wechat-article-fixed.ts filename
  references that cause runtime crash (files were never renamed)
- Restore frontmatter quote stripping for title/summary values
- Restore --title CLI parameter functionality (was silently ignored)
- Fix summary extraction to properly skip headings, quotes, lists
- Fix argument parsing to reject single-dash args as file paths
- Remove debug console.error logs left from development
2026-01-25 16:09:24 -06:00
Jim Liu 宝玉 fe647a11bb Merge pull request #20 from JadeLiang003/fix/windows-copy-paste-support
Fix: Windows compatibility for baoyu-post-to-wechat
2026-01-25 16:04:01 -06:00
JadeLiang 5a11b49b00 Fix: Windows compatibility for baoyu-post-to-wechat
## Problem
The baoyu-post-to-wechat skill failed on Windows due to:
1. `new URL(import.meta.url).pathname` returns incorrect path format on Windows
2. Copy/paste operations used `xdotool` (Linux tool) which doesn't exist on Windows

## Solution
1. **md-to-wechat.ts**: Use `fileURLToPath()` to correctly resolve file paths on Windows
2. **wechat-article.ts**: Use CDP `Input.dispatchKeyEvent` for copy/paste operations
   - Replaces system-dependent tools (xdotool/osascript)
   - Works consistently across Windows/macOS/Linux
   - More reliable as it operates within Chrome session

## Changes
- Import `fileURLToPath` from 'node:url'
- Replace `new URL(import.meta.url).pathname` with `fileURLToPath(import.meta.url)`
- Replace system tool calls with CDP keyboard events for Ctrl+C and Ctrl+V
- Add platform-specific modifier handling (Cmd for macOS, Ctrl for Windows/Linux)

## Testing
- Tested successfully on Windows 11
- Markdown conversion works correctly
- HTML copy/paste to WeChat editor works correctly

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-25 15:24:04 +08:00
Jim Liu 宝玉 04692515bb fix: remove opacity of watermark 2026-01-24 17:23:04 -06:00
Jim Liu 宝玉 eb54c2a2e7 chore: release v1.21.3 2026-01-24 17:20:02 -06:00
Jim Liu 宝玉 a39c8eb0bd refactor(baoyu-article-illustrator): extract content to reference files
- Simplify SKILL.md by extracting usage and prompt templates
- Add references/usage.md for command syntax
- Add references/prompt-construction.md for prompt templates
- Add default_output_dir preference option
- Reorganize workflow from 5 to 6 steps with Pre-check phase
2026-01-24 17:19:42 -06:00
Jim Liu 宝玉 715555c2ab chore: release v1.21.2 2026-01-24 13:37:58 -06:00
Jim Liu 宝玉 02b039f2ff feat(baoyu-image-gen): add parallel generation documentation 2026-01-24 13:37:42 -06:00
Jim Liu 宝玉 00306414fe docs(release-skills): add skill grouping and user confirmation workflow 2026-01-24 13:37:38 -06:00
Jim Liu 宝玉 db1179e057 chore: release v1.21.1 2026-01-24 13:25:15 -06:00
Jim Liu 宝玉 7b0c9ca372 chore: release v1.21.0 2026-01-24 03:33:10 -06:00
25 changed files with 949 additions and 539 deletions
+1 -1
View File
@@ -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.20.0" "version": "1.22.0"
}, },
"plugins": [ "plugins": [
{ {
+141 -39
View File
@@ -186,18 +186,61 @@ Japanese (CHANGELOG.ja.md):
- コネクションプールのメモリリークを修正 - コネクションプールのメモリリークを修正
``` ```
### Step 5: Check README Updates ### Step 5: Group Changes by Skill/Module
**IMPORTANT**: Before updating version file, check if README needs updates. Analyze commits since last tag and group by affected skill/module:
1. **Scan for README files**: `README*.md` in project root 1. **Identify changed files** per commit
2. **For each skill/feature changed**, check if README documentation matches: 2. **Group by skill/module**:
- Options/flags documented correctly - `skills/<skill-name>/*` → Group under that skill
- Usage examples reflect current syntax - Root files (CLAUDE.md, etc.) → Group as "project"
- Feature descriptions are accurate - Multiple skills in one commit → Split into multiple groups
3. **If README is outdated**: 3. **For each group**, identify related README updates needed
- Update README.md (and README.zh.md if exists) to reflect changes
- Include in release commit **Example Grouping**:
```
baoyu-cover-image:
- feat: add new style options
- fix: handle transparent backgrounds
→ README updates: options table
baoyu-comic:
- refactor: improve panel layout algorithm
→ No README updates needed
project:
- docs: update CLAUDE.md architecture section
```
### Step 6: Commit Each Skill/Module Separately
For each skill/module group (in order of changes):
1. **Check README updates needed**:
- Scan `README*.md` for mentions of this skill/module
- Verify options/flags documented correctly
- Update usage examples if syntax changed
- Update feature descriptions if behavior changed
2. **Stage and commit**:
```bash
git add skills/<skill-name>/*
git add README.md README.zh.md # If updated for this skill
git commit -m "<type>(<skill-name>): <meaningful description>"
```
3. **Commit message format**:
- Use conventional commit format: `<type>(<scope>): <description>`
- `<type>`: feat, fix, refactor, docs, perf, etc.
- `<scope>`: skill name or "project"
- `<description>`: Clear, meaningful description of changes
**Example Commits**:
```bash
git commit -m "feat(baoyu-cover-image): add watercolor and minimalist styles"
git commit -m "fix(baoyu-comic): improve panel layout for long dialogues"
git commit -m "docs(project): update architecture documentation"
```
**Common README Updates Needed**: **Common README Updates Needed**:
| Change Type | README Section to Check | | Change Type | README Section to Check |
@@ -208,11 +251,13 @@ Japanese (CHANGELOG.ja.md):
| Breaking changes | Migration notes, deprecation warnings | | Breaking changes | Migration notes, deprecation warnings |
| Restructured internals | Architecture section (if exposed to users) | | Restructured internals | Architecture section (if exposed to users) |
### Step 6: Update Version File ### Step 7: Generate Changelog and Update Version
1. Read version file (JSON/TOML/text) 1. **Generate multi-language changelogs** (as described in Step 4)
2. Update version number 2. **Update version file**:
3. Write back (preserve formatting) - Read version file (JSON/TOML/text)
- Update version number
- Write back (preserve formatting)
**Version Paths by File Type**: **Version Paths by File Type**:
@@ -224,25 +269,77 @@ Japanese (CHANGELOG.ja.md):
| marketplace.json | `$.metadata.version` | | marketplace.json | `$.metadata.version` |
| VERSION / version.txt | Direct content | | VERSION / version.txt | Direct content |
### Step 7: Commit and Tag ### Step 8: User Confirmation
```bash Before creating the release commit, ask user to confirm:
git add <all modified files>
git commit -m "chore: release v{VERSION}" **Use AskUserQuestion with two questions**:
git tag v{VERSION}
1. **Version bump** (single select):
- Show recommended version based on Step 3 analysis
- Options: recommended (with label), other semver options
- Example: `1.2.3 → 1.3.0 (Recommended)`, `1.2.3 → 1.2.4`, `1.2.3 → 2.0.0`
2. **Push to remote** (single select):
- Options: "Yes, push after commit", "No, keep local only"
**Example Output Before Confirmation**:
``` ```
Commits created:
1. feat(baoyu-cover-image): add watercolor and minimalist styles
2. fix(baoyu-comic): improve panel layout for long dialogues
3. docs(project): update architecture documentation
Changelog preview (en):
## 1.3.0 - 2026-01-22
### Features
- Add watercolor and minimalist styles to cover-image
### Fixes
- Improve panel layout for long dialogues in comic
Ready to create release commit and tag.
```
### Step 9: Create Release Commit and Tag
After user confirmation:
1. **Stage version and changelog files**:
```bash
git add <version-file>
git add CHANGELOG*.md
```
2. **Create release commit**:
```bash
git commit -m "chore: release v{VERSION}"
```
3. **Create tag**:
```bash
git tag v{VERSION}
```
4. **Push if user confirmed** (Step 8):
```bash
git push origin main
git push origin v{VERSION}
```
**Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution. **Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution.
**Important**: Do NOT push to remote. User will push manually when ready.
**Post-Release Output**: **Post-Release Output**:
``` ```
Release v1.3.0 created locally. Release v1.3.0 created.
To publish: Commits:
git push origin main 1. feat(baoyu-cover-image): add watercolor and minimalist styles
git push origin v1.3.0 2. fix(baoyu-comic): improve panel layout for long dialogues
3. docs(project): update architecture documentation
4. chore: release v1.3.0
Tag: v1.3.0
Status: Pushed to origin # or "Local only - run git push when ready"
``` ```
## Configuration (.releaserc.yml) ## Configuration (.releaserc.yml)
@@ -307,31 +404,36 @@ Project detected:
Last tag: v1.2.3 Last tag: v1.2.3
Proposed version: v1.3.0 Proposed version: v1.3.0
Changes detected: Changes grouped by skill/module:
feat: add user authentication baoyu-cover-image:
feat: support oauth2 login - feat: add watercolor style
fix: memory leak in pool - feat: add minimalist style
→ Commit: feat(baoyu-cover-image): add watercolor and minimalist styles
→ README updates: options table
baoyu-comic:
- fix: panel layout for long dialogues
→ Commit: fix(baoyu-comic): improve panel layout for long dialogues
→ No README updates
Changelog preview (en): Changelog preview (en):
## 1.3.0 - 2026-01-22 ## 1.3.0 - 2026-01-22
### Features ### Features
- Add user authentication module - Add watercolor and minimalist styles to cover-image
- Support OAuth2 login
### Fixes ### Fixes
- Fix memory leak in connection pool - Improve panel layout for long dialogues in comic
Changelog preview (zh): Changelog preview (zh):
## 1.3.0 - 2026-01-22 ## 1.3.0 - 2026-01-22
### 新功能 ### 新功能
- 新增用户认证模块 - 为 cover-image 添加水彩和极简风格
- 支持 OAuth2 登录
### 修复 ### 修复
- 修复连接池内存泄漏问题 - 改进 comic 长对话的面板布局
Files to modify: Commits to create:
- package.json 1. feat(baoyu-cover-image): add watercolor and minimalist styles
- CHANGELOG.md 2. fix(baoyu-comic): improve panel layout for long dialogues
- CHANGELOG.zh.md 3. chore: release v1.3.0
No changes made. Run without --dry-run to execute. No changes made. Run without --dry-run to execute.
``` ```
+41
View File
@@ -2,6 +2,47 @@
English | [中文](./CHANGELOG.zh.md) English | [中文](./CHANGELOG.zh.md)
## 1.22.0 - 2026-01-25
### Features
- `baoyu-article-illustrator`: adds `imgs-subdir` output directory option; improves style selection to always ask and show preferred_style from EXTEND.md.
- `baoyu-cover-image`: adds `default_output_dir` preference supporting `same-dir`, `imgs-subdir`, and `independent` options with Step 1.5 for output directory selection.
- `baoyu-post-to-wechat`: adds theme selection (default/grace/simple) with AskUserQuestion before posting; adds HTML preview step; simplifies image placeholders to `WECHATIMGPH_N` format; refactors copy/paste to cross-platform helpers.
### Refactor
- `baoyu-post-to-x`: simplifies image placeholders from `[[IMAGE_PLACEHOLDER_N]]` to `XIMGPH_N` format.
## 1.21.4 - 2026-01-25
### Fixes
- `baoyu-post-to-wechat`: adds Windows compatibility—uses `fileURLToPath` for correct path resolution, replaces system-dependent copy/paste tools (osascript/xdotool) with CDP keyboard events for cross-platform support.
- `baoyu-post-to-wechat`: fixes regressions from Windows compatibility PR—corrects broken `-fixed` filename references, restores frontmatter quote stripping, restores `--title` CLI parameter, fixes summary extraction to skip headings/quotes/lists, fixes argument parsing for single-dash flags, removes debug logs.
- `baoyu-article-illustrator`, `baoyu-cover-image`, `baoyu-xhs-images`: removes opacity option from watermark configuration.
## 1.21.3 - 2026-01-24
### Refactor
- `baoyu-article-illustrator`: simplifies SKILL.md by extracting content to reference files—adds `references/usage.md` for command syntax, `references/prompt-construction.md` for prompt templates. Reorganizes workflow from 5 to 6 steps with new Pre-check phase. Adds `default_output_dir` preference option.
## 1.21.2 - 2026-01-24
### Features
- `baoyu-image-gen`: adds parallel generation documentation with recommended 4 concurrent subagents for batch operations.
### Documentation
- `release-skills`: adds skill/module grouping workflow and user confirmation step before release.
## 1.21.1 - 2026-01-24
### Documentation
- `baoyu-comic`: adds character sheet compression step after generation to reduce token usage when used as reference image.
## 1.21.0 - 2026-01-24
### Features
- `baoyu-cover-image`: expands aspect ratio options—adds 4:3, 3:2, 3:4 ratios; changes default from 2.35:1 to 16:9 for better versatility. Aspect ratio is now always confirmed unless explicitly specified via `--aspect` flag.
- `baoyu-image-gen`: refactors Google provider to support both Gemini multimodal and Imagen models with unified API. Adds `--imageSize` parameter support (1K/2K/4K) for Gemini models.
## 1.20.0 - 2026-01-24 ## 1.20.0 - 2026-01-24
### Features ### Features
+41
View File
@@ -2,6 +2,47 @@
[English](./CHANGELOG.md) | 中文 [English](./CHANGELOG.md) | 中文
## 1.22.0 - 2026-01-25
### 新功能
- `baoyu-article-illustrator`:新增 `imgs-subdir` 输出目录选项;改进风格选择,始终询问并展示 EXTEND.md 中的 preferred_style。
- `baoyu-cover-image`:新增 `default_output_dir` 偏好设置,支持 `same-dir``imgs-subdir``independent` 选项,新增 Step 1.5 输出目录选择流程。
- `baoyu-post-to-wechat`:发布前新增主题选择(default/grace/simple);新增 HTML 预览步骤;图片占位符简化为 `WECHATIMGPH_N` 格式;重构复制粘贴为跨平台辅助函数。
### 重构
- `baoyu-post-to-x`:图片占位符从 `[[IMAGE_PLACEHOLDER_N]]` 简化为 `XIMGPH_N` 格式。
## 1.21.4 - 2026-01-25
### 修复
- `baoyu-post-to-wechat`:新增 Windows 兼容性——使用 `fileURLToPath` 正确解析路径,将系统依赖的复制粘贴工具(osascript/xdotool)替换为 CDP 键盘事件,实现跨平台支持。
- `baoyu-post-to-wechat`:修复 Windows 兼容性 PR 引入的回退问题——修正错误的 `-fixed` 文件名引用、恢复 frontmatter 引号剥离、恢复 `--title` CLI 参数、修复摘要提取逻辑以正确跳过标题/引用/列表、修复单横线参数解析、移除调试日志。
- `baoyu-article-illustrator``baoyu-cover-image``baoyu-xhs-images`:移除水印配置中的透明度选项。
## 1.21.3 - 2026-01-24
### 重构
- `baoyu-article-illustrator`:简化 SKILL.md,提取内容至引用文件——新增 `references/usage.md` 用于命令语法,`references/prompt-construction.md` 用于提示词模板。工作流从 5 步重组为 6 步,新增 Pre-check 预检阶段。新增 `default_output_dir` 偏好设置选项。
## 1.21.2 - 2026-01-24
### 新功能
- `baoyu-image-gen`:添加并行生成文档,推荐使用 4 个并发 subagent 进行批量操作。
### 文档
- `release-skills`:新增按 skill/module 分组提交流程和发布前用户确认步骤。
## 1.21.1 - 2026-01-24
### 文档
- `baoyu-comic`:在角色参考图生成后添加压缩步骤,减少作为参考图使用时的 token 消耗。
## 1.21.0 - 2026-01-24
### 新功能
- `baoyu-cover-image`:扩展宽高比选项——新增 4:3、3:2、3:4 比例;默认值从 2.35:1 改为 16:9 以提高通用性。现在除非通过 `--aspect` 标志明确指定,否则始终确认宽高比。
- `baoyu-image-gen`:重构 Google provider 以统一支持 Gemini 多模态和 Imagen 模型。为 Gemini 模型新增 `--imageSize` 参数支持(1K/2K/4K)。
## 1.20.0 - 2026-01-24 ## 1.20.0 - 2026-01-24
### 新功能 ### 新功能
+172 -301
View File
@@ -7,236 +7,201 @@ description: Analyzes article structure, identifies positions requiring visual a
Analyze articles, identify illustration positions, generate images with Type × Style consistency. Analyze articles, identify illustration positions, generate images with Type × Style consistency.
## Usage
```bash
# Auto-select type and style based on content
/baoyu-article-illustrator path/to/article.md
# Specify type
/baoyu-article-illustrator path/to/article.md --type infographic
# Specify style
/baoyu-article-illustrator path/to/article.md --style blueprint
# Combine type and style
/baoyu-article-illustrator path/to/article.md --type flowchart --style notion
# Specify density
/baoyu-article-illustrator path/to/article.md --density rich
# Direct content input
/baoyu-article-illustrator
[paste content]
```
## Options
| Option | Description |
|--------|-------------|
| `--type <name>` | Illustration type (see Type Gallery) |
| `--style <name>` | Visual style (see Style Gallery) |
| `--density <level>` | Image count: minimal / balanced / rich |
## Two Dimensions ## Two Dimensions
| Dimension | Controls | Examples | | Dimension | Controls | Examples |
|-----------|----------|----------| |-----------|----------|----------|
| **Type** | Information structure, content layout | infographic, scene, flowchart, comparison, framework, timeline | | **Type** | Information structure, layout | infographic, scene, flowchart, comparison, framework, timeline |
| **Style** | Visual aesthetics, colors, mood | notion, warm, minimal, blueprint, watercolor, elegant | | **Style** | Visual aesthetics, mood | notion, warm, minimal, blueprint, watercolor, elegant |
Type × Style can be freely combined. Example: `--type infographic --style blueprint` creates technical data visualization with schematic aesthetics. Type × Style can be freely combined. Example: `--type infographic --style blueprint`
## Type Gallery ## Type Gallery
| Type | Description | Best For | | Type | Best For |
|------|-------------|----------| |------|----------|
| `infographic` | Data visualization, charts, metrics | Technical articles, data analysis, comparisons | | `infographic` | Data, metrics, technical articles |
| `scene` | Atmospheric illustration, mood rendering | Narrative articles, personal stories, emotional content | | `scene` | Narratives, personal stories, emotional content |
| `flowchart` | Process diagrams, step visualization | Tutorials, workflows, decision trees | | `flowchart` | Tutorials, workflows, processes |
| `comparison` | Side-by-side, before/after contrast | Product comparisons, option evaluations | | `comparison` | Side-by-side, before/after, options |
| `framework` | Concept maps, relationship diagrams | Methodologies, models, architecture design | | `framework` | Methodologies, models, architecture |
| `timeline` | Chronological progression | History, project progress, evolution | | `timeline` | History, progress, evolution |
## Density Options
| Density | Count | Description |
|---------|-------|-------------|
| `minimal` | 1-2 | Core concepts only |
| `balanced` (Default) | 3-5 | Major sections coverage |
| `rich` | 6+ | Rich visual support |
## Style Gallery ## Style Gallery
| Style | Description | Best For | | Style | Best For |
|-------|-------------|----------| |-------|----------|
| `notion` (Default) | Minimalist hand-drawn line art | Knowledge sharing, SaaS, productivity | | `notion` (Default) | Knowledge sharing, SaaS, productivity |
| `elegant` | Refined, sophisticated | Business, thought leadership | | `elegant` | Business, thought leadership |
| `warm` | Friendly, approachable | Personal growth, lifestyle, education | | `warm` | Personal growth, lifestyle, education |
| `minimal` | Ultra-clean, zen-like | Philosophy, minimalism, core concepts | | `minimal` | Philosophy, core concepts |
| `blueprint` | Technical schematics | Architecture, system design, engineering | | `blueprint` | Architecture, system design |
| `watercolor` | Soft artistic with natural warmth | Lifestyle, travel, creative | | `watercolor` | Lifestyle, travel, creative |
| `editorial` | Magazine-style infographic | Tech explainers, journalism | | `editorial` | Tech explainers, journalism |
| `scientific` | Academic precise diagrams | Biology, chemistry, technical research | | `scientific` | Academic, technical research |
Full style specifications: `references/styles/<style>.md` Full styles: [references/styles.md](references/styles.md)
## Type × Style Compatibility
| | notion | warm | minimal | blueprint | watercolor | elegant | editorial | scientific |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| infographic | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ |
| scene | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ |
| flowchart | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ |
| comparison | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
| framework | ✓✓ | ✓ | ✓✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓✓ |
| timeline | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ |
✓✓ = highly recommended | ✓ = compatible | ✗ = not recommended
## Auto Selection ## Auto Selection
| Content Signals | Recommended Type | Recommended Style | | Content Signals | Type | Style |
|-----------------|------------------|-------------------| |-----------------|------|-------|
| API, metrics, data, comparison, numbers | infographic | blueprint, notion | | API, metrics, data, numbers | infographic | blueprint, notion |
| Story, emotion, journey, experience, personal | scene | warm, watercolor | | Story, emotion, journey | scene | warm, watercolor |
| How-to, steps, workflow, process, tutorial | flowchart | notion, minimal | | How-to, steps, workflow | flowchart | notion, minimal |
| vs, pros/cons, before/after, alternatives | comparison | notion, elegant | | vs, pros/cons, before/after | comparison | notion, elegant |
| Framework, model, architecture, principles | framework | blueprint, notion | | Framework, model, architecture | framework | blueprint, notion |
| History, timeline, progress, evolution | timeline | elegant, warm | | History, timeline, progress | timeline | elegant, warm |
## Output Directory
```
illustrations/{topic-slug}/
├── source-{slug}.{ext}
├── outline.md
├── prompts/
│ └── illustration-{slug}.md
└── NN-{type}-{slug}.png
```
**Slug**: Extract 2-4 word topic in kebab-case.
**Conflict**: Append `-YYYYMMDD-HHMMSS` if exists.
## Workflow ## Workflow
### Progress Copy this checklist and track progress:
``` ```
- [ ] Step 1: Setup & Analyze Progress:
- [ ] Step 2: Confirm Settings ⚠️ REQUIRED - [ ] Step 1: Pre-check
- [ ] Step 3: Generate Outline - [ ] Step 2: Setup & Analyze
- [ ] Step 4: Generate Images - [ ] Step 3: Confirm Settings ⚠️ REQUIRED
- [ ] Step 5: Finalize - [ ] Step 4: Generate Outline
- [ ] Step 5: Generate Images
- [ ] Step 6: Finalize
``` ```
### Step 1: Setup & Analyze ---
**1.1 Load Preferences (EXTEND.md)** ### Step 1: Pre-check
Use Bash to check EXTEND.md existence (priority order): **1.1 Determine Input Type**
| Input | Output Directory | Next |
|-------|------------------|------|
| 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
# Check project-level first
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project" test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user" test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
``` ```
┌──────────────────────────────────────────────────────────┬───────────────────┐ | Result | Action |
│ Path │ Location │ |--------|--------|
├──────────────────────────────────────────────────────────┼───────────────────┤ | Found | Read, parse, display summary |
│ .baoyu-skills/baoyu-article-illustrator/EXTEND.md │ Project directory │ | Not found | Ask with AskUserQuestion (see references/config/first-time-setup.md) |
├──────────────────────────────────────────────────────────┼───────────────────┤
│ $HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md │ User home │
└──────────────────────────────────────────────────────────┴───────────────────┘
┌───────────┬───────────────────────────────────────────────────────────────────────────┐ **Supports**: Watermark | Preferred type/style | Custom styles | Language | Output directory
│ Result │ Action │
├───────────┼───────────────────────────────────────────────────────────────────────────┤
│ Found │ Read, parse, display summary │
├───────────┼───────────────────────────────────────────────────────────────────────────┤
│ Not found │ Ask user with AskUserQuestion (see references/config/first-time-setup.md) │
└───────────┴───────────────────────────────────────────────────────────────────────────┘
**EXTEND.md Supports**: Watermark | Preferred type/style | Custom style definitions | Language preference ---
Schema: `references/config/preferences-schema.md` ### Step 2: Setup & Analyze
**1.2 Analyze Content** **2.1 Analyze Content**
Read article, detect language, classify content.
| Analysis | Description | | Analysis | Description |
|----------|-------------| |----------|-------------|
| Content type | Technical / Tutorial / Methodology / Narrative | | Content type | Technical / Tutorial / Methodology / Narrative |
| Core arguments | 2-5 main points that MUST be visualized | | 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 type | Based on content signals |
| Recommended density | Based on article length and complexity | | Recommended density | Based on length and complexity |
**1.3 Extract Core Arguments** **2.2 Extract Core Arguments**
Extract 2-5 core arguments that MUST be visualized:
- Main thesis - Main thesis
- Key concepts reader needs - Key concepts reader needs
- Comparisons/contrasts being made - Comparisons/contrasts
- Framework/model proposed - Framework/model proposed
**CRITICAL**: If article uses metaphors (e.g., "电锯切西瓜"), do NOT illustrate literally. Visualize the **underlying concept** instead. **CRITICAL**: If article uses metaphors (e.g., "电锯切西瓜"), do NOT illustrate literally. Visualize the **underlying concept**.
**1.4 Identify Positions** **2.3 Identify Positions**
**What to Illustrate**: **Illustrate**:
- Core arguments (REQUIRED) - Core arguments (REQUIRED)
- Abstract concepts needing visualization - Abstract concepts
- Data comparisons, metrics - Data comparisons
- Processes, workflows - Processes, workflows
**What NOT to Illustrate**: **Do NOT Illustrate**:
- Metaphors literally - Metaphors literally
- Decorative scenes without information - Decorative scenes
- Generic illustrations - Generic illustrations
### Step 2: Confirm Settings ⚠️ ---
### Step 3: Confirm Settings ⚠️
**Do NOT skip.** Use AskUserQuestion with 3-4 questions in ONE call. **Do NOT skip.** Use AskUserQuestion with 3-4 questions in ONE call.
**Question 1: Illustration Type** **Q1: Illustration Type**
- [Recommended based on analysis] (Recommended)
- infographic / scene / flowchart / comparison / framework / timeline / mixed
Based on content analysis, recommend type: **Q2: Density**
- [Recommended type based on signals] (Recommended) - minimal (1-2) - Core concepts only
- infographic - Data visualization, charts - balanced (3-5) (Recommended) - Major sections
- scene - Atmospheric, mood rendering - rich (6+) - Comprehensive support
- flowchart - Process, steps
- comparison - Side-by-side contrast
- framework - Concept relationships
- timeline - Chronological progression
- mixed - Combine multiple types
**Question 2: Density** **Q3: Style** (ALWAYS ask, even with preferred_style in EXTEND.md)
- minimal (1-2 images) - Core concepts only If EXTEND.md has `preferred_style`:
- balanced (3-5 images) (Recommended) - Major sections - [Custom style name + brief description] (Recommended)
- rich (6+ images) - Comprehensive visual support - [Top compatible built-in style 1]
- [Top compatible built-in style 2]
- [Top compatible built-in style 3]
**Question 3: Style** If no `preferred_style`:
- [Best compatible from matrix] (Recommended)
- [Other ✓✓ style 1]
- [Other ✓✓ style 2]
- [Other ✓ style]
Based on recommended Type, suggest compatible styles (see Type × Style Compatibility matrix): Style selection based on Type × Style compatibility matrix (references/styles.md).
- [Best compatible style for recommended type] (Recommended) Full specs: `references/styles/<style>.md`
- [Other highly compatible styles: ✓✓ from matrix]
- [Compatible styles: ✓ from matrix]
**Question 4** (only if source ≠ user language): **Q4** (only if source ≠ user language):
- Language: Source language / User language - Language: Source / User language
### Step 3: Generate Outline ---
Based on confirmed Type + Density + Style, generate illustration outline. ### Step 4: Generate Outline
**Outline Format** (`outline.md`): Save as `outline.md`:
```yaml ```yaml
--- ---
@@ -249,44 +214,48 @@ image_count: 4
## Illustration 1 ## Illustration 1
**Position**: [section] / [paragraph] **Position**: [section] / [paragraph]
**Purpose**: [why this illustration helps] **Purpose**: [why this helps]
**Visual Content**: [what to show] **Visual Content**: [what to show]
**Type Application**: [how type applies here] **Type Application**: [how type applies]
**Filename**: 01-infographic-concept-name.png **Filename**: 01-infographic-concept-name.png
## Illustration 2 ## Illustration 2
... ...
``` ```
**Outline Requirements**: **Requirements**:
- Each illustration position justified by content needs - Each position justified by content needs
- Type applied consistently across all illustrations - Type applied consistently
- Style characteristics reflected in visual descriptions - Style reflected in descriptions
- Count matches density selection - Count matches density
### Step 4: Generate Images ---
**4.1 Create Prompts** ### Step 5: Generate Images
Follow Prompt Construction principles below. Save each to `prompts/illustration-{slug}.md`. **5.1 Create Prompts**
**4.2 Select Generation Skill** Follow [references/prompt-construction.md](references/prompt-construction.md). Save to `prompts/illustration-{slug}.md`.
Check available image generation skills. If multiple, ask user to choose. **5.2 Select Generation Skill**
**4.3 Apply Watermark** (if enabled in preferences) Check available skills. If multiple, ask user.
Add to prompt: `Include a subtle watermark "[content]" positioned at [position] with approximately [opacity*100]% visibility.` **5.3 Apply Watermark** (if enabled)
**4.4 Generate** Add: `Include a subtle watermark "[content]" at [position].`
**5.4 Generate**
1. Generate sequentially 1. Generate sequentially
2. After each: "Generated X/N" 2. After each: "Generated X/N"
3. On failure: auto-retry once, then log and continue 3. On failure: retry once, then log and continue
### Step 5: Finalize ---
**5.1 Update Article** ### Step 6: Finalize
**6.1 Update Article**
Insert after corresponding paragraph: Insert after corresponding paragraph:
```markdown ```markdown
@@ -295,158 +264,60 @@ Insert after corresponding paragraph:
Alt text: concise description in article's language. Alt text: concise description in article's language.
**5.2 Output Summary** **6.2 Output Summary**
``` ```
Article Illustration Complete! Article Illustration Complete!
Article: [path] Article: [path]
Type: [type name] Type: [type] | Density: [level] | Style: [style]
Density: [minimal/balanced/rich] Location: [directory]
Style: [style name]
Location: [directory path]
Images: X/N generated Images: X/N generated
Positions: Positions:
- 01-infographic-xxx.png → After "[Section]" - 01-xxx.png → After "[Section]"
- 02-infographic-yyy.png → After "[Section]" - 02-yyy.png → After "[Section]"
[If failures] [If failures]
Failed: Failed:
- NN-type-zzz.png: [reason] - NN-zzz.png: [reason]
``` ```
## Prompt Construction ---
### Principles ## Output Directory
Good prompts must include:
1. **Layout Structure First**: Describe composition, zones, flow direction
2. **Specific Data/Labels**: Use actual numbers, terms from article
3. **Visual Relationships**: How elements connect
4. **Semantic Colors**: Meaning-based color choices (red=warning, green=efficient)
5. **Style Characteristics**: Line treatment, texture, mood
6. **Aspect Ratio**: End with ratio and complexity level
### Type-Specific Prompts
**Infographic**:
``` ```
[Title] - Data Visualization illustrations/{topic-slug}/
├── source-{slug}.{ext}
Layout: [grid/radial/hierarchical] ├── outline.md
├── prompts/
ZONES: │ └── illustration-{slug}.md
- Zone 1: [data point with specific values] └── NN-{type}-{slug}.png
- Zone 2: [comparison with metrics]
- Zone 3: [summary/conclusion]
LABELS: [specific numbers, percentages, terms from article]
COLORS: [semantic color mapping]
STYLE: [style characteristics]
ASPECT: 16:9
``` ```
**Scene**: **Slug**: 2-4 word topic in kebab-case.
``` **Conflict**: Append `-YYYYMMDD-HHMMSS` if exists.
[Title] - Atmospheric Scene
FOCAL POINT: [main subject]
ATMOSPHERE: [lighting, mood, environment]
MOOD: [emotion to convey]
COLOR TEMPERATURE: [warm/cool/neutral]
STYLE: [style characteristics]
ASPECT: 16:9
```
**Flowchart**:
```
[Title] - Process Flow
Layout: [left-right/top-down/circular]
STEPS:
1. [Step name] - [brief description]
2. [Step name] - [brief description]
...
CONNECTIONS: [arrow types, decision points]
STYLE: [style characteristics]
ASPECT: 16:9
```
**Comparison**:
```
[Title] - Comparison View
LEFT SIDE - [Option A]:
- [Point 1]
- [Point 2]
RIGHT SIDE - [Option B]:
- [Point 1]
- [Point 2]
DIVIDER: [visual separator]
STYLE: [style characteristics]
ASPECT: 16:9
```
**Framework**:
```
[Title] - Conceptual Framework
STRUCTURE: [hierarchical/network/matrix]
NODES:
- [Concept 1] - [role]
- [Concept 2] - [role]
RELATIONSHIPS: [how nodes connect]
STYLE: [style characteristics]
ASPECT: 16:9
```
**Timeline**:
```
[Title] - Chronological View
DIRECTION: [horizontal/vertical]
EVENTS:
- [Date/Period 1]: [milestone]
- [Date/Period 2]: [milestone]
MARKERS: [visual indicators]
STYLE: [style characteristics]
ASPECT: 16:9
```
### What to Avoid
- Vague descriptions ("a nice image")
- Literal metaphor illustrations
- Missing concrete labels/annotations
- Generic decorative elements
## Modification ## Modification
| Action | Steps | | Action | Steps |
|--------|-------| |--------|-------|
| **Edit** | Update prompt → Regenerate → Update reference | | **Edit** | Update prompt → Regenerate → Update reference |
| **Add** | Identify position → Create prompt → Generate → Update outline → Insert reference | | **Add** | Identify position → Create prompt → Generate → Update outline → Insert |
| **Delete** | Delete files → Remove reference → Update outline | | **Delete** | Delete files → Remove reference → Update outline |
## References ## References
| File | Content | | File | Content |
|------|---------| |------|---------|
| [references/styles.md](references/styles.md) | Style gallery & compatibility matrix | | [references/usage.md](references/usage.md) | Command syntax and options |
| [references/styles.md](references/styles.md) | Style gallery & compatibility |
| [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 ## Extension Support
Custom configurations via EXTEND.md. See **Step 1.1** for paths and supported options. Custom configurations via EXTEND.md. See **Step 1.5** for paths and supported options.
@@ -15,7 +15,6 @@ watermark:
enabled: false enabled: false
content: "" content: ""
position: bottom-right # bottom-right|bottom-left|bottom-center|top-right position: bottom-right # bottom-right|bottom-left|bottom-center|top-right
opacity: 0.7 # 0.1-1.0
preferred_style: preferred_style:
name: null # Built-in or custom style name name: null # Built-in or custom style name
@@ -23,6 +22,8 @@ preferred_style:
language: null # zh|en|ja|ko|auto language: null # zh|en|ja|ko|auto
default_output_dir: null # same-dir|illustrations-subdir|independent
custom_styles: custom_styles:
- name: my-style - name: my-style
description: "Style description" description: "Style description"
@@ -44,10 +45,10 @@ custom_styles:
| `watermark.enabled` | bool | false | Enable watermark | | `watermark.enabled` | bool | false | Enable watermark |
| `watermark.content` | string | "" | Watermark text (@username or custom) | | `watermark.content` | string | "" | Watermark text (@username or custom) |
| `watermark.position` | enum | bottom-right | Position on image | | `watermark.position` | enum | bottom-right | Position on image |
| `watermark.opacity` | float | 0.7 | Transparency (0.1-1.0) |
| `preferred_style.name` | string | null | Style name or null | | `preferred_style.name` | string | null | Style name or null |
| `preferred_style.description` | string | "" | Custom notes/override | | `preferred_style.description` | string | "" | Custom notes/override |
| `language` | string | null | Output language (null = auto-detect) | | `language` | string | null | Output language (null = auto-detect) |
| `default_output_dir` | enum | null | Output directory preference (null = ask each time) |
| `custom_styles` | array | [] | User-defined styles | | `custom_styles` | array | [] | User-defined styles |
## Position Options ## Position Options
@@ -59,6 +60,14 @@ custom_styles:
| `bottom-center` | Bottom center | | `bottom-center` | Bottom center |
| `top-right` | Upper right corner | | `top-right` | Upper right corner |
## Output Directory Options
| Value | Description |
|-------|-------------|
| `same-dir` | Same directory as article |
| `illustrations-subdir` | `{article-dir}/illustrations/` subdirectory |
| `independent` | `illustrations/{topic-slug}/` in working directory |
## Custom Style Fields ## Custom Style Fields
| Field | Required | Description | | Field | Required | Description |
@@ -94,7 +103,6 @@ watermark:
enabled: true enabled: true
content: "@myaccount" content: "@myaccount"
position: bottom-right position: bottom-right
opacity: 0.5
preferred_style: preferred_style:
name: notion name: notion
@@ -0,0 +1,127 @@
# Prompt Construction
## Principles
Good prompts must include:
1. **Layout Structure First**: Describe composition, zones, flow direction
2. **Specific Data/Labels**: Use actual numbers, terms from article
3. **Visual Relationships**: How elements connect
4. **Semantic Colors**: Meaning-based color choices (red=warning, green=efficient)
5. **Style Characteristics**: Line treatment, texture, mood
6. **Aspect Ratio**: End with ratio and complexity level
## Type-Specific Templates
### Infographic
```
[Title] - Data Visualization
Layout: [grid/radial/hierarchical]
ZONES:
- Zone 1: [data point with specific values]
- Zone 2: [comparison with metrics]
- Zone 3: [summary/conclusion]
LABELS: [specific numbers, percentages, terms from article]
COLORS: [semantic color mapping]
STYLE: [style characteristics]
ASPECT: 16:9
```
### Scene
```
[Title] - Atmospheric Scene
FOCAL POINT: [main subject]
ATMOSPHERE: [lighting, mood, environment]
MOOD: [emotion to convey]
COLOR TEMPERATURE: [warm/cool/neutral]
STYLE: [style characteristics]
ASPECT: 16:9
```
### Flowchart
```
[Title] - Process Flow
Layout: [left-right/top-down/circular]
STEPS:
1. [Step name] - [brief description]
2. [Step name] - [brief description]
...
CONNECTIONS: [arrow types, decision points]
STYLE: [style characteristics]
ASPECT: 16:9
```
### Comparison
```
[Title] - Comparison View
LEFT SIDE - [Option A]:
- [Point 1]
- [Point 2]
RIGHT SIDE - [Option B]:
- [Point 1]
- [Point 2]
DIVIDER: [visual separator]
STYLE: [style characteristics]
ASPECT: 16:9
```
### Framework
```
[Title] - Conceptual Framework
STRUCTURE: [hierarchical/network/matrix]
NODES:
- [Concept 1] - [role]
- [Concept 2] - [role]
RELATIONSHIPS: [how nodes connect]
STYLE: [style characteristics]
ASPECT: 16:9
```
### Timeline
```
[Title] - Chronological View
DIRECTION: [horizontal/vertical]
EVENTS:
- [Date/Period 1]: [milestone]
- [Date/Period 2]: [milestone]
MARKERS: [visual indicators]
STYLE: [style characteristics]
ASPECT: 16:9
```
## What to Avoid
- Vague descriptions ("a nice image")
- Literal metaphor illustrations
- Missing concrete labels/annotations
- Generic decorative elements
## Watermark Integration
If watermark enabled in preferences, append:
```
Include a subtle watermark "[content]" positioned at [position] with approximately [opacity*100]% visibility.
```
@@ -0,0 +1,66 @@
# Usage
## Command Syntax
```bash
# Auto-select type and style based on content
/baoyu-article-illustrator path/to/article.md
# Specify type
/baoyu-article-illustrator path/to/article.md --type infographic
# Specify style
/baoyu-article-illustrator path/to/article.md --style blueprint
# Combine type and style
/baoyu-article-illustrator path/to/article.md --type flowchart --style notion
# Specify density
/baoyu-article-illustrator path/to/article.md --density rich
# Direct content input (paste mode)
/baoyu-article-illustrator
[paste content]
```
## Options
| Option | Description |
|--------|-------------|
| `--type <name>` | Illustration type (see Type Gallery in SKILL.md) |
| `--style <name>` | Visual style (see references/styles.md) |
| `--density <level>` | Image count: minimal / balanced / rich |
## Input Modes
| Mode | Trigger | Output Directory |
|------|---------|------------------|
| File path | `path/to/article.md` | Use `default_output_dir` preference, or ask if not set |
| Paste content | No path argument | `illustrations/{topic-slug}/` |
## Output Directory Options
| Value | Path |
|-------|------|
| `same-dir` | `{article-dir}/` |
| `illustrations-subdir` | `{article-dir}/illustrations/` |
| `independent` | `illustrations/{topic-slug}/` |
Configure in EXTEND.md: `default_output_dir: illustrations-subdir`
## Examples
**Technical article with data**:
```bash
/baoyu-article-illustrator api-design.md --type infographic --style blueprint
```
**Personal story**:
```bash
/baoyu-article-illustrator journey.md --type scene --style warm
```
**Tutorial with steps**:
```bash
/baoyu-article-illustrator how-to-deploy.md --type flowchart --density rich
```
+6
View File
@@ -201,6 +201,12 @@ npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
--image characters/characters.png --ar 4:3 --image characters/characters.png --ar 4:3
``` ```
**Compress character sheet** (recommended):
Compress to reduce token usage when used as reference image:
- Use available image compression skill (if any)
- Or system tools: `pngquant`, `optipng`, `sips` (macOS)
- **Keep PNG format**, lossless compression preferred
**7.2 Generate each page WITH character reference**: **7.2 Generate each page WITH character reference**:
| Skill Capability | Strategy | | Skill Capability | Strategy |
@@ -15,7 +15,6 @@ watermark:
enabled: false enabled: false
content: "" content: ""
position: bottom-right # bottom-right|bottom-left|bottom-center|top-right position: bottom-right # bottom-right|bottom-left|bottom-center|top-right
opacity: 0.5 # 0.1-1.0 (lower default for comics)
preferred_art: null # ligne-claire|manga|realistic|ink-brush|chalk preferred_art: null # ligne-claire|manga|realistic|ink-brush|chalk
preferred_tone: null # neutral|warm|dramatic|romantic|energetic|vintage|action preferred_tone: null # neutral|warm|dramatic|romantic|energetic|vintage|action
@@ -42,7 +41,6 @@ character_presets:
| `watermark.enabled` | bool | false | Enable watermark | | `watermark.enabled` | bool | false | Enable watermark |
| `watermark.content` | string | "" | Watermark text (@username or custom) | | `watermark.content` | string | "" | Watermark text (@username or custom) |
| `watermark.position` | enum | bottom-right | Position on image | | `watermark.position` | enum | bottom-right | Position on image |
| `watermark.opacity` | float | 0.5 | Transparency (0.1-1.0, lower for comics) |
| `preferred_art` | string | null | Art style (ligne-claire, manga, realistic, ink-brush, chalk) | | `preferred_art` | string | null | Art style (ligne-claire, manga, realistic, ink-brush, chalk) |
| `preferred_tone` | string | null | Tone (neutral, warm, dramatic, romantic, energetic, vintage, action) | | `preferred_tone` | string | null | Tone (neutral, warm, dramatic, romantic, energetic, vintage, action) |
| `preferred_layout` | string | null | Layout preference or null | | `preferred_layout` | string | null | Layout preference or null |
@@ -113,7 +111,6 @@ watermark:
enabled: true enabled: true
content: "@comicstudio" content: "@comicstudio"
position: bottom-right position: bottom-right
opacity: 0.4
preferred_art: manga preferred_art: manga
preferred_tone: neutral preferred_tone: neutral
@@ -28,19 +28,6 @@ description: Watermark configuration guide for baoyu-comic
| `bottom-center` | Webtoon vertical scroll, centered designs | Text-heavy bottom area | | `bottom-center` | Webtoon vertical scroll, centered designs | Text-heavy bottom area |
| `top-right` | **Not recommended for comics** | Always - conflicts with page numbers | | `top-right` | **Not recommended for comics** | Always - conflicts with page numbers |
## Opacity for Comics
Comics typically use lower opacity than infographics to avoid disrupting panel flow:
| Opacity | Visual Effect | Use Case |
|---------|---------------|----------|
| 0.3 | Very subtle, barely visible | **Recommended for comics** |
| 0.4 | Light presence | Balance of visibility and subtlety |
| 0.5 | Noticeable | Standard comics default |
| 0.6+ | Strong presence | Not recommended (distracting) |
**Default**: 0.5 (compared to 0.7 for infographics)
## Content Format ## Content Format
| Format | Example | Style | | Format | Example | Style |
@@ -63,10 +50,9 @@ Comics typically use lower opacity than infographics to avoid disrupting panel f
When watermark is enabled, add to image generation prompt: When watermark is enabled, add to image generation prompt:
``` ```
Include a subtle watermark "[content]" positioned at [position] Include a subtle watermark "[content]" positioned at [position].
with approximately [opacity*100]% visibility. The watermark should The watermark should be legible but not distracting from the comic panels
be legible but not distracting from the comic panels and storytelling. and storytelling. Ensure watermark does not overlap speech bubbles or key action.
Ensure watermark does not overlap speech bubbles or key action.
``` ```
## Common Issues ## Common Issues
@@ -76,5 +62,5 @@ Ensure watermark does not overlap speech bubbles or key action.
| Watermark invisible on dark panels | Adjust contrast or add subtle outline | | Watermark invisible on dark panels | Adjust contrast or add subtle outline |
| Watermark overlaps speech bubble | Change position or lower on page | | Watermark overlaps speech bubble | Change position or lower on page |
| Watermark inconsistent across pages | Use session ID for consistency | | Watermark inconsistent across pages | Use session ID for consistency |
| Watermark too prominent | Decrease opacity (0.3-0.4 for comics) | | Watermark too prominent | Change position or reduce size |
| Conflicts with page number | Never use top-right position | | Conflicts with page number | Never use top-right position |
+91 -34
View File
@@ -40,7 +40,7 @@ Generate elegant cover images for articles with 4-dimensional customization.
| `--style <name>` | Cover style (see Style Gallery) | | `--style <name>` | Cover style (see Style Gallery) |
| `--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 |
| `--aspect <ratio>` | 2.35:1 (default), 16:9, 1:1 | | `--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 |
@@ -218,10 +218,17 @@ Default: `balanced`
## File Structure ## File Structure
Each session creates an independent directory named by content slug: Output directory depends on `default_output_dir` preference:
| Preference | Output Path |
|------------|-------------|
| `same-dir` | `{article-dir}/` |
| `imgs-subdir` | `{article-dir}/imgs/` |
| `independent` (default) | `cover-image/{topic-slug}/` |
| Pasted content | `cover-image/{topic-slug}/` (always) |
``` ```
cover-image/{topic-slug}/ <output-dir>/
├── source-{slug}.{ext} # Source files (text, images, etc.) ├── source-{slug}.{ext} # Source files (text, images, etc.)
├── prompts/cover.md # Generation prompt ├── prompts/cover.md # Generation prompt
└── cover.png # Output image └── cover.png # Output image
@@ -250,7 +257,7 @@ Copy and track progress:
``` ```
Cover Image Progress: Cover Image Progress:
- [ ] Step 0: Check preferences (EXTEND.md) ⚠️ REQUIRED if not found - [ ] Step 0: Check preferences (EXTEND.md) ⚠️ REQUIRED if not found
- [ ] Step 1: Analyze content - [ ] Step 1: Analyze content + determine output directory ⚠️ MUST ask if not configured
- [ ] Step 2: Confirm options (4 dimensions) ⚠️ REQUIRED unless --quick or all specified - [ ] Step 2: Confirm options (4 dimensions) ⚠️ REQUIRED unless --quick or all specified
- [ ] Step 3: Create prompt - [ ] Step 3: Create prompt
- [ ] Step 4: Generate image - [ ] Step 4: Generate image
@@ -260,9 +267,9 @@ Cover Image Progress:
### Flow ### Flow
``` ```
Input → [Step 0: Preferences/Setup] → Analyze → [Confirm: 4 Dimensions] → Prompt → Generate → Complete Input → [Step 0: Preferences/Setup] → Analyze → [Output Dir ⚠️] → [Confirm: 4 Dimensions] → Prompt → Generate → Complete
(skip if --quick or all dimensions specified) (skip if --quick or all specified)
``` ```
### Step 0: Load Preferences (EXTEND.md) ⚠️ ### Step 0: Load Preferences (EXTEND.md) ⚠️
@@ -307,6 +314,7 @@ Preferences loaded from [project/user]:
• Text: [preferred_text or "title-only"] • Text: [preferred_text or "title-only"]
• Mood: [preferred_mood or "balanced"] • Mood: [preferred_mood or "balanced"]
• Aspect: [default_aspect] • Aspect: [default_aspect]
• Output: [default_output_dir or "not set — will ask in Step 1.5"]
• Quick mode: [enabled/disabled] • Quick mode: [enabled/disabled]
• Language: [language or "auto"] • Language: [language or "auto"]
``` ```
@@ -357,15 +365,32 @@ options:
header: "Aspect" header: "Aspect"
question: "Default aspect ratio for cover images?" question: "Default aspect ratio for cover images?"
options: options:
- label: "2.35:1 (Recommended)" - label: "16:9 (Recommended)"
description: "Cinematic widescreen, best for article headers" description: "Standard widescreen - YouTube, presentations, versatile"
- label: "16:9" - label: "2.35:1"
description: "Standard widescreen, versatile" description: "Cinematic widescreen - article headers, blog posts"
- label: "1:1" - label: "1:1"
description: "Square, social media friendly" description: "Square - Instagram, WeChat, social cards"
- label: "3:4"
description: "Portrait - Xiaohongshu, Pinterest, mobile content"
``` ```
**Q5: Quick Mode** Note: More ratios (4:3, 3:2) available during generation. This sets the default recommendation.
**Q5: Default Output Directory**
```yaml
header: "Output"
question: "Default output directory for cover images?"
options:
- label: "Independent (Recommended)"
description: "cover-image/{topic-slug}/ - separate from article"
- label: "Same directory"
description: "{article-dir}/ - alongside the article file"
- label: "imgs subdirectory"
description: "{article-dir}/imgs/ - images folder near article"
```
**Q6: Quick Mode**
```yaml ```yaml
header: "Quick" header: "Quick"
question: "Enable quick mode by default?" question: "Enable quick mode by default?"
@@ -376,7 +401,7 @@ options:
description: "Skip confirmation, use auto-selection" description: "Skip confirmation, use auto-selection"
``` ```
**Q6: Save Location** **Q7: Save Location**
```yaml ```yaml
header: "Save" header: "Save"
question: "Where to save preferences?" question: "Where to save preferences?"
@@ -391,7 +416,7 @@ options:
Full setup details: `references/config/first-time-setup.md` Full setup details: `references/config/first-time-setup.md`
**EXTEND.md Supports**: Watermark | Preferred type | Preferred style | Preferred text | Preferred mood | Default aspect ratio | Quick mode | Custom style definitions | Language preference **EXTEND.md Supports**: Watermark | Preferred type | Preferred style | Preferred text | Preferred mood | Default aspect ratio | Default output directory | Quick mode | Custom style definitions | Language preference
Schema: `references/config/preferences-schema.md` Schema: `references/config/preferences-schema.md`
@@ -413,19 +438,45 @@ Read source content, save it if needed, and perform analysis.
- Note user's input language (from conversation) - Note user's input language (from conversation)
- Compare with language preference in EXTEND.md - Compare with language preference in EXTEND.md
**1.5 Determine Output Directory** ⚠️
**MUST ask user when `default_output_dir` is not set in EXTEND.md and input is a file path.**
| Input | Behavior |
|-------|----------|
| File path + preference set | Use configured `default_output_dir` |
| File path + **no preference** | ⚠️ **MUST** ask with AskUserQuestion ↓ |
| Pasted content | `cover-image/{topic-slug}/` (always independent) |
`default_output_dir` preference values:
| Preference Value | Output Path |
|------------------|-------------|
| `same-dir` | `{article-dir}/` |
| `imgs-subdir` | `{article-dir}/imgs/` |
| `independent` | `cover-image/{topic-slug}/` |
**AskUserQuestion** (when no preference, file path input only):
- `{article-dir}/imgs/` - Images subdirectory near article
- `{article-dir}/` - Same directory as article
- `cover-image/{topic-slug}/` - Independent directory
- Save as default - Remember this choice for future runs
### Step 2: Confirm Options ⚠️ ### Step 2: Confirm Options ⚠️
**Purpose**: Validate all 4 dimensions. **Skip if `--quick` flag OR all 4 dimensions specified via CLI.** **Purpose**: Validate all 4 dimensions + aspect ratio.
**Skip Conditions**: **Skip Conditions**:
| Condition | Behavior | | Condition | Skipped Questions | Still Asked |
|-----------|----------| |-----------|-------------------|-------------|
| `--quick` flag | Auto-select missing dimensions, skip to Step 3 | | `--quick` flag | Type, Style, Text, Mood | **Aspect Ratio** (unless `--aspect` specified) |
| All 4 dimensions specified | Use specified values, skip to Step 3 | | All 4 dimensions + `--aspect` specified | All | None |
| `quick_mode: true` in EXTEND.md | Auto-select missing dimensions, skip to Step 3 | | `quick_mode: true` in EXTEND.md | Type, Style, Text, Mood | **Aspect Ratio** (unless `--aspect` specified) |
| Otherwise | Show confirmation (current behavior) | | Otherwise | None | All 5 questions |
**Quick Mode Output** (when skipping confirmation): **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** (when skipping 4 dimensions):
``` ```
Quick Mode: Auto-selected dimensions Quick Mode: Auto-selected dimensions
@@ -433,9 +484,8 @@ Quick Mode: Auto-selected dimensions
• Style: [style] ([reason]) • Style: [style] ([reason])
• Text: [text] ([reason]) • Text: [text] ([reason])
• Mood: [mood] ([reason]) • Mood: [mood] ([reason])
• Aspect: [aspect]
Generating... [Then ask Question 5: Aspect Ratio]
``` ```
**Confirmation Flow** (when NOT skipping): **Confirmation Flow** (when NOT skipping):
@@ -513,19 +563,27 @@ options:
description: "High contrast, vivid colors, dynamic" description: "High contrast, vivid colors, dynamic"
``` ```
**Question 5: Aspect Ratio** (if not specified via `--aspect`) **Question 5: Aspect Ratio** (ALWAYS ask unless `--aspect` specified via CLI)
Note: Even if user has a preset in EXTEND.md, still ask this question. The preset is shown as the recommended option.
```yaml ```yaml
header: "Aspect" header: "Aspect"
question: "Cover aspect ratio?" question: "Cover aspect ratio?"
multiSelect: false multiSelect: false
options: options:
- label: "2.35:1 (Recommended)" - label: "[user preset or 16:9] (Recommended)"
description: "Cinematic widescreen, best for article headers" description: "[based on preset or default: Standard widescreen, versatile]"
- label: "16:9" - label: "2.35:1"
description: "Standard widescreen, versatile" description: "Cinematic widescreen - article headers, blog posts"
- label: "4:3"
description: "Traditional screen - PPT slides, classic displays"
- label: "3:2"
description: "Photography ratio - blog articles, Medium posts"
- label: "1:1" - label: "1:1"
description: "Square, social media friendly" description: "Square - Instagram, WeChat moments, social cards"
- label: "3:4"
description: "Portrait - Xiaohongshu, Pinterest, mobile-first content"
``` ```
**After response**: Proceed to Step 3 with confirmed dimensions. **After response**: Proceed to Step 3 with confirmed dimensions.
@@ -602,9 +660,8 @@ Style notes: [key characteristics from style definition]
**Watermark Application** (if enabled in preferences): **Watermark Application** (if enabled in preferences):
Add to prompt: Add to prompt:
``` ```
Include a subtle watermark "[content]" positioned at [position] Include a subtle watermark "[content]" positioned at [position].
with approximately [opacity*100]% visibility. The watermark should The watermark should be legible but not distracting from the main content.
be legible but not distracting from the main content.
``` ```
Reference: `references/config/watermark-guide.md` Reference: `references/config/watermark-guide.md`
@@ -15,7 +15,6 @@ watermark:
enabled: false enabled: false
content: "" content: ""
position: bottom-right # bottom-right|bottom-left|bottom-center|top-right position: bottom-right # bottom-right|bottom-left|bottom-center|top-right
opacity: 0.7 # 0.1-1.0
preferred_type: null # hero|conceptual|typography|metaphor|scene|minimal or null for auto-select preferred_type: null # hero|conceptual|typography|metaphor|scene|minimal or null for auto-select
@@ -52,7 +51,6 @@ custom_styles:
| `watermark.enabled` | bool | false | Enable watermark | | `watermark.enabled` | bool | false | Enable watermark |
| `watermark.content` | string | "" | Watermark text (@username or custom) | | `watermark.content` | string | "" | Watermark text (@username or custom) |
| `watermark.position` | enum | bottom-right | Position on image | | `watermark.position` | enum | bottom-right | Position on image |
| `watermark.opacity` | float | 0.7 | Transparency (0.1-1.0) |
| `preferred_type` | string | null | Type name or null for auto | | `preferred_type` | string | null | Type name or null for auto |
| `preferred_style` | string | null | Style name or null for auto | | `preferred_style` | string | null | Style name or null for auto |
| `preferred_text` | string | title-only | Text density level | | `preferred_text` | string | title-only | Text density level |
@@ -145,7 +143,6 @@ watermark:
enabled: true enabled: true
content: "myblog.com" content: "myblog.com"
position: bottom-right position: bottom-right
opacity: 0.5
preferred_type: conceptual preferred_type: conceptual
@@ -28,15 +28,6 @@ description: Watermark configuration guide for baoyu-cover-image
| `bottom-center` | Centered designs | Text-heavy bottom area | | `bottom-center` | Centered designs | Text-heavy bottom area |
| `top-right` | Bottom-heavy content | Title/header in top-right | | `top-right` | Bottom-heavy content | Title/header in top-right |
## Opacity Examples
| Opacity | Visual Effect | Use Case |
|---------|---------------|----------|
| 0.3 | Very subtle, barely visible | Clean aesthetic priority |
| 0.5 | Balanced, noticeable but not distracting | Default recommendation |
| 0.7 | Clearly visible | Brand recognition priority |
| 0.9 | Strong presence | Anti-copy protection |
## Content Format ## Content Format
| Format | Example | Style | | Format | Example | Style |
@@ -51,16 +42,14 @@ description: Watermark configuration guide for baoyu-cover-image
1. **Consistency**: Use same watermark across all covers 1. **Consistency**: Use same watermark across all covers
2. **Legibility**: Ensure watermark readable on both light/dark areas 2. **Legibility**: Ensure watermark readable on both light/dark areas
3. **Size**: Keep subtle - should not distract from content 3. **Size**: Keep subtle - should not distract from content
4. **Contrast**: Adjust opacity based on cover background
## Prompt Integration ## Prompt Integration
When watermark is enabled, add to image generation prompt: When watermark is enabled, add to image generation prompt:
``` ```
Include a subtle watermark "[content]" positioned at [position] Include a subtle watermark "[content]" positioned at [position].
with approximately [opacity*100]% visibility. The watermark should The watermark should be legible but not distracting from the main content.
be legible but not distracting from the main content.
``` ```
## Cover-Specific Considerations ## Cover-Specific Considerations
@@ -75,7 +64,6 @@ be legible but not distracting from the main content.
| Issue | Solution | | Issue | Solution |
|-------|----------| |-------|----------|
| Watermark invisible | Increase opacity or adjust position | | Watermark invisible | Adjust position or check contrast |
| Watermark too prominent | Decrease opacity (0.3-0.5) | | Watermark too prominent | Change position or reduce size |
| Watermark overlaps title | Change position or reduce title area | | Watermark overlaps title | Change position or reduce title area |
| Inconsistent appearance | Use fixed opacity/position in preferences |
+20 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: baoyu-image-gen name: baoyu-image-gen
description: Generates images using official OpenAI and Google APIs via AI SDK. Supports text-to-image, reference images, aspect ratios, and quality presets. Use when user asks to "generate image with API", "use official API for images", "create image with OpenAI/Google", or needs API-based generation instead of browser-based. description: AI image generation with OpenAI and Google APIs. Supports text-to-image, reference images, aspect ratios, and parallel generation (recommended 4 concurrent subagents). Use when user asks to generate, create, or draw images.
--- ---
# Image Generation (AI SDK) # Image Generation (AI SDK)
@@ -118,6 +118,25 @@ Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
- Google Imagen: uses `aspectRatio` parameter - Google Imagen: uses `aspectRatio` parameter
- OpenAI: maps to closest supported size - OpenAI: maps to closest supported size
## Parallel Generation
Supports concurrent image generation via background subagents for batch operations.
| Setting | Value |
|---------|-------|
| Recommended concurrency | 4 subagents |
| Max concurrency | 8 subagents |
| Use case | Batch generation (slides, comics, infographics) |
**Agent Implementation**:
```
# Launch multiple generations in parallel using Task tool
# Each Task runs as background subagent with run_in_background=true
# Collect results via TaskOutput when all complete
```
**Best Practice**: When generating 4+ images, spawn background subagents (recommended 4 concurrent) instead of sequential execution.
## Error Handling ## Error Handling
- Missing API key → error with setup instructions - Missing API key → error with setup instructions
@@ -2,19 +2,25 @@ import path from "node:path";
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import type { CliArgs } from "../types"; import type { CliArgs } from "../types";
const GOOGLE_MULTIMODAL_MODELS = ["gemini-3-pro-image-preview"]; const GOOGLE_MULTIMODAL_MODELS = ["gemini-3-pro-image-preview", "gemini-3-flash-preview"];
const GOOGLE_IMAGEN_MODELS = ["imagen-3.0-generate-002", "imagen-3.0-generate-001"]; const GOOGLE_IMAGEN_MODELS = ["imagen-3.0-generate-002", "imagen-3.0-generate-001"];
export function getDefaultModel(): string { export function getDefaultModel(): string {
return process.env.GOOGLE_IMAGE_MODEL || "gemini-3-pro-image-preview"; return process.env.GOOGLE_IMAGE_MODEL || "gemini-3-pro-image-preview";
} }
function normalizeGoogleModelId(model: string): string {
return model.startsWith("models/") ? model.slice("models/".length) : model;
}
function isGoogleMultimodal(model: string): boolean { function isGoogleMultimodal(model: string): boolean {
return GOOGLE_MULTIMODAL_MODELS.some((m) => model.includes(m)); const normalized = normalizeGoogleModelId(model);
return GOOGLE_MULTIMODAL_MODELS.some((m) => normalized.includes(m));
} }
function isGoogleImagen(model: string): boolean { function isGoogleImagen(model: string): boolean {
return GOOGLE_IMAGEN_MODELS.some((m) => model.includes(m)); const normalized = normalizeGoogleModelId(model);
return GOOGLE_IMAGEN_MODELS.some((m) => normalized.includes(m));
} }
function getGoogleApiKey(): string | null { function getGoogleApiKey(): string | null {
@@ -26,6 +32,44 @@ function getGoogleImageSize(args: CliArgs): "1K" | "2K" | "4K" {
return args.quality === "2k" ? "2K" : "1K"; return args.quality === "2k" ? "2K" : "1K";
} }
function getGoogleBaseUrl(): string {
const base = process.env.GOOGLE_BASE_URL || "https://generativelanguage.googleapis.com";
return base.replace(/\/+$/g, "");
}
function buildGoogleUrl(pathname: string): string {
const base = getGoogleBaseUrl();
const cleanedPath = pathname.replace(/^\/+/g, "");
if (base.endsWith("/v1beta")) return `${base}/${cleanedPath}`;
return `${base}/v1beta/${cleanedPath}`;
}
function toModelPath(model: string): string {
const modelId = normalizeGoogleModelId(model);
return `models/${modelId}`;
}
async function postGoogleJson<T>(pathname: string, body: unknown): Promise<T> {
const apiKey = getGoogleApiKey();
if (!apiKey) throw new Error("GOOGLE_API_KEY or GEMINI_API_KEY is required");
const res = await fetch(buildGoogleUrl(pathname), {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": apiKey,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Google API error (${res.status}): ${err}`);
}
return (await res.json()) as T;
}
function buildPromptWithAspect(prompt: string, ar: string | null, quality: CliArgs["quality"]): string { function buildPromptWithAspect(prompt: string, ar: string | null, quality: CliArgs["quality"]): string {
let result = prompt; let result = prompt;
if (ar) { if (ar) {
@@ -37,6 +81,11 @@ function buildPromptWithAspect(prompt: string, ar: string | null, quality: CliAr
return result; return result;
} }
function addAspectRatioToPrompt(prompt: string, ar: string | null): string {
if (!ar) return prompt;
return `${prompt} Aspect ratio: ${ar}.`;
}
async function readImageAsBase64(p: string): Promise<{ data: string; mimeType: string }> { async function readImageAsBase64(p: string): Promise<{ data: string; mimeType: string }> {
const buf = await readFile(p); const buf = await readFile(p);
const ext = path.extname(p).toLowerCase(); const ext = path.extname(p).toLowerCase();
@@ -47,53 +96,74 @@ async function readImageAsBase64(p: string): Promise<{ data: string; mimeType: s
return { data: buf.toString("base64"), mimeType }; return { data: buf.toString("base64"), mimeType };
} }
function extractInlineImageData(response: {
candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>;
}): string | null {
for (const candidate of response.candidates || []) {
for (const part of candidate.content?.parts || []) {
const data = part.inlineData?.data;
if (typeof data === "string" && data.length > 0) return data;
}
}
return null;
}
function extractPredictedImageData(response: {
predictions?: Array<any>;
generatedImages?: Array<any>;
}): string | null {
const candidates = [...(response.predictions || []), ...(response.generatedImages || [])];
for (const candidate of candidates) {
if (!candidate || typeof candidate !== "object") continue;
if (typeof candidate.imageBytes === "string") return candidate.imageBytes;
if (typeof candidate.bytesBase64Encoded === "string") return candidate.bytesBase64Encoded;
if (typeof candidate.data === "string") return candidate.data;
const image = candidate.image;
if (image && typeof image === "object") {
if (typeof image.imageBytes === "string") return image.imageBytes;
if (typeof image.bytesBase64Encoded === "string") return image.bytesBase64Encoded;
if (typeof image.data === "string") return image.data;
}
}
return null;
}
async function generateWithGemini( async function generateWithGemini(
prompt: string, prompt: string,
model: string, model: string,
args: CliArgs args: CliArgs
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const { GoogleGenAI } = await import("@google/genai"); const promptWithAspect = addAspectRatioToPrompt(prompt, args.aspectRatio);
const parts: Array<{ text?: string; inlineData?: { data: string; mimeType: string } }> = [];
const apiKey = getGoogleApiKey();
if (!apiKey) throw new Error("GOOGLE_API_KEY or GEMINI_API_KEY is required");
const ai = new GoogleGenAI({
apiKey,
httpOptions: {
baseUrl: process.env.GOOGLE_BASE_URL || undefined,
},
});
const input: Array<{ type: "text" | "image"; text?: string; data?: string; mime_type?: string }> = [];
for (const refPath of args.referenceImages) { for (const refPath of args.referenceImages) {
const { data, mimeType } = await readImageAsBase64(refPath); const { data, mimeType } = await readImageAsBase64(refPath);
input.push({ type: "image", data, mime_type: mimeType }); parts.push({ inlineData: { data, mimeType } });
} }
input.push({ type: "text", text: prompt }); parts.push({ text: promptWithAspect });
const imageConfig: { image_size: "1K" | "2K" | "4K"; aspect_ratio?: string } = { const imageConfig: { imageSize: "1K" | "2K" | "4K" } = {
image_size: getGoogleImageSize(args), imageSize: getGoogleImageSize(args),
}; };
if (args.aspectRatio) {
imageConfig.aspect_ratio = args.aspectRatio;
}
console.log("Generating image with Gemini...", imageConfig); console.log("Generating image with Gemini...", imageConfig);
const interaction = await ai.interactions.create({ const response = await postGoogleJson<{
model, candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>;
input, }>(`${toModelPath(model)}:generateContent`, {
response_modalities: ["image"], contents: [
generation_config: { {
image_config: imageConfig, role: "user",
parts,
},
],
generationConfig: {
responseModalities: ["IMAGE"],
imageConfig,
}, },
}); });
console.log("Generation completed."); console.log("Generation completed.");
for (const output of interaction.outputs || []) { const imageData = extractInlineImageData(response);
if (output.type === "image" && output.data) { if (imageData) return Uint8Array.from(Buffer.from(imageData, "base64"));
return Uint8Array.from(Buffer.from(output.data, "base64"));
}
}
throw new Error("No image in response"); throw new Error("No image in response");
} }
@@ -103,30 +173,40 @@ async function generateWithImagen(
model: string, model: string,
args: CliArgs args: CliArgs
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const { experimental_generateImage: generateImage } = await import("ai");
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
const google = createGoogleGenerativeAI({
apiKey: getGoogleApiKey() || undefined,
baseURL: process.env.GOOGLE_BASE_URL,
});
const fullPrompt = buildPromptWithAspect(prompt, args.aspectRatio, args.quality); const fullPrompt = buildPromptWithAspect(prompt, args.aspectRatio, args.quality);
const imageSize = getGoogleImageSize(args);
if (imageSize === "4K") {
console.error("Warning: Imagen models do not support 4K imageSize, using 2K instead.");
}
const result = await generateImage({ const parameters: Record<string, unknown> = {
model: google.image(model), sampleCount: args.n,
prompt: fullPrompt, };
n: args.n, if (args.aspectRatio) {
aspectRatio: args.aspectRatio || undefined, parameters.aspectRatio = args.aspectRatio;
}
if (imageSize === "1K" || imageSize === "2K") {
parameters.imageSize = imageSize;
} else {
parameters.imageSize = "2K";
}
const response = await postGoogleJson<{
predictions?: Array<any>;
generatedImages?: Array<any>;
}>(`${toModelPath(model)}:predict`, {
instances: [
{
prompt: fullPrompt,
},
],
parameters,
}); });
const img = result.images[0]; const imageData = extractPredictedImageData(response);
if (!img) throw new Error("No image in response"); if (imageData) return Uint8Array.from(Buffer.from(imageData, "base64"));
if (img.uint8Array) return img.uint8Array; throw new Error("No image in response");
if (img.base64) return Uint8Array.from(Buffer.from(img.base64, "base64"));
throw new Error("Cannot extract image data");
} }
export async function generateImage( export async function generateImage(
+18 -1
View File
@@ -56,8 +56,25 @@ npx -y bun ${SKILL_DIR}/scripts/wechat-browser.ts --title "标题" --content "
### Article (文章) ### Article (文章)
Before posting, ask user to choose a theme using AskUserQuestion:
| Theme | Description |
|-------|-------------|
| `default` | 经典主题 - 传统排版,标题居中带底边,二级标题白字彩底 |
| `grace` | 优雅主题 - 文字阴影,圆角卡片,精致引用块 (by @brzhang) |
| `simple` | 简洁主题 - 现代极简风,不对称圆角,清爽留白 (by @okooo5km) |
Default: `default`. If user has already specified a theme, skip the question.
**Workflow**:
1. Generate HTML preview and print the full `htmlPath` from JSON output so user can click to preview:
```bash ```bash
npx -y bun ${SKILL_DIR}/scripts/wechat-article.ts --markdown article.md --theme grace npx -y bun ${SKILL_DIR}/scripts/md-to-wechat.ts article.md --theme <chosen-theme>
```
2. Post to WeChat:
```bash
npx -y bun ${SKILL_DIR}/scripts/wechat-article.ts --markdown article.md --theme <chosen-theme>
``` ```
## Detailed References ## Detailed References
@@ -53,7 +53,7 @@ Regular paragraph with **bold** and *italic*.
## Image Handling ## Image Handling
1. **Parse**: Images in markdown are replaced with `[[IMAGE_PLACEHOLDER_N]]` 1. **Parse**: Images in markdown are replaced with `WECHATIMGPH_N`
2. **Render**: HTML is generated with placeholders in text 2. **Render**: HTML is generated with placeholders in text
3. **Paste**: HTML content is pasted into WeChat editor 3. **Paste**: HTML content is pasted into WeChat editor
4. **Replace**: For each placeholder: 4. **Replace**: For each placeholder:
@@ -81,7 +81,7 @@ Claude:
3. Opens Chrome, navigates to WeChat editor 3. Opens Chrome, navigates to WeChat editor
4. Pastes HTML content 4. Pastes HTML content
5. For each image: 5. For each image:
- Selects [[IMAGE_PLACEHOLDER_1]] - Selects WECHATIMGPH_1
- Scrolls into view - Scrolls into view
- Presses Backspace to delete - Presses Backspace to delete
- Pastes image - Pastes image
@@ -3,6 +3,7 @@ import path from 'node:path';
import { mkdir, writeFile } from 'node:fs/promises'; import { mkdir, writeFile } from 'node:fs/promises';
import os from 'node:os'; import os from 'node:os';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import https from 'node:https'; import https from 'node:https';
import http from 'node:http'; import http from 'node:http';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
@@ -111,17 +112,11 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
return { frontmatter, body: match[2]! }; return { frontmatter, body: match[2]! };
} }
async function parseMarkdownForWechat( export async function convertMarkdown(markdownPath: string, options?: { title?: string; theme?: string }): Promise<ParsedResult> {
markdownPath: string,
options?: { title?: string; theme?: string; tempDir?: string },
): Promise<ParsedResult> {
const content = fs.readFileSync(markdownPath, 'utf-8');
const baseDir = path.dirname(markdownPath); const baseDir = path.dirname(markdownPath);
const tempDir = options?.tempDir ?? path.join(os.tmpdir(), 'wechat-article-images'); const content = fs.readFileSync(markdownPath, 'utf-8');
const theme = options?.theme ?? 'default'; const theme = options?.theme ?? 'default';
await mkdir(tempDir, { recursive: true });
const { frontmatter, body } = parseFrontmatter(content); const { frontmatter, body } = parseFrontmatter(content);
let title = options?.title ?? frontmatter.title ?? ''; let title = options?.title ?? frontmatter.title ?? '';
@@ -129,9 +124,9 @@ async function parseMarkdownForWechat(
const h1Match = body.match(/^#\s+(.+)$/m); const h1Match = body.match(/^#\s+(.+)$/m);
if (h1Match) title = h1Match[1]!; if (h1Match) title = h1Match[1]!;
} }
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
const author = frontmatter.author ?? ''; const author = frontmatter.author || '';
let summary = frontmatter.summary ?? frontmatter.description ?? ''; let summary = frontmatter.description || frontmatter.summary || '';
if (!summary) { if (!summary) {
const lines = body.split('\n'); const lines = body.split('\n');
@@ -161,20 +156,23 @@ async function parseMarkdownForWechat(
let imageCounter = 0; let imageCounter = 0;
const modifiedBody = body.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => { const modifiedBody = body.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
const placeholder = `[[IMAGE_PLACEHOLDER_${++imageCounter}]]`; const placeholder = `WECHATIMGPH_${++imageCounter}`;
images.push({ src, placeholder }); images.push({ src, placeholder });
return placeholder; return placeholder;
}); });
const modifiedMarkdown = `---\n${Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`).join('\n')}\n---\n${modifiedBody}`; const modifiedMarkdown = `---\n${Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`).join('\n')}\n---\n${modifiedBody}`;
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-article-images-'));
const tempMdPath = path.join(tempDir, 'temp-article.md'); const tempMdPath = path.join(tempDir, 'temp-article.md');
await writeFile(tempMdPath, modifiedMarkdown, 'utf-8'); await writeFile(tempMdPath, modifiedMarkdown, 'utf-8');
const scriptDir = path.dirname(new URL(import.meta.url).pathname); const __filename = fileURLToPath(import.meta.url);
const renderScript = path.join(scriptDir, 'md', 'render.ts'); const __dirname = path.dirname(__filename);
const renderScript = path.join(__dirname, 'md', 'render.ts');
console.error(`[md-to-wechat] Rendering markdown with theme: ${theme}`); console.error(`[md-to-wechat] Rendering markdown with theme: ${theme}`);
const result = spawnSync('npx', ['-y', 'bun', renderScript, tempMdPath, '--theme', theme], { const result = spawnSync('npx', ['-y', 'bun', renderScript, tempMdPath, '--theme', theme], {
stdio: ['inherit', 'pipe', 'pipe'], stdio: ['inherit', 'pipe', 'pipe'],
cwd: baseDir, cwd: baseDir,
@@ -226,8 +224,8 @@ Output JSON format:
"htmlPath": "/tmp/wechat-article-images/temp-article.html", "htmlPath": "/tmp/wechat-article-images/temp-article.html",
"contentImages": [ "contentImages": [
{ {
"placeholder": "[[IMAGE_PLACEHOLDER_1]]", "placeholder": "WECHATIMGPH_1",
"localPath": "/tmp/wechat-article-images/img.png", "localPath": "/tmp/wechat-image/img.png",
"originalPath": "imgs/image.png" "originalPath": "imgs/image.png"
} }
] ]
@@ -262,7 +260,7 @@ async function main(): Promise<void> {
} }
if (!markdownPath) { if (!markdownPath) {
console.error('Error: Markdown file path required'); console.error('Error: Markdown file path is required');
process.exit(1); process.exit(1);
} }
@@ -271,7 +269,7 @@ async function main(): Promise<void> {
process.exit(1); process.exit(1);
} }
const result = await parseMarkdownForWechat(markdownPath, { title, theme }); const result = await convertMarkdown(markdownPath, { title, theme });
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
} }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import process from 'node:process'; import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { launchChrome, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, type ChromeSession, type CdpConnection } from './cdp.ts'; import { launchChrome, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, type ChromeSession, type CdpConnection } from './cdp.ts';
const WECHAT_URL = 'https://mp.weixin.qq.com/'; const WECHAT_URL = 'https://mp.weixin.qq.com/';
@@ -65,8 +66,9 @@ async function clickMenuByText(session: ChromeSession, text: string): Promise<vo
} }
async function copyImageToClipboard(imagePath: string): Promise<void> { async function copyImageToClipboard(imagePath: string): Promise<void> {
const scriptDir = path.dirname(new URL(import.meta.url).pathname); const __filename = fileURLToPath(import.meta.url);
const copyScript = path.join(scriptDir, './copy-to-clipboard.ts'); const __dirname = path.dirname(__filename);
const copyScript = path.join(__dirname, './copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' }); const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
if (result.status !== 0) throw new Error(`Failed to copy image: ${imagePath}`); if (result.status !== 0) throw new Error(`Failed to copy image: ${imagePath}`);
} }
@@ -78,6 +80,30 @@ async function pasteInEditor(session: ChromeSession): Promise<void> {
await session.cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'v', code: 'KeyV', modifiers, windowsVirtualKeyCode: 86 }, { sessionId: session.sessionId }); await session.cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'v', code: 'KeyV', modifiers, windowsVirtualKeyCode: 86 }, { sessionId: session.sessionId });
} }
async function sendCopy(cdp?: CdpConnection, sessionId?: string): Promise<void> {
if (process.platform === 'darwin') {
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "c" using command down']);
} else if (process.platform === 'linux') {
spawnSync('xdotool', ['key', 'ctrl+c']);
} else if (cdp && sessionId) {
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'c', code: 'KeyC', modifiers: 2, windowsVirtualKeyCode: 67 }, { sessionId });
await sleep(50);
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'c', code: 'KeyC', modifiers: 2, windowsVirtualKeyCode: 67 }, { sessionId });
}
}
async function sendPaste(cdp?: CdpConnection, sessionId?: string): Promise<void> {
if (process.platform === 'darwin') {
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']);
} else if (process.platform === 'linux') {
spawnSync('xdotool', ['key', 'ctrl+v']);
} else if (cdp && sessionId) {
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'v', code: 'KeyV', modifiers: 2, windowsVirtualKeyCode: 86 }, { sessionId });
await sleep(50);
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'v', code: 'KeyV', modifiers: 2, windowsVirtualKeyCode: 86 }, { sessionId });
}
}
async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Promise<void> { async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Promise<void> {
const absolutePath = path.isAbsolute(htmlFilePath) ? htmlFilePath : path.resolve(process.cwd(), htmlFilePath); const absolutePath = path.isAbsolute(htmlFilePath) ? htmlFilePath : path.resolve(process.cwd(), htmlFilePath);
const fileUrl = `file://${absolutePath}`; const fileUrl = `file://${absolutePath}`;
@@ -108,30 +134,24 @@ async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Pr
}, { sessionId }); }, { sessionId });
await sleep(300); await sleep(300);
console.log('[wechat] Copying with system Cmd+C...'); console.log('[wechat] Copying content...');
if (process.platform === 'darwin') { await sendCopy(cdp, sessionId);
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "c" using command down']);
} else {
spawnSync('xdotool', ['key', 'ctrl+c']);
}
await sleep(1000); await sleep(1000);
console.log('[wechat] Closing HTML tab...'); console.log('[wechat] Closing HTML tab...');
await cdp.send('Target.closeTarget', { targetId }); await cdp.send('Target.closeTarget', { targetId });
} }
async function pasteFromClipboardInEditor(): Promise<void> { async function pasteFromClipboardInEditor(session: ChromeSession): Promise<void> {
if (process.platform === 'darwin') { console.log('[wechat] Pasting content...');
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']); await sendPaste(session.cdp, session.sessionId);
} else {
spawnSync('xdotool', ['key', 'ctrl+v']);
}
await sleep(1000); await sleep(1000);
} }
async function parseMarkdownWithPlaceholders(markdownPath: string, theme?: string): Promise<{ title: string; author: string; summary: string; htmlPath: string; contentImages: ImageInfo[] }> { async function parseMarkdownWithPlaceholders(markdownPath: string, theme?: string): Promise<{ title: string; author: string; summary: string; htmlPath: string; contentImages: ImageInfo[] }> {
const scriptDir = path.dirname(new URL(import.meta.url).pathname); const __filename = fileURLToPath(import.meta.url);
const mdToWechatScript = path.join(scriptDir, 'md-to-wechat.ts'); const __dirname = path.dirname(__filename);
const mdToWechatScript = path.join(__dirname, 'md-to-wechat.ts');
const args = ['-y', 'bun', mdToWechatScript, markdownPath]; const args = ['-y', 'bun', mdToWechatScript, markdownPath];
if (theme) args.push('--theme', theme); if (theme) args.push('--theme', theme);
@@ -295,6 +315,10 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
console.log('[wechat] Clicking on editor...'); console.log('[wechat] Clicking on editor...');
await clickElement(session, '.ProseMirror'); await clickElement(session, '.ProseMirror');
await sleep(1000);
console.log('[wechat] Ensuring editor focus...');
await clickElement(session, '.ProseMirror');
await sleep(500); await sleep(500);
if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) { if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) {
@@ -302,7 +326,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
await copyHtmlFromBrowser(cdp, effectiveHtmlFile); await copyHtmlFromBrowser(cdp, effectiveHtmlFile);
await sleep(500); await sleep(500);
console.log('[wechat] Pasting into editor...'); console.log('[wechat] Pasting into editor...');
await pasteFromClipboardInEditor(); await pasteFromClipboardInEditor(session);
await sleep(3000); await sleep(3000);
if (contentImages.length > 0) { if (contentImages.length > 0) {
@@ -328,7 +352,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
await sleep(200); await sleep(200);
console.log('[wechat] Pasting image...'); console.log('[wechat] Pasting image...');
await pasteFromClipboardInEditor(); await pasteFromClipboardInEditor(session);
await sleep(3000); await sleep(3000);
} }
console.log('[wechat] All images inserted.'); console.log('[wechat] All images inserted.');
@@ -67,7 +67,7 @@ Code blocks become blockquotes (X doesn't support code)
1. **Cover Image**: First image or `cover_image` from frontmatter 1. **Cover Image**: First image or `cover_image` from frontmatter
2. **Remote Images**: Automatically downloaded to temp directory 2. **Remote Images**: Automatically downloaded to temp directory
3. **Placeholders**: Images in content use `[[IMAGE_PLACEHOLDER_N]]` format 3. **Placeholders**: Images in content use `XIMGPH_N` format
4. **Insertion**: Placeholders are found, selected, and replaced with actual images 4. **Insertion**: Placeholders are found, selected, and replaced with actual images
## Markdown to HTML Script ## Markdown to HTML Script
@@ -92,7 +92,7 @@ JSON output:
"coverImage": "/path/to/cover.jpg", "coverImage": "/path/to/cover.jpg",
"contentImages": [ "contentImages": [
{ {
"placeholder": "[[IMAGE_PLACEHOLDER_1]]", "placeholder": "XIMGPH_1",
"localPath": "/tmp/x-article-images/img.png", "localPath": "/tmp/x-article-images/img.png",
"blockIndex": 5 "blockIndex": 5
} }
+5 -5
View File
@@ -280,7 +280,7 @@ export async function parseMarkdown(
let imageCounter = 0; let imageCounter = 0;
const { html, totalBlocks } = convertMarkdownToHtml(body, (src, alt) => { const { html, totalBlocks } = convertMarkdownToHtml(body, (src, alt) => {
const placeholder = `[[IMAGE_PLACEHOLDER_${++imageCounter}]]`; const placeholder = `XIMGPH_${++imageCounter}`;
const currentBlockIndex = images.length; // Will be set properly after HTML generation const currentBlockIndex = images.length; // Will be set properly after HTML generation
images.push({ src, alt, blockIndex: -1 }); // blockIndex set later images.push({ src, alt, blockIndex: -1 }); // blockIndex set later
@@ -292,7 +292,7 @@ export async function parseMarkdown(
let blockIdx = 0; let blockIdx = 0;
for (const line of htmlLines) { for (const line of htmlLines) {
for (let i = 0; i < images.length; i++) { for (let i = 0; i < images.length; i++) {
const placeholder = `[[IMAGE_PLACEHOLDER_${i + 1}]]`; const placeholder = `XIMGPH_${i + 1}`;
if (line.includes(placeholder)) { if (line.includes(placeholder)) {
images[i]!.blockIndex = blockIdx; images[i]!.blockIndex = blockIdx;
} }
@@ -312,7 +312,7 @@ export async function parseMarkdown(
// First image becomes cover if no cover specified // First image becomes cover if no cover specified
if (isFirstImage && !coverImagePath) { if (isFirstImage && !coverImagePath) {
coverImagePath = localPath; coverImagePath = localPath;
coverPlaceholder = `[[IMAGE_PLACEHOLDER_${i + 1}]]`; coverPlaceholder = `XIMGPH_${i + 1}`;
isFirstImage = false; isFirstImage = false;
// Don't add to contentImages, it's the cover // Don't add to contentImages, it's the cover
continue; continue;
@@ -320,7 +320,7 @@ export async function parseMarkdown(
isFirstImage = false; isFirstImage = false;
contentImages.push({ contentImages.push({
placeholder: `[[IMAGE_PLACEHOLDER_${i + 1}]]`, placeholder: `XIMGPH_${i + 1}`,
localPath, localPath,
originalPath: img.src, originalPath: img.src,
blockIndex: img.blockIndex, blockIndex: img.blockIndex,
@@ -331,7 +331,7 @@ export async function parseMarkdown(
let finalHtml = html; let finalHtml = html;
if (coverPlaceholder) { if (coverPlaceholder) {
// Remove the placeholder and its containing <p> tag // Remove the placeholder and its containing <p> tag
finalHtml = finalHtml.replace(new RegExp(`<p>${coverPlaceholder.replace(/[[\]]/g, '\\$&')}</p>\\n?`, 'g'), ''); finalHtml = finalHtml.replace(new RegExp(`<p>${coverPlaceholder}</p>\\n?`, 'g'), '');
} }
// Resolve cover image path // Resolve cover image path
+2 -3
View File
@@ -359,9 +359,8 @@ With confirmed outline + style + layout:
**Watermark Application** (if enabled in preferences): **Watermark Application** (if enabled in preferences):
Add to each image generation prompt: Add to each image generation prompt:
``` ```
Include a subtle watermark "[content]" positioned at [position] Include a subtle watermark "[content]" positioned at [position].
with approximately [opacity*100]% visibility. The watermark should The watermark should be legible but not distracting from the main content.
be legible but not distracting from the main content.
``` ```
Reference: `references/config/watermark-guide.md` Reference: `references/config/watermark-guide.md`
@@ -15,7 +15,6 @@ watermark:
enabled: false enabled: false
content: "" content: ""
position: bottom-right # bottom-right|bottom-left|bottom-center|top-right position: bottom-right # bottom-right|bottom-left|bottom-center|top-right
opacity: 0.7 # 0.1-1.0
preferred_style: preferred_style:
name: null # Built-in or custom style name name: null # Built-in or custom style name
@@ -46,7 +45,6 @@ custom_styles:
| `watermark.enabled` | bool | false | Enable watermark | | `watermark.enabled` | bool | false | Enable watermark |
| `watermark.content` | string | "" | Watermark text (@username or custom) | | `watermark.content` | string | "" | Watermark text (@username or custom) |
| `watermark.position` | enum | bottom-right | Position on image | | `watermark.position` | enum | bottom-right | Position on image |
| `watermark.opacity` | float | 0.7 | Transparency (0.1-1.0) |
| `preferred_style.name` | string | null | Style name or null | | `preferred_style.name` | string | null | Style name or null |
| `preferred_style.description` | string | "" | Custom notes/override | | `preferred_style.description` | string | "" | Custom notes/override |
| `preferred_layout` | string | null | Layout preference or null | | `preferred_layout` | string | null | Layout preference or null |
@@ -97,7 +95,6 @@ watermark:
enabled: true enabled: true
content: "@myxhsaccount" content: "@myxhsaccount"
position: bottom-right position: bottom-right
opacity: 0.5
preferred_style: preferred_style:
name: notion name: notion
@@ -28,15 +28,6 @@ description: Watermark configuration guide for baoyu-xhs-images
| `bottom-center` | Centered designs | Text-heavy bottom area | | `bottom-center` | Centered designs | Text-heavy bottom area |
| `top-right` | Bottom-heavy content | Title/header in top-right | | `top-right` | Bottom-heavy content | Title/header in top-right |
## Opacity Examples
| Opacity | Visual Effect | Use Case |
|---------|---------------|----------|
| 0.3 | Very subtle, barely visible | Clean aesthetic priority |
| 0.5 | Balanced, noticeable but not distracting | Default recommendation |
| 0.7 | Clearly visible | Brand recognition priority |
| 0.9 | Strong presence | Anti-copy protection |
## Content Format ## Content Format
| Format | Example | Style | | Format | Example | Style |
@@ -51,23 +42,21 @@ description: Watermark configuration guide for baoyu-xhs-images
1. **Consistency**: Use same watermark across all images in series 1. **Consistency**: Use same watermark across all images in series
2. **Legibility**: Ensure watermark readable on both light/dark areas 2. **Legibility**: Ensure watermark readable on both light/dark areas
3. **Size**: Keep subtle - should not distract from content 3. **Size**: Keep subtle - should not distract from content
4. **Contrast**: Opacity may need adjustment per image background
## Prompt Integration ## Prompt Integration
When watermark is enabled, add to image generation prompt: When watermark is enabled, add to image generation prompt:
``` ```
Include a subtle watermark "[content]" positioned at [position] Include a subtle watermark "[content]" positioned at [position].
with approximately [opacity*100]% visibility. The watermark should The watermark should be legible but not distracting from the main content.
be legible but not distracting from the main content.
``` ```
## Common Issues ## Common Issues
| Issue | Solution | | Issue | Solution |
|-------|----------| |-------|----------|
| Watermark invisible | Increase opacity or adjust position | | Watermark invisible | Adjust position or check contrast |
| Watermark too prominent | Decrease opacity (0.3-0.5) | | Watermark too prominent | Change position or reduce size |
| Watermark overlaps content | Change position | | Watermark overlaps content | Change position |
| Inconsistent across images | Use session ID for consistency | | Inconsistent across images | Use session ID for consistency |