Compare commits

...

19 Commits

Author SHA1 Message Date
Jim Liu 宝玉 9e2ee0659b chore: release v1.15.2 2026-01-23 12:02:37 -06:00
Jim Liu 宝玉 94c9309aa6 chore: release v1.15.1 2026-01-22 16:21:11 -06:00
Jim Liu 宝玉 eb92bdb9df chore: release v1.15.0 2026-01-22 11:47:56 -06:00
Jim Liu 宝玉 e07d33fed0 chore: release v1.14.0 2026-01-22 01:24:52 -06:00
Jim Liu 宝玉 e964feb5e5 Merge pull request #14 from fkysly/fix/video-ready-detection
fix(x-video): improve video ready detection
2026-01-21 21:42:55 -06:00
fkysly 7669556736 fix(x-video): improve video ready detection
- Type text while video uploads in background instead of waiting
- Fix video ready detection by checking tweet button state instead of
  progressbar presence (X always has progressbar elements for video
  playback, causing false negatives)
2026-01-22 09:56:02 +08:00
Jim Liu 宝玉 97da7ab4eb chore: release v1.13.0 2026-01-21 19:40:46 -06:00
Jim Liu 宝玉 235868343c chore: release v1.12.0 2026-01-21 11:28:41 -06:00
Jim Liu 宝玉 f43dc2be56 Merge pull request #13 from threehotpot-bot/feature/quote-tweet
feat: add quote tweet support
2026-01-21 10:39:22 -06:00
Jim Liu 宝玉 776afba5d8 chore: release v1.11.0 2026-01-21 10:16:12 -06:00
threenrm d793c19a72 fix: use CDP Input.insertText for reliable text insertion
The previous document.execCommand('insertText') caused text to be
duplicated multiple times. Switch to CDP's Input.insertText for
more reliable text input.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 18:21:36 +08:00
threenrm 8cc8d25ad1 feat: add quote tweet support
Add x-quote.ts script to quote existing tweets with comments.
This allows users to share content while giving credit to the original creator.

- New script: scripts/x-quote.ts for quote tweet functionality
- Uses CDP to navigate through X's native quote UI
- Supports preview mode and --submit flag
- Updated SKILL.md with documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 18:14:54 +08:00
Jim Liu 宝玉 0d677ea171 docs: Update readme, use npx skills add to install 2026-01-21 01:59:42 -06:00
Jim Liu 宝玉 e519fcdb27 chore: release v1.10.0 2026-01-21 01:00:42 -06:00
Jim Liu 宝玉 ea14c42716 Merge pull request #11 from fkysly/feature/video-support
feat: add video posting support
2026-01-21 00:59:32 -06:00
Jim Liu 宝玉 22d46f32f4 docs(release-skills): add Chinese trigger words to description
Adds "发布", "push", "推送", "新版本", "更新版本" to skill description
for better auto-detection when user requests release in Chinese.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:21:28 -06:00
Jim Liu 宝玉 64726e9df1 docs: add mandatory release checklist to prevent missing CHANGELOG
- Add CRITICAL checklist to release-skills SKILL.md
- Add Release Process section to CLAUDE.md
- Ensures CHANGELOG updates are never skipped

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:20:11 -06:00
Jim Liu 宝玉 3ea311dfed feat: add chalkboard style and xhs-images previews
- Add chalkboard style to baoyu-xhs-images and baoyu-comic
- Update chalkboard style for article-illustrator, cover-image, infographic
- Remove tech style from baoyu-xhs-images
- Add style and layout preview screenshots for xhs-images
- Bump version to 1.9.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 21:52:53 -06:00
fkysly cdc5a9c41c feat: add video posting support
- Add x-video.ts script for posting videos to X
- Video is uploaded first, then text is added (fixes text being cleared issue)
- Uses DOM.setFileInputFiles for direct video upload via CDP
- Supports MP4, MOV, WebM formats
- Waits for video processing before allowing submission
- Update SKILL.md with video posting documentation
2026-01-21 10:59:00 +08:00
79 changed files with 5680 additions and 1140 deletions
+5 -3
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.8.0"
"version": "1.15.2"
},
"plugins": [
{
@@ -31,7 +31,8 @@
"source": "./",
"strict": false,
"skills": [
"./skills/baoyu-danger-gemini-web"
"./skills/baoyu-danger-gemini-web",
"./skills/baoyu-image-gen"
]
},
{
@@ -41,7 +42,8 @@
"strict": false,
"skills": [
"./skills/baoyu-danger-x-to-markdown",
"./skills/baoyu-compress-image"
"./skills/baoyu-compress-image",
"./skills/baoyu-url-to-markdown"
]
}
]
+289 -157
View File
@@ -1,183 +1,315 @@
---
name: release-skills
description: Release workflow for baoyu-skills plugin. This skill should be used when the user wants to create a new release version. It analyzes changes since the last version tag, updates changelogs (EN/CN), bumps the version in marketplace.json, commits changes, and creates a version tag. Supports dry-run mode and breaking change detection.
description: Universal release workflow. Auto-detects version files and changelogs. Supports Node.js, Python, Rust, Claude Plugin, and generic projects. Use when user says "release", "发布", "new version", "bump version", "push", "推送".
---
# Release Skills
Automate the release process for baoyu-skills plugin: analyze changes, update changelogs, bump version, commit, and tag.
Universal release workflow supporting any project type with multi-language changelog.
## When to Use
## Quick Start
Trigger this skill when user requests:
- "release", "发布", "create release", "new version"
- "bump version", "update version"
- "prepare release"
Just run `/release-skills` - auto-detects your project configuration.
## Workflow
## Supported Projects
### Step 1: Analyze Changes Since Last Tag
```bash
# Get the latest version tag
LAST_TAG=$(git tag --sort=-v:refname | head -1)
# Show changes since last tag
git log ${LAST_TAG}..HEAD --oneline
git diff ${LAST_TAG}..HEAD --stat
```
Categorize changes by type based on commit messages and file changes:
| Type | Prefix | Description |
|------|--------|-------------|
| feat | `feat:` | New features, new skills |
| fix | `fix:` | Bug fixes |
| docs | `docs:` | Documentation only |
| refactor | `refactor:` | Code refactoring |
| style | `style:` | Formatting, styling |
| chore | `chore:` | Build, tooling, maintenance |
**Breaking Change Detection**: If changes include:
- Removed skills or scripts
- Changed API/interfaces
- Renamed public functions/options
Warn user: "Breaking changes detected. Consider major version bump (--major flag)."
### Step 2: Determine Version Bump
Current version location: `.claude-plugin/marketplace.json``metadata.version`
Version rules:
- **Patch** (0.6.1 → 0.6.2): Bug fixes, docs updates, minor improvements
- **Minor** (0.6.x → 0.7.0): New features, new skills, significant enhancements
- **Major** (0.x → 1.0): Breaking changes, only when user explicitly requests with `--major`
Default behavior:
- If changes include `feat:` or new skills → Minor bump
- Otherwise → Patch bump
### Step 3: Check and Update README
Before updating changelogs, check if README files need updates based on changes:
**When to update README**:
- New skills added → Add to skill list
- Skills removed → Remove from skill list
- Skill renamed → Update references
- New features affecting usage → Update usage section
- Breaking changes → Update migration notes
**Files to sync**:
- `README.md` (English)
- `README.zh.md` (Chinese)
If changes include new skills or significant feature changes, update both README files to reflect the new capabilities. Keep both files in sync with the same structure and information.
### Step 4: Update Changelogs
Files to update:
- `CHANGELOG.md` (English)
- `CHANGELOG.zh.md` (Chinese)
Format (insert after header, before previous version):
```markdown
## {NEW_VERSION} - {YYYY-MM-DD}
### Features
- `skill-name`: description of new feature
### Fixes
- `skill-name`: description of fix
### Documentation
- description of docs changes
### Other
- description of other changes
```
Only include sections that have changes. Omit empty sections.
For Chinese changelog, translate the content maintaining the same structure.
### Step 5: Update marketplace.json
Update `.claude-plugin/marketplace.json`:
```json
{
"metadata": {
"version": "{NEW_VERSION}"
}
}
```
### Step 6: Commit Changes
```bash
git add README.md README.zh.md CHANGELOG.md CHANGELOG.zh.md .claude-plugin/marketplace.json
git commit -m "chore: release v{NEW_VERSION}"
```
**Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution.
### Step 7: Create Version Tag
```bash
git tag v{NEW_VERSION}
```
**Important**: Do NOT push to remote. User will push manually when ready.
| Project Type | Version File | Auto-Detected |
|--------------|--------------|---------------|
| Node.js | package.json | ✓ |
| Python | pyproject.toml | ✓ |
| Rust | Cargo.toml | ✓ |
| Claude Plugin | marketplace.json | ✓ |
| Generic | VERSION / version.txt | ✓ |
## Options
| Flag | Description |
|------|-------------|
| `--dry-run` | Preview changes without executing. Show what would be updated. |
| `--major` | Force major version bump (0.x → 1.0 or 1.x → 2.0) |
| `--dry-run` | Preview changes without executing |
| `--major` | Force major version bump |
| `--minor` | Force minor version bump |
| `--patch` | Force patch version bump |
| `--pre <tag>` | (Reserved) Create pre-release version, e.g., `--pre beta``0.7.0-beta.1` |
## Workflow
### Step 1: Detect Project Configuration
1. Check for `.releaserc.yml` (optional config override)
2. Auto-detect version file by scanning (priority order):
- `package.json` (Node.js)
- `pyproject.toml` (Python)
- `Cargo.toml` (Rust)
- `marketplace.json` or `.claude-plugin/marketplace.json` (Claude Plugin)
- `VERSION` or `version.txt` (Generic)
3. Scan for changelog files using glob patterns:
- `CHANGELOG*.md`
- `HISTORY*.md`
- `CHANGES*.md`
4. Identify language of each changelog by filename suffix
5. Display detected configuration
**Language Detection Rules**:
| Filename Pattern | Language |
|------------------|----------|
| `CHANGELOG.md` (no suffix) | en (default) |
| `CHANGELOG.zh.md` / `CHANGELOG_CN.md` / `CHANGELOG.zh-CN.md` | zh |
| `CHANGELOG.ja.md` / `CHANGELOG_JP.md` | ja |
| `CHANGELOG.ko.md` / `CHANGELOG_KR.md` | ko |
| `CHANGELOG.de.md` / `CHANGELOG_DE.md` | de |
| `CHANGELOG.fr.md` / `CHANGELOG_FR.md` | fr |
| `CHANGELOG.es.md` / `CHANGELOG_ES.md` | es |
| `CHANGELOG.{lang}.md` | Corresponding language code |
**Output Example**:
```
Project detected:
Version file: package.json (1.2.3)
Changelogs:
- CHANGELOG.md (en)
- CHANGELOG.zh.md (zh)
- CHANGELOG.ja.md (ja)
```
### Step 2: Analyze Changes Since Last Tag
```bash
LAST_TAG=$(git tag --sort=-v:refname | head -1)
git log ${LAST_TAG}..HEAD --oneline
git diff ${LAST_TAG}..HEAD --stat
```
Categorize by conventional commit types:
| Type | Description |
|------|-------------|
| feat | New features |
| fix | Bug fixes |
| docs | Documentation |
| refactor | Code refactoring |
| perf | Performance improvements |
| test | Test changes |
| style | Formatting, styling |
| chore | Maintenance (skip in changelog) |
**Breaking Change Detection**:
- Commit message starts with `BREAKING CHANGE`
- Commit body/footer contains `BREAKING CHANGE:`
- Removed public APIs, renamed exports, changed interfaces
If breaking changes detected, warn user: "Breaking changes detected. Consider major version bump (--major flag)."
### Step 3: Determine Version Bump
Rules (in priority order):
1. User flag `--major/--minor/--patch` → Use specified
2. BREAKING CHANGE detected → Major bump (1.x.x → 2.0.0)
3. `feat:` commits present → Minor bump (1.2.x → 1.3.0)
4. Otherwise → Patch bump (1.2.3 → 1.2.4)
Display version change: `1.2.3 → 1.3.0`
### Step 4: Generate Multi-language Changelogs
For each detected changelog file:
1. **Identify language** from filename suffix
2. **Generate content in that language**:
- Section titles in target language
- Change descriptions written naturally in target language (not translated)
- Date format: YYYY-MM-DD (universal)
3. **Insert at file head** (preserve existing content)
**Section Title Translations** (built-in):
| Type | en | zh | ja | ko | de | fr | es |
|------|----|----|----|----|----|----|-----|
| feat | Features | 新功能 | 新機能 | 새로운 기능 | Funktionen | Fonctionnalités | Características |
| fix | Fixes | 修复 | 修正 | 수정 | Fehlerbehebungen | Corrections | Correcciones |
| docs | Documentation | 文档 | ドキュメント | 문서 | Dokumentation | Documentation | Documentación |
| refactor | Refactor | 重构 | リファクタリング | 리팩토링 | Refactoring | Refactorisation | Refactorización |
| perf | Performance | 性能优化 | パフォーマンス | 성능 | Leistung | Performance | Rendimiento |
| breaking | Breaking Changes | 破坏性变更 | 破壊的変更 | 주요 변경사항 | Breaking Changes | Changements majeurs | Cambios importantes |
**Changelog Format**:
```markdown
## {VERSION} - {YYYY-MM-DD}
### Features
- Description of new feature
### Fixes
- Description of fix
### Documentation
- Description of docs changes
```
Only include sections that have changes. Omit empty sections.
**Multi-language Example**:
English (CHANGELOG.md):
```markdown
## 1.3.0 - 2026-01-22
### Features
- Add user authentication module
- Support OAuth2 login
### Fixes
- Fix memory leak in connection pool
```
Chinese (CHANGELOG.zh.md):
```markdown
## 1.3.0 - 2026-01-22
### 新功能
- 新增用户认证模块
- 支持 OAuth2 登录
### 修复
- 修复连接池内存泄漏问题
```
Japanese (CHANGELOG.ja.md):
```markdown
## 1.3.0 - 2026-01-22
### 新機能
- ユーザー認証モジュールを追加
- OAuth2 ログインをサポート
### 修正
- コネクションプールのメモリリークを修正
```
### Step 5: Update Version File
1. Read version file (JSON/TOML/text)
2. Update version number
3. Write back (preserve formatting)
**Version Paths by File Type**:
| File | Path |
|------|------|
| package.json | `$.version` |
| pyproject.toml | `project.version` |
| Cargo.toml | `package.version` |
| marketplace.json | `$.metadata.version` |
| VERSION / version.txt | Direct content |
### Step 6: Commit and Tag
```bash
git add <all modified files>
git commit -m "chore: release v{VERSION}"
git tag v{VERSION}
```
**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**:
```
Release v1.3.0 created locally.
To publish:
git push origin main
git push origin v1.3.0
```
## Configuration (.releaserc.yml)
Optional config file in project root to override defaults:
```yaml
# .releaserc.yml - Optional configuration
# Version file (auto-detected if not specified)
version:
file: package.json
path: $.version # JSONPath for JSON, dotted path for TOML
# Changelog files (auto-detected if not specified)
changelog:
files:
- path: CHANGELOG.md
lang: en
- path: CHANGELOG.zh.md
lang: zh
- path: CHANGELOG.ja.md
lang: ja
# Section mapping (conventional commit type → changelog section)
# Use null to skip a type in changelog
sections:
feat: Features
fix: Fixes
docs: Documentation
refactor: Refactor
perf: Performance
test: Tests
chore: null
# Commit message format
commit:
message: "chore: release v{version}"
# Tag format
tag:
prefix: v # Results in v1.0.0
sign: false
# Additional files to include in release commit
include:
- README.md
- package.json
```
## Dry-Run Mode
When `--dry-run` is specified:
1. Show all changes since last tag
2. Show proposed version bump (current → new)
3. Show draft changelog entries (EN and CN)
4. Show files that would be modified
5. Do NOT make any actual changes
Output format:
```
=== DRY RUN MODE ===
Last tag: v0.6.1
Proposed version: v0.7.0
Project detected:
Version file: package.json (1.2.3)
Changelogs: CHANGELOG.md (en), CHANGELOG.zh.md (zh)
Last tag: v1.2.3
Proposed version: v1.3.0
Changes detected:
- feat: new skill baoyu-foo added
- fix: baoyu-bar timeout issue
- docs: updated README
feat: add user authentication
feat: support oauth2 login
fix: memory leak in pool
Changelog preview (EN):
## 0.7.0 - 2026-01-17
### Features
- `baoyu-foo`: new skill for ...
### Fixes
- `baoyu-bar`: fixed timeout issue
Changelog preview (en):
## 1.3.0 - 2026-01-22
### Features
- Add user authentication module
- Support OAuth2 login
### Fixes
- Fix memory leak in connection pool
README updates needed: Yes/No
(If yes, show proposed changes)
Changelog preview (zh):
## 1.3.0 - 2026-01-22
### 新功能
- 新增用户认证模块
- 支持 OAuth2 登录
### 修复
- 修复连接池内存泄漏问题
Files to modify:
- README.md (if updates needed)
- README.zh.md (if updates needed)
- CHANGELOG.md
- CHANGELOG.zh.md
- .claude-plugin/marketplace.json
- package.json
- CHANGELOG.md
- CHANGELOG.zh.md
No changes made. Run without --dry-run to execute.
```
@@ -188,16 +320,16 @@ No changes made. Run without --dry-run to execute.
/release-skills # Auto-detect version bump
/release-skills --dry-run # Preview only
/release-skills --minor # Force minor bump
/release-skills --patch # Force patch bump
/release-skills --major # Force major bump (with confirmation)
```
## Post-Release Reminder
## When to Use
After successful release, remind user:
```
Release v{NEW_VERSION} created locally.
Trigger this skill when user requests:
- "release", "发布", "create release", "new version", "新版本"
- "bump version", "update version", "更新版本"
- "prepare release"
- "push to remote" (with uncommitted changes)
To publish:
git push origin main
git push origin v{NEW_VERSION}
```
**Important**: If user says "just push" or "直接 push" with uncommitted changes, STILL follow all steps above first.
+4
View File
@@ -6,6 +6,7 @@ yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
@@ -145,3 +146,6 @@ tests-data/
.baoyu-skills/
x-to-markdown/
xhs-images/
url-to-markdown/
cover-image/
slide-deck/
+68
View File
@@ -2,6 +2,74 @@
English | [中文](./CHANGELOG.zh.md)
## 1.15.2 - 2026-01-23
### Documentation
- `release-skills`: comprehensive SKILL.md rewrite—adds multi-language changelog support, .releaserc.yml configuration, dry-run mode, language detection rules, and section title translations for 7 languages.
## 1.15.1 - 2026-01-22
### Refactor
- `baoyu-xhs-images`: restructures reference documents into modular architecture—reorganizes scattered files into `config/` (settings), `elements/` (visual building blocks), `presets/` (style definitions), and `workflows/` (process guides) directories for improved maintainability.
## 1.15.0 - 2026-01-22
### Features
- `baoyu-xhs-images`: adds user preferences support via EXTEND.md—configure watermark (content, position, opacity), preferred style, preferred layout, and custom styles. New Step 0 checks for preferences at project (`.baoyu-skills/`) or user (`~/.baoyu-skills/`) level with first-time setup flow.
### Documentation
- `baoyu-xhs-images`: adds three reference documents—`preferences-schema.md` (YAML schema), `watermark-guide.md` (position and opacity guide), `first-time-setup.md` (setup flow).
## 1.14.0 - 2026-01-22
### Fixes
- `baoyu-post-to-x`: improves video ready detection for more reliable video posting.
### Documentation
- `baoyu-slide-deck`: comprehensive SKILL.md enhancement—adds slide count guidance (recommended 8-25, max 30), audience guidelines table with audience-specific principles, style selection principles with content-type recommendations, layout selection tips with common mistakes to avoid, visual hierarchy principles, content density guidelines (McKinsey-style high-density principles), color selection guide, typography principles with font recommendations (English and Chinese fonts with multilingual pairing), and visual elements reference (backgrounds, typography treatments, geometric accents).
## 1.13.0 - 2026-01-21
### Features
- `baoyu-url-to-markdown`: new utility skill for fetching any URL via Chrome CDP and converting to clean markdown. Supports two capture modes—auto (immediate capture on page load) and wait (user-controlled capture for login-required pages).
### Improvements
- `baoyu-xhs-images`: updates style recommendations—replaces `tech` references with `notion` and `chalkboard` for technical and educational content.
## 1.12.0 - 2026-01-21
### Refactor
- `baoyu-post-to-x`: extracts shared utilities to `x-utils.ts`—consolidates Chrome detection, CDP connection, clipboard operations, and helper functions from `x-article.ts`, `x-browser.ts`, `x-quote.ts`, and `x-video.ts` into a single reusable module.
## 1.11.0 - 2026-01-21
### Features
- `baoyu-image-gen`: new AI SDK-based image generation skill using official OpenAI and Google APIs. Supports text-to-image, reference images (Google multimodal), aspect ratios, and quality presets (`normal`, `2k`). Auto-detects provider based on available API keys.
- `baoyu-slide-deck`: adds Layout Gallery with 24 layout types—10 slide-specific layouts (`title-hero`, `quote-callout`, `key-stat`, `split-screen`, `icon-grid`, `two-columns`, `three-columns`, `image-caption`, `agenda`, `bullet-list`) and 14 infographic-derived layouts (`linear-progression`, `binary-comparison`, `comparison-matrix`, `hierarchical-layers`, `hub-spoke`, `bento-grid`, `funnel`, `dashboard`, `venn-diagram`, `circular-flow`, `winding-roadmap`, `tree-branching`, `iceberg`, `bridge`).
### Documentation
- `README.md`, `README.zh.md`: adds baoyu-image-gen documentation with usage examples, options table, and environment variables; adds Environment Configuration section for API key setup.
## 1.10.0 - 2026-01-21
### Features
- `baoyu-post-to-x`: adds video posting support—new `x-video.ts` script for posting text with video files (MP4, MOV, WebM). Supports preview mode and handles video processing timeouts.
## 1.9.0 - 2026-01-20
### Features
- `baoyu-xhs-images`: adds `chalkboard` style—black chalkboard background with colorful chalk drawings for education and tutorial content.
- `baoyu-comic`: adds `chalkboard` style—educational chalk drawings on black chalkboard for tutorials, explainers, and knowledge comics.
### Improvements
- `baoyu-article-illustrator`, `baoyu-cover-image`, `baoyu-infographic`: updates `chalkboard` style with enhanced visual guidelines.
### Breaking Changes
- `baoyu-xhs-images`: removes `tech` style (use `minimal` or `notion` for technical content).
### Documentation
- `README.md`, `README.zh.md`: adds style and layout preview galleries for xhs-images (9 styles, 6 layouts).
## 1.8.0 - 2026-01-20
### Features
+68
View File
@@ -2,6 +2,74 @@
[English](./CHANGELOG.md) | 中文
## 1.15.2 - 2026-01-23
### 文档
- `release-skills`:SKILL.md 全面重写——新增多语言 changelog 支持、.releaserc.yml 配置文件、dry-run 模式、语言检测规则、7 种语言的章节标题翻译。
## 1.15.1 - 2026-01-22
### 重构
- `baoyu-xhs-images`:参考文档模块化重构——将分散的文件整理为 `config/`(配置设置)、`elements/`(视觉构建块)、`presets/`(风格预设)、`workflows/`(流程指南)四个目录,提升可维护性。
## 1.15.0 - 2026-01-22
### 新功能
- `baoyu-xhs-images`:新增用户偏好设置支持(通过 EXTEND.md 配置)——可设置水印(内容、位置、透明度)、首选风格、首选布局和自定义风格。新增 Step 0 检查项目级(`.baoyu-skills/`)或用户级(`~/.baoyu-skills/`)偏好设置,首次使用时引导设置。
### 文档
- `baoyu-xhs-images`:新增三个参考文档——`preferences-schema.md`YAML 配置模式)、`watermark-guide.md`(水印位置和透明度指南)、`first-time-setup.md`(首次设置流程)。
## 1.14.0 - 2026-01-22
### 修复
- `baoyu-post-to-x`:改进视频就绪检测,提升视频发布稳定性。
### 文档
- `baoyu-slide-deck`:SKILL.md 全面增强——新增幻灯片数量指南(推荐 8-25 张,最多 30 张)、受众指南表格及各受众特定原则、风格选择原则与内容类型推荐、布局选择技巧与常见错误提示、视觉层次原则、内容密度指南(麦肯锡风格高密度原则)、配色选择指南、字体排版原则与字体推荐(中英文字体及多语言搭配方案)、视觉元素参考(背景处理、字体处理、几何装饰)。
## 1.13.0 - 2026-01-21
### 新功能
- `baoyu-url-to-markdown`:新增 URL 转 Markdown 工具技能,通过 Chrome CDP 抓取任意网页并转换为干净的 Markdown 格式。支持两种抓取模式——自动模式(页面加载后立即抓取)和等待模式(用户控制抓取时机,适用于需要登录的页面)。
### 改进
- `baoyu-xhs-images`:更新风格推荐——将 `tech` 风格引用替换为 `notion``chalkboard`,用于技术和教育内容。
## 1.12.0 - 2026-01-21
### 重构
- `baoyu-post-to-x`:提取公共工具函数到 `x-utils.ts`——将 `x-article.ts``x-browser.ts``x-quote.ts``x-video.ts` 中重复的 Chrome 检测、CDP 连接、剪贴板操作等功能整合为统一的可复用模块。
## 1.11.0 - 2026-01-21
### 新功能
- `baoyu-image-gen`:新增基于 AI SDK 的图像生成技能,使用官方 OpenAI 和 Google API。支持文生图、参考图(Google 多模态)、宽高比和质量预设(`normal``2k`)。根据可用的 API 密钥自动选择服务商。
- `baoyu-slide-deck`:新增布局库(Layout Gallery),包含 24 种布局类型——10 种幻灯片专用布局(`title-hero` 标题主图、`quote-callout` 引用突出、`key-stat` 关键数据、`split-screen` 分屏、`icon-grid` 图标网格、`two-columns` 双栏、`three-columns` 三栏、`image-caption` 图片说明、`agenda` 议程、`bullet-list` 要点列表)和 14 种信息图衍生布局(`linear-progression` 线性流程、`binary-comparison` 二元对比、`comparison-matrix` 对比矩阵、`hierarchical-layers` 层级、`hub-spoke` 中心辐射、`bento-grid` 便当盒、`funnel` 漏斗、`dashboard` 仪表盘、`venn-diagram` 韦恩图、`circular-flow` 循环流程、`winding-roadmap` 蜿蜒路线图、`tree-branching` 树状分支、`iceberg` 冰山、`bridge` 桥接)。
### 文档
- `README.md``README.zh.md`:新增 baoyu-image-gen 文档,包含用法示例、选项表和环境变量说明;新增环境配置章节,介绍 API 密钥设置方法。
## 1.10.0 - 2026-01-21
### 新功能
- `baoyu-post-to-x`:新增视频发布支持——新增 `x-video.ts` 脚本,支持发布带视频的推文(MP4、MOV、WebM 格式)。支持预览模式,自动处理视频上传等待。
## 1.9.0 - 2026-01-20
### 新功能
- `baoyu-xhs-images`:新增 `chalkboard`(黑板)风格——黑色黑板背景配彩色粉笔绘画,适合教育和教程内容。
- `baoyu-comic`:新增 `chalkboard`(黑板)风格——黑色黑板上的教育粉笔画,适合教程、讲解和知识漫画。
### 改进
- `baoyu-article-illustrator``baoyu-cover-image``baoyu-infographic`:更新 `chalkboard` 风格,增强视觉指南。
### 破坏性变更
- `baoyu-xhs-images`:移除 `tech` 风格(技术内容改用 `minimal``notion` 风格)。
### 文档
- `README.md``README.zh.md`:新增 xhs-images 风格和布局预览图库(9 种风格、6 种布局)。
## 1.8.0 - 2026-01-20
### 新功能
+10
View File
@@ -78,6 +78,16 @@ npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --promptfiles system.m
`.claude-plugin/marketplace.json` defines plugin metadata and skill paths. Version follows semver.
## Release Process
**IMPORTANT**: When user requests release/发布/push, ALWAYS use `/release-skills` workflow.
**Never skip**:
1. `CHANGELOG.md` + `CHANGELOG.zh.md` - Both must be updated
2. `marketplace.json` version bump
3. `README.md` + `README.zh.md` if applicable
4. All files committed together before tag
## Adding New Skills
**IMPORTANT**: All skills MUST use `baoyu-` prefix to avoid conflicts when users import this plugin.
+140 -4
View File
@@ -14,7 +14,7 @@ Skills shared by Baoyu for improving daily work efficiency with Claude Code.
### Quick Install (Recommended)
```bash
npx add-skill jimliu/baoyu-skills
npx skills add jimliu/baoyu-skills
```
### Register as Plugin Marketplace
@@ -54,8 +54,8 @@ Simply tell Claude Code:
| Plugin | Description | Skills |
|--------|-------------|--------|
| **content-skills** | Content generation and publishing | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat) |
| **ai-generation-skills** | AI-powered generation backends | [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | Utility tools for content processing | [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image) |
| **ai-generation-skills** | AI-powered generation backends | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | Utility tools for content processing | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image) |
## Update Skills
@@ -99,7 +99,18 @@ Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-1
/baoyu-xhs-images 今日星座运势
```
**Styles** (visual aesthetics): `cute` (default), `fresh`, `tech`, `warm`, `bold`, `minimal`, `retro`, `pop`, `notion`
**Styles** (visual aesthetics): `cute` (default), `fresh`, `warm`, `bold`, `minimal`, `retro`, `pop`, `notion`, `chalkboard`
**Style Previews**:
| | | |
|:---:|:---:|:---:|
| ![cute](./screenshots/xhs-images-styles/cute.webp) | ![fresh](./screenshots/xhs-images-styles/fresh.webp) | ![warm](./screenshots/xhs-images-styles/warm.webp) |
| cute | fresh | warm |
| ![bold](./screenshots/xhs-images-styles/bold.webp) | ![minimal](./screenshots/xhs-images-styles/minimal.webp) | ![retro](./screenshots/xhs-images-styles/retro.webp) |
| bold | minimal | retro |
| ![pop](./screenshots/xhs-images-styles/pop.webp) | ![notion](./screenshots/xhs-images-styles/notion.webp) | ![chalkboard](./screenshots/xhs-images-styles/chalkboard.webp) |
| pop | notion | chalkboard |
**Layouts** (information density):
| Layout | Density | Best for |
@@ -111,6 +122,15 @@ Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-1
| `comparison` | 2 sides | Before/after, pros/cons |
| `flow` | 3-6 steps | Processes, timelines |
**Layout Previews**:
| | | |
|:---:|:---:|:---:|
| ![sparse](./screenshots/xhs-images-layouts/sparse.webp) | ![balanced](./screenshots/xhs-images-layouts/balanced.webp) | ![dense](./screenshots/xhs-images-layouts/dense.webp) |
| sparse | balanced | dense |
| ![list](./screenshots/xhs-images-layouts/list.webp) | ![comparison](./screenshots/xhs-images-layouts/comparison.webp) | ![flow](./screenshots/xhs-images-layouts/flow.webp) |
| list | comparison | flow |
#### baoyu-infographic
Generate professional infographics with 20 layout types and 17 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics.
@@ -495,6 +515,55 @@ Prerequisites: Google Chrome installed. First run requires QR code login (sessio
AI-powered generation backends.
#### baoyu-image-gen
AI SDK-based image generation using official OpenAI and Google APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
```bash
# Basic generation (auto-detect provider)
/baoyu-image-gen --prompt "A cute cat" --image cat.png
# With aspect ratio
/baoyu-image-gen --prompt "A landscape" --image landscape.png --ar 16:9
# High quality (2k)
/baoyu-image-gen --prompt "A banner" --image banner.png --quality 2k
# Specific provider
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openai
# With reference images (Google multimodal only)
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
```
**Options**:
| Option | Description |
|--------|-------------|
| `--prompt`, `-p` | Prompt text |
| `--promptfiles` | Read prompt from files (concatenated) |
| `--image` | Output image path (required) |
| `--provider` | `google` or `openai` (default: google) |
| `--model`, `-m` | Model ID |
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size` | Size (e.g., `1024x1024`) |
| `--quality` | `normal` or `2k` (default: normal) |
| `--ref` | Reference images (Google multimodal only) |
**Environment Variables** (see [Environment Configuration](#environment-configuration) for setup):
| Variable | Description | Default |
|----------|-------------|---------|
| `OPENAI_API_KEY` | OpenAI API key | - |
| `GOOGLE_API_KEY` | Google API key | - |
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` |
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | - |
| `GOOGLE_BASE_URL` | Custom Google endpoint | - |
**Provider Auto-Selection**:
1. If `--provider` specified → use it
2. If only one API key available → use that provider
3. If both available → default to Google
#### baoyu-danger-gemini-web
Interacts with Gemini Web to generate text and images.
@@ -517,6 +586,35 @@ Interacts with Gemini Web to generate text and images.
Utility tools for content processing.
#### baoyu-url-to-markdown
Fetch any URL via Chrome CDP and convert to clean markdown. Supports two capture modes for different scenarios.
```bash
# Auto mode (default) - capture when page loads
/baoyu-url-to-markdown https://example.com/article
# Wait mode - for login-required pages
/baoyu-url-to-markdown https://example.com/private --wait
# Save to specific file
/baoyu-url-to-markdown https://example.com/article -o output.md
```
**Capture Modes**:
| Mode | Description | Best For |
|------|-------------|----------|
| Auto (default) | Captures immediately after page load | Public pages, static content |
| Wait (`--wait`) | Waits for user signal before capture | Login-required, dynamic content |
**Options**:
| Option | Description |
|--------|-------------|
| `<url>` | URL to fetch |
| `-o <path>` | Output file path |
| `--wait` | Wait for user signal before capturing |
| `--timeout <ms>` | Page load timeout (default: 30000) |
#### baoyu-danger-x-to-markdown
Converts X (Twitter) content to markdown format. Supports tweet threads and X Articles.
@@ -548,6 +646,44 @@ Compress images to reduce file size while maintaining quality.
/baoyu-compress-image path/to/images/ --quality 80
```
## Environment Configuration
Some skills require API keys or custom configuration. Environment variables can be set in `.env` files:
**Load Priority** (higher priority overrides lower):
1. CLI environment variables (e.g., `OPENAI_API_KEY=xxx /baoyu-image-gen ...`)
2. `process.env` (system environment)
3. `<cwd>/.baoyu-skills/.env` (project-level)
4. `~/.baoyu-skills/.env` (user-level)
**Setup**:
```bash
# Create user-level config directory
mkdir -p ~/.baoyu-skills
# Create .env file
cat > ~/.baoyu-skills/.env << 'EOF'
# OpenAI
OPENAI_API_KEY=sk-xxx
OPENAI_IMAGE_MODEL=gpt-image-1.5
# OPENAI_BASE_URL=https://api.openai.com/v1
# Google
GOOGLE_API_KEY=xxx
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
# GOOGLE_BASE_URL=https://generativelanguage.googleapis.com/v1beta
EOF
```
**Project-level config** (for team sharing):
```bash
mkdir -p .baoyu-skills
# Add .baoyu-skills/.env to .gitignore to avoid committing secrets
echo ".baoyu-skills/.env" >> .gitignore
```
## Customization
All skills support customization via `EXTEND.md` files. Create an extension file to override default styles, add custom configurations, or define your own presets.
+140 -4
View File
@@ -14,7 +14,7 @@
### 快速安装(推荐)
```bash
npx add-skill jimliu/baoyu-skills
npx skills add jimliu/baoyu-skills
```
### 注册插件市场
@@ -54,8 +54,8 @@ npx add-skill jimliu/baoyu-skills
| 插件 | 说明 | 包含技能 |
|------|------|----------|
| **content-skills** | 内容生成和发布 | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat) |
| **ai-generation-skills** | AI 生成后端 | [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | 内容处理工具 | [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image) |
| **ai-generation-skills** | AI 生成后端 | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | 内容处理工具 | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image) |
## 更新技能
@@ -99,7 +99,18 @@ npx add-skill jimliu/baoyu-skills
/baoyu-xhs-images 今日星座运势
```
**风格**(视觉美学):`cute`(默认)、`fresh``tech``warm``bold``minimal``retro``pop``notion`
**风格**(视觉美学):`cute`(默认)、`fresh``warm``bold``minimal``retro``pop``notion``chalkboard`
**风格预览**
| | | |
|:---:|:---:|:---:|
| ![cute](./screenshots/xhs-images-styles/cute.webp) | ![fresh](./screenshots/xhs-images-styles/fresh.webp) | ![warm](./screenshots/xhs-images-styles/warm.webp) |
| cute | fresh | warm |
| ![bold](./screenshots/xhs-images-styles/bold.webp) | ![minimal](./screenshots/xhs-images-styles/minimal.webp) | ![retro](./screenshots/xhs-images-styles/retro.webp) |
| bold | minimal | retro |
| ![pop](./screenshots/xhs-images-styles/pop.webp) | ![notion](./screenshots/xhs-images-styles/notion.webp) | ![chalkboard](./screenshots/xhs-images-styles/chalkboard.webp) |
| pop | notion | chalkboard |
**布局**(信息密度):
| 布局 | 密度 | 适用场景 |
@@ -111,6 +122,15 @@ npx add-skill jimliu/baoyu-skills
| `comparison` | 双栏 | 对比、优劣 |
| `flow` | 3-6 步 | 流程、时间线 |
**布局预览**
| | | |
|:---:|:---:|:---:|
| ![sparse](./screenshots/xhs-images-layouts/sparse.webp) | ![balanced](./screenshots/xhs-images-layouts/balanced.webp) | ![dense](./screenshots/xhs-images-layouts/dense.webp) |
| sparse | balanced | dense |
| ![list](./screenshots/xhs-images-layouts/list.webp) | ![comparison](./screenshots/xhs-images-layouts/comparison.webp) | ![flow](./screenshots/xhs-images-layouts/flow.webp) |
| list | comparison | flow |
#### baoyu-infographic
专业信息图生成器,支持 20 种布局和 17 种视觉风格。分析内容后推荐布局×风格组合,生成可发布的信息图。
@@ -495,6 +515,55 @@ npx add-skill jimliu/baoyu-skills
AI 驱动的生成后端。
#### baoyu-image-gen
基于 AI SDK 的图像生成,使用官方 OpenAI 和 Google API。支持文生图、参考图、宽高比和质量预设。
```bash
# 基础生成(自动检测服务商)
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png
# 指定宽高比
/baoyu-image-gen --prompt "风景图" --image landscape.png --ar 16:9
# 高质量(2k 分辨率)
/baoyu-image-gen --prompt "横幅图" --image banner.png --quality 2k
# 指定服务商
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openai
# 带参考图(仅 Google 多模态支持)
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
```
**选项**
| 选项 | 说明 |
|------|------|
| `--prompt`, `-p` | 提示词文本 |
| `--promptfiles` | 从文件读取提示词(多文件拼接) |
| `--image` | 输出图片路径(必需) |
| `--provider` | `google``openai`(默认:google |
| `--model`, `-m` | 模型 ID |
| `--ar` | 宽高比(如 `16:9``1:1``4:3` |
| `--size` | 尺寸(如 `1024x1024` |
| `--quality` | `normal``2k`(默认:normal |
| `--ref` | 参考图片(仅 Google 多模态支持) |
**环境变量**(配置方法见[环境配置](#环境配置)):
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `OPENAI_API_KEY` | OpenAI API 密钥 | - |
| `GOOGLE_API_KEY` | Google API 密钥 | - |
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-1.5` |
| `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` |
| `OPENAI_BASE_URL` | 自定义 OpenAI 端点 | - |
| `GOOGLE_BASE_URL` | 自定义 Google 端点 | - |
**服务商自动选择**
1. 如果指定了 `--provider` → 使用指定的
2. 如果只有一个 API 密钥 → 使用对应服务商
3. 如果两个都有 → 默认使用 Google
#### baoyu-danger-gemini-web
与 Gemini Web 交互,生成文本和图片。
@@ -517,6 +586,35 @@ AI 驱动的生成后端。
内容处理工具。
#### baoyu-url-to-markdown
通过 Chrome CDP 抓取任意 URL 并转换为干净的 Markdown。支持两种抓取模式,适应不同场景。
```bash
# 自动模式(默认)- 页面加载后立即抓取
/baoyu-url-to-markdown https://example.com/article
# 等待模式 - 适用于需要登录的页面
/baoyu-url-to-markdown https://example.com/private --wait
# 保存到指定文件
/baoyu-url-to-markdown https://example.com/article -o output.md
```
**抓取模式**
| 模式 | 说明 | 适用场景 |
|------|------|----------|
| 自动(默认) | 页面加载后立即抓取 | 公开页面、静态内容 |
| 等待(`--wait`) | 等待用户信号后抓取 | 需登录页面、动态内容 |
**选项**
| 选项 | 说明 |
|------|------|
| `<url>` | 要抓取的 URL |
| `-o <path>` | 输出文件路径 |
| `--wait` | 等待用户信号后抓取 |
| `--timeout <ms>` | 页面加载超时(默认:30000) |
#### baoyu-danger-x-to-markdown
将 X (Twitter) 内容转换为 markdown 格式。支持推文串和 X 文章。
@@ -548,6 +646,44 @@ AI 驱动的生成后端。
/baoyu-compress-image path/to/images/ --quality 80
```
## 环境配置
部分技能需要 API 密钥或自定义配置。环境变量可以在 `.env` 文件中设置:
**加载优先级**(高优先级覆盖低优先级):
1. 命令行环境变量(如 `OPENAI_API_KEY=xxx /baoyu-image-gen ...`
2. `process.env`(系统环境变量)
3. `<cwd>/.baoyu-skills/.env`(项目级)
4. `~/.baoyu-skills/.env`(用户级)
**配置方法**
```bash
# 创建用户级配置目录
mkdir -p ~/.baoyu-skills
# 创建 .env 文件
cat > ~/.baoyu-skills/.env << 'EOF'
# OpenAI
OPENAI_API_KEY=sk-xxx
OPENAI_IMAGE_MODEL=gpt-image-1.5
# OPENAI_BASE_URL=https://api.openai.com/v1
# Google
GOOGLE_API_KEY=xxx
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
# GOOGLE_BASE_URL=https://generativelanguage.googleapis.com/v1beta
EOF
```
**项目级配置**(团队共享):
```bash
mkdir -p .baoyu-skills
# 将 .baoyu-skills/.env 添加到 .gitignore 避免提交密钥
echo ".baoyu-skills/.env" >> .gitignore
```
## 自定义扩展
所有技能支持通过 `EXTEND.md` 文件自定义。创建扩展文件可覆盖默认样式、添加自定义配置或定义个人预设。
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

@@ -1,56 +1,60 @@
# chalkboard
Classic classroom chalkboard style for educational content
Black chalkboard background with colorful chalk drawing style
## Design Aesthetic
Classic classroom chalkboard aesthetic with hand-drawn chalk illustrations. Nostalgic educational feel with imperfect, sketchy lines. Colorful chalk creates visual hierarchy while maintaining authentic chalkboard warmth.
Classic classroom chalkboard aesthetic with hand-drawn chalk illustrations. Nostalgic educational feel with imperfect, sketchy lines that capture the warmth of traditional teaching. Colorful chalk creates visual hierarchy while maintaining the authentic chalkboard experience.
## Background
- Color: Chalkboard Black (#1A1A1A) or Dark Green-Black (#1C2B1C)
- Texture: Realistic chalkboard texture with dust and eraser marks
- Texture: Realistic chalkboard texture with subtle scratches, dust particles, and faint eraser marks
## Typography
Hand-drawn chalk lettering style with visible chalk texture. Imperfect baseline adds authenticity. White or bright colored chalk for emphasis.
## Color Palette
| Role | Color | Hex | Usage |
|------|-------|-----|-------|
| Background | Chalkboard Black | #1A1A1A | Primary background |
| Alt Background | Green-Black | #1C2B1C | Traditional green |
| Primary | Chalk White | #F5F5F5 | Main elements |
| Accent 1 | Chalk Yellow | #FFE566 | Highlights |
| Alt Background | Green-Black | #1C2B1C | Traditional green board |
| Primary Text | Chalk White | #F5F5F5 | Main text, outlines |
| Accent 1 | Chalk Yellow | #FFE566 | Highlights, emphasis |
| Accent 2 | Chalk Pink | #FF9999 | Secondary highlights |
| Accent 3 | Chalk Blue | #66B3FF | Diagrams, links |
| Accent 4 | Chalk Green | #90EE90 | Success, growth |
| Accent 4 | Chalk Green | #90EE90 | Success, nature |
| Accent 5 | Chalk Orange | #FFB366 | Warnings, energy |
## Visual Elements
- Hand-drawn chalk illustrations
- Chalk dust effects
- Doodles: stars, arrows, underlines
- Mathematical formulas
- Eraser smudges and chalk residue
- Stick figures and icons
- Connection lines with hand feel
- Checkmarks and annotations
- Hand-drawn chalk illustrations with sketchy, imperfect lines
- Chalk dust effects around text and key elements
- Doodles: stars, arrows, underlines, circles, checkmarks
- Mathematical formulas and simple diagrams
- Eraser smudges and chalk residue textures
- Wooden frame border optional
- Stick figures and simple icons
- Connection lines with hand-drawn feel
## Style Rules
### Do
- Maintain chalk texture on all elements
- Use imperfect hand-drawn quality
- Add chalk dust and smudge effects
- Create hierarchy with color
- Include playful annotations
- Maintain authentic chalk texture on all elements
- Use imperfect, hand-drawn quality throughout
- Add subtle chalk dust and smudge effects
- Create visual hierarchy with color variety
- Include playful doodles and annotations
### Don't
- Use perfect geometric shapes
- Create clean digital lines
- Create clean digital-looking lines
- Add photorealistic elements
- Use gradients or gloss
- Use gradients or glossy effects
- Make it look computerized
## Best For
@@ -0,0 +1,61 @@
# chalkboard
Black chalkboard background with colorful chalk drawing style
## Design Aesthetic
Classic classroom chalkboard aesthetic with hand-drawn chalk illustrations. Nostalgic educational feel with imperfect, sketchy lines that capture the warmth of traditional teaching. Colorful chalk creates visual hierarchy while maintaining the authentic chalkboard experience.
## Background
- Color: Chalkboard Black (#1A1A1A) or Dark Green-Black (#1C2B1C)
- Texture: Realistic chalkboard texture with subtle scratches, dust particles, and faint eraser marks
## Typography
Hand-drawn chalk lettering style with visible chalk texture. Imperfect baseline adds authenticity. White or bright colored chalk for emphasis.
## Color Palette
| Role | Color | Hex | Usage |
|------|-------|-----|-------|
| Background | Chalkboard Black | #1A1A1A | Primary background |
| Alt Background | Green-Black | #1C2B1C | Traditional green board |
| Primary Text | Chalk White | #F5F5F5 | Main text, outlines |
| Accent 1 | Chalk Yellow | #FFE566 | Highlights, emphasis |
| Accent 2 | Chalk Pink | #FF9999 | Secondary highlights |
| Accent 3 | Chalk Blue | #66B3FF | Diagrams, links |
| Accent 4 | Chalk Green | #90EE90 | Success, nature |
| Accent 5 | Chalk Orange | #FFB366 | Warnings, energy |
## Visual Elements
- Hand-drawn chalk illustrations with sketchy, imperfect lines
- Chalk dust effects around text and key elements
- Doodles: stars, arrows, underlines, circles, checkmarks
- Mathematical formulas and simple diagrams
- Eraser smudges and chalk residue textures
- Wooden frame border optional
- Stick figures and simple icons
- Connection lines with hand-drawn feel
## Style Rules
### Do
- Maintain authentic chalk texture on all elements
- Use imperfect, hand-drawn quality throughout
- Add subtle chalk dust and smudge effects
- Create visual hierarchy with color variety
- Include playful doodles and annotations
### Don't
- Use perfect geometric shapes
- Create clean digital-looking lines
- Add photorealistic elements
- Use gradients or glossy effects
## Best For
Educational content, tutorials, classroom themes, teaching materials, workshops, informal learning sessions, knowledge sharing
@@ -2,25 +2,60 @@
Black chalkboard background with colorful chalk drawing style
## Color Palette
## Design Aesthetic
- Primary: Chalk White (#F5F5F5), Chalk Yellow (#FFE566), Chalk Pink (#FF9999)
- Background: Chalkboard Black (#1A1A1A), Dark Green-Black (#1C2B1C)
- Accents: Chalk Blue (#66B3FF), Chalk Green (#90EE90), Chalk Orange (#FFB366)
Classic classroom chalkboard aesthetic with hand-drawn chalk illustrations. Nostalgic educational feel with imperfect, sketchy lines that capture the warmth of traditional teaching. Colorful chalk creates visual hierarchy while maintaining the authentic chalkboard experience.
## Visual Elements
## Background
- Chalkboard texture with subtle dust and smudges
- Hand-drawn chalk illustrations with sketchy lines
- Chalk dust effects around text and drawings
- Imperfect lines with visible texture
- Doodles, arrows, underlines, stars
- Eraser marks and chalk residue
- Color: Chalkboard Black (#1A1A1A) or Dark Green-Black (#1C2B1C)
- Texture: Realistic chalkboard texture with subtle scratches, dust particles, and faint eraser marks
## Typography
- Hand-drawn chalk lettering, slightly uneven, with chalk texture
Hand-drawn chalk lettering style with visible chalk texture. Imperfect baseline adds authenticity. White or bright colored chalk for emphasis.
## Color Palette
| Role | Color | Hex | Usage |
|------|-------|-----|-------|
| Background | Chalkboard Black | #1A1A1A | Primary background |
| Alt Background | Green-Black | #1C2B1C | Traditional green board |
| Primary Text | Chalk White | #F5F5F5 | Main text, outlines |
| Accent 1 | Chalk Yellow | #FFE566 | Highlights, emphasis |
| Accent 2 | Chalk Pink | #FF9999 | Secondary highlights |
| Accent 3 | Chalk Blue | #66B3FF | Diagrams, links |
| Accent 4 | Chalk Green | #90EE90 | Success, nature |
| Accent 5 | Chalk Orange | #FFB366 | Warnings, energy |
## Visual Elements
- Hand-drawn chalk illustrations with sketchy, imperfect lines
- Chalk dust effects around text and key elements
- Doodles: stars, arrows, underlines, circles, checkmarks
- Mathematical formulas and simple diagrams
- Eraser smudges and chalk residue textures
- Wooden frame border optional
- Stick figures and simple icons
- Connection lines with hand-drawn feel
## Style Rules
### Do
- Maintain authentic chalk texture on all elements
- Use imperfect, hand-drawn quality throughout
- Add subtle chalk dust and smudge effects
- Create visual hierarchy with color variety
- Include playful doodles and annotations
### Don't
- Use perfect geometric shapes
- Create clean digital-looking lines
- Add photorealistic elements
- Use gradients or glossy effects
## Best For
Educational content, tutorials, classroom themes, back-to-school, teaching materials
Educational content, tutorials, classroom themes, teaching materials, workshops, informal learning sessions, knowledge sharing
+219
View File
@@ -0,0 +1,219 @@
---
name: baoyu-image-gen
description: AI SDK-based image generation using official OpenAI and Google APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
---
# Image Generation (AI SDK)
Official API-based image generation via AI SDK. Supports OpenAI (DALL-E, GPT Image) and Google (Imagen, Gemini multimodal).
## Script Directory
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
**Agent Execution Instructions**:
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
3. Replace all `${SKILL_DIR}` in this document with the actual path
**Script Reference**:
| Script | Purpose |
|--------|---------|
| `scripts/main.ts` | CLI entry point for image generation |
## Quick Start
```bash
# Basic generation (auto-detect provider)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png
# With aspect ratio
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A landscape" --image landscape.png --ar 16:9
# High quality (2k)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png --quality 2k
# Specific provider
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png --provider openai
# From prompt files
npx -y bun ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
# With reference images (Google multimodal only)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
```
## Commands
### Basic Image Generation
```bash
# Generate with prompt
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A sunset over mountains" --image sunset.png
# Shorthand
npx -y bun ${SKILL_DIR}/scripts/main.ts -p "A cute robot" --image robot.png
```
### Aspect Ratios
```bash
# Common ratios: 1:1, 16:9, 9:16, 4:3, 3:4, 2.35:1
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A portrait" --image portrait.png --ar 3:4
# Or specify exact size
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Banner" --image banner.png --size 1792x1024
```
### Reference Images (Google Multimodal)
```bash
# Image editing with reference
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make it blue" --image blue.png --ref original.png
# Multiple references
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Combine these styles" --image out.png --ref a.png b.png
```
### Quality Presets
```bash
# Normal quality (default)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png --quality normal
# High quality (2k resolution)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png --quality 2k
```
### Output Formats
```bash
# Plain output (prints saved path)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png
# JSON output
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png --json
```
## Options
| Option | Description |
|--------|-------------|
| `--prompt <text>`, `-p` | Prompt text |
| `--promptfiles <files...>` | Read prompt from files (concatenated) |
| `--image <path>` | Output image path (required) |
| `--provider google\|openai` | Force provider (default: google) |
| `--model <id>`, `-m` | Model ID |
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size <WxH>` | Size (e.g., `1024x1024`) |
| `--quality normal\|2k` | Quality preset (default: normal) |
| `--ref <files...>` | Reference images (Google multimodal only) |
| `--n <count>` | Number of images |
| `--json` | JSON output |
| `--help`, `-h` | Show help |
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `OPENAI_API_KEY` | OpenAI API key | - |
| `GOOGLE_API_KEY` | Google API key | - |
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` |
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | - |
| `GOOGLE_BASE_URL` | Custom Google endpoint | - |
**Load Priority**: CLI args > `process.env` > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
## Provider & Model Strategy
### Auto-Selection
1. If `--provider` specified → use it
2. If only one API key available → use that provider
3. If both available → default to Google (multimodal LLMs more versatile)
### API Selection by Model Type
| Model Category | API Function | Example Models |
|----------------|--------------|----------------|
| Google Multimodal | `generateText` | `gemini-2.0-flash-exp-image-generation` |
| Google Imagen | `experimental_generateImage` | `imagen-3.0-generate-002` |
| OpenAI | `experimental_generateImage` | `gpt-image-1`, `dall-e-3` |
### Available Models
**Google**:
- `gemini-3-pro-image-preview` - Default, multimodal generation
- `gemini-2.0-flash-exp-image-generation` - Gemini 2.0 Flash
- `imagen-3.0-generate-002` - Imagen 3
**OpenAI**:
- `gpt-image-1.5` - Default, GPT Image 1.5
- `gpt-image-1` - GPT Image 1
- `dall-e-3` - DALL-E 3
## Quality Presets
| Preset | OpenAI | Google | Use Case |
|--------|--------|--------|----------|
| `normal` | 1024x1024 | Default | Covers, illustrations |
| `2k` | 2048x2048 | "2048px" in prompt | Infographics, slides |
## Aspect Ratio Handling
- **Multimodal LLMs**: Embedded in prompt (e.g., `"... aspect ratio 16:9"`)
- **Image-only models**: Uses `aspectRatio` or `size` parameter
- **Common ratios**: 1:1, 16:9, 9:16, 4:3, 3:4, 2.35:1
## Examples
### Generate Cover Image
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts \
--prompt "A minimalist tech illustration with blue gradients" \
--image cover.png --ar 2.35:1 --quality 2k
```
### Generate Social Media Post
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts \
--prompt "Instagram post about coffee" \
--image post.png --ar 1:1
```
### Edit Image with Reference
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts \
--prompt "Change the background to sunset" \
--image edited.png --ref original.png --provider google
```
### Batch Generation from Prompt File
```bash
# Create prompt file with detailed instructions
npx -y bun ${SKILL_DIR}/scripts/main.ts \
--promptfiles style-guide.md scene-description.md \
--image scene.png
```
## Error Handling
- **Missing API key**: Clear error with setup instructions
- **Generation failure**: Auto-retry once, then error
- **Invalid aspect ratio**: Warning, proceed with default
- **Reference images with image-only model**: Warning, ignore refs
## Extension Support
Custom configurations via EXTEND.md.
**Check paths** (priority order):
1. `.baoyu-skills/baoyu-image-gen/EXTEND.md` (project)
2. `~/.baoyu-skills/baoyu-image-gen/EXTEND.md` (user)
If found, load before workflow. Extension content overrides defaults.
+576
View File
@@ -0,0 +1,576 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { homedir } from "node:os";
import { mkdir, readFile, writeFile } from "node:fs/promises";
type Provider = "google" | "openai";
type Quality = "normal" | "2k";
type CliArgs = {
prompt: string | null;
promptFiles: string[];
imagePath: string | null;
provider: Provider | null;
model: string | null;
aspectRatio: string | null;
size: string | null;
quality: Quality;
referenceImages: string[];
n: number;
json: boolean;
help: boolean;
};
const GOOGLE_MULTIMODAL_MODELS = [
"gemini-3-pro-image-preview",
"gemini-2.0-flash-exp-image-generation",
"gemini-2.5-flash-preview-native-audio-dialog",
];
const GOOGLE_IMAGEN_MODELS = ["imagen-3.0-generate-002", "imagen-3.0-generate-001"];
const OPENAI_IMAGE_MODELS = ["gpt-image-1.5", "gpt-image-1", "dall-e-3", "dall-e-2"];
function printUsage(): void {
console.log(`Usage:
npx -y bun scripts/main.ts --prompt "A cat" --image cat.png
npx -y bun scripts/main.ts --prompt "A landscape" --image landscape.png --ar 16:9
npx -y bun scripts/main.ts --promptfiles system.md content.md --image out.png
Options:
-p, --prompt <text> Prompt text
--promptfiles <files...> Read prompt from files (concatenated)
--image <path> Output image path (required)
--provider google|openai Force provider (auto-detect by default)
-m, --model <id> Model ID
--ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3)
--size <WxH> Size (e.g., 1024x1024)
--quality normal|2k Quality preset (default: normal)
--ref <files...> Reference images (Google multimodal only)
--n <count> Number of images (default: 1)
--json JSON output
-h, --help Show help
Environment variables:
OPENAI_API_KEY OpenAI API key
GOOGLE_API_KEY Google API key
OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-1.5)
GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview)
OPENAI_BASE_URL Custom OpenAI endpoint
GOOGLE_BASE_URL Custom Google endpoint
Env file load order: CLI args > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
}
function parseArgs(argv: string[]): CliArgs {
const out: CliArgs = {
prompt: null,
promptFiles: [],
imagePath: null,
provider: null,
model: null,
aspectRatio: null,
size: null,
quality: "normal",
referenceImages: [],
n: 1,
json: false,
help: false,
};
const positional: string[] = [];
const takeMany = (i: number): { items: string[]; next: number } => {
const items: string[] = [];
let j = i + 1;
while (j < argv.length) {
const v = argv[j]!;
if (v.startsWith("-")) break;
items.push(v);
j++;
}
return { items, next: j - 1 };
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i]!;
if (a === "--help" || a === "-h") {
out.help = true;
continue;
}
if (a === "--json") {
out.json = true;
continue;
}
if (a === "--prompt" || a === "-p") {
const v = argv[++i];
if (!v) throw new Error(`Missing value for ${a}`);
out.prompt = v;
continue;
}
if (a === "--promptfiles") {
const { items, next } = takeMany(i);
if (items.length === 0) throw new Error("Missing files for --promptfiles");
out.promptFiles.push(...items);
i = next;
continue;
}
if (a === "--image") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --image");
out.imagePath = v;
continue;
}
if (a === "--provider") {
const v = argv[++i];
if (v !== "google" && v !== "openai") throw new Error(`Invalid provider: ${v}`);
out.provider = v;
continue;
}
if (a === "--model" || a === "-m") {
const v = argv[++i];
if (!v) throw new Error(`Missing value for ${a}`);
out.model = v;
continue;
}
if (a === "--ar") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --ar");
out.aspectRatio = v;
continue;
}
if (a === "--size") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --size");
out.size = v;
continue;
}
if (a === "--quality") {
const v = argv[++i];
if (v !== "normal" && v !== "2k") throw new Error(`Invalid quality: ${v}`);
out.quality = v;
continue;
}
if (a === "--ref" || a === "--reference") {
const { items, next } = takeMany(i);
if (items.length === 0) throw new Error(`Missing files for ${a}`);
out.referenceImages.push(...items);
i = next;
continue;
}
if (a === "--n") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --n");
out.n = parseInt(v, 10);
if (isNaN(out.n) || out.n < 1) throw new Error(`Invalid count: ${v}`);
continue;
}
if (a.startsWith("-")) {
throw new Error(`Unknown option: ${a}`);
}
positional.push(a);
}
if (!out.prompt && out.promptFiles.length === 0 && positional.length > 0) {
out.prompt = positional.join(" ");
}
return out;
}
async function loadEnvFile(p: string): Promise<Record<string, string>> {
try {
const content = await readFile(p, "utf8");
const env: Record<string, string> = {};
for (const line of content.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const idx = trimmed.indexOf("=");
if (idx === -1) continue;
const key = trimmed.slice(0, idx).trim();
let val = trimmed.slice(idx + 1).trim();
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
env[key] = val;
}
return env;
} catch {
return {};
}
}
async function loadEnv(): Promise<void> {
const home = homedir();
const cwd = process.cwd();
const homeEnv = await loadEnvFile(path.join(home, ".baoyu-skills", ".env"));
const cwdEnv = await loadEnvFile(path.join(cwd, ".baoyu-skills", ".env"));
for (const [k, v] of Object.entries(homeEnv)) {
if (!process.env[k]) process.env[k] = v;
}
for (const [k, v] of Object.entries(cwdEnv)) {
if (!process.env[k]) process.env[k] = v;
}
}
async function readPromptFromFiles(files: string[]): Promise<string> {
const parts: string[] = [];
for (const f of files) {
parts.push(await readFile(f, "utf8"));
}
return parts.join("\n\n");
}
async function readPromptFromStdin(): Promise<string | null> {
if (process.stdin.isTTY) return null;
try {
const t = await Bun.stdin.text();
const v = t.trim();
return v.length > 0 ? v : null;
} catch {
return null;
}
}
function normalizeOutputImagePath(p: string): string {
const full = path.resolve(p);
const ext = path.extname(full);
if (ext) return full;
return `${full}.png`;
}
function detectProvider(args: CliArgs): Provider {
if (args.provider) return args.provider;
const hasGoogle = !!process.env.GOOGLE_API_KEY;
const hasOpenai = !!process.env.OPENAI_API_KEY;
if (hasGoogle && !hasOpenai) return "google";
if (hasOpenai && !hasGoogle) return "openai";
if (hasGoogle && hasOpenai) return "google";
throw new Error(
"No API key found. Set GOOGLE_API_KEY or OPENAI_API_KEY.\n" +
"Create ~/.baoyu-skills/.env or <cwd>/.baoyu-skills/.env with your keys."
);
}
function getDefaultModel(provider: Provider): string {
if (provider === "google") {
return process.env.GOOGLE_IMAGE_MODEL || "gemini-3-pro-image-preview";
}
return process.env.OPENAI_IMAGE_MODEL || "gpt-image-1.5";
}
function isGoogleMultimodal(model: string): boolean {
return GOOGLE_MULTIMODAL_MODELS.some((m) => model.includes(m));
}
function isGoogleImagen(model: string): boolean {
return GOOGLE_IMAGEN_MODELS.some((m) => model.includes(m));
}
function buildPromptWithAspect(prompt: string, ar: string | null, quality: Quality): string {
let result = prompt;
if (ar) {
result += ` Aspect ratio: ${ar}.`;
}
if (quality === "2k") {
result += " High resolution 2048px.";
}
return result;
}
function parseAspectRatio(ar: string): { width: number; height: number } | null {
const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
if (!match) return null;
const w = parseFloat(match[1]!);
const h = parseFloat(match[2]!);
if (w <= 0 || h <= 0) return null;
return { width: w, height: h };
}
function getOpenAISize(ar: string | null, quality: Quality): string {
const base = quality === "2k" ? 2048 : 1024;
if (!ar) return `${base}x${base}`;
const parsed = parseAspectRatio(ar);
if (!parsed) return `${base}x${base}`;
const ratio = parsed.width / parsed.height;
if (Math.abs(ratio - 1) < 0.1) return `${base}x${base}`;
if (ratio > 1.5) return quality === "2k" ? "2048x1024" : "1792x1024";
if (ratio < 0.67) return quality === "2k" ? "1024x2048" : "1024x1792";
return `${base}x${base}`;
}
async function readImageAsBase64(p: string): Promise<{ data: string; mimeType: string }> {
const buf = await readFile(p);
const ext = path.extname(p).toLowerCase();
let mimeType = "image/png";
if (ext === ".jpg" || ext === ".jpeg") mimeType = "image/jpeg";
else if (ext === ".gif") mimeType = "image/gif";
else if (ext === ".webp") mimeType = "image/webp";
return { data: buf.toString("base64"), mimeType };
}
async function generateWithGoogleMultimodal(
prompt: string,
model: string,
args: CliArgs
): Promise<Uint8Array> {
const { generateText } = await import("ai");
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
const google = createGoogleGenerativeAI({
apiKey: process.env.GOOGLE_API_KEY,
baseURL: process.env.GOOGLE_BASE_URL,
});
const fullPrompt = buildPromptWithAspect(prompt, args.aspectRatio, args.quality);
const messages: any[] = [];
const content: any[] = [];
for (const refPath of args.referenceImages) {
const { data, mimeType } = await readImageAsBase64(refPath);
content.push({ type: "image", image: data, mimeType });
}
content.push({ type: "text", text: fullPrompt });
messages.push({ role: "user", content });
const result = await generateText({
model: google(model, { useSearchGrounding: false }),
messages,
providerOptions: {
google: {
responseModalities: ["TEXT", "IMAGE"],
},
},
});
const files = (result as any).files;
if (!files || files.length === 0) {
const expRes = (result as any).response?.body?.candidates?.[0]?.content?.parts;
if (expRes) {
for (const part of expRes) {
if (part.inlineData?.data) {
return Uint8Array.from(Buffer.from(part.inlineData.data, "base64"));
}
}
}
throw new Error("No image in response");
}
const img = files[0];
if (img.uint8Array) return img.uint8Array;
if (img.base64) return Uint8Array.from(Buffer.from(img.base64, "base64"));
throw new Error("Cannot extract image data");
}
async function generateWithGoogleImagen(
prompt: string,
model: string,
args: CliArgs
): Promise<Uint8Array> {
const { experimental_generateImage: generateImage } = await import("ai");
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
const google = createGoogleGenerativeAI({
apiKey: process.env.GOOGLE_API_KEY,
baseURL: process.env.GOOGLE_BASE_URL,
});
const fullPrompt = buildPromptWithAspect(prompt, args.aspectRatio, args.quality);
const result = await generateImage({
model: google.image(model),
prompt: fullPrompt,
n: args.n,
aspectRatio: args.aspectRatio || undefined,
});
const img = result.images[0];
if (!img) throw new Error("No image in response");
if (img.uint8Array) return img.uint8Array;
if (img.base64) return Uint8Array.from(Buffer.from(img.base64, "base64"));
throw new Error("Cannot extract image data");
}
async function generateWithOpenAI(
prompt: string,
model: string,
args: CliArgs
): Promise<Uint8Array> {
const baseURL = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
const size = args.size || getOpenAISize(args.aspectRatio, args.quality);
const body: Record<string, any> = {
model,
prompt,
size,
};
if (model.includes("dall-e-3")) {
body.quality = args.quality === "2k" ? "hd" : "standard";
}
const res = await fetch(`${baseURL}/images/generations`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`OpenAI API error: ${err}`);
}
const result = (await res.json()) as { data: Array<{ url?: string; b64_json?: string }> };
const img = result.data[0];
if (img?.b64_json) {
return Uint8Array.from(Buffer.from(img.b64_json, "base64"));
}
if (img?.url) {
const imgRes = await fetch(img.url);
if (!imgRes.ok) throw new Error("Failed to download image");
const buf = await imgRes.arrayBuffer();
return new Uint8Array(buf);
}
throw new Error("No image in response");
}
async function generate(
provider: Provider,
model: string,
prompt: string,
args: CliArgs
): Promise<Uint8Array> {
if (provider === "google") {
if (isGoogleMultimodal(model)) {
return generateWithGoogleMultimodal(prompt, model, args);
}
if (isGoogleImagen(model)) {
if (args.referenceImages.length > 0) {
console.error("Warning: Reference images not supported with Imagen models, ignoring.");
}
return generateWithGoogleImagen(prompt, model, args);
}
return generateWithGoogleMultimodal(prompt, model, args);
}
if (args.referenceImages.length > 0) {
console.error("Warning: Reference images not supported with OpenAI, ignoring.");
}
return generateWithOpenAI(prompt, model, args);
}
async function main(): Promise<void> {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
printUsage();
return;
}
await loadEnv();
let prompt: string | null = args.prompt;
if (!prompt && args.promptFiles.length > 0) prompt = await readPromptFromFiles(args.promptFiles);
if (!prompt) prompt = await readPromptFromStdin();
if (!prompt) {
console.error("Error: Prompt is required");
printUsage();
process.exitCode = 1;
return;
}
if (!args.imagePath) {
console.error("Error: --image is required");
printUsage();
process.exitCode = 1;
return;
}
const provider = detectProvider(args);
const model = args.model || getDefaultModel(provider);
const outputPath = normalizeOutputImagePath(args.imagePath);
let imageData: Uint8Array;
let retried = false;
while (true) {
try {
imageData = await generate(provider, model, prompt, args);
break;
} catch (e) {
if (!retried) {
retried = true;
console.error("Generation failed, retrying...");
continue;
}
throw e;
}
}
const dir = path.dirname(outputPath);
await mkdir(dir, { recursive: true });
await writeFile(outputPath, imageData);
if (args.json) {
console.log(
JSON.stringify(
{
savedImage: outputPath,
provider,
model,
prompt: prompt.slice(0, 200),
},
null,
2
)
);
} else {
console.log(outputPath);
}
}
main().catch((e) => {
const msg = e instanceof Error ? e.message : String(e);
console.error(msg);
process.exit(1);
});
@@ -1,29 +1,61 @@
# chalkboard
Colorful chalk drawings on dark board
Black chalkboard background with colorful chalk drawing style
## Color Palette
## Design Aesthetic
- Primary: Chalk colors - white, yellow, pink, blue, green
- Background: Dark green (#2D4A3E) or black chalkboard
- Accents: Bright chalk highlights, smudge effects
Classic classroom chalkboard aesthetic with hand-drawn chalk illustrations. Nostalgic educational feel with imperfect, sketchy lines that capture the warmth of traditional teaching. Colorful chalk creates visual hierarchy while maintaining the authentic chalkboard experience.
## Visual Elements
## Background
- Chalk texture on all elements
- Dusty, slightly smudged appearance
- Hand-drawn diagrams and doodles
- Eraser marks and imperfections
- Underlines and arrows
- Chalk dust particles
- Color: Chalkboard Black (#1A1A1A) or Dark Green-Black (#1C2B1C)
- Texture: Realistic chalkboard texture with subtle scratches, dust particles, and faint eraser marks
## Typography
- Handwritten chalk lettering
- Varied chalk colors for emphasis
- Boxed and underlined headers
- Casual, teacherly handwriting
Hand-drawn chalk lettering style with visible chalk texture. Imperfect baseline adds authenticity. White or bright colored chalk for emphasis.
## Color Palette
| Role | Color | Hex | Usage |
|------|-------|-----|-------|
| Background | Chalkboard Black | #1A1A1A | Primary background |
| Alt Background | Green-Black | #1C2B1C | Traditional green board |
| Primary Text | Chalk White | #F5F5F5 | Main text, outlines |
| Accent 1 | Chalk Yellow | #FFE566 | Highlights, emphasis |
| Accent 2 | Chalk Pink | #FF9999 | Secondary highlights |
| Accent 3 | Chalk Blue | #66B3FF | Diagrams, links |
| Accent 4 | Chalk Green | #90EE90 | Success, nature |
| Accent 5 | Chalk Orange | #FFB366 | Warnings, energy |
## Visual Elements
- Hand-drawn chalk illustrations with sketchy, imperfect lines
- Chalk dust effects around text and key elements
- Doodles: stars, arrows, underlines, circles, checkmarks
- Mathematical formulas and simple diagrams
- Eraser smudges and chalk residue textures
- Wooden frame border optional
- Stick figures and simple icons
- Connection lines with hand-drawn feel
## Style Rules
### Do
- Maintain authentic chalk texture on all elements
- Use imperfect, hand-drawn quality throughout
- Add subtle chalk dust and smudge effects
- Create visual hierarchy with color variety
- Include playful doodles and annotations
### Don't
- Use perfect geometric shapes
- Create clean digital-looking lines
- Add photorealistic elements
- Use gradients or glossy effects
## Best For
Educational content, classroom explanations, tutorials, math and science concepts
Educational content, tutorials, classroom themes, teaching materials, workshops, informal learning sessions, knowledge sharing
+54 -2
View File
@@ -1,11 +1,11 @@
---
name: baoyu-post-to-x
description: Post content and articles to X (Twitter). Supports regular posts with images and X Articles (long-form Markdown). Uses real Chrome with CDP to bypass anti-automation.
description: Post content and articles to X (Twitter). Supports regular posts with images/videos and X Articles (long-form Markdown). Uses real Chrome with CDP to bypass anti-automation.
---
# Post to X (Twitter)
Post content, images, and long-form articles to X using real Chrome browser (bypasses anti-bot detection).
Post content, images, videos, and long-form articles to X using real Chrome browser (bypasses anti-bot detection).
## Script Directory
@@ -20,6 +20,8 @@ Post content, images, and long-form articles to X using real Chrome browser (byp
| Script | Purpose |
|--------|---------|
| `scripts/x-browser.ts` | Regular posts (text + images) |
| `scripts/x-video.ts` | Video posts (text + video) |
| `scripts/x-quote.ts` | Quote tweet with comment |
| `scripts/x-article.ts` | Long-form article publishing (Markdown) |
| `scripts/md-to-html.ts` | Markdown → HTML conversion |
| `scripts/copy-to-clipboard.ts` | Copy content to clipboard |
@@ -62,6 +64,56 @@ npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello!" --image ./photo.png --subm
---
## Video Posts
Text + video file (MP4, MOV, WebM).
```bash
# Preview mode (doesn't post)
npx -y bun ${SKILL_DIR}/scripts/x-video.ts "Check out this video!" --video ./clip.mp4
# Actually post
npx -y bun ${SKILL_DIR}/scripts/x-video.ts "Amazing content" --video ./demo.mp4 --submit
```
**Parameters**:
| Parameter | Description |
|-----------|-------------|
| `<text>` | Post content (positional argument) |
| `--video <path>` | Video file path (required) |
| `--submit` | Actually post (default: preview only) |
| `--profile <dir>` | Custom Chrome profile directory |
**Video Limits**:
- Regular accounts: 140 seconds max
- X Premium: up to 60 minutes
- Supported formats: MP4, MOV, WebM
- Processing time: 30-60 seconds depending on file size
---
## Quote Tweets
Quote an existing tweet with your comment - a way to share content while giving credit to the original creator.
```bash
# Preview mode (doesn't post)
npx -y bun ${SKILL_DIR}/scripts/x-quote.ts https://x.com/user/status/123456789 "Great insight!"
# Actually post
npx -y bun ${SKILL_DIR}/scripts/x-quote.ts https://x.com/user/status/123456789 "I agree!" --submit
```
**Parameters**:
| Parameter | Description |
|-----------|-------------|
| `<tweet-url>` | URL of the tweet to quote (positional argument) |
| `<comment>` | Your comment text (positional argument, optional) |
| `--submit` | Actually post (default: preview only) |
| `--profile <dir>` | Custom Chrome profile directory |
---
## X Articles
Long-form Markdown articles (requires X Premium).
+15 -161
View File
@@ -1,12 +1,23 @@
import { spawn, spawnSync } from 'node:child_process';
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { parseMarkdown } from './md-to-html.js';
import {
CHROME_CANDIDATES_BASIC,
CdpConnection,
copyHtmlToClipboard,
copyImageToClipboard,
findChromeExecutable,
getDefaultProfileDir,
getFreePort,
pasteFromClipboard,
sleep,
waitForChromeDebugPort,
} from './x-utils.js';
const X_ARTICLES_URL = 'https://x.com/compose/articles';
@@ -41,163 +52,6 @@ const I18N_SELECTORS = {
],
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close(() => reject(new Error('Unable to allocate port')));
return;
}
server.close((err) => (err ? reject(err) : resolve(address.port)));
});
});
}
function findChromeExecutable(): string | undefined {
const override = process.env.X_BROWSER_CHROME_PATH?.trim();
if (override && fs.existsSync(override)) return override;
const candidates: string[] = [];
switch (process.platform) {
case 'darwin':
candidates.push(
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
);
break;
case 'win32':
candidates.push(
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
);
break;
default:
candidates.push('/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser');
break;
}
for (const p of candidates) if (fs.existsSync(p)) return p;
return undefined;
}
function getDefaultProfileDir(): string {
const base = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
return path.join(base, 'x-browser-profile');
}
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url, { redirect: 'follow' });
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json() as Promise<T>;
}
async function waitForChromeDebugPort(port: number, timeoutMs: number): Promise<string> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`);
if (version.webSocketDebuggerUrl) return version.webSocketDebuggerUrl;
} catch {}
await sleep(200);
}
throw new Error('Chrome debug port not ready');
}
class CdpConnection {
private ws: WebSocket;
private nextId = 0;
private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> | null }>();
private constructor(ws: WebSocket) {
this.ws = ws;
this.ws.addEventListener('message', (event) => {
try {
const data = typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data as ArrayBuffer);
const msg = JSON.parse(data) as { id?: number; result?: unknown; error?: { message?: string } };
if (msg.id) {
const pending = this.pending.get(msg.id);
if (pending) {
this.pending.delete(msg.id);
if (pending.timer) clearTimeout(pending.timer);
if (msg.error?.message) pending.reject(new Error(msg.error.message));
else pending.resolve(msg.result);
}
}
} catch {}
});
this.ws.addEventListener('close', () => {
for (const [id, pending] of this.pending.entries()) {
this.pending.delete(id);
if (pending.timer) clearTimeout(pending.timer);
pending.reject(new Error('CDP connection closed'));
}
});
}
static async connect(url: string, timeoutMs: number): Promise<CdpConnection> {
const ws = new WebSocket(url);
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('CDP connection timeout')), timeoutMs);
ws.addEventListener('open', () => { clearTimeout(timer); resolve(); });
ws.addEventListener('error', () => { clearTimeout(timer); reject(new Error('CDP connection failed')); });
});
return new CdpConnection(ws);
}
async send<T = unknown>(method: string, params?: Record<string, unknown>, options?: { sessionId?: string; timeoutMs?: number }): Promise<T> {
const id = ++this.nextId;
const message: Record<string, unknown> = { id, method };
if (params) message.params = params;
if (options?.sessionId) message.sessionId = options.sessionId;
const timeoutMs = options?.timeoutMs ?? 30_000;
return new Promise<T>((resolve, reject) => {
const timer = timeoutMs > 0 ? setTimeout(() => { this.pending.delete(id); reject(new Error(`CDP timeout: ${method}`)); }, timeoutMs) : null;
this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer });
this.ws.send(JSON.stringify(message));
});
}
close(): void {
try { this.ws.close(); } catch {}
}
}
function getScriptDir(): string {
return path.dirname(new URL(import.meta.url).pathname);
}
function copyImageToClipboard(imagePath: string): boolean {
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
return result.status === 0;
}
function copyHtmlToClipboard(htmlPath: string): boolean {
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'html', '--file', htmlPath], { stdio: 'inherit' });
return result.status === 0;
}
function pasteFromClipboard(targetApp?: string, retries = 3, delayMs = 500): boolean {
const pasteScript = path.join(getScriptDir(), 'paste-from-clipboard.ts');
const args = ['npx', '-y', 'bun', pasteScript, '--retries', String(retries), '--delay', String(delayMs)];
if (targetApp) {
args.push('--app', targetApp);
}
const result = spawnSync(args[0]!, args.slice(1), { stdio: 'inherit' });
return result.status === 0;
}
interface ArticleOptions {
markdownPath: string;
coverImage?: string;
@@ -225,7 +79,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
await writeFile(htmlPath, parsed.html, 'utf-8');
console.log(`[x-article] HTML saved to: ${htmlPath}`);
const chromePath = options.chromePath ?? findChromeExecutable();
const chromePath = options.chromePath ?? findChromeExecutable(CHROME_CANDIDATES_BASIC);
if (!chromePath) throw new Error('Chrome not found');
await mkdir(profileDir, { recursive: true });
@@ -246,7 +100,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
try {
const wsUrl = await waitForChromeDebugPort(port, 30_000);
cdp = await CdpConnection.connect(wsUrl, 30_000);
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 30_000 });
// Get page target
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
+15 -197
View File
@@ -1,203 +1,21 @@
import { spawn, spawnSync } from 'node:child_process';
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import { mkdir } from 'node:fs/promises';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import {
CHROME_CANDIDATES_FULL,
CdpConnection,
copyImageToClipboard,
findChromeExecutable,
getDefaultProfileDir,
getFreePort,
pasteFromClipboard,
sleep,
waitForChromeDebugPort,
} from './x-utils.js';
const X_COMPOSE_URL = 'https://x.com/compose/post';
function getScriptDir(): string {
return path.dirname(new URL(import.meta.url).pathname);
}
function copyImageToClipboard(imagePath: string): boolean {
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
return result.status === 0;
}
function pasteFromClipboard(targetApp?: string, retries = 3, delayMs = 500): boolean {
const pasteScript = path.join(getScriptDir(), 'paste-from-clipboard.ts');
const args = ['npx', '-y', 'bun', pasteScript, '--retries', String(retries), '--delay', String(delayMs)];
if (targetApp) {
args.push('--app', targetApp);
}
const result = spawnSync(args[0]!, args.slice(1), { stdio: 'inherit' });
return result.status === 0;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close(() => reject(new Error('Unable to allocate a free TCP port.')));
return;
}
const port = address.port;
server.close((err) => {
if (err) reject(err);
else resolve(port);
});
});
});
}
function findChromeExecutable(): string | undefined {
const override = process.env.X_BROWSER_CHROME_PATH?.trim();
if (override && fs.existsSync(override)) return override;
const candidates: string[] = [];
switch (process.platform) {
case 'darwin':
candidates.push(
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
);
break;
case 'win32':
candidates.push(
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
);
break;
default:
candidates.push(
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/snap/bin/chromium',
'/usr/bin/microsoft-edge',
);
break;
}
for (const p of candidates) {
if (fs.existsSync(p)) return p;
}
return undefined;
}
function getDefaultProfileDir(): string {
const base = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
return path.join(base, 'x-browser-profile');
}
async function fetchJson<T = unknown>(url: string): Promise<T> {
const res = await fetch(url, { redirect: 'follow' });
if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`);
return (await res.json()) as T;
}
async function waitForChromeDebugPort(port: number, timeoutMs: number): Promise<string> {
const start = Date.now();
let lastError: unknown = null;
while (Date.now() - start < timeoutMs) {
try {
const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`);
if (version.webSocketDebuggerUrl) return version.webSocketDebuggerUrl;
lastError = new Error('Missing webSocketDebuggerUrl');
} catch (error) {
lastError = error;
}
await sleep(200);
}
throw new Error(`Chrome debug port not ready: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
}
class CdpConnection {
private ws: WebSocket;
private nextId = 0;
private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> | null }>();
private eventHandlers = new Map<string, Set<(params: unknown) => void>>();
private constructor(ws: WebSocket) {
this.ws = ws;
this.ws.addEventListener('message', (event) => {
try {
const data = typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data as ArrayBuffer);
const msg = JSON.parse(data) as { id?: number; method?: string; params?: unknown; result?: unknown; error?: { message?: string } };
if (msg.method) {
const handlers = this.eventHandlers.get(msg.method);
if (handlers) handlers.forEach((h) => h(msg.params));
}
if (msg.id) {
const pending = this.pending.get(msg.id);
if (pending) {
this.pending.delete(msg.id);
if (pending.timer) clearTimeout(pending.timer);
if (msg.error?.message) pending.reject(new Error(msg.error.message));
else pending.resolve(msg.result);
}
}
} catch {}
});
this.ws.addEventListener('close', () => {
for (const [id, pending] of this.pending.entries()) {
this.pending.delete(id);
if (pending.timer) clearTimeout(pending.timer);
pending.reject(new Error('CDP connection closed.'));
}
});
}
static async connect(url: string, timeoutMs: number): Promise<CdpConnection> {
const ws = new WebSocket(url);
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('CDP connection timeout.')), timeoutMs);
ws.addEventListener('open', () => { clearTimeout(timer); resolve(); });
ws.addEventListener('error', () => { clearTimeout(timer); reject(new Error('CDP connection failed.')); });
});
return new CdpConnection(ws);
}
on(method: string, handler: (params: unknown) => void): void {
if (!this.eventHandlers.has(method)) this.eventHandlers.set(method, new Set());
this.eventHandlers.get(method)!.add(handler);
}
async send<T = unknown>(method: string, params?: Record<string, unknown>, options?: { sessionId?: string; timeoutMs?: number }): Promise<T> {
const id = ++this.nextId;
const message: Record<string, unknown> = { id, method };
if (params) message.params = params;
if (options?.sessionId) message.sessionId = options.sessionId;
const timeoutMs = options?.timeoutMs ?? 15_000;
const result = await new Promise<unknown>((resolve, reject) => {
const timer = timeoutMs > 0 ? setTimeout(() => { this.pending.delete(id); reject(new Error(`CDP timeout: ${method}`)); }, timeoutMs) : null;
this.pending.set(id, { resolve, reject, timer });
this.ws.send(JSON.stringify(message));
});
return result as T;
}
close(): void {
try { this.ws.close(); } catch {}
}
}
interface XBrowserOptions {
text?: string;
images?: string[];
@@ -210,7 +28,7 @@ interface XBrowserOptions {
export async function postToX(options: XBrowserOptions): Promise<void> {
const { text, images = [], submit = false, timeoutMs = 120_000, profileDir = getDefaultProfileDir() } = options;
const chromePath = options.chromePath ?? findChromeExecutable();
const chromePath = options.chromePath ?? findChromeExecutable(CHROME_CANDIDATES_FULL);
if (!chromePath) throw new Error('Chrome not found. Set X_BROWSER_CHROME_PATH env var.');
await mkdir(profileDir, { recursive: true });
@@ -231,8 +49,8 @@ export async function postToX(options: XBrowserOptions): Promise<void> {
let cdp: CdpConnection | null = null;
try {
const wsUrl = await waitForChromeDebugPort(port, 30_000);
cdp = await CdpConnection.connect(wsUrl, 30_000);
const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true });
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 15_000 });
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
let pageTarget = targets.targetInfos.find((t) => t.type === 'page' && t.url.includes('x.com'));
+247
View File
@@ -0,0 +1,247 @@
import { spawn } from 'node:child_process';
import { mkdir } from 'node:fs/promises';
import process from 'node:process';
import {
CHROME_CANDIDATES_FULL,
CdpConnection,
findChromeExecutable,
getDefaultProfileDir,
getFreePort,
sleep,
waitForChromeDebugPort,
} from './x-utils.js';
function extractTweetUrl(urlOrId: string): string | null {
// If it's already a full URL, normalize it
if (urlOrId.match(/(?:x\.com|twitter\.com)\/\w+\/status\/\d+/)) {
return urlOrId.replace(/twitter\.com/, 'x.com').split('?')[0];
}
return null;
}
interface QuoteOptions {
tweetUrl: string;
comment?: string;
submit?: boolean;
timeoutMs?: number;
profileDir?: string;
chromePath?: string;
}
export async function quotePost(options: QuoteOptions): Promise<void> {
const { tweetUrl, comment, submit = false, timeoutMs = 120_000, profileDir = getDefaultProfileDir() } = options;
const chromePath = options.chromePath ?? findChromeExecutable(CHROME_CANDIDATES_FULL);
if (!chromePath) throw new Error('Chrome not found. Set X_BROWSER_CHROME_PATH env var.');
await mkdir(profileDir, { recursive: true });
const port = await getFreePort();
console.log(`[x-quote] Launching Chrome (profile: ${profileDir})`);
console.log(`[x-quote] Opening tweet: ${tweetUrl}`);
const chrome = spawn(chromePath, [
`--remote-debugging-port=${port}`,
`--user-data-dir=${profileDir}`,
'--no-first-run',
'--no-default-browser-check',
'--disable-blink-features=AutomationControlled',
'--start-maximized',
tweetUrl,
], { stdio: 'ignore' });
let cdp: CdpConnection | null = null;
try {
const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true });
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 15_000 });
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
let pageTarget = targets.targetInfos.find((t) => t.type === 'page' && t.url.includes('x.com'));
if (!pageTarget) {
const { targetId } = await cdp.send<{ targetId: string }>('Target.createTarget', { url: tweetUrl });
pageTarget = { targetId, url: tweetUrl, type: 'page' };
}
const { sessionId } = await cdp.send<{ sessionId: string }>('Target.attachToTarget', { targetId: pageTarget.targetId, flatten: true });
await cdp.send('Page.enable', {}, { sessionId });
await cdp.send('Runtime.enable', {}, { sessionId });
console.log('[x-quote] Waiting for tweet to load...');
await sleep(3000);
// Wait for retweet button to appear (indicates tweet loaded and user logged in)
const waitForRetweetButton = async (): Promise<boolean> => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const result = await cdp!.send<{ result: { value: boolean } }>('Runtime.evaluate', {
expression: `!!document.querySelector('[data-testid="retweet"]')`,
returnByValue: true,
}, { sessionId });
if (result.result.value) return true;
await sleep(1000);
}
return false;
};
const retweetFound = await waitForRetweetButton();
if (!retweetFound) {
console.log('[x-quote] Tweet not found or not logged in. Please log in to X in the browser window.');
console.log('[x-quote] Waiting for login...');
const loggedIn = await waitForRetweetButton();
if (!loggedIn) throw new Error('Timed out waiting for tweet. Please log in first or check the tweet URL.');
}
// Click the retweet button
console.log('[x-quote] Clicking retweet button...');
await cdp.send('Runtime.evaluate', {
expression: `document.querySelector('[data-testid="retweet"]')?.click()`,
}, { sessionId });
await sleep(1000);
// Wait for and click the "Quote" option in the menu
console.log('[x-quote] Selecting quote option...');
const waitForQuoteOption = async (): Promise<boolean> => {
const start = Date.now();
while (Date.now() - start < 10_000) {
const result = await cdp!.send<{ result: { value: boolean } }>('Runtime.evaluate', {
expression: `!!document.querySelector('[data-testid="Dropdown"] [role="menuitem"]:nth-child(2)')`,
returnByValue: true,
}, { sessionId });
if (result.result.value) return true;
await sleep(200);
}
return false;
};
const quoteOptionFound = await waitForQuoteOption();
if (!quoteOptionFound) {
throw new Error('Quote option not found. The menu may not have opened.');
}
// Click the quote option (second menu item)
await cdp.send('Runtime.evaluate', {
expression: `document.querySelector('[data-testid="Dropdown"] [role="menuitem"]:nth-child(2)')?.click()`,
}, { sessionId });
await sleep(2000);
// Wait for the quote compose dialog
console.log('[x-quote] Waiting for quote compose dialog...');
const waitForQuoteDialog = async (): Promise<boolean> => {
const start = Date.now();
while (Date.now() - start < 10_000) {
const result = await cdp!.send<{ result: { value: boolean } }>('Runtime.evaluate', {
expression: `!!document.querySelector('[data-testid="tweetTextarea_0"]')`,
returnByValue: true,
}, { sessionId });
if (result.result.value) return true;
await sleep(200);
}
return false;
};
const dialogFound = await waitForQuoteDialog();
if (!dialogFound) {
throw new Error('Quote compose dialog not found.');
}
// Type the comment if provided
if (comment) {
console.log('[x-quote] Typing comment...');
// Use CDP Input.insertText for more reliable text insertion
await cdp.send('Runtime.evaluate', {
expression: `document.querySelector('[data-testid="tweetTextarea_0"]')?.focus()`,
}, { sessionId });
await sleep(200);
await cdp.send('Input.insertText', {
text: comment,
}, { sessionId });
await sleep(500);
}
if (submit) {
console.log('[x-quote] Submitting quote post...');
await cdp.send('Runtime.evaluate', {
expression: `document.querySelector('[data-testid="tweetButton"]')?.click()`,
}, { sessionId });
await sleep(2000);
console.log('[x-quote] Quote post submitted!');
} else {
console.log('[x-quote] Quote composed (preview mode). Add --submit to post.');
console.log('[x-quote] Browser will stay open for 30 seconds for preview...');
await sleep(30_000);
}
} finally {
if (cdp) {
try { await cdp.send('Browser.close', {}, { timeoutMs: 5_000 }); } catch {}
cdp.close();
}
setTimeout(() => {
if (!chrome.killed) try { chrome.kill('SIGKILL'); } catch {}
}, 2_000).unref?.();
try { chrome.kill('SIGTERM'); } catch {}
}
}
function printUsage(): never {
console.log(`Quote a tweet on X (Twitter) using real Chrome browser
Usage:
npx -y bun x-quote.ts <tweet-url> [options] [comment]
Options:
--submit Actually post (default: preview only)
--profile <dir> Chrome profile directory
--help Show this help
Examples:
npx -y bun x-quote.ts https://x.com/user/status/123456789 "Great insight!"
npx -y bun x-quote.ts https://x.com/user/status/123456789 "I agree!" --submit
`);
process.exit(0);
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) printUsage();
let tweetUrl: string | undefined;
let submit = false;
let profileDir: string | undefined;
const commentParts: string[] = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i]!;
if (arg === '--submit') {
submit = true;
} else if (arg === '--profile' && args[i + 1]) {
profileDir = args[++i];
} else if (!arg.startsWith('-')) {
// First non-option argument is the tweet URL
if (!tweetUrl && arg.match(/(?:x\.com|twitter\.com)\/\w+\/status\/\d+/)) {
tweetUrl = extractTweetUrl(arg) ?? undefined;
} else {
commentParts.push(arg);
}
}
}
if (!tweetUrl) {
console.error('Error: Please provide a tweet URL.');
console.error('Example: npx -y bun x-quote.ts https://x.com/user/status/123456789 "Your comment"');
process.exit(1);
}
const comment = commentParts.join(' ').trim() || undefined;
await quotePost({ tweetUrl, comment, submit, profileDir });
}
await main().catch((err) => {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});
+242
View File
@@ -0,0 +1,242 @@
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
export type PlatformCandidates = {
darwin?: string[];
win32?: string[];
default: string[];
};
export const CHROME_CANDIDATES_BASIC: PlatformCandidates = {
darwin: [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
],
win32: [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
],
default: [
'/usr/bin/google-chrome',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
],
};
export const CHROME_CANDIDATES_FULL: PlatformCandidates = {
darwin: [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
],
win32: [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
],
default: [
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/snap/bin/chromium',
'/usr/bin/microsoft-edge',
],
};
function getCandidatesForPlatform(candidates: PlatformCandidates): string[] {
if (process.platform === 'darwin' && candidates.darwin?.length) return candidates.darwin;
if (process.platform === 'win32' && candidates.win32?.length) return candidates.win32;
return candidates.default;
}
export function findChromeExecutable(candidates: PlatformCandidates): string | undefined {
const override = process.env.X_BROWSER_CHROME_PATH?.trim();
if (override && fs.existsSync(override)) return override;
for (const candidate of getCandidatesForPlatform(candidates)) {
if (fs.existsSync(candidate)) return candidate;
}
return undefined;
}
export function getDefaultProfileDir(): string {
const base = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
return path.join(base, 'x-browser-profile');
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function getFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close(() => reject(new Error('Unable to allocate a free TCP port.')));
return;
}
const port = address.port;
server.close((err) => {
if (err) reject(err);
else resolve(port);
});
});
});
}
async function fetchJson<T = unknown>(url: string): Promise<T> {
const res = await fetch(url, { redirect: 'follow' });
if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`);
return (await res.json()) as T;
}
export async function waitForChromeDebugPort(
port: number,
timeoutMs: number,
options?: { includeLastError?: boolean },
): Promise<string> {
const start = Date.now();
let lastError: unknown = null;
while (Date.now() - start < timeoutMs) {
try {
const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`);
if (version.webSocketDebuggerUrl) return version.webSocketDebuggerUrl;
lastError = new Error('Missing webSocketDebuggerUrl');
} catch (error) {
lastError = error;
}
await sleep(200);
}
if (options?.includeLastError && lastError) {
throw new Error(`Chrome debug port not ready: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
}
throw new Error('Chrome debug port not ready');
}
type PendingRequest = {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
timer: ReturnType<typeof setTimeout> | null;
};
export class CdpConnection {
private ws: WebSocket;
private nextId = 0;
private pending = new Map<number, PendingRequest>();
private eventHandlers = new Map<string, Set<(params: unknown) => void>>();
private defaultTimeoutMs: number;
private constructor(ws: WebSocket, options?: { defaultTimeoutMs?: number }) {
this.ws = ws;
this.defaultTimeoutMs = options?.defaultTimeoutMs ?? 15_000;
this.ws.addEventListener('message', (event) => {
try {
const data = typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data as ArrayBuffer);
const msg = JSON.parse(data) as { id?: number; method?: string; params?: unknown; result?: unknown; error?: { message?: string } };
if (msg.method) {
const handlers = this.eventHandlers.get(msg.method);
if (handlers) handlers.forEach((h) => h(msg.params));
}
if (msg.id) {
const pending = this.pending.get(msg.id);
if (pending) {
this.pending.delete(msg.id);
if (pending.timer) clearTimeout(pending.timer);
if (msg.error?.message) pending.reject(new Error(msg.error.message));
else pending.resolve(msg.result);
}
}
} catch {}
});
this.ws.addEventListener('close', () => {
for (const [id, pending] of this.pending.entries()) {
this.pending.delete(id);
if (pending.timer) clearTimeout(pending.timer);
pending.reject(new Error('CDP connection closed.'));
}
});
}
static async connect(url: string, timeoutMs: number, options?: { defaultTimeoutMs?: number }): Promise<CdpConnection> {
const ws = new WebSocket(url);
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('CDP connection timeout.')), timeoutMs);
ws.addEventListener('open', () => { clearTimeout(timer); resolve(); });
ws.addEventListener('error', () => { clearTimeout(timer); reject(new Error('CDP connection failed.')); });
});
return new CdpConnection(ws, options);
}
on(method: string, handler: (params: unknown) => void): void {
if (!this.eventHandlers.has(method)) this.eventHandlers.set(method, new Set());
this.eventHandlers.get(method)!.add(handler);
}
async send<T = unknown>(method: string, params?: Record<string, unknown>, options?: { sessionId?: string; timeoutMs?: number }): Promise<T> {
const id = ++this.nextId;
const message: Record<string, unknown> = { id, method };
if (params) message.params = params;
if (options?.sessionId) message.sessionId = options.sessionId;
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
const result = await new Promise<unknown>((resolve, reject) => {
const timer = timeoutMs > 0
? setTimeout(() => { this.pending.delete(id); reject(new Error(`CDP timeout: ${method}`)); }, timeoutMs)
: null;
this.pending.set(id, { resolve, reject, timer });
this.ws.send(JSON.stringify(message));
});
return result as T;
}
close(): void {
try { this.ws.close(); } catch {}
}
}
export function getScriptDir(): string {
return path.dirname(new URL(import.meta.url).pathname);
}
function runBunScript(scriptPath: string, args: string[]): boolean {
const result = spawnSync('npx', ['-y', 'bun', scriptPath, ...args], { stdio: 'inherit' });
return result.status === 0;
}
export function copyImageToClipboard(imagePath: string): boolean {
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
return runBunScript(copyScript, ['image', imagePath]);
}
export function copyHtmlToClipboard(htmlPath: string): boolean {
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
return runBunScript(copyScript, ['html', '--file', htmlPath]);
}
export function pasteFromClipboard(targetApp?: string, retries = 3, delayMs = 500): boolean {
const pasteScript = path.join(getScriptDir(), 'paste-from-clipboard.ts');
const args = ['--retries', String(retries), '--delay', String(delayMs)];
if (targetApp) args.push('--app', targetApp);
return runBunScript(pasteScript, args);
}
+260
View File
@@ -0,0 +1,260 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import {
CHROME_CANDIDATES_FULL,
CdpConnection,
findChromeExecutable,
getDefaultProfileDir,
getFreePort,
sleep,
waitForChromeDebugPort,
} from './x-utils.js';
const X_COMPOSE_URL = 'https://x.com/compose/post';
interface XVideoOptions {
text?: string;
videoPath: string;
submit?: boolean;
timeoutMs?: number;
profileDir?: string;
chromePath?: string;
}
export async function postVideoToX(options: XVideoOptions): Promise<void> {
const { text, videoPath, submit = false, timeoutMs = 120_000, profileDir = getDefaultProfileDir() } = options;
const chromePath = options.chromePath ?? findChromeExecutable(CHROME_CANDIDATES_FULL);
if (!chromePath) throw new Error('Chrome not found. Set X_BROWSER_CHROME_PATH env var.');
if (!fs.existsSync(videoPath)) throw new Error(`Video not found: ${videoPath}`);
const absVideoPath = path.resolve(videoPath);
console.log(`[x-video] Video: ${absVideoPath}`);
await mkdir(profileDir, { recursive: true });
const port = await getFreePort();
console.log(`[x-video] Launching Chrome (profile: ${profileDir})`);
const chrome = spawn(chromePath, [
`--remote-debugging-port=${port}`,
`--user-data-dir=${profileDir}`,
'--no-first-run',
'--no-default-browser-check',
'--disable-blink-features=AutomationControlled',
'--start-maximized',
X_COMPOSE_URL,
], { stdio: 'ignore' });
let cdp: CdpConnection | null = null;
try {
const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true });
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 30_000 });
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
let pageTarget = targets.targetInfos.find((t) => t.type === 'page' && t.url.includes('x.com'));
if (!pageTarget) {
const { targetId } = await cdp.send<{ targetId: string }>('Target.createTarget', { url: X_COMPOSE_URL });
pageTarget = { targetId, url: X_COMPOSE_URL, type: 'page' };
}
const { sessionId } = await cdp.send<{ sessionId: string }>('Target.attachToTarget', { targetId: pageTarget.targetId, flatten: true });
await cdp.send('Page.enable', {}, { sessionId });
await cdp.send('Runtime.enable', {}, { sessionId });
await cdp.send('DOM.enable', {}, { sessionId });
await cdp.send('Input.setIgnoreInputEvents', { ignore: false }, { sessionId });
console.log('[x-video] Waiting for X editor...');
await sleep(3000);
const waitForEditor = async (): Promise<boolean> => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const result = await cdp!.send<{ result: { value: boolean } }>('Runtime.evaluate', {
expression: `!!document.querySelector('[data-testid="tweetTextarea_0"]')`,
returnByValue: true,
}, { sessionId });
if (result.result.value) return true;
await sleep(1000);
}
return false;
};
const editorFound = await waitForEditor();
if (!editorFound) {
console.log('[x-video] Editor not found. Please log in to X in the browser window.');
console.log('[x-video] Waiting for login...');
const loggedIn = await waitForEditor();
if (!loggedIn) throw new Error('Timed out waiting for X editor. Please log in first.');
}
// Upload video FIRST (before typing text to avoid text being cleared)
console.log('[x-video] Uploading video...');
const { root } = await cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, { sessionId });
const { nodeId } = await cdp.send<{ nodeId: number }>('DOM.querySelector', {
nodeId: root.nodeId,
selector: 'input[type="file"][accept*="video"], input[data-testid="fileInput"], input[type="file"]',
}, { sessionId });
if (!nodeId || nodeId === 0) {
throw new Error('Could not find file input for video upload.');
}
await cdp.send('DOM.setFileInputFiles', {
nodeId,
files: [absVideoPath],
}, { sessionId });
console.log('[x-video] Video file set, uploading in background...');
// Wait a moment for upload to start, then type text while video processes
await sleep(2000);
// Type text while video uploads in background
if (text) {
console.log('[x-video] Typing text...');
await cdp.send('Runtime.evaluate', {
expression: `
const editor = document.querySelector('[data-testid="tweetTextarea_0"]');
if (editor) {
editor.focus();
document.execCommand('insertText', false, ${JSON.stringify(text)});
}
`,
}, { sessionId });
await sleep(500);
}
// Wait for video to finish processing by checking if tweet button is enabled
console.log('[x-video] Waiting for video processing...');
const waitForVideoReady = async (maxWaitMs = 180_000): Promise<boolean> => {
const start = Date.now();
let dots = 0;
while (Date.now() - start < maxWaitMs) {
const result = await cdp!.send<{ result: { value: { hasMedia: boolean; buttonEnabled: boolean } } }>('Runtime.evaluate', {
expression: `(() => {
const hasMedia = !!document.querySelector('[data-testid="attachments"] video, [data-testid="videoPlayer"], video');
const tweetBtn = document.querySelector('[data-testid="tweetButton"]');
const buttonEnabled = tweetBtn && !tweetBtn.disabled && tweetBtn.getAttribute('aria-disabled') !== 'true';
return { hasMedia, buttonEnabled };
})()`,
returnByValue: true,
}, { sessionId });
const { hasMedia, buttonEnabled } = result.result.value;
if (hasMedia && buttonEnabled) {
console.log('');
return true;
}
process.stdout.write('.');
dots++;
if (dots % 60 === 0) console.log(''); // New line every 60 dots
await sleep(2000);
}
console.log('');
return false;
};
const videoReady = await waitForVideoReady();
if (videoReady) {
console.log('[x-video] Video ready!');
} else {
console.log('[x-video] Video may still be processing. Please check browser window.');
}
if (submit) {
console.log('[x-video] Submitting post...');
await cdp.send('Runtime.evaluate', {
expression: `document.querySelector('[data-testid="tweetButton"]')?.click()`,
}, { sessionId });
await sleep(5000);
console.log('[x-video] Post submitted!');
} else {
console.log('[x-video] Post composed (preview mode). Add --submit to post.');
console.log('[x-video] Browser stays open for review.');
}
} finally {
if (cdp) {
cdp.close();
}
// Don't kill Chrome in preview mode, let user review
if (submit) {
setTimeout(() => {
if (!chrome.killed) try { chrome.kill('SIGKILL'); } catch {}
}, 2_000).unref?.();
try { chrome.kill('SIGTERM'); } catch {}
}
}
}
function printUsage(): never {
console.log(`Post video to X (Twitter) using real Chrome browser
Usage:
npx -y bun x-video.ts [options] --video <path> [text]
Options:
--video <path> Video file path (required, supports mp4/mov/webm)
--submit Actually post (default: preview only)
--profile <dir> Chrome profile directory
--help Show this help
Examples:
npx -y bun x-video.ts --video ./clip.mp4 "Check out this video!"
npx -y bun x-video.ts --video ./demo.mp4 --submit
npx -y bun x-video.ts --video ./video.mp4 "Multi-line text
works too"
Notes:
- Video is uploaded first, then text is added (to avoid text being cleared)
- Video processing may take 30-60 seconds depending on file size
- Maximum video length on X: 140 seconds (regular) or 60 min (Premium)
- Supported formats: MP4, MOV, WebM
`);
process.exit(0);
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) printUsage();
let videoPath: string | undefined;
let submit = false;
let profileDir: string | undefined;
const textParts: string[] = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i]!;
if (arg === '--video' && args[i + 1]) {
videoPath = args[++i]!;
} else if (arg === '--submit') {
submit = true;
} else if (arg === '--profile' && args[i + 1]) {
profileDir = args[++i];
} else if (!arg.startsWith('-')) {
textParts.push(arg);
}
}
const text = textParts.join(' ').trim() || undefined;
if (!videoPath) {
console.error('Error: --video <path> is required.');
printUsage();
}
await postVideoToX({ text, videoPath, submit, profileDir });
}
await main().catch((err) => {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});
+270 -2
View File
@@ -41,11 +41,74 @@ Transform content into professional slide deck images with flexible style option
| `--style <name>` | Visual style (see Style Gallery) |
| `--audience <type>` | Target audience: beginners, intermediate, experts, executives, general |
| `--lang <code>` | Output language (en, zh, ja, etc.) |
| `--slides <number>` | Target slide count |
| `--slides <number>` | Target slide count (recommended: 8-25, max: 30) |
| `--outline-only` | Generate outline only, skip image generation |
**Slide Count Guidance**:
| Content Length | Recommended Slides |
|----------------|-------------------|
| Short (< 1000 words) | 5-10 |
| Medium (1000-3000 words) | 10-18 |
| Long (3000-5000 words) | 15-25 |
| Very Long (> 5000 words) | 20-30 (consider splitting) |
Maximum 30 slides per deck. For longer content, split into multiple decks.
## Audience Guidelines
Design decisions should adapt to target audience. Use `--audience` to set.
| Audience | Content Density | Visual Style | Terminology | Slides |
|----------|-----------------|--------------|-------------|--------|
| `beginners` | Low | Friendly, illustrative | Plain language | 8-15 |
| `intermediate` | Medium | Balanced, structured | Some jargon OK | 10-20 |
| `experts` | High | Data-rich, precise | Technical terms | 12-25 |
| `executives` | Medium-High | Clean, impactful | Business language | 8-12 |
| `general` | Medium | Accessible, engaging | Minimal jargon | 10-18 |
### Audience-Specific Principles
**Beginners**:
- One concept per slide
- Visual metaphors over abstract diagrams
- Step-by-step progression
- Generous whitespace
**Experts**:
- Multiple data points per slide acceptable
- Technical diagrams with precise labels
- Assume domain knowledge
- Dense but organized information
**Executives**:
- Lead with insights, not data
- "So what?" on every slide
- Decision-enabling content
- Bottom-line upfront (BLUF)
## Style Gallery
### Style Selection Principles
**Content-First Approach**:
1. Analyze content topic, mood, and industry before selecting
2. Consider target audience expectations
3. Match style to subject matter (not personal preference)
**Quick Reference by Content Type**:
| Content Type | Recommended Styles |
|--------------|-------------------|
| Technical/Architecture | `blueprint`, `intuition-machine` |
| Educational/Tutorials | `sketch-notes`, `chalkboard` |
| Corporate/Business | `corporate`, `minimal` |
| Creative/Artistic | `vector-illustration`, `watercolor` |
| Product/SaaS | `notion`, `bold-editorial` |
| Scientific/Research | `scientific`, `editorial-infographic` |
**Note**: Full style specifications in `references/styles/<style>.md`
### Available Styles
| Style | Description | Best For |
|-------|-------------|----------|
| `blueprint` (Default) | Technical schematics, grid texture | Architecture, system design |
@@ -87,6 +150,72 @@ Transform content into professional slide deck images with flexible style option
| lifestyle, wellness, travel, artistic, natural | `watercolor` |
| Default | `blueprint` |
## Layout Gallery
Optional layout hints for individual slides. Specify in outline's `// LAYOUT` section.
### Slide-Specific Layouts
| Layout | Description | Best For |
|--------|-------------|----------|
| `title-hero` | Large centered title + subtitle | Cover slides, section breaks |
| `quote-callout` | Featured quote with attribution | Testimonials, key insights |
| `key-stat` | Single large number as focal point | Impact statistics, metrics |
| `split-screen` | Half image, half text | Feature highlights, comparisons |
| `icon-grid` | Grid of icons with labels | Features, capabilities, benefits |
| `two-columns` | Content in balanced columns | Paired information, dual points |
| `three-columns` | Content in three columns | Triple comparisons, categories |
| `image-caption` | Full-bleed image + text overlay | Visual storytelling, emotional |
| `agenda` | Numbered list with highlights | Session overview, roadmap |
| `bullet-list` | Structured bullet points | Simple content, lists |
### Infographic-Derived Layouts
| Layout | Description | Best For |
|--------|-------------|----------|
| `linear-progression` | Sequential flow left-to-right | Timelines, step-by-step |
| `binary-comparison` | Side-by-side A vs B | Before/after, pros-cons |
| `comparison-matrix` | Multi-factor grid | Feature comparisons |
| `hierarchical-layers` | Pyramid or stacked levels | Priority, importance |
| `hub-spoke` | Central node with radiating items | Concept maps, ecosystems |
| `bento-grid` | Varied-size tiles | Overview, summary |
| `funnel` | Narrowing stages | Conversion, filtering |
| `dashboard` | Metrics with charts/numbers | KPIs, data display |
| `venn-diagram` | Overlapping circles | Relationships, intersections |
| `circular-flow` | Continuous cycle | Recurring processes |
| `winding-roadmap` | Curved path with milestones | Journey, timeline |
| `tree-branching` | Parent-child hierarchy | Org charts, taxonomies |
| `iceberg` | Visible vs hidden layers | Surface vs depth |
| `bridge` | Gap with connection | Problem-solution |
**Usage**: Add `Layout: <name>` in slide's `// LAYOUT` section to guide visual composition.
### Layout Selection Tips
**Match Layout to Content**:
| Content Type | Recommended Layouts |
|--------------|-------------------|
| Single narrative | `bullet-list`, `image-caption` |
| Two concepts | `split-screen`, `binary-comparison` |
| Three items | `three-columns`, `icon-grid` |
| Process/Steps | `linear-progression`, `winding-roadmap` |
| Data/Metrics | `dashboard`, `key-stat` |
| Relationships | `hub-spoke`, `venn-diagram` |
| Hierarchy | `hierarchical-layers`, `tree-branching` |
**Layout Flow Patterns**:
| Position | Recommended Layouts |
|----------|-------------------|
| Opening | `title-hero`, `agenda` |
| Middle | Content-specific layouts |
| Closing | `quote-callout`, `key-stat` |
**Common Mistakes to Avoid**:
- ✗ Using 3-column layout for 2 items (leaves columns empty)
- ✗ Stacking charts/tables below text (use side-by-side instead)
- ✗ Image layouts without actual images
- ✗ Quote layouts for emphasis (use only for real quotes with attribution)
## Design Philosophy
This deck is designed for **reading and sharing**, not live presentation:
@@ -95,6 +224,142 @@ This deck is designed for **reading and sharing**, not live presentation:
- Include **all necessary context** within each slide
- Optimize for **social media sharing** and offline reading
### Visual Hierarchy Principles
| Principle | Description |
|-----------|-------------|
| Focal Point | ONE dominant element per slide draws attention first |
| Rule of Thirds | Position key elements at grid intersections |
| Z-Pattern | Guide eye: top-left → top-right → bottom-left → bottom-right |
| Size Contrast | Headlines 2-3x larger than body text |
| Breathing Room | Minimum 10% margin from all edges |
### Content Density
Professional presentations balance information density with clarity.
| Level | Description | Use When |
|-------|-------------|----------|
| High | Multiple data points, detailed charts, dense text | Expert audience, technical reviews, data-driven decisions |
| Medium | Key points with supporting details, moderate visuals | General business, mixed audiences |
| Low | One main idea, large visuals, minimal text | Beginners, keynotes, emotional impact |
**High-Density Principles** (McKinsey-style):
- Every element earns its space
- Data speaks louder than decoration
- Annotations explain insights, not describe data
- White space is strategic, not filler
- Information hierarchy guides the eye
**Density by Slide Type**:
| Slide Type | Recommended Density |
|------------|-------------------|
| Cover/Title | Low |
| Agenda/Overview | Medium |
| Content/Analysis | Medium-High |
| Data/Metrics | High |
| Quote/Impact | Low |
| Summary/Takeaway | Medium |
### Color Selection
**Content-First Approach**:
1. Analyze content topic, mood, and industry
2. Consider target audience expectations
3. Match palette to subject matter (not defaults)
4. Ensure strong contrast for readability
**Quick Palette Guide**:
| Content Type | Recommended Palettes |
|--------------|---------------------|
| Technical/Architecture | Blues, grays, blueprint tones |
| Educational/Friendly | Warm colors, earth tones |
| Corporate/Professional | Navy, gold, structured palettes |
| Creative/Artistic | Bold colors, unexpected combinations |
| Scientific/Medical | Clean whites, precise color coding |
**Note**: Full color specs in `references/styles/<style>.md`
### Typography Principles
| Element | Treatment |
|---------|-----------|
| Headlines | Bold, 2-3x body size, narrative style |
| Body Text | Regular weight, readable size |
| Captions | Smaller, lighter weight |
| Data Labels | Monospace for technical content |
| Emphasis | Use bold or color, not underlines |
### Font Recommendations
Fonts for AI-generated slides. Specify in prompts for consistent rendering.
**English Fonts**:
| Font | Style | Best For |
|------|-------|----------|
| Liter | Sans-serif, geometric | Modern, clean, technical |
| HedvigLettersSans | Sans-serif, distinctive | Brand-forward, creative |
| Oranienbaum | High-contrast serif | Elegant, classical |
| SortsMillGoudy | Classical serif | Traditional, readable |
| Coda | Round sans-serif | Friendly, approachable |
**Chinese Fonts**:
| Font | Style | Best For |
|------|-------|----------|
| MiSans | Modern sans-serif | Clean, versatile, screen-optimized |
| Noto Sans SC | Neutral sans-serif | Standard, multilingual |
| siyuanSongti | Refined Song typeface | Elegant, editorial |
| alimamashuheiti | Geometric sans-serif | Commercial, structured |
| LXGW Bright | Song-Kai hybrid | Warm, readable |
**Multilingual Pairing**:
| Use Case | English | Chinese |
|----------|---------|---------|
| Technical | Liter | MiSans |
| Editorial | Oranienbaum | siyuanSongti |
| Friendly | Coda | LXGW Bright |
| Corporate | HedvigLettersSans | alimamashuheiti |
### Consistency Requirements
| Element | Guideline |
|---------|-----------|
| Spacing | Consistent margins and padding throughout |
| Colors | Maximum 3-4 colors per slide, palette consistent across deck |
| Typography | Same font families and sizes for same content types |
| Visual Language | Repeat patterns, shapes, and treatments |
## Visual Elements Reference
Quick reference for visual treatments. Full specs in style definitions.
### Background Treatments
| Treatment | Description | Best For |
|-----------|-------------|----------|
| Solid color | Single background color | Clean, minimal |
| Split background | Two colors, diagonal or vertical | Contrast, sections |
| Gradient | Subtle vertical or diagonal fade | Modern, dynamic |
| Textured | Pattern or texture overlay | Character, style |
### Typography Treatments
| Treatment | Description | Best For |
|-----------|-------------|----------|
| Size contrast | 3-4x difference headline vs body | Impact, hierarchy |
| All-caps headers | Uppercase with letter spacing | Authority, structure |
| Monospace data | Fixed-width for numbers/code | Technical, precision |
| Hand-drawn | Organic, imperfect letterforms | Friendly, approachable |
### Geometric Accents
| Element | Description | Best For |
|---------|-------------|----------|
| Diagonal dividers | Angled section separators | Energy, movement |
| Corner brackets | L-shaped frames | Focus, framing |
| Circles/hexagons | Shape frames for images | Modern, tech |
| Underline accents | Thick lines under headers | Emphasis, hierarchy |
## File Management
### Output Directory
@@ -169,7 +434,10 @@ If `--outline-only`, stop here.
1. Read `references/base-prompt.md`
2. Combine with style instructions from outline
3. Add slide-specific content
4. Save to `prompts/` directory
4. If `Layout:` specified in outline, include layout guidance in prompt:
- Reference layout characteristics for image composition
- Example: `Layout: hub-spoke` → "Central concept in middle with related items radiating outward"
5. Save to `prompts/` directory
### Step 5: Generate Images
@@ -67,6 +67,7 @@ Sub-headline: [supporting tagline]
[Detailed visual description - specific elements, composition, mood]
// LAYOUT
Layout: [optional: layout name from gallery, e.g., title-hero]
[Composition, hierarchy, spatial arrangement]
```
@@ -93,6 +94,7 @@ Body:
[Detailed visual description]
// LAYOUT
Layout: [optional: layout name from gallery]
[Composition, hierarchy, spatial arrangement]
```
@@ -115,6 +117,7 @@ Body: [optional summary points or next steps]
[Visual that reinforces the core message]
// LAYOUT
Layout: [optional: layout name from gallery]
[Clean, impactful composition]
```
+169
View File
@@ -0,0 +1,169 @@
---
name: baoyu-url-to-markdown
description: Fetch any URL and convert to markdown using Chrome CDP. Supports two modes - auto-capture on page load, or wait for user signal (for pages requiring login). Use when user wants to save a webpage as markdown.
---
# URL to Markdown
Fetches any URL via Chrome CDP and converts HTML to clean markdown.
## Script Directory
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
**Agent Execution Instructions**:
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
3. Replace all `${SKILL_DIR}` in this document with the actual path
**Script Reference**:
| Script | Purpose |
|--------|---------|
| `scripts/main.ts` | CLI entry point for URL fetching |
## Features
- Chrome CDP for full JavaScript rendering
- Two capture modes: auto or wait-for-user
- Clean markdown output with metadata
- Handles login-required pages via wait mode
## Usage
```bash
# Auto mode (default) - capture when page loads
npx -y bun ${SKILL_DIR}/scripts/main.ts <url>
# Wait mode - wait for user signal before capture
npx -y bun ${SKILL_DIR}/scripts/main.ts <url> --wait
# Save to specific file
npx -y bun ${SKILL_DIR}/scripts/main.ts <url> -o output.md
```
## Options
| Option | Description |
|--------|-------------|
| `<url>` | URL to fetch |
| `-o <path>` | Output file path (default: auto-generated) |
| `--wait` | Wait for user signal before capturing |
| `--timeout <ms>` | Page load timeout (default: 30000) |
## Capture Modes
### Auto Mode (default)
Page loads → waits for network idle → captures immediately.
Best for:
- Public pages
- Static content
- No login required
### Wait Mode (`--wait`)
Page opens → user can interact (login, scroll, etc.) → user signals ready → captures.
Best for:
- Login-required pages
- Dynamic content needing interaction
- Pages with lazy loading
**Agent workflow for wait mode**:
1. Run script with `--wait` flag
2. Script outputs: `Page opened. Press Enter when ready to capture...`
3. Use `AskUserQuestion` to ask user if page is ready
4. When user confirms, send newline to stdin to trigger capture
## Output Format
```markdown
---
url: https://example.com/page
title: "Page Title"
description: "Meta description if available"
author: "Author if available"
published: "2024-01-01"
captured_at: "2024-01-15T10:30:00Z"
---
# Page Title
Converted markdown content...
```
## Mode Selection Guide
When user requests URL capture, help select appropriate mode:
**Suggest Auto Mode when**:
- URL is public (no login wall visible)
- Content appears static
- User doesn't mention login requirements
**Suggest Wait Mode when**:
- User mentions needing to log in
- Site known to require authentication
- User wants to scroll/interact before capture
- Content is behind paywall
**Ask user when unclear**:
```
The page may require login or interaction before capturing.
Which mode should I use?
1. Auto - Capture immediately when loaded
2. Wait - Wait for you to interact first
```
## Output Directory
Each capture creates a file organized by domain:
```
url-to-markdown/
└── <domain>/
└── <slug>.md
```
**Path Components**:
- `<domain>`: Site domain (e.g., `example.com`, `github.com`)
- `<slug>`: Generated from page title or URL path (kebab-case)
**Slug Generation**:
1. Extract from page title (preferred) or URL path
2. Convert to kebab-case, 2-6 words
3. Example: "Getting Started with React" → `getting-started-with-react`
**Conflict Resolution**:
If `url-to-markdown/<domain>/<slug>.md` already exists:
- Append timestamp: `<slug>-YYYYMMDD-HHMMSS.md`
- Example: `getting-started.md` exists → `getting-started-20260118-143052.md`
## Error Handling
| Error | Resolution |
|-------|------------|
| Chrome not found | Install Chrome or set `URL_CHROME_PATH` env |
| Page timeout | Increase `--timeout` value |
| Capture failed | Try wait mode for complex pages |
| Empty content | Page may need JS rendering time |
## Environment Variables
| Variable | Description |
|----------|-------------|
| `URL_CHROME_PATH` | Custom Chrome executable path |
| `URL_DATA_DIR` | Custom data directory |
| `URL_CHROME_PROFILE_DIR` | Custom Chrome profile directory |
## Extension Support
Custom configurations via EXTEND.md.
**Check paths** (priority order):
1. `.baoyu-skills/baoyu-url-to-markdown/EXTEND.md` (project)
2. `~/.baoyu-skills/baoyu-url-to-markdown/EXTEND.md` (user)
If found, load before workflow. Extension content overrides defaults.
+290
View File
@@ -0,0 +1,290 @@
import { spawn, type ChildProcess } from "node:child_process";
import fs from "node:fs";
import { mkdir } from "node:fs/promises";
import net from "node:net";
import process from "node:process";
import { resolveUrlToMarkdownChromeProfileDir } from "./paths.js";
import { CDP_CONNECT_TIMEOUT_MS, NETWORK_IDLE_TIMEOUT_MS } from "./constants.js";
type CdpSendOptions = { sessionId?: string; timeoutMs?: number };
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchWithTimeout(url: string, init: RequestInit & { timeoutMs?: number } = {}): Promise<Response> {
const { timeoutMs, ...rest } = init;
if (!timeoutMs || timeoutMs <= 0) return fetch(url, rest);
const ctl = new AbortController();
const t = setTimeout(() => ctl.abort(), timeoutMs);
try {
return await fetch(url, { ...rest, signal: ctl.signal });
} finally {
clearTimeout(t);
}
}
export class CdpConnection {
private ws: WebSocket;
private nextId = 0;
private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> | null }>();
private eventHandlers = new Map<string, Set<(params: unknown) => void>>();
private constructor(ws: WebSocket) {
this.ws = ws;
this.ws.addEventListener("message", (event) => {
try {
const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data as ArrayBuffer);
const msg = JSON.parse(data) as { id?: number; method?: string; params?: unknown; result?: unknown; error?: { message?: string } };
if (msg.id) {
const p = this.pending.get(msg.id);
if (p) {
this.pending.delete(msg.id);
if (p.timer) clearTimeout(p.timer);
if (msg.error?.message) p.reject(new Error(msg.error.message));
else p.resolve(msg.result);
}
} else if (msg.method) {
const handlers = this.eventHandlers.get(msg.method);
if (handlers) {
for (const h of handlers) h(msg.params);
}
}
} catch {}
});
this.ws.addEventListener("close", () => {
for (const [id, p] of this.pending.entries()) {
this.pending.delete(id);
if (p.timer) clearTimeout(p.timer);
p.reject(new Error("CDP connection closed."));
}
});
}
static async connect(url: string, timeoutMs: number): Promise<CdpConnection> {
const ws = new WebSocket(url);
await new Promise<void>((resolve, reject) => {
const t = setTimeout(() => reject(new Error("CDP connection timeout.")), timeoutMs);
ws.addEventListener("open", () => { clearTimeout(t); resolve(); });
ws.addEventListener("error", () => { clearTimeout(t); reject(new Error("CDP connection failed.")); });
});
return new CdpConnection(ws);
}
on(event: string, handler: (params: unknown) => void): void {
let handlers = this.eventHandlers.get(event);
if (!handlers) {
handlers = new Set();
this.eventHandlers.set(event, handlers);
}
handlers.add(handler);
}
off(event: string, handler: (params: unknown) => void): void {
this.eventHandlers.get(event)?.delete(handler);
}
async send<T = unknown>(method: string, params?: Record<string, unknown>, opts?: CdpSendOptions): Promise<T> {
const id = ++this.nextId;
const msg: Record<string, unknown> = { id, method };
if (params) msg.params = params;
if (opts?.sessionId) msg.sessionId = opts.sessionId;
const timeoutMs = opts?.timeoutMs ?? 15_000;
const out = await new Promise<unknown>((resolve, reject) => {
const t = timeoutMs > 0 ? setTimeout(() => { this.pending.delete(id); reject(new Error(`CDP timeout: ${method}`)); }, timeoutMs) : null;
this.pending.set(id, { resolve, reject, timer: t });
this.ws.send(JSON.stringify(msg));
});
return out as T;
}
close(): void {
try { this.ws.close(); } catch {}
}
}
export async function getFreePort(): Promise<number> {
return await new Promise((resolve, reject) => {
const srv = net.createServer();
srv.unref();
srv.on("error", reject);
srv.listen(0, "127.0.0.1", () => {
const addr = srv.address();
if (!addr || typeof addr === "string") {
srv.close(() => reject(new Error("Unable to allocate a free TCP port.")));
return;
}
const port = addr.port;
srv.close((err) => (err ? reject(err) : resolve(port)));
});
});
}
export function findChromeExecutable(): string | null {
const override = process.env.URL_CHROME_PATH?.trim();
if (override && fs.existsSync(override)) return override;
const candidates: string[] = [];
switch (process.platform) {
case "darwin":
candidates.push(
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
);
break;
case "win32":
candidates.push(
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
);
break;
default:
candidates.push(
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
"/usr/bin/microsoft-edge"
);
break;
}
for (const p of candidates) {
if (fs.existsSync(p)) return p;
}
return null;
}
export async function waitForChromeDebugPort(port: number, timeoutMs: number): Promise<string> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const res = await fetchWithTimeout(`http://127.0.0.1:${port}/json/version`, { timeoutMs: 5_000 });
if (!res.ok) throw new Error(`status=${res.status}`);
const j = (await res.json()) as { webSocketDebuggerUrl?: string };
if (j.webSocketDebuggerUrl) return j.webSocketDebuggerUrl;
} catch {}
await sleep(200);
}
throw new Error("Chrome debug port not ready");
}
export async function launchChrome(url: string, port: number, headless: boolean = false): Promise<ChildProcess> {
const chrome = findChromeExecutable();
if (!chrome) throw new Error("Chrome executable not found. Install Chrome or set URL_CHROME_PATH env.");
const profileDir = resolveUrlToMarkdownChromeProfileDir();
await mkdir(profileDir, { recursive: true });
const args = [
`--remote-debugging-port=${port}`,
`--user-data-dir=${profileDir}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-popup-blocking",
];
if (headless) args.push("--headless=new");
args.push(url);
return spawn(chrome, args, { stdio: "ignore" });
}
export async function waitForNetworkIdle(cdp: CdpConnection, sessionId: string, timeoutMs: number = NETWORK_IDLE_TIMEOUT_MS): Promise<void> {
return new Promise((resolve) => {
let timer: ReturnType<typeof setTimeout> | null = null;
let pending = 0;
const cleanup = () => {
if (timer) clearTimeout(timer);
cdp.off("Network.requestWillBeSent", onRequest);
cdp.off("Network.loadingFinished", onFinish);
cdp.off("Network.loadingFailed", onFinish);
};
const done = () => { cleanup(); resolve(); };
const resetTimer = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(done, timeoutMs);
};
const onRequest = () => { pending++; resetTimer(); };
const onFinish = () => { pending = Math.max(0, pending - 1); if (pending <= 2) resetTimer(); };
cdp.on("Network.requestWillBeSent", onRequest);
cdp.on("Network.loadingFinished", onFinish);
cdp.on("Network.loadingFailed", onFinish);
resetTimer();
});
}
export async function waitForPageLoad(cdp: CdpConnection, sessionId: string, timeoutMs: number = 30_000): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
cdp.off("Page.loadEventFired", handler);
resolve();
}, timeoutMs);
const handler = () => {
clearTimeout(timer);
cdp.off("Page.loadEventFired", handler);
resolve();
};
cdp.on("Page.loadEventFired", handler);
});
}
export async function createTargetAndAttach(cdp: CdpConnection, url: string): Promise<{ targetId: string; sessionId: string }> {
const { targetId } = await cdp.send<{ targetId: string }>("Target.createTarget", { url });
const { sessionId } = await cdp.send<{ sessionId: string }>("Target.attachToTarget", { targetId, flatten: true });
await cdp.send("Network.enable", {}, { sessionId });
await cdp.send("Page.enable", {}, { sessionId });
return { targetId, sessionId };
}
export async function navigateAndWait(cdp: CdpConnection, sessionId: string, url: string, timeoutMs: number): Promise<void> {
const loadPromise = new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("Page load timeout")), timeoutMs);
const handler = (params: unknown) => {
const p = params as { name?: string };
if (p.name === "load" || p.name === "DOMContentLoaded") {
clearTimeout(timer);
cdp.off("Page.lifecycleEvent", handler);
resolve();
}
};
cdp.on("Page.lifecycleEvent", handler);
});
await cdp.send("Page.navigate", { url }, { sessionId });
await loadPromise;
}
export async function evaluateScript<T>(cdp: CdpConnection, sessionId: string, expression: string, timeoutMs: number = 30_000): Promise<T> {
const result = await cdp.send<{ result: { value?: T; type?: string; description?: string } }>(
"Runtime.evaluate",
{ expression, returnByValue: true, awaitPromise: true },
{ sessionId, timeoutMs }
);
return result.result.value as T;
}
export async function autoScroll(cdp: CdpConnection, sessionId: string, steps: number = 8, waitMs: number = 600): Promise<void> {
let lastHeight = await evaluateScript<number>(cdp, sessionId, "document.body.scrollHeight");
for (let i = 0; i < steps; i++) {
await evaluateScript<void>(cdp, sessionId, "window.scrollTo(0, document.body.scrollHeight)");
await sleep(waitMs);
const newHeight = await evaluateScript<number>(cdp, sessionId, "document.body.scrollHeight");
if (newHeight === lastHeight) break;
lastHeight = newHeight;
}
await evaluateScript<void>(cdp, sessionId, "window.scrollTo(0, 0)");
}
export function killChrome(chrome: ChildProcess): void {
try { chrome.kill("SIGTERM"); } catch {}
setTimeout(() => {
if (!chrome.killed) {
try { chrome.kill("SIGKILL"); } catch {}
}
}, 2_000).unref?.();
}
@@ -0,0 +1,13 @@
import { resolveUrlToMarkdownChromeProfileDir } from "./paths.js";
export const DEFAULT_USER_AGENT =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
export const USER_DATA_DIR = resolveUrlToMarkdownChromeProfileDir();
export const DEFAULT_TIMEOUT_MS = 30_000;
export const CDP_CONNECT_TIMEOUT_MS = 15_000;
export const NETWORK_IDLE_TIMEOUT_MS = 1_500;
export const POST_LOAD_DELAY_MS = 800;
export const SCROLL_STEP_WAIT_MS = 600;
export const SCROLL_MAX_STEPS = 8;
@@ -0,0 +1,223 @@
export interface PageMetadata {
url: string;
title: string;
description?: string;
author?: string;
published?: string;
captured_at: string;
}
export interface ConversionResult {
metadata: PageMetadata;
markdown: string;
}
export const cleanupAndExtractScript = `
(function() {
const removeSelectors = [
'script', 'style', 'noscript', 'iframe', 'svg', 'canvas',
'header nav', 'footer', '.sidebar', '.nav', '.navigation',
'.advertisement', '.ad', '.ads', '.cookie-banner', '.popup',
'[role="banner"]', '[role="navigation"]', '[role="complementary"]'
];
for (const sel of removeSelectors) {
try {
document.querySelectorAll(sel).forEach(el => el.remove());
} catch {}
}
document.querySelectorAll('*').forEach(el => {
el.removeAttribute('style');
el.removeAttribute('onclick');
el.removeAttribute('onload');
el.removeAttribute('onerror');
});
const baseUrl = document.baseURI || location.href;
document.querySelectorAll('a[href]').forEach(a => {
try {
const href = a.getAttribute('href');
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {
a.setAttribute('href', new URL(href, baseUrl).href);
}
} catch {}
});
document.querySelectorAll('img[src]').forEach(img => {
try {
const src = img.getAttribute('src');
if (src) img.setAttribute('src', new URL(src, baseUrl).href);
} catch {}
});
const getMeta = (names) => {
for (const name of names) {
const el = document.querySelector(\`meta[name="\${name}"], meta[property="\${name}"]\`);
if (el) {
const content = el.getAttribute('content');
if (content) return content.trim();
}
}
return undefined;
};
const getTitle = () => {
const ogTitle = getMeta(['og:title', 'twitter:title']);
if (ogTitle) return ogTitle;
const h1 = document.querySelector('h1');
if (h1) return h1.textContent?.trim();
return document.title?.trim() || '';
};
const getPublished = () => {
const timeEl = document.querySelector('time[datetime]');
if (timeEl) return timeEl.getAttribute('datetime');
return getMeta(['article:published_time', 'datePublished', 'date']);
};
const main = document.querySelector('main, article, [role="main"], .main-content, .post-content, .article-content, .content');
const html = main ? main.innerHTML : document.body.innerHTML;
return {
title: getTitle(),
description: getMeta(['description', 'og:description', 'twitter:description']),
author: getMeta(['author', 'article:author', 'twitter:creator']),
published: getPublished(),
html: html
};
})()
`;
function decodeHtmlEntities(text: string): string {
return text
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x27;/g, "'")
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
.replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)));
}
function stripTags(html: string): string {
return html.replace(/<[^>]+>/g, '');
}
function normalizeWhitespace(text: string): string {
return text.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
}
export function htmlToMarkdown(html: string): string {
let md = html;
md = md.replace(/<br\s*\/?>/gi, '\n');
md = md.replace(/<hr\s*\/?>/gi, '\n\n---\n\n');
md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `\n\n# ${stripTags(c).trim()}\n\n`);
md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, (_, c) => `\n\n## ${stripTags(c).trim()}\n\n`);
md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, (_, c) => `\n\n### ${stripTags(c).trim()}\n\n`);
md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, (_, c) => `\n\n#### ${stripTags(c).trim()}\n\n`);
md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, (_, c) => `\n\n##### ${stripTags(c).trim()}\n\n`);
md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, (_, c) => `\n\n###### ${stripTags(c).trim()}\n\n`);
md = md.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, (_, c) => `**${stripTags(c).trim()}**`);
md = md.replace(/<b[^>]*>([\s\S]*?)<\/b>/gi, (_, c) => `**${stripTags(c).trim()}**`);
md = md.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi, (_, c) => `*${stripTags(c).trim()}*`);
md = md.replace(/<i[^>]*>([\s\S]*?)<\/i>/gi, (_, c) => `*${stripTags(c).trim()}*`);
md = md.replace(/<del[^>]*>([\s\S]*?)<\/del>/gi, (_, c) => `~~${stripTags(c).trim()}~~`);
md = md.replace(/<s[^>]*>([\s\S]*?)<\/s>/gi, (_, c) => `~~${stripTags(c).trim()}~~`);
md = md.replace(/<mark[^>]*>([\s\S]*?)<\/mark>/gi, (_, c) => `==${stripTags(c).trim()}==`);
md = md.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
const t = stripTags(text).trim();
if (!t || href.startsWith('javascript:')) return t;
return `[${t}](${href})`;
});
md = md.replace(/<img[^>]*src=["']([^"']+)["'][^>]*alt=["']([^"']*)["'][^>]*\/?>/gi, (_, src, alt) => `![${alt}](${src})`);
md = md.replace(/<img[^>]*alt=["']([^"']*)["'][^>]*src=["']([^"']+)["'][^>]*\/?>/gi, (_, alt, src) => `![${alt}](${src})`);
md = md.replace(/<img[^>]*src=["']([^"']+)["'][^>]*\/?>/gi, (_, src) => `![](${src})`);
md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, code) => `\n\n\`\`\`\n${decodeHtmlEntities(stripTags(code)).trim()}\n\`\`\`\n\n`);
md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => `\n\n\`\`\`\n${decodeHtmlEntities(stripTags(code)).trim()}\n\`\`\`\n\n`);
md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, code) => `\`${decodeHtmlEntities(stripTags(code)).trim()}\``);
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
const lines = stripTags(content).trim().split('\n');
return '\n\n' + lines.map(l => `> ${l.trim()}`).join('\n') + '\n\n';
});
md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
const lis = items.match(/<li[^>]*>([\s\S]*?)<\/li>/gi) || [];
const lines = lis.map(li => {
const content = li.replace(/<li[^>]*>([\s\S]*?)<\/li>/i, '$1');
return `- ${stripTags(content).trim()}`;
});
return '\n\n' + lines.join('\n') + '\n\n';
});
md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (_, items) => {
const lis = items.match(/<li[^>]*>([\s\S]*?)<\/li>/gi) || [];
const lines = lis.map((li, i) => {
const content = li.replace(/<li[^>]*>([\s\S]*?)<\/li>/i, '$1');
return `${i + 1}. ${stripTags(content).trim()}`;
});
return '\n\n' + lines.join('\n') + '\n\n';
});
md = md.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, table) => {
const rows: string[][] = [];
const trMatches = table.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || [];
for (const tr of trMatches) {
const cells: string[] = [];
const cellMatches = tr.match(/<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/gi) || [];
for (const cell of cellMatches) {
const content = cell.replace(/<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/i, '$1');
cells.push(stripTags(content).trim().replace(/\|/g, '\\|'));
}
if (cells.length > 0) rows.push(cells);
}
if (rows.length === 0) return '';
const colCount = Math.max(...rows.map(r => r.length));
const normalizedRows = rows.map(r => {
while (r.length < colCount) r.push('');
return r;
});
const header = `| ${normalizedRows[0].join(' | ')} |`;
const sep = `| ${normalizedRows[0].map(() => '---').join(' | ')} |`;
const body = normalizedRows.slice(1).map(r => `| ${r.join(' | ')} |`).join('\n');
return '\n\n' + header + '\n' + sep + (body ? '\n' + body : '') + '\n\n';
});
md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, c) => `\n\n${stripTags(c).trim()}\n\n`);
md = md.replace(/<div[^>]*>([\s\S]*?)<\/div>/gi, (_, c) => `\n${stripTags(c).trim()}\n`);
md = md.replace(/<span[^>]*>([\s\S]*?)<\/span>/gi, (_, c) => stripTags(c));
md = stripTags(md);
md = decodeHtmlEntities(md);
md = normalizeWhitespace(md);
return md;
}
export function formatMetadataYaml(meta: PageMetadata): string {
const lines = ['---'];
lines.push(`url: ${meta.url}`);
lines.push(`title: "${meta.title.replace(/"/g, '\\"')}"`);
if (meta.description) lines.push(`description: "${meta.description.replace(/"/g, '\\"')}"`);
if (meta.author) lines.push(`author: "${meta.author.replace(/"/g, '\\"')}"`);
if (meta.published) lines.push(`published: "${meta.published}"`);
lines.push(`captured_at: "${meta.captured_at}"`);
lines.push('---');
return lines.join('\n');
}
export function createMarkdownDocument(result: ConversionResult): string {
const yaml = formatMetadataYaml(result.metadata);
const titleRegex = new RegExp(`^#\\s+${result.metadata.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\n`, 'i');
const hasTitle = titleRegex.test(result.markdown);
const title = result.metadata.title && !hasTitle ? `\n\n# ${result.metadata.title}\n\n` : '\n\n';
return yaml + title + result.markdown;
}
@@ -0,0 +1,176 @@
import { createInterface } from "node:readline";
import { writeFile, mkdir, access } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { CdpConnection, getFreePort, launchChrome, waitForChromeDebugPort, waitForNetworkIdle, waitForPageLoad, autoScroll, evaluateScript, killChrome } from "./cdp.js";
import { cleanupAndExtractScript, htmlToMarkdown, createMarkdownDocument, type PageMetadata, type ConversionResult } from "./html-to-markdown.js";
import { resolveUrlToMarkdownDataDir } from "./paths.js";
import { DEFAULT_TIMEOUT_MS, CDP_CONNECT_TIMEOUT_MS, NETWORK_IDLE_TIMEOUT_MS, POST_LOAD_DELAY_MS, SCROLL_STEP_WAIT_MS, SCROLL_MAX_STEPS } from "./constants.js";
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
interface Args {
url: string;
output?: string;
wait: boolean;
timeout: number;
}
function parseArgs(argv: string[]): Args {
const args: Args = { url: "", wait: false, timeout: DEFAULT_TIMEOUT_MS };
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--wait" || arg === "-w") {
args.wait = true;
} else if (arg === "-o" || arg === "--output") {
args.output = argv[++i];
} else if (arg === "--timeout" || arg === "-t") {
args.timeout = parseInt(argv[++i], 10) || DEFAULT_TIMEOUT_MS;
} else if (!arg.startsWith("-") && !args.url) {
args.url = arg;
}
}
return args;
}
function generateSlug(title: string, url: string): string {
const text = title || new URL(url).pathname.replace(/\//g, "-");
return text
.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 50) || "page";
}
function formatTimestamp(): string {
const now = new Date();
const pad = (n: number) => n.toString().padStart(2, "0");
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
}
async function generateOutputPath(url: string, title: string): Promise<string> {
const domain = new URL(url).hostname.replace(/^www\./, "");
const slug = generateSlug(title, url);
const dataDir = resolveUrlToMarkdownDataDir();
const basePath = path.join(dataDir, domain, `${slug}.md`);
if (!(await fileExists(basePath))) {
return basePath;
}
const timestampSlug = `${slug}-${formatTimestamp()}`;
return path.join(dataDir, domain, `${timestampSlug}.md`);
}
async function waitForUserSignal(): Promise<void> {
console.log("Page opened. Press Enter when ready to capture...");
const rl = createInterface({ input: process.stdin, output: process.stdout });
await new Promise<void>((resolve) => {
rl.once("line", () => { rl.close(); resolve(); });
});
}
async function captureUrl(args: Args): Promise<ConversionResult> {
const port = await getFreePort();
const chrome = await launchChrome(args.url, port, false);
let cdp: CdpConnection | null = null;
try {
const wsUrl = await waitForChromeDebugPort(port, 30_000);
cdp = await CdpConnection.connect(wsUrl, CDP_CONNECT_TIMEOUT_MS);
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; type: string; url: string }> }>("Target.getTargets");
const pageTarget = targets.targetInfos.find(t => t.type === "page" && t.url.startsWith("http"));
if (!pageTarget) throw new Error("No page target found");
const { sessionId } = await cdp.send<{ sessionId: string }>("Target.attachToTarget", { targetId: pageTarget.targetId, flatten: true });
await cdp.send("Network.enable", {}, { sessionId });
await cdp.send("Page.enable", {}, { sessionId });
if (args.wait) {
await waitForUserSignal();
} else {
console.log("Waiting for page to load...");
await Promise.race([
waitForPageLoad(cdp, sessionId, 15_000),
sleep(8_000)
]);
await waitForNetworkIdle(cdp, sessionId, NETWORK_IDLE_TIMEOUT_MS);
await sleep(POST_LOAD_DELAY_MS);
console.log("Scrolling to trigger lazy load...");
await autoScroll(cdp, sessionId, SCROLL_MAX_STEPS, SCROLL_STEP_WAIT_MS);
await sleep(POST_LOAD_DELAY_MS);
}
console.log("Capturing page content...");
const extracted = await evaluateScript<{ title: string; description?: string; author?: string; published?: string; html: string }>(
cdp, sessionId, cleanupAndExtractScript, args.timeout
);
const metadata: PageMetadata = {
url: args.url,
title: extracted.title || "",
description: extracted.description,
author: extracted.author,
published: extracted.published,
captured_at: new Date().toISOString()
};
const markdown = htmlToMarkdown(extracted.html);
return { metadata, markdown };
} finally {
if (cdp) {
try { await cdp.send("Browser.close", {}, { timeoutMs: 5_000 }); } catch {}
cdp.close();
}
killChrome(chrome);
}
}
async function main(): Promise<void> {
const args = parseArgs(process.argv);
if (!args.url) {
console.error("Usage: bun main.ts <url> [-o output.md] [--wait] [--timeout ms]");
process.exit(1);
}
try {
new URL(args.url);
} catch {
console.error(`Invalid URL: ${args.url}`);
process.exit(1);
}
console.log(`Fetching: ${args.url}`);
console.log(`Mode: ${args.wait ? "wait" : "auto"}`);
const result = await captureUrl(args);
const outputPath = args.output || await generateOutputPath(args.url, result.metadata.title);
const outputDir = path.dirname(outputPath);
await mkdir(outputDir, { recursive: true });
const document = createMarkdownDocument(result);
await writeFile(outputPath, document, "utf-8");
console.log(`Saved: ${outputPath}`);
console.log(`Title: ${result.metadata.title || "(no title)"}`);
}
main().catch((err) => {
console.error("Error:", err instanceof Error ? err.message : String(err));
process.exit(1);
});
@@ -0,0 +1,29 @@
import os from "node:os";
import path from "node:path";
import process from "node:process";
const APP_DATA_DIR = "baoyu-skills";
const URL_TO_MARKDOWN_DATA_DIR = "url-to-markdown";
const PROFILE_DIR_NAME = "chrome-profile";
export function resolveUserDataRoot(): string {
if (process.platform === "win32") {
return process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
}
if (process.platform === "darwin") {
return path.join(os.homedir(), "Library", "Application Support");
}
return process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
}
export function resolveUrlToMarkdownDataDir(): string {
const override = process.env.URL_DATA_DIR?.trim();
if (override) return path.resolve(override);
return path.join(process.cwd(), URL_TO_MARKDOWN_DATA_DIR);
}
export function resolveUrlToMarkdownChromeProfileDir(): string {
const override = process.env.URL_CHROME_PROFILE_DIR?.trim();
if (override) return path.resolve(override);
return path.join(resolveUserDataRoot(), APP_DATA_DIR, URL_TO_MARKDOWN_DATA_DIR, PROFILE_DIR_NAME);
}
+217 -101
View File
@@ -1,6 +1,6 @@
---
name: baoyu-xhs-images
description: Xiaohongshu (Little Red Book) infographic series generator with multiple style options. Breaks down content into 1-10 cartoon-style infographics. Use when user asks to create "小红书图片", "XHS images", or "RedNote infographics".
description: Generates Xiaohongshu (Little Red Book) infographic series with 9 visual styles and 6 layouts. Breaks content into 1-10 cartoon-style images optimized for XHS engagement. Use when user mentions "小红书图片", "XHS images", "RedNote infographics", "小红书种草", or wants social media infographics for Chinese platforms.
---
# Xiaohongshu Infographic Series Generator
@@ -20,7 +20,7 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
/baoyu-xhs-images posts/ai-future/article.md --layout dense
# Combine style and layout
/baoyu-xhs-images posts/ai-future/article.md --style tech --layout list
/baoyu-xhs-images posts/ai-future/article.md --style notion --layout list
# Direct content input
/baoyu-xhs-images
@@ -42,7 +42,7 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
| Dimension | Controls | Options |
|-----------|----------|---------|
| **Style** | Visual aesthetics: colors, lines, decorations | cute, fresh, tech, warm, bold, minimal, retro, pop, notion |
| **Style** | Visual aesthetics: colors, lines, decorations | cute, fresh, warm, bold, minimal, retro, pop, notion, chalkboard |
| **Layout** | Information structure: density, arrangement | sparse, balanced, dense, list, comparison, flow |
Style × Layout can be freely combined. Example: `--style notion --layout dense` creates an intellectual-looking knowledge card with high information density.
@@ -53,15 +53,15 @@ Style × Layout can be freely combined. Example: `--style notion --layout dense`
|-------|-------------|
| `cute` (Default) | Sweet, adorable, girly - classic Xiaohongshu aesthetic |
| `fresh` | Clean, refreshing, natural |
| `tech` | Modern, smart, digital |
| `warm` | Cozy, friendly, approachable |
| `bold` | High impact, attention-grabbing |
| `minimal` | Ultra-clean, sophisticated |
| `retro` | Vintage, nostalgic, trendy |
| `pop` | Vibrant, energetic, eye-catching |
| `notion` | Minimalist hand-drawn line art, intellectual |
| `chalkboard` | Colorful chalk on black board, educational |
Detailed style definitions: `references/styles/<style>.md`
Detailed style definitions: `references/presets/<style>.md`
## Layout Gallery
@@ -74,7 +74,7 @@ Detailed style definitions: `references/styles/<style>.md`
| `comparison` | Side-by-side contrast layout |
| `flow` | Process and timeline layout (3-6 steps) |
Detailed layout definitions: `references/layouts/<layout>.md`
Detailed layout definitions: `references/elements/canvas.md`
## Auto Selection
@@ -82,13 +82,44 @@ Detailed layout definitions: `references/layouts/<layout>.md`
|-----------------|-------|--------|
| Beauty, fashion, cute, girl, pink | `cute` | sparse/balanced |
| Health, nature, clean, fresh, organic | `fresh` | balanced/flow |
| Tech, AI, code, digital, app, tool | `tech` | dense/list |
| Life, story, emotion, feeling, warm | `warm` | balanced |
| Warning, important, must, critical | `bold` | list/comparison |
| Professional, business, elegant, simple | `minimal` | sparse/balanced |
| Classic, vintage, old, traditional | `retro` | balanced |
| Fun, exciting, wow, amazing | `pop` | sparse/list |
| Knowledge, concept, productivity, SaaS | `notion` | dense/list |
| Education, tutorial, learning, teaching, classroom | `chalkboard` | balanced/dense |
## Outline Strategies
Three differentiated outline strategies for different content goals:
### Strategy A: Story-Driven (故事驱动型)
| Aspect | Description |
|--------|-------------|
| **Concept** | Personal experience as main thread, emotional resonance first |
| **Features** | Start from pain point, show before/after change, strong authenticity |
| **Best for** | Reviews, personal shares, transformation stories |
| **Structure** | Hook → Problem → Discovery → Experience → Conclusion |
### Strategy B: Information-Dense (信息密集型)
| Aspect | Description |
|--------|-------------|
| **Concept** | Value-first, efficient information delivery |
| **Features** | Clear structure, explicit points, professional credibility |
| **Best for** | Tutorials, comparisons, product reviews, checklists |
| **Structure** | Core conclusion → Info card → Pros/Cons → Recommendation |
### Strategy C: Visual-First (视觉优先型)
| Aspect | Description |
|--------|-------------|
| **Concept** | Visual impact as core, minimal text |
| **Features** | Large images, atmospheric, instant appeal |
| **Best for** | High-aesthetic products, lifestyle, mood-based content |
| **Structure** | Hero image → Detail shots → Lifestyle scene → CTA |
## File Structure
@@ -97,11 +128,11 @@ Each session creates an independent directory named by content slug:
```
xhs-images/{topic-slug}/
├── source-{slug}.{ext} # Source files (text, images, etc.)
├── analysis.md # Deep analysis results
├── outline-style-[slug].md # Variant A (e.g., outline-style-tech.md)
├── outline-style-[slug].md # Variant B (e.g., outline-style-notion.md)
├── outline-style-[slug].md # Variant C (e.g., outline-style-minimal.md)
├── outline.md # Final selected
├── analysis.md # Deep analysis + questions asked
├── outline-strategy-a.md # Strategy A: Story-driven
├── outline-strategy-b.md # Strategy B: Information-dense
├── outline-strategy-c.md # Strategy C: Visual-first
├── outline.md # Final selected/merged outline
├── prompts/
│ ├── 01-cover-[slug].md
│ ├── 02-content-[slug].md
@@ -127,6 +158,52 @@ Copy all sources with naming `source-{slug}.{ext}`:
## Workflow
### Progress Checklist
Copy and track progress:
```
XHS Infographic Progress:
- [ ] Step 0: Check preferences (EXTEND.md)
- [ ] Step 1: Analyze content → analysis.md
- [ ] Step 2: Confirmation 1 - Content understanding ⚠️ REQUIRED
- [ ] Step 3: Generate 3 outline + style variants
- [ ] Step 4: Confirmation 2 - Outline & style selection ⚠️ REQUIRED
- [ ] Step 5: Generate images (sequential)
- [ ] Step 6: Completion report
```
### Flow
```
Input → Analyze → [Confirm 1] → 3 Outlines → [Confirm 2: Outline + Style] → Generate → Complete
```
### Step 0: Check Preferences
**Check paths** (priority order):
1. `.baoyu-skills/baoyu-xhs-images/EXTEND.md` (project)
2. `~/.baoyu-skills/baoyu-xhs-images/EXTEND.md` (user)
**If preferences found**:
1. Parse YAML frontmatter
2. Display current preferences summary:
```
Loaded preferences from [path]:
- Watermark: [enabled/disabled] "[content]" at [position]
- Style: [name] - [description]
- Layout: [layout]
- Language: [lang]
```
3. Continue to Step 1
**If NO preferences found**:
1. Ask user with AskUserQuestion (see `references/config/first-time-setup.md`)
2. Create EXTEND.md with user choices
3. Continue to Step 1
Schema reference: `references/config/preferences-schema.md`
### Step 1: Analyze Content → `analysis.md`
Read source content, save it if needed, and perform deep analysis.
@@ -136,7 +213,7 @@ Read source content, save it if needed, and perform deep analysis.
- If user provides a file path: use as-is
- If user pastes content: save to `source.md` in target directory
2. Read source content
3. **Deep analysis** following `references/analysis-framework.md`:
3. **Deep analysis** following `references/workflows/analysis-framework.md`:
- Content type classification (种草/干货/测评/教程/避坑...)
- Hook analysis (爆款标题潜力)
- Target audience identification
@@ -145,70 +222,101 @@ Read source content, save it if needed, and perform deep analysis.
- Swipe flow design
4. Detect source language
5. Determine recommended image count (2-10)
6. Select 3 style+layout combinations
6. **Generate clarifying questions** (see Step 2)
7. **Save to `analysis.md`**
### Step 2: Generate 3 Outline Variants
### Step 2: Confirmation 1 - Content Understanding ⚠️
Based on analysis, create three distinct style variants.
**Purpose**: Validate understanding + collect missing info. **Do NOT skip.**
**For each variant**:
1. **Generate outline** (`outline-style-[slug].md`):
- YAML front matter with style, layout, image_count
- Cover design with hook
- Each image: layout, core message, text content, visual concept
- **Written in user's preferred language**
- Reference: `references/outline-template.md`
**Display summary**:
- Content type + topic identified
- Key points extracted
- Tone detected
- Source images count
| Variant | Selection Logic | Example Filename |
|---------|-----------------|------------------|
| A | Primary recommendation | `outline-style-tech.md` |
| B | Alternative style | `outline-style-notion.md` |
| C | Different audience/mood | `outline-style-minimal.md` |
**Use AskUserQuestion** for:
1. Core selling point (multiSelect: true)
2. Target audience
3. Style preference: Authentic sharing / Professional review / Aesthetic mood / Auto
4. Additional context (optional)
**All variants are preserved after selection for reference.**
**After response**: Update `analysis.md` → Step 3
### Step 3: User Confirms All Options
### Step 3: Generate 3 Outline + Style Variants
**IMPORTANT**: Present ALL options in a single confirmation step using AskUserQuestion. Do NOT interrupt workflow with multiple separate confirmations.
Based on analysis + user context, create three distinct strategy variants. Each variant includes both **outline structure** and **visual style recommendation**.
**Determine which questions to ask**:
**For each strategy**:
| Question | When to Ask |
|----------|-------------|
| Style variant | Always (required) |
| Default layout | Only if user might want to override |
| Language | Only if `source_language ≠ user_language` |
| Strategy | Filename | Outline | Recommended Style |
|----------|----------|---------|-------------------|
| A | `outline-strategy-a.md` | Story-driven: emotional, before/after | warm, cute, fresh |
| B | `outline-strategy-b.md` | Information-dense: structured, factual | notion, minimal, chalkboard |
| C | `outline-strategy-c.md` | Visual-first: atmospheric, minimal text | bold, pop, retro |
**Language handling**:
- If source language = user language: Just inform user (e.g., "Images will be in Chinese")
- If different: Ask which language to use
**Outline format** (YAML front matter + content):
```yaml
---
strategy: a # a, b, or c
name: Story-Driven
style: warm # recommended style for this strategy
style_reason: "Warm tones enhance emotional storytelling and personal connection"
layout: balanced # primary layout
image_count: 5
---
**AskUserQuestion format**:
## P1 Cover
**Type**: cover
**Hook**: "入冬后脸不干了🥹终于找到对的面霜"
**Visual**: Product hero shot with cozy winter atmosphere
**Layout**: sparse
```
Question 1 (Style): Which style variant?
- A: tech + dense (Recommended) - 专业科技感,适合干货
- B: notion + list - 清爽知识卡片
- C: minimal + balanced - 简约高端风格
- Custom: 自定义风格描述
## P2 Problem
**Type**: pain-point
**Message**: Previous struggles with dry skin
**Visual**: Before state, relatable scenario
**Layout**: balanced
Question 2 (Layout) - only if relevant:
- Keep variant default (Recommended)
- sparse / balanced / dense / list / comparison / flow
Question 3 (Language) - only if mismatch:
- 中文 (匹配原文)
- English (your preference)
...
```
**After confirmation**:
1. Copy selected `outline-style-[slug].md``outline.md`
2. Update YAML front matter with confirmed options
3. If custom style: regenerate outline with that style
4. User may edit `outline.md` directly for fine-tuning
**Differentiation requirements**:
- Each strategy MUST have different outline structure AND different recommended style
- Adapt page count: A typically 4-6, B typically 3-5, C typically 3-4
- Include `style_reason` explaining why this style fits the strategy
- Consider user's style preference from Step 2
### Step 4: Generate Images
Reference: `references/workflows/outline-template.md`
### Step 4: Confirmation 2 - Outline & Style Selection ⚠️
**Purpose**: User chooses outline strategy AND confirms visual style. **Do NOT skip.**
**Display each strategy**:
- Strategy name + page count + recommended style
- Page-by-page summary (P1 → P2 → P3...)
**Use AskUserQuestion** with two questions:
**Question 1: Outline Strategy**
- Strategy A (Recommended if "authentic sharing")
- Strategy B (Recommended if "professional review")
- Strategy C (Recommended if "aesthetic mood")
- Combine: specify pages from each
**Question 2: Visual Style**
- Use strategy's recommended style (show which style)
- Or select from: cute / fresh / warm / bold / minimal / retro / pop / notion / chalkboard
- Or type custom style description
**After response**:
- Single strategy → copy to `outline.md` with confirmed style
- Combination → merge specified pages with confirmed style
- Custom request → regenerate based on feedback
- Update `outline.md` frontmatter with final style choice
### Step 5: Generate Images
With confirmed outline + style + layout:
@@ -217,6 +325,15 @@ With confirmed outline + style + layout:
2. Generate image using confirmed style and layout
3. Report progress after each generation
**Watermark Application** (if enabled in preferences):
Add to each image generation prompt:
```
Include a subtle watermark "[content]" positioned at [position]
with approximately [opacity*100]% visibility. The watermark should
be legible but not distracting from the main content.
```
Reference: `references/config/watermark-guide.md`
**Image Generation Skill Selection**:
- Check available image generation skills
- If multiple skills available, ask user preference
@@ -227,22 +344,23 @@ If image generation skill supports `--sessionId`:
2. Use same session ID for all images
3. Ensures visual consistency across generated images
### Step 5: Completion Report
### Step 6: Completion Report
```
Xiaohongshu Infographic Series Complete!
Topic: [topic]
Strategy: [A/B/C/Combined]
Style: [style name]
Layout: [layout name or "varies"]
Location: [directory path]
Images: N total
✓ analysis.md
✓ outline-style-tech.md
✓ outline-style-notion.md
✓ outline-style-minimal.md
✓ outline.md (selected: tech + dense)
✓ outline-strategy-a.md
✓ outline-strategy-b.md
✓ outline-strategy-c.md
✓ outline.md (selected: [strategy])
Files:
- 01-cover-[slug].png ✓ Cover (sparse)
@@ -253,25 +371,11 @@ Files:
## Image Modification
### Edit Single Image
1. Identify image to edit (e.g., `03-content-chatgpt.png`)
2. Update prompt in `prompts/03-content-chatgpt.md` if needed
3. Regenerate image using same session ID
### Add New Image
1. Specify insertion position (e.g., after image 3)
2. Create new prompt with appropriate slug
3. Generate new image
4. **Renumber files**: All subsequent images increment NN by 1
5. Update `outline.md` with new image entry
### Delete Image
1. Remove image file and prompt file
2. **Renumber files**: All subsequent images decrement NN by 1
3. Update `outline.md` to remove image entry
| Action | Steps |
|--------|-------|
| **Edit** | Update prompt → Regenerate with same session ID |
| **Add** | Specify position → Create prompt → Generate → Renumber subsequent files (NN+1) → Update outline |
| **Delete** | Remove files → Renumber subsequent (NN-1) → Update outline |
## Content Breakdown Principles
@@ -285,37 +389,49 @@ Files:
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| cute | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ |
| fresh | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ |
| tech | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
| warm | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ |
| bold | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
| minimal | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ |
| retro | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ |
| pop | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
| notion | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
| chalkboard | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ |
## References
Detailed templates and guidelines in `references/` directory:
- `analysis-framework.md` - XHS-specific content analysis
- `outline-template.md` - Outline format and examples
- `styles/<style>.md` - Detailed style definitions
- `layouts/<layout>.md` - Detailed layout definitions
- `base-prompt.md` - Base prompt template
Detailed templates in `references/` directory:
**Elements** (Visual building blocks):
- `elements/canvas.md` - Aspect ratios, safe zones, grid layouts
- `elements/image-effects.md` - Cutout, stroke, filters
- `elements/typography.md` - Decorated text (花字), tags, text direction
- `elements/decorations.md` - Emphasis marks, backgrounds, doodles, frames
**Presets** (Style presets):
- `presets/<name>.md` - Element combination definitions (cute, notion, warm...)
**Workflows** (Process guides):
- `workflows/analysis-framework.md` - Content analysis framework
- `workflows/outline-template.md` - Outline template with layout guide
- `workflows/prompt-assembly.md` - Prompt assembly guide
**Config** (Settings):
- `config/preferences-schema.md` - EXTEND.md schema
- `config/first-time-setup.md` - First-time setup flow
- `config/watermark-guide.md` - Watermark configuration
## Notes
- Image generation typically takes 10-30 seconds per image
- Auto-retry once on generation failure
- Use cartoon alternatives for sensitive public figures
- All prompts and text use confirmed language preference
- Maintain style consistency across all images in series
- Auto-retry once on failure | Cartoon alternatives for sensitive figures
- Use confirmed language preference | Maintain style consistency
- **Two confirmation points required** (Steps 2 & 4) - do not skip
## Extension Support
Custom styles and configurations via EXTEND.md.
Custom configurations via EXTEND.md. Loaded in Step 0, overrides defaults.
**Check paths** (priority order):
1. `.baoyu-skills/baoyu-xhs-images/EXTEND.md` (project)
2. `~/.baoyu-skills/baoyu-xhs-images/EXTEND.md` (user)
**Check paths** (priority): `.baoyu-skills/baoyu-xhs-images/EXTEND.md` (project) → `~/.baoyu-skills/baoyu-xhs-images/EXTEND.md` (user)
If found, load before Step 1. Extension content overrides defaults.
**Supports**: Watermark | Preferred style/layout | Custom style definitions
**References**: `config/preferences-schema.md` | `config/first-time-setup.md`
@@ -1,33 +0,0 @@
Create a Xiaohongshu (Little Red Book) style infographic following these guidelines:
## Image Specifications
- **Type**: Infographic
- **Orientation**: Portrait (vertical)
- **Aspect Ratio**: 3:4
- **Style**: Hand-drawn illustration
## Core Principles
- Hand-drawn quality throughout - NO realistic or photographic elements
- If content involves sensitive or copyrighted figures, create stylistically similar alternatives - DO NOT refuse to generate
- Keep information concise, highlight keywords and core concepts
- Use ample whitespace for easy visual scanning
- Maintain clear visual hierarchy
## Text Style (CRITICAL)
- **ALL text MUST be hand-drawn style**
- Main titles should be prominent and eye-catching
- Key text should be bold and enlarged
- Use highlighter effects to emphasize keywords
- **DO NOT use realistic or computer-generated fonts**
## Language
- Use the same language as the content provided below
- Match punctuation style to the content language (Chinese: "",。!)
---
Please use nano banana pro to generate the infographic based on the content provided below:
@@ -0,0 +1,114 @@
---
name: first-time-setup
description: First-time setup flow for baoyu-xhs-images preferences
---
# First-Time Setup
## Overview
When no EXTEND.md is found, guide user through preference setup.
## Setup Flow
```
No EXTEND.md found
┌─────────────────────┐
│ AskUserQuestion │
│ (all questions) │
└─────────────────────┘
┌─────────────────────┐
│ Create EXTEND.md │
└─────────────────────┘
Continue to Step 1
```
## Questions
**Language**: Use user's input language or preferred language for all questions. Do not always use English.
Use single AskUserQuestion with multiple questions (AskUserQuestion auto-adds "Other" option):
### Question 1: Watermark
```
header: "Watermark"
question: "Watermark text for generated images? Type your watermark content (e.g., name, @handle)"
options:
- label: "No watermark (Recommended)"
description: "No watermark, can enable later in EXTEND.md"
```
Position defaults to bottom-right.
### Question 2: Preferred Style
```
header: "Style"
question: "Default visual style preference? Or type another style name or your custom style"
options:
- label: "None (Recommended)"
description: "Auto-select based on content analysis"
- label: "cute"
description: "Sweet, adorable - classic XHS aesthetic"
- label: "notion"
description: "Minimalist hand-drawn, intellectual"
```
### Question 3: Save Location
```
header: "Save"
question: "Where to save preferences?"
options:
- label: "Project"
description: ".baoyu-skills/ (this project only)"
- label: "User"
description: "~/.baoyu-skills/ (all projects)"
```
## Save Locations
| Choice | Path | Scope |
|--------|------|-------|
| Project | `.baoyu-skills/baoyu-xhs-images/EXTEND.md` | Current project |
| User | `~/.baoyu-skills/baoyu-xhs-images/EXTEND.md` | All projects |
## After Setup
1. Create directory if needed
2. Write EXTEND.md with frontmatter
3. Confirm: "Preferences saved to [path]"
4. Continue to Step 1
## EXTEND.md Template
```yaml
---
version: 1
watermark:
enabled: [true/false]
content: "[user input or empty]"
position: bottom-right
opacity: 0.7
preferred_style:
name: [selected style or null]
description: ""
preferred_layout: null
language: null
custom_styles: []
---
```
## Modifying Preferences Later
Users can edit EXTEND.md directly or run setup again:
- Delete EXTEND.md to trigger setup
- Edit YAML frontmatter for quick changes
- Full schema: `config/preferences-schema.md`
@@ -0,0 +1,121 @@
---
name: preferences-schema
description: EXTEND.md YAML schema for baoyu-xhs-images user preferences
---
# Preferences Schema
## Full Schema
```yaml
---
version: 1
watermark:
enabled: false
content: ""
position: bottom-right # bottom-right|bottom-left|bottom-center|top-right
opacity: 0.7 # 0.1-1.0
preferred_style:
name: null # Built-in or custom style name
description: "" # Override/notes
preferred_layout: null # sparse|balanced|dense|list|comparison|flow
language: null # zh|en|ja|ko|auto
custom_styles:
- name: my-style
description: "Style description"
color_palette:
primary: ["#FED7E2", "#FEEBC8"]
background: "#FFFAF0"
accents: ["#FF69B4", "#FF6B6B"]
visual_elements: "Hearts, stars, sparkles"
typography: "Rounded, bubbly hand lettering"
best_for: "Lifestyle, beauty"
---
```
## Field Reference
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `version` | int | 1 | Schema version |
| `watermark.enabled` | bool | false | Enable watermark |
| `watermark.content` | string | "" | Watermark text (@username or custom) |
| `watermark.position` | enum | bottom-right | Position on image |
| `watermark.opacity` | float | 0.7 | Transparency (0.1-1.0) |
| `preferred_style.name` | string | null | Style name or null |
| `preferred_style.description` | string | "" | Custom notes/override |
| `preferred_layout` | string | null | Layout preference or null |
| `language` | string | null | Output language (null = auto-detect) |
| `custom_styles` | array | [] | User-defined styles |
## Position Options
| Value | Description |
|-------|-------------|
| `bottom-right` | Lower right corner (default, most common) |
| `bottom-left` | Lower left corner |
| `bottom-center` | Bottom center |
| `top-right` | Upper right corner |
## Custom Style Fields
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Unique style identifier (kebab-case) |
| `description` | Yes | What the style conveys |
| `color_palette.primary` | No | Main colors (array) |
| `color_palette.background` | No | Background color |
| `color_palette.accents` | No | Accent colors (array) |
| `visual_elements` | No | Decorative elements |
| `typography` | No | Font/lettering style |
| `best_for` | No | Recommended content types |
## Example: Minimal Preferences
```yaml
---
version: 1
watermark:
enabled: true
content: "@myusername"
preferred_style:
name: notion
---
```
## Example: Full Preferences
```yaml
---
version: 1
watermark:
enabled: true
content: "@myxhsaccount"
position: bottom-right
opacity: 0.5
preferred_style:
name: notion
description: "Clean knowledge cards for tech content"
preferred_layout: dense
language: zh
custom_styles:
- name: corporate
description: "Professional B2B style"
color_palette:
primary: ["#1E3A5F", "#4A90D9"]
background: "#F5F7FA"
accents: ["#00B4D8", "#48CAE4"]
visual_elements: "Clean lines, subtle gradients, geometric shapes"
typography: "Modern sans-serif, professional"
best_for: "Business, SaaS, enterprise"
---
```
@@ -0,0 +1,73 @@
---
name: watermark-guide
description: Watermark configuration guide for baoyu-xhs-images
---
# Watermark Guide
## Position Diagram
```
┌─────────────────────────────┐
│ [top-right]│
│ │
│ │
│ IMAGE CONTENT │
│ │
│ │
│[bottom-left][bottom-center][bottom-right]│
└─────────────────────────────┘
```
## Position Recommendations
| Position | Best For | Avoid When |
|----------|----------|------------|
| `bottom-right` | Default choice, most common | Key info in bottom-right |
| `bottom-left` | Right-heavy layouts | Key info in bottom-left |
| `bottom-center` | Centered designs | Text-heavy bottom area |
| `top-right` | Bottom-heavy content | Title/header in top-right |
## 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
| Format | Example | Style |
|--------|---------|-------|
| Handle | `@username` | Most common for XHS |
| Text | `MyBrand` | Simple branding |
| Chinese | `小红书:用户名` | Platform specific |
| URL | `myblog.com` | Cross-platform |
## Best Practices
1. **Consistency**: Use same watermark across all images in series
2. **Legibility**: Ensure watermark readable on both light/dark areas
3. **Size**: Keep subtle - should not distract from content
4. **Contrast**: Opacity may need adjustment per image background
## Prompt Integration
When watermark is enabled, add to image generation prompt:
```
Include a subtle watermark "[content]" positioned at [position]
with approximately [opacity*100]% visibility. The watermark should
be legible but not distracting from the main content.
```
## Common Issues
| Issue | Solution |
|-------|----------|
| Watermark invisible | Increase opacity or adjust position |
| Watermark too prominent | Decrease opacity (0.3-0.5) |
| Watermark overlaps content | Change position |
| Inconsistent across images | Use session ID for consistency |
@@ -0,0 +1,108 @@
# Canvas & Layout
Core canvas specifications and layout grids for Xiaohongshu infographics.
## Aspect Ratios
| Name | Ratio | Pixels | Note |
|------|-------|--------|------|
| portrait-3-4 | 3:4 | 1242×1660 | Highest traffic on XHS (recommended) |
| square | 1:1 | 1242×1242 | Second recommended |
| portrait-2-3 | 2:3 | 1242×1863 | Taller format |
**Default**: portrait-3-4 for maximum engagement.
## Safe Zones
Avoid placing critical content in these areas:
| Zone | Position | Reason |
|------|----------|--------|
| bottom-overlay | Bottom 10% | Title bar overlay on mobile |
| top-right | Top-right corner | Like/share button overlay |
| bottom-right | Bottom-right corner | Watermark position |
```
┌─────────────────────────────┐
│ [like/share]│ ← top-right: avoid
│ │
│ │
│ ✓ SAFE CONTENT AREA │
│ │
│ │
│ [title bar overlay area] │ ← bottom 10%: avoid key info
└─────────────────────────────┘
```
## Grid Layouts
### Density-Based Layouts
| Layout | Info Density | Whitespace | Points/Image | Best For |
|--------|--------------|------------|--------------|----------|
| sparse | Low | 60-70% | 1-2 | Covers, quotes, impactful statements |
| balanced | Medium | 40-50% | 3-4 | Standard content, tutorials |
| dense | High | 20-30% | 5-8 | Knowledge cards, cheat sheets |
### Structure-Based Layouts
| Layout | Structure | Items | Best For |
|--------|-----------|-------|----------|
| list | Vertical enumeration | 4-7 | Rankings, checklists, step guides |
| comparison | Left vs Right | 2 sections | Before/after, pros/cons |
| flow | Connected nodes | 3-6 steps | Processes, timelines, workflows |
## Layout by Position
| Position | Recommended Layout | Why |
|----------|-------------------|-----|
| Cover | sparse | Maximum visual impact, clear title |
| Setup | balanced | Context without overwhelming |
| Core | balanced/dense/list | Based on content density |
| Payoff | balanced/list | Clear takeaways |
| Ending | sparse | Clean CTA, memorable close |
## Grid Cells
For multi-element compositions:
| Name | Cells | Use Case |
|------|-------|----------|
| single | 1 | Hero image, maximum impact |
| dual | 2 | Before/after, comparison |
| triptych | 3 | Steps, process flow |
| quad | 4 | Product showcase |
| six-grid | 6 | Checklist, collection |
| nine-grid | 9 | Multi-image gallery |
## Visual Balance
### Sparse Layout
- Single focal point centered
- Breathing room on all sides
- Symmetrical composition
### Balanced Layout
- Top-weighted title
- Evenly distributed content below
- Clear visual hierarchy
### Dense Layout
- Organized grid structure
- Clear section boundaries
- Compact but readable spacing
### List Layout
- Left-aligned items
- Clear number/bullet hierarchy
- Consistent item format
### Comparison Layout
- Symmetrical left/right
- Clear visual contrast
- Divider between sections
### Flow Layout
- Directional flow (top→bottom or left→right)
- Connected nodes with arrows
- Clear progression indicators
@@ -0,0 +1,145 @@
# Decorative Assets
Visual embellishments and decorative elements for Xiaohongshu infographics.
## Emphasis Marks (强调标记)
Elements to draw attention to specific content.
| Name | Description | Use Case |
|------|-------------|----------|
| red-arrow | Red arrow pointing to target | Product features, key points |
| circle-mark | Circle highlight annotation | Highlighting details |
| underline | Straight or wavy underline | Text emphasis |
| star-burst | Starburst explosion effect | Special offers, wow factor |
| checkmark | Checkmark/tick symbol | Completed items, pros |
| cross-mark | X mark symbol | Cons, things to avoid |
| exclamation | Exclamation point decoration | Important warnings |
| question | Question mark decoration | FAQ, curiosity |
| numbering | Circled numbers | Steps, rankings |
| bracket | Bracket highlighting | Grouping, emphasis |
## Backgrounds (背景)
Base layer treatments.
| Name | Description | Use Case |
|------|-------------|----------|
| solid-saturated | High-saturation solid color | Bold, energetic |
| solid-pastel | Soft pastel solid color | Cute, gentle |
| gradient-linear | Linear color gradient | Modern, dynamic |
| gradient-radial | Radial color gradient | Spotlight effect |
| frosted-glass | Frosted glass blur effect | Layered compositions |
| paper-texture | Paper or craft texture | Handmade aesthetic |
| fabric-texture | Fabric/cloth texture | Cozy, tactile |
| chalkboard | Blackboard texture | Educational content |
| grid | Subtle grid pattern | Structured, organized |
| dots | Polka dot pattern | Playful, retro |
## Doodles & Emoji (涂鸦)
Hand-drawn decorative elements.
| Name | Description | Use Case |
|------|-------------|----------|
| hand-drawn-lines | Sketchy hand-drawn lines | Connections, borders |
| stars-sparkles | Stars and sparkle effects | Magic, excellence |
| flowers | Floral decorations | Beauty, feminine |
| hearts | Heart symbols | Love, favorites |
| clouds | Cloud shapes | Dreamy, thoughts |
| arrows-curvy | Curved directional arrows | Flow, direction |
| squiggles | Wavy squiggle lines | Energy, movement |
| confetti | Scattered confetti | Celebration |
| leaves | Leaf decorations | Nature, fresh |
| bubbles | Circular bubble shapes | Playful, light |
## Emoji Integration
| Category | Examples | Use Case |
|----------|----------|----------|
| Reactions | 🥹 😍 🤯 | Emotional emphasis |
| Objects | ✨ 💡 🎯 | Visual markers |
| Actions | 👇 👆 ➡️ | Directional cues |
| Nature | 🌸 🌿 ☀️ | Thematic decoration |
## Frames (边框)
Container and border treatments.
| Name | Description | Use Case |
|------|-------------|----------|
| polaroid | Instant photo frame | Photo showcase |
| film-strip | Film negative border | Cinematic, retro |
| phone-screenshot | Mobile device mockup | App/screen content |
| torn-paper | Torn paper edge effect | Scrapbook aesthetic |
| rounded-rect | Rounded rectangle border | Clean containers |
| decorative | Ornate decorative border | Premium, elegant |
| tape-corners | Washi tape corners | Crafty, casual |
| stamp-border | Stamp perforated edge | Vintage, postal |
## Dividers (分隔线)
Section separators.
| Name | Description | Use Case |
|------|-------------|----------|
| line-simple | Simple horizontal line | Clean separation |
| line-dashed | Dashed line | Subtle division |
| line-wavy | Wavy line | Playful separation |
| dots-row | Row of dots | Decorative division |
| ornamental | Decorative flourish | Elegant separation |
## Stickers (贴纸)
Pre-composed decorative elements.
| Name | Description | Use Case |
|------|-------------|----------|
| badge-new | "NEW" badge | New products |
| badge-hot | "HOT" badge | Trending items |
| badge-sale | Sale/discount badge | Promotions |
| seal-quality | Quality seal | Recommendations |
| ribbon-award | Award ribbon | Best picks |
| tag-price | Price tag shape | Pricing info |
## Style-Specific Decorations
### Cute Style
- Hearts, stars, sparkles
- Ribbon decorations, sticker-style
- Cute character elements
### Notion Style
- Simple line doodles
- Geometric shapes, stick figures
- Maximum whitespace, minimal decoration
### Warm Style
- Sun rays, coffee cups, cozy items
- Warm lighting effects
- Friendly, inviting decorations
### Fresh Style
- Plant leaves, clouds, water drops
- Simple geometric shapes
- Open, breathing composition
### Bold Style
- Exclamation marks, arrows
- Warning icons, strong shapes
- High contrast elements
### Pop Style
- Bold shapes, speech bubbles
- Comic-style effects, starburst
- Dynamic, energetic decorations
### Retro Style
- Halftone dots, vintage badges
- Classic icons, tape effects
- Aged texture overlays
### Chalkboard Style
- Chalk dust effects
- Hand-drawn doodles
- Mathematical formulas, simple icons
@@ -0,0 +1,92 @@
# Image Processing Layer
Visual effects applied to image elements in Xiaohongshu infographics.
## AI Cutout (抠图)
Subject extraction styles for product/figure isolation.
| Name | Description | Use Case |
|------|-------------|----------|
| clean | Sharp edges, precise boundaries | Product photography, tech items |
| soft | Soft transition, feathered edges | Portrait cutout, organic subjects |
| stylized | Hand-drawn edge treatment | Artistic compositions |
## Stroke Effects (描边)
Border treatments for cutout elements.
| Name | Description | Use Case |
|------|-------------|----------|
| white-solid | White solid line border | Classic sticker feel, high contrast |
| colored-solid | Colored solid line border | Playful vibe, brand colors |
| dashed | Dashed/dotted border | Handmade aesthetic, casual |
| double | Double-layer stroke | Emphasis effect, premium feel |
| glow | Soft outer glow | Dreamy, soft aesthetic |
| shadow | Drop shadow effect | Depth, floating element |
**Stroke Width Guidelines**:
- Thin: 2-4px - Subtle, elegant
- Medium: 5-8px - Standard visibility
- Thick: 10-15px - Bold emphasis
## Filters (滤镜)
Color grading and mood presets popular on XHS.
| Name | Chinese | Description | Mood |
|------|---------|-------------|------|
| clear-glow | 清透感 | Transparent, radiant, luminous | Fresh, youthful |
| film-grain | 胶片感 | Vintage film aesthetic, grain texture | Nostalgic, artistic |
| cream-skin | 奶油肌 | Smooth, creamy complexion tones | Soft, flattering |
| japanese-magazine | 日杂感 | Lifestyle magazine aesthetic | Curated, aspirational |
| high-saturation | 高饱和 | Vibrant, punchy colors | Energetic, eye-catching |
| muted-tones | 莫兰迪 | Morandi-style desaturated palette | Sophisticated, calm |
| warm-tone | 暖色调 | Golden hour warmth | Cozy, inviting |
| cool-tone | 冷色调 | Blue-shifted coolness | Modern, clean |
## Texture Overlays
Additional texture effects.
| Name | Description | Use Case |
|------|-------------|----------|
| paper | Paper or fabric texture | Handmade feel |
| noise | Fine grain noise | Analog aesthetic |
| halftone | Dot pattern | Retro print style |
| scratch | Light scratch marks | Vintage wear |
## Blending Modes
For layered compositions.
| Mode | Effect | Use Case |
|------|--------|----------|
| multiply | Darken, merge | Shadow effects |
| screen | Lighten, glow | Light effects |
| overlay | Contrast boost | Vibrant compositions |
| soft-light | Subtle blending | Natural layering |
## Effect Combinations
Common effect stacks for different styles:
### Cute Style
- Filter: clear-glow or cream-skin
- Stroke: white-solid (medium)
- Texture: none
### Notion Style
- Filter: none or muted-tones
- Stroke: white-solid (thin) or none
- Texture: paper (subtle)
### Retro Style
- Filter: film-grain
- Stroke: double or dashed
- Texture: halftone, scratch
### Bold Style
- Filter: high-saturation
- Stroke: colored-solid (thick)
- Texture: none
@@ -0,0 +1,96 @@
# Typography System
Text styling elements for Xiaohongshu infographics.
## Decorated Text (花字)
Stylized text treatments for emphasis and visual appeal.
| Name | Description | Use Case |
|------|-------------|----------|
| gradient | Gradient color fill | Title emphasis, modern feel |
| stroke-text | Outlined text with stroke | Cover headlines, high visibility |
| shadow-3d | 3D shadow/extrusion effect | Key terms, depth |
| highlight | Highlighter marker effect | Critical information, key points |
| neon | Neon glow effect | Tech content, night aesthetic |
| handwritten | Authentic handwritten style | Personal touch, casual |
| bubble | Rounded, inflated letterforms | Cute, playful content |
| brush | Brush stroke texture | Artistic, dynamic |
## Tags & Labels (标签)
Structured text containers.
| Name | Description | Use Case |
|------|-------------|----------|
| black-white | Black background, white text | Brand names, prices, categories |
| white-black | White background, black text | Clean labels, minimal style |
| bubble | Speech bubble style | Dialogue, annotations, callouts |
| pointer | Arrow pointer with label | Product callouts, pointing to features |
| ribbon | Ribbon/banner shape | Special offers, highlights |
| stamp | Stamp/seal style | Authenticity, recommendations |
| pill | Rounded pill shape | Tags, categories, keywords |
## Text Hierarchy
Recommended text sizing for visual hierarchy.
| Level | Role | Relative Size | Style |
|-------|------|---------------|-------|
| H1 | Main title | 100% | Bold, decorated |
| H2 | Section header | 70-80% | Semi-bold |
| H3 | Subsection | 50-60% | Medium weight |
| Body | Content text | 40-50% | Regular |
| Caption | Small notes | 30-35% | Light |
## Text Direction
| Direction | Description | Use Case |
|-----------|-------------|----------|
| horizontal | Standard left-to-right | Default for most content |
| vertical | Top-to-bottom columns | Magazine style, traditional Chinese |
| curved | Text following a curve | Decorative, around shapes |
| diagonal | Angled text | Dynamic compositions |
## Text Effects
| Effect | Description | Use Case |
|--------|-------------|----------|
| shadow | Drop shadow behind text | Readability on busy backgrounds |
| outline | Outline around letterforms | High contrast visibility |
| glow | Soft glow around text | Dreamy, emphasis |
| underline-wavy | Wavy underline decoration | Playful emphasis |
| strikethrough | Crossed out text | Before/after, corrections |
## Language Considerations
### Chinese Text (中文)
- Punctuation: 「」()、。!?
- Spacing: No spaces between characters
- Line height: 1.5-1.8x for readability
### Mixed Text
- English in Chinese context: Maintain consistent baseline
- Numbers: Use consistent number style (lining vs old-style)
## Style-Specific Typography
### Cute Style
- Rounded, bubbly hand lettering
- Soft shadows, playful decorations
- Pink/pastel color accents
### Notion Style
- Clean hand-drawn lettering
- Simple sans-serif labels
- Minimal decoration
### Bold Style
- Impactful hand lettering with shadows
- High contrast colors
- Strong outlines
### Chalkboard Style
- Chalk texture on all text
- Visible imperfections
- Multi-color chalk variety
@@ -1,30 +0,0 @@
# balanced
Standard content layout
## Information Density
- 3-4 key points per image
- Whitespace: 40-50% of canvas
## Structure
- Title at top
- 3-4 bullet points or sections below
## Text Elements
- Title + 3-4 bullet points or key messages
## Visual Balance
- Top-weighted title
- Evenly distributed content below
## Best For
Regular content pages, tutorials, explanations
## Best Style Pairings
All styles work well
@@ -1,31 +0,0 @@
# comparison
Side-by-side contrast layout
## Information Density
- 2 main sections with 2-4 points each
- Whitespace: 30-40% of canvas
## Structure
- Left vs Right layout
- Before/After, Pros/Cons format
- Clear divider between sections
## Text Elements
- Title + left label + right label + mirrored points
## Visual Balance
- Symmetrical left/right
- Clear visual contrast
## Best For
Comparisons, transformations, decision helpers, 对比图
## Best Style Pairings
bold (dramatic contrast), tech (data comparison), warm (before/after stories)
@@ -1,31 +0,0 @@
# dense
High information density, knowledge card style
## Information Density
- 5-8 key points per image
- Whitespace: 20-30% of canvas
## Structure
- Multiple sections, structured grid
- More text, compact but organized
## Text Elements
- Title + multiple sections with headers + numerous points
## Visual Balance
- Organized chaos
- Clear section boundaries
- Compact spacing
## Best For
Summary cards, cheat sheets, comprehensive guides, 干货总结
## Best Style Pairings
tech, notion, minimal (clean styles prevent visual overload)
@@ -1,30 +0,0 @@
# flow
Process and timeline layout
## Information Density
- 3-6 steps/stages
- Whitespace: 30-40% of canvas
## Structure
- Connected nodes with arrows
- Sequential flow, directional
## Text Elements
- Title + step labels + optional descriptions per step
## Visual Balance
- Directional flow (top→bottom or left→right)
- Clear progression
## Best For
Processes, timelines, cause-effect chains, workflows
## Best Style Pairings
tech (process diagrams), notion (simple flows), fresh (organic flows)
@@ -1,31 +0,0 @@
# list
Enumeration and ranking format
## Information Density
- 4-7 items
- Whitespace: 30-40% of canvas
## Structure
- Numbered or bulleted vertical list
- Consistent item format
## Text Elements
- Title + numbered/bulleted items
- Consistent format per item
## Visual Balance
- Left-aligned list
- Clear number/bullet hierarchy
## Best For
Top N lists, checklists, step-by-step guides, rankings
## Best Style Pairings
All styles, especially cute (checklist), bold (rankings)
@@ -1,31 +0,0 @@
# sparse
Minimal information, maximum impact
## Information Density
- 1-2 key points per image
- Whitespace: 60-70% of canvas
## Structure
- Single focal point centered
- One core message
- Single centered focal point
## Text Elements
- Title only, or title + one subtitle/tagline
## Visual Balance
- Centered, symmetrical
- Breathing room on all sides
## Best For
Covers, quotes, impactful statements, emotional content
## Best Style Pairings
Any style, especially effective with bold, minimal, notion
@@ -0,0 +1,72 @@
---
name: bold
category: impact
---
# Bold Style
High impact, attention-grabbing aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: clean
stroke: colored-solid | double
filter: high-saturation
typography:
decorated: shadow-3d | stroke-text
tags: black-white | ribbon
direction: horizontal | diagonal
decorations:
emphasis: exclamation | star-burst | red-arrow
background: solid-saturated | gradient-linear
doodles: arrows-curvy | squiggles
frames: none
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Vibrant red, orange, yellow | #E53E3E, #DD6B20, #F6E05E |
| Background | Deep black, dark charcoal | #000000, #1A1A1A |
| Accents | White, neon yellow | #FFFFFF, #F7FF00 |
## Visual Elements
- Exclamation marks, arrows, warning icons
- Strong shapes, high contrast elements
- Dramatic compositions
- Bold geometric forms
## Typography
- Bold, impactful hand lettering with shadows
- High contrast text treatments
- Large, commanding headlines
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Impactful statements |
| balanced | ✓ | Warning content |
| dense | ✓ | Critical information cards |
| list | ✓✓ | Must-know lists, rankings |
| comparison | ✓✓ | Dramatic contrasts |
| flow | ✓ | Critical process steps |
## Best For
- Important tips and warnings
- Must-know content
- Critical announcements
- Rankings and comparisons
- Attention-grabbing hooks
@@ -0,0 +1,97 @@
---
name: chalkboard
category: educational
---
# Chalkboard Style
Black chalkboard background with colorful chalk drawing aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual | triptych
image_effects:
cutout: stylized
stroke: none
filter: none
typography:
decorated: handwritten
tags: none
direction: horizontal | vertical
decorations:
emphasis: underline | circle-mark | arrows-curvy
background: chalkboard
doodles: hand-drawn-lines | stars-sparkles
frames: none
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Background | Chalkboard black, green-black | #1A1A1A, #1C2B1C |
| Primary Text | Chalk white | #F5F5F5 |
| Accent 1 | Chalk yellow | #FFE566 |
| Accent 2 | Chalk pink | #FF9999 |
| Accent 3 | Chalk blue | #66B3FF |
| Accent 4 | Chalk green | #90EE90 |
| Accent 5 | Chalk orange | #FFB366 |
## Visual Elements
- Hand-drawn chalk illustrations with sketchy, imperfect lines
- Chalk dust effects around text and key elements
- Doodles: stars, arrows, underlines, circles, checkmarks
- Mathematical formulas and simple diagrams
- Eraser smudges and chalk residue textures
- Stick figures and simple icons
- Connection lines with hand-drawn feel
## Typography
- Hand-drawn chalk lettering style
- Visible chalk texture on all text
- Imperfect baseline adds authenticity
- White or bright colored chalk for emphasis
## Style Rules
### Do
- Maintain authentic chalk texture on all elements
- Use imperfect, hand-drawn quality throughout
- Add subtle chalk dust and smudge effects
- Create visual hierarchy with color variety
- Include playful doodles and annotations
### Don't
- Use perfect geometric shapes
- Create clean digital-looking lines
- Add photorealistic elements
- Use gradients or glossy effects
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Educational covers |
| balanced | ✓✓ | Standard lessons |
| dense | ✓✓ | Detailed tutorials |
| list | ✓✓ | Learning checklists |
| comparison | ✓ | Concept comparisons |
| flow | ✓✓ | Process explanations |
## Best For
- Educational content
- Tutorials and how-to's
- Classroom themes
- Teaching materials
- Workshops
- Informal learning sessions
- Knowledge sharing
@@ -0,0 +1,72 @@
---
name: cute
category: sweet
---
# Cute Style
Sweet, adorable, girly - classic Xiaohongshu aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual | quad
image_effects:
cutout: soft
stroke: white-solid | colored-solid
filter: clear-glow | cream-skin
typography:
decorated: bubble | highlight
tags: pill | bubble
direction: horizontal
decorations:
emphasis: star-burst | hearts
background: solid-pastel | gradient-linear
doodles: hearts | stars-sparkles | flowers
frames: polaroid | tape-corners
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Pink, peach, mint, lavender | #FED7E2, #FEEBC8, #C6F6D5, #E9D8FD |
| Background | Cream, soft pink | #FFFAF0, #FFF5F7 |
| Accents | Hot pink, coral | #FF69B4, #FF6B6B |
## Visual Elements
- Hearts, stars, sparkles, cute faces
- Ribbon decorations, sticker-style
- Cute stickers, emoji icons
- Soft, rounded shapes
## Typography
- Rounded, bubbly hand lettering
- Soft shadows, playful decorations
- Pink/pastel color accents on text
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Covers, emotional impact |
| balanced | ✓✓ | Standard cute content |
| dense | ✓ | Cute knowledge cards |
| list | ✓✓ | Checklists, cute rankings |
| comparison | ✓ | Before/after transformations |
| flow | ✓ | Cute step guides |
## Best For
- Lifestyle content
- Beauty and skincare
- Fashion and style
- Daily tips and hacks
- Personal shares
@@ -0,0 +1,72 @@
---
name: fresh
category: natural
---
# Fresh Style
Clean, refreshing, natural aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | triptych
image_effects:
cutout: soft
stroke: white-solid | none
filter: clear-glow | cool-tone
typography:
decorated: none | highlight
tags: pill | white-black
direction: horizontal
decorations:
emphasis: checkmark | circle-mark
background: solid-white | solid-pastel
doodles: leaves | clouds | bubbles
frames: rounded-rect | none
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Mint green, sky blue, light yellow | #9AE6B4, #90CDF4, #FAF089 |
| Background | Pure white, soft mint | #FFFFFF, #F0FFF4 |
| Accents | Leaf green, water blue | #48BB78, #4299E1 |
## Visual Elements
- Plant leaves, clouds, water drops
- Simple geometric shapes
- Breathing room, open composition
- Natural, organic elements
## Typography
- Clean, light hand lettering with breathing room
- Airy spacing
- Fresh color accents
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Clean covers |
| balanced | ✓✓ | Standard fresh content |
| dense | ✓ | Organized information |
| list | ✓ | Wellness tips |
| comparison | ✓ | Before/after health |
| flow | ✓✓ | Organic processes |
## Best For
- Health and wellness
- Minimalist lifestyle
- Self-care content
- Nature-related topics
- Clean living tips
@@ -0,0 +1,72 @@
---
name: minimal
category: elegant
---
# Minimal Style
Ultra-clean, sophisticated aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single
image_effects:
cutout: clean
stroke: none | white-solid
filter: none | muted-tones
typography:
decorated: none
tags: white-black | pill
direction: horizontal
decorations:
emphasis: underline | circle-mark
background: solid-white | solid-pastel
doodles: hand-drawn-lines
frames: none | rounded-rect
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Black, white | #000000, #FFFFFF |
| Background | Off-white, pure white | #FAFAFA, #FFFFFF |
| Accents | Single color (content-derived) | Blue, green, or coral |
## Visual Elements
- Single focal point, thin lines
- Maximum whitespace
- Simple, clean decorations
- Restrained visual elements
## Typography
- Clean, simple hand lettering
- Minimal weight variations
- Elegant spacing
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Elegant statements |
| balanced | ✓✓ | Professional content |
| dense | ✓✓ | Clean knowledge cards |
| list | ✓ | Simple lists |
| comparison | ✓ | Clean comparisons |
| flow | ✓ | Elegant processes |
## Best For
- Professional content
- Serious topics
- Elegant presentations
- High-end products
- Business content
@@ -0,0 +1,73 @@
---
name: notion
category: minimal
---
# Notion Style
Minimalist hand-drawn line art, intellectual aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: clean
stroke: none | white-solid
filter: none | muted-tones
typography:
decorated: none | handwritten
tags: black-white | pill
direction: horizontal
decorations:
emphasis: circle-mark | underline
background: solid-white | paper-texture
doodles: hand-drawn-lines | arrows-curvy
frames: none | rounded-rect
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Black, dark gray | #1A1A1A, #4A4A4A |
| Background | Pure white, off-white | #FFFFFF, #FAFAFA |
| Accents | Pastel blue, pastel yellow, pastel pink | #A8D4F0, #F9E79F, #FADBD8 |
## Visual Elements
- Simple line doodles, hand-drawn wobble effect
- Geometric shapes, stick figures
- Maximum whitespace, single-weight ink lines
- Clean, uncluttered compositions
## Typography
- Clean hand-drawn lettering
- Simple sans-serif labels
- Minimal decoration on text
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Concept covers |
| balanced | ✓✓ | Standard explanations |
| dense | ✓✓ | Knowledge cards, cheat sheets |
| list | ✓✓ | Productivity tips, tool lists |
| comparison | ✓✓ | Data comparisons |
| flow | ✓✓ | Process diagrams |
## Best For
- Knowledge sharing
- Concept explanations
- SaaS content
- Productivity tips
- Tech tutorials
- Professional content
@@ -0,0 +1,72 @@
---
name: pop
category: energetic
---
# Pop Style
Vibrant, energetic, eye-catching aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | quad
image_effects:
cutout: stylized
stroke: colored-solid | double
filter: high-saturation
typography:
decorated: stroke-text | shadow-3d
tags: bubble | ribbon
direction: horizontal | curved
decorations:
emphasis: star-burst | exclamation
background: solid-saturated | dots
doodles: stars-sparkles | confetti | squiggles
frames: none
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Bright red, yellow, blue, green | #F56565, #ECC94B, #4299E1, #48BB78 |
| Background | White, light gray | #FFFFFF, #F7FAFC |
| Accents | Neon pink, electric purple | #FF69B4, #9F7AEA |
## Visual Elements
- Bold shapes, speech bubbles
- Comic-style effects, starburst
- Dynamic, energetic compositions
- High-energy decorations
## Typography
- Dynamic, energetic hand lettering with outlines
- Bold color combinations
- Playful, expressive forms
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Exciting announcements |
| balanced | ✓✓ | Fun tutorials |
| dense | ✓ | Packed information |
| list | ✓✓ | Fun facts lists |
| comparison | ✓✓ | Dynamic comparisons |
| flow | ✓ | Energetic processes |
## Best For
- Exciting announcements
- Fun facts
- Engaging tutorials
- Entertainment content
- Youth-oriented content
@@ -0,0 +1,72 @@
---
name: retro
category: vintage
---
# Retro Style
Vintage, nostalgic, trendy aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: stylized
stroke: dashed | double
filter: film-grain | muted-tones
typography:
decorated: brush | handwritten
tags: stamp | ribbon
direction: horizontal
decorations:
emphasis: star-burst | numbering
background: paper-texture | dots
doodles: stars-sparkles | squiggles
frames: polaroid | film-strip | stamp-border
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Muted orange, dusty pink, faded teal | #E07A4D, #D4A5A5, #6B9999 |
| Background | Aged paper, sepia tones | #F5E6D3, #E8DCC8 |
| Accents | Faded red, vintage gold | #C55A5A, #B8860B |
## Visual Elements
- Halftone dots, vintage badges
- Classic icons, tape effects
- Aged texture overlays
- Nostalgic decorative elements
## Typography
- Vintage-style hand lettering
- Classic feel with imperfections
- Aged texture on text
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Vintage covers |
| balanced | ✓✓ | Classic content |
| dense | ✓ | Vintage knowledge cards |
| list | ✓✓ | Classic rankings |
| comparison | ✓ | Then vs now |
| flow | ✓ | Historical timelines |
## Best For
- Throwback content
- Classic tips
- Timeless advice
- Vintage aesthetics
- Nostalgic shares
@@ -0,0 +1,72 @@
---
name: warm
category: cozy
---
# Warm Style
Cozy, friendly, approachable aesthetic.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single | dual
image_effects:
cutout: soft
stroke: white-solid | glow
filter: warm-tone | cream-skin
typography:
decorated: highlight | handwritten
tags: ribbon | bubble
direction: horizontal
decorations:
emphasis: star-burst | hearts
background: solid-pastel | gradient-radial
doodles: clouds | stars-sparkles
frames: polaroid | tape-corners
```
## Color Palette
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Warm orange, golden yellow, terracotta | #ED8936, #F6AD55, #C05621 |
| Background | Cream, soft peach | #FFFAF0, #FED7AA |
| Accents | Deep brown, soft red | #744210, #E57373 |
## Visual Elements
- Sun rays, coffee cups, cozy items
- Warm lighting effects
- Friendly, inviting decorations
- Soft, comfortable shapes
## Typography
- Friendly, rounded hand lettering
- Warm color accents
- Comfortable, approachable feel
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✓✓ | Emotional covers |
| balanced | ✓✓ | Personal stories |
| dense | ✓ | Detailed experiences |
| list | ✓ | Life lessons |
| comparison | ✓✓ | Before/after stories |
| flow | ✓ | Journey narratives |
## Best For
- Personal stories
- Life lessons
- Emotional content
- Comfort and lifestyle
- Heartfelt shares
@@ -1,23 +0,0 @@
# bold
High impact, attention-grabbing
## Color Palette
- Primary: Vibrant red (#E53E3E), orange (#DD6B20), yellow (#F6E05E)
- Background: Deep black (#000000), dark charcoal
- Accents: White, neon yellow
## Visual Elements
- Exclamation marks, arrows, warning icons
- Strong shapes, high contrast elements
- Dramatic compositions
## Typography
- Bold, impactful hand lettering with shadows
## Best For
Important tips, warnings, must-know content
@@ -1,23 +0,0 @@
# cute
Sweet, adorable, girly - classic Xiaohongshu aesthetic
## Color Palette
- Primary: Pink (#FED7E2), peach (#FEEBC8), mint (#C6F6D5), lavender (#E9D8FD)
- Background: Cream (#FFFAF0), soft pink (#FFF5F7)
- Accents: Hot pink, coral
## Visual Elements
- Hearts, stars, sparkles, cute faces
- Ribbon decorations, sticker-style
- Cute stickers, emoji icons
## Typography
- Rounded, bubbly hand lettering
## Best For
Lifestyle, beauty, fashion, daily tips
@@ -1,23 +0,0 @@
# fresh
Clean, refreshing, natural
## Color Palette
- Primary: Mint green (#9AE6B4), sky blue (#90CDF4), light yellow (#FAF089)
- Background: Pure white (#FFFFFF), soft mint (#F0FFF4)
- Accents: Leaf green, water blue
## Visual Elements
- Plant leaves, clouds, water drops
- Simple geometric shapes
- Breathing room, open composition
## Typography
- Clean, light hand lettering with breathing room
## Best For
Health, wellness, minimalist lifestyle, self-care
@@ -1,23 +0,0 @@
# minimal
Ultra-clean, sophisticated
## Color Palette
- Primary: Black (#000000), white (#FFFFFF)
- Background: Off-white (#FAFAFA), pure white
- Accents: Single color (content-derived - blue, green, or coral)
## Visual Elements
- Single focal point, thin lines
- Maximum whitespace
- Simple, clean decorations
## Typography
- Clean, simple hand lettering
## Best For
Professional content, serious topics, elegant presentations
@@ -1,23 +0,0 @@
# notion
Minimalist hand-drawn line art, intellectual
## Color Palette
- Primary: Black (#1A1A1A), dark gray (#4A4A4A)
- Background: Pure white (#FFFFFF), off-white (#FAFAFA)
- Accents: Pastel blue (#A8D4F0), pastel yellow (#F9E79F), pastel pink (#FADBD8)
## Visual Elements
- Simple line doodles, hand-drawn wobble effect
- Geometric shapes, stick figures
- Maximum whitespace, single-weight ink lines
## Typography
- Clean hand-drawn lettering, simple sans-serif labels
## Best For
Knowledge sharing, concept explanations, SaaS content, productivity tips
@@ -1,23 +0,0 @@
# pop
Vibrant, energetic, eye-catching
## Color Palette
- Primary: Bright red (#F56565), yellow (#ECC94B), blue (#4299E1), green (#48BB78)
- Background: White (#FFFFFF), light gray
- Accents: Neon pink, electric purple
## Visual Elements
- Bold shapes, speech bubbles
- Comic-style effects, starburst
- Dynamic, energetic compositions
## Typography
- Dynamic, energetic hand lettering with outlines
## Best For
Exciting announcements, fun facts, engaging tutorials
@@ -1,23 +0,0 @@
# retro
Vintage, nostalgic, trendy
## Color Palette
- Primary: Muted orange, dusty pink (#FED7E2 at 70%), faded teal
- Background: Aged paper (#F5E6D3), sepia tones
- Accents: Faded red, vintage gold
## Visual Elements
- Halftone dots, vintage badges
- Classic icons, tape effects
- Aged texture overlays
## Typography
- Vintage-style hand lettering, classic feel
## Best For
Throwback content, classic tips, timeless advice
@@ -1,23 +0,0 @@
# tech
Modern, smart, digital
## Color Palette
- Primary: Deep blue (#1A365D), purple (#6B46C1), cyan (#00D4FF)
- Background: Dark gray (#1A202C), near-black (#0D1117)
- Accents: Neon green (#00FF88), electric blue
## Visual Elements
- Circuit patterns, data icons
- Geometric grids, glowing effects
- Tech-inspired decorations
## Typography
- Monospace-style hand lettering, subtle glow
## Best For
Tech tutorials, AI content, digital tools, productivity
@@ -1,23 +0,0 @@
# warm
Cozy, friendly, approachable
## Color Palette
- Primary: Warm orange (#ED8936), golden yellow (#F6AD55), terracotta (#C05621)
- Background: Cream (#FFFAF0), soft peach (#FED7AA)
- Accents: Deep brown (#744210), soft red
## Visual Elements
- Sun rays, coffee cups, cozy items
- Warm lighting effects
- Friendly, inviting decorations
## Typography
- Friendly, rounded hand lettering
## Best For
Personal stories, life lessons, emotional content
@@ -25,9 +25,9 @@ Unlike other platforms, Xiaohongshu content must prioritize:
| Type | Characteristics | Best Style | Best Layout |
|------|----------------|------------|-------------|
| 种草/安利 | Product recommendation, benefits focus | cute/fresh | balanced/list |
| 干货分享 | Knowledge, tips, how-to | notion/tech | dense/list |
| 干货分享 | Knowledge, tips, how-to | notion | dense/list |
| 个人故事 | Personal experience, emotional | warm | balanced |
| 测评对比 | Review, comparison, pros/cons | tech/bold | comparison |
| 测评对比 | Review, comparison, pros/cons | bold/notion | comparison |
| 教程步骤 | Step-by-step guide | fresh/notion | flow/list |
| 避坑指南 | Warnings, mistakes to avoid | bold | list/comparison |
| 清单合集 | Collections, recommendations | cute/minimal | list/dense |
@@ -55,10 +55,10 @@ Evaluate title/hook potential using these patterns:
| Audience | Interests | Preferred Style | Content Focus |
|----------|-----------|-----------------|---------------|
| 学生党 | 省钱、学习、校园 | cute/fresh | 平价、教程、学习方法 |
| 打工人 | 效率、职场、减压 | minimal/tech | 工具、技巧、摸鱼 |
| 打工人 | 效率、职场、减压 | minimal/notion | 工具、技巧、摸鱼 |
| 宝妈 | 育儿、家居、省心 | warm/fresh | 实用、安全、经验 |
| 精致女孩 | 美妆、穿搭、仪式感 | cute/retro | 好看、氛围、品质 |
| 技术宅 | 工具、效率、极客 | tech/notion | 深度、专业、新奇 |
| 技术宅 | 工具、效率、极客 | notion/chalkboard | 深度、专业、新奇 |
| 美食爱好者 | 探店、食谱、测评 | warm/pop | 好吃、简单、颜值 |
| 旅行达人 | 攻略、打卡、小众 | fresh/retro | 省钱、避坑、拍照 |
@@ -165,7 +165,7 @@ recommended_image_count: 6
## Content Signals
- "AI工具" → tech + dense
- "AI工具" → notion + dense
- "效率" → notion + list
- "干货" → minimal + dense
@@ -180,7 +180,7 @@ recommended_image_count: 6
## Recommended Approaches
1. **Tech + Dense** - 专业科技感,适合干货分享 (recommended)
1. **Notion + Dense** - 知识卡片风格,适合干货分享 (recommended)
2. **Notion + List** - 清爽知识卡片风格
3. **Minimal + Balanced** - 简约高端,适合职场人群
```
@@ -1,13 +1,13 @@
# Xiaohongshu Outline Template
Template for generating infographic series outlines.
Template for generating infographic series outlines with layout specifications.
## File Naming
Outline files use style slug in the name:
- `outline-style-tech.md` - Tech style variant
- `outline-style-notion.md` - Notion style variant
- `outline-style-minimal.md` - Minimal style variant
Outline files use strategy identifier in the name:
- `outline-strategy-a.md` - Story-driven variant
- `outline-strategy-b.md` - Information-dense variant
- `outline-strategy-c.md` - Visual-first variant
- `outline.md` - Final selected (copied from chosen variant)
## Image File Naming
@@ -37,13 +37,43 @@ NN-{type}-[slug].md (in prompts/)
- Must be unique within the series
- Keep short but descriptive (2-4 words)
## Layout Selection Guide
### Density-Based Layouts
| Layout | When to Use | Info Points | Whitespace |
|--------|-------------|-------------|------------|
| sparse | Covers, quotes, impact statements | 1-2 | 60-70% |
| balanced | Standard content, tutorials | 3-4 | 40-50% |
| dense | Knowledge cards, cheat sheets | 5-8 | 20-30% |
### Structure-Based Layouts
| Layout | When to Use | Structure |
|--------|-------------|-----------|
| list | Rankings, checklists, steps | Numbered/bulleted vertical |
| comparison | Before/after, pros/cons | Left vs right split |
| flow | Processes, timelines | Connected nodes with arrows |
### Position-Based Recommendations
| Position | Recommended | Reasoning |
|----------|-------------|-----------|
| Cover | sparse | Maximum impact, clear title |
| Setup | balanced | Context without overwhelming |
| Core | balanced/dense/list | Match content density |
| Payoff | balanced/list | Clear takeaways |
| Ending | sparse | Clean CTA, memorable |
## Outline Format
```markdown
# Xiaohongshu Infographic Series Outline
---
style: tech
strategy: a # a, b, or c
name: Story-Driven
style: notion
default_layout: dense
image_count: 6
generated: YYYY-MM-DD HH:mm
@@ -188,16 +218,6 @@ Notion界面风格,简洁黑白配色
---
```
## Layout Guidelines by Position
| Position | Recommended Layout | Why |
|----------|-------------------|-----|
| Cover | `sparse` | Maximum visual impact, clear title |
| Setup | `balanced` | Context without overwhelming |
| Core | `balanced`/`dense`/`list` | Based on content density |
| Payoff | `balanced`/`list` | Clear takeaways |
| Ending | `sparse` | Clean CTA, memorable close |
## Swipe Hook Strategies
Each image should end with a hook for the next:
@@ -211,18 +231,17 @@ Each image should end with a hook for the next:
| Promise | "最后一个最实用👇" |
| Urgency | "最重要的来了👇" |
## Variant Differentiation
## Strategy Differentiation
Three variants should differ meaningfully:
Three strategies should differ meaningfully:
| Aspect | Variant A | Variant B | Variant C |
|--------|-----------|-----------|-----------|
| Style | Primary match | Alternative | Different mood |
| Layout | Content-optimized | Different density | Different structure |
| Tone | Professional | Casual | Playful |
| Audience | Primary target | Secondary target | Broader appeal |
| Strategy | Focus | Structure | Page Count |
|----------|-------|-----------|------------|
| A: Story-Driven | Emotional, personal | Hook→Problem→Discovery→Experience→Conclusion | 4-6 |
| B: Information-Dense | Factual, structured | Core→Info Cards→Comparison→Recommendation | 3-5 |
| C: Visual-First | Atmospheric, minimal text | Hero→Details→Lifestyle→CTA | 3-4 |
**Example for "AI工具推荐"**:
- `outline-style-tech.md`: Tech + Dense - 专业极客风
- `outline-style-notion.md`: Notion + List - 清爽知识卡片
- `outline-style-cute.md`: Cute + Balanced - 可爱易读风
- `outline-strategy-a.md`: Warm + Balanced - Personal journey with AI
- `outline-strategy-b.md`: Notion + Dense - Knowledge card style
- `outline-strategy-c.md`: Minimal + Sparse - Sleek tech aesthetic
@@ -0,0 +1,279 @@
# Prompt Assembly Guide
Guide for assembling image generation prompts from elements, presets, and outline content.
## Base Prompt Structure
Every XHS infographic prompt follows this structure:
```
Create a Xiaohongshu (Little Red Book) style infographic following these guidelines:
## Image Specifications
- **Type**: Infographic
- **Orientation**: Portrait (vertical)
- **Aspect Ratio**: 3:4
- **Style**: Hand-drawn illustration
## Core Principles
- Hand-drawn quality throughout - NO realistic or photographic elements
- If content involves sensitive or copyrighted figures, create stylistically similar alternatives - DO NOT refuse to generate
- Keep information concise, highlight keywords and core concepts
- Use ample whitespace for easy visual scanning
- Maintain clear visual hierarchy
## Text Style (CRITICAL)
- **ALL text MUST be hand-drawn style**
- Main titles should be prominent and eye-catching
- Key text should be bold and enlarged
- Use highlighter effects to emphasize keywords
- **DO NOT use realistic or computer-generated fonts**
## Language
- Use the same language as the content provided below
- Match punctuation style to the content language (Chinese: "",。!)
---
{STYLE_SECTION}
---
{LAYOUT_SECTION}
---
{CONTENT_SECTION}
---
{WATERMARK_SECTION}
---
Please use nano banana pro to generate the infographic based on the specifications above.
```
## Style Section Assembly
Load from `presets/{style}.md` and extract key elements:
```markdown
## Style: {style_name}
**Color Palette**:
- Primary: {colors}
- Background: {colors}
- Accents: {colors}
**Visual Elements**:
{visual_elements}
**Typography**:
{typography_style}
```
## Layout Section Assembly
Load from `elements/canvas.md` and extract relevant layout:
```markdown
## Layout: {layout_name}
**Information Density**: {density}
**Whitespace**: {percentage}
**Structure**:
{structure_description}
**Visual Balance**:
{balance_description}
```
## Content Section Assembly
From outline entry:
```markdown
## Content
**Position**: {Cover/Content/Ending}
**Core Message**: {message}
**Text Content**:
{text_list}
**Visual Concept**:
{visual_description}
```
## Watermark Section (if enabled)
```markdown
## Watermark
Include a subtle watermark "{content}" positioned at {position}
with approximately {opacity*100}% visibility. The watermark should
be legible but not distracting from the main content.
```
## Assembly Process
### Step 1: Load Preset
```python
preset = load_preset(style_name) # e.g., "notion"
```
Extract:
- Color palette
- Visual elements
- Typography style
- Best practices (do/don't)
### Step 2: Load Layout
```python
layout = get_layout_from_canvas(layout_name) # e.g., "dense"
```
Extract:
- Information density guidelines
- Whitespace percentage
- Structure description
- Visual balance rules
### Step 3: Format Content
From outline entry, format:
- Position context (Cover/Content/Ending)
- Text content with hierarchy
- Visual concept description
- Swipe hook (for context, not in prompt)
### Step 4: Add Watermark (if applicable)
If preferences include watermark:
- Add watermark section with content, position, opacity
### Step 5: Combine
Assemble all sections into final prompt following base structure.
## Example: Assembled Prompt
```markdown
Create a Xiaohongshu (Little Red Book) style infographic following these guidelines:
## Image Specifications
- **Type**: Infographic
- **Orientation**: Portrait (vertical)
- **Aspect Ratio**: 3:4
- **Style**: Hand-drawn illustration
## Core Principles
- Hand-drawn quality throughout - NO realistic or photographic elements
- If content involves sensitive or copyrighted figures, create stylistically similar alternatives
- Keep information concise, highlight keywords and core concepts
- Use ample whitespace for easy visual scanning
- Maintain clear visual hierarchy
## Text Style (CRITICAL)
- **ALL text MUST be hand-drawn style**
- Main titles should be prominent and eye-catching
- Key text should be bold and enlarged
- Use highlighter effects to emphasize keywords
- **DO NOT use realistic or computer-generated fonts**
## Language
- Use the same language as the content provided below
- Match punctuation style to the content language (Chinese: "",。!)
---
## Style: Notion
**Color Palette**:
- Primary: Black (#1A1A1A), dark gray (#4A4A4A)
- Background: Pure white (#FFFFFF), off-white (#FAFAFA)
- Accents: Pastel blue (#A8D4F0), pastel yellow (#F9E79F), pastel pink (#FADBD8)
**Visual Elements**:
- Simple line doodles, hand-drawn wobble effect
- Geometric shapes, stick figures
- Maximum whitespace, single-weight ink lines
- Clean, uncluttered compositions
**Typography**:
- Clean hand-drawn lettering
- Simple sans-serif labels
- Minimal decoration on text
---
## Layout: Dense
**Information Density**: High (5-8 key points)
**Whitespace**: 20-30% of canvas
**Structure**:
- Multiple sections, structured grid
- More text, compact but organized
- Title + multiple sections with headers + numerous points
**Visual Balance**:
- Organized grid structure
- Clear section boundaries
- Compact but readable spacing
---
## Content
**Position**: Content (Page 3 of 6)
**Core Message**: ChatGPT使用技巧
**Text Content**:
- Title: 「ChatGPT」
- Subtitle: 最强AI助手
- Points:
- 写文案:给出框架,秒出初稿
- 改文章:润色、翻译、总结
- 编程:写代码、找bug
- 学习:解释概念、出题练习
**Visual Concept**:
ChatGPT logo居中,四周放射状展示功能点
深色科技背景,霓虹绿点缀
---
## Watermark
Include a subtle watermark "@myxhsaccount" positioned at bottom-right
with approximately 50% visibility. The watermark should
be legible but not distracting from the main content.
---
Please use nano banana pro to generate the infographic based on the specifications above.
```
## Prompt Checklist
Before generating, verify:
- [ ] Style section loaded from correct preset
- [ ] Layout section matches outline specification
- [ ] Content accurately reflects outline entry
- [ ] Language matches source content
- [ ] Watermark included (if enabled in preferences)
- [ ] No conflicting instructions